diff --git a/July/article/Path-To-A-Clean(er)-React-Architecture-(Part7)-Domain-Logic.md b/July/article/Path-To-A-Clean(er)-React-Architecture-(Part7)-Domain-Logic.md
index 62b9686..160afd6 100644
--- a/July/article/Path-To-A-Clean(er)-React-Architecture-(Part7)-Domain-Logic.md
+++ b/July/article/Path-To-A-Clean(er)-React-Architecture-(Part7)-Domain-Logic.md
@@ -1,4 +1,4 @@
-## π [Path To A Clean(er) React Architecture (Part 7) - Domain Logic](https://overreacted.io/goodbye-clean-code/)
+## π [Path To A Clean(er) React Architecture (Part 7) - Domain Logic](https://profy.dev/article/react-architecture-domain-logic)
### ποΈ λ²μ λ μ§: 2024.07.17
diff --git a/July/article/Path-To-A-Clean(er)-React-Architecture-(Part8)-React-Query.md b/July/article/Path-To-A-Clean(er)-React-Architecture-(Part8)-React-Query.md
new file mode 100644
index 0000000..feb9ef0
--- /dev/null
+++ b/July/article/Path-To-A-Clean(er)-React-Architecture-(Part8)-React-Query.md
@@ -0,0 +1,544 @@
+## π [Path To A Clean(er) React Architecture (Part 8) - React-Query](https://profy.dev/article/react-architecture-tanstack-query)
+
+### ποΈ λ²μ λ μ§: 2024.07.21
+
+### π§ λ²μν ν¬λ£¨: λ§μ€ν°μ(λͺ
μ¬μ)
+
+---
+
+## κΉλν 리μ‘νΈ μ€κ³λ‘μ κΈΈ(Part8) React-Query
+
+Johannes Kettmann
+Updated On July 19, 2024
+Keyword : **React**, **Refactoring**, **Component**, **React-Query**
+
+리μ‘νΈμ λΉμμ‘΄μ μΈ νΉμ±μ μλ μ κ²μ
λλ€:
+
+- ννΈμΌλ‘λ μ νμ μμ λ₯Ό μ 곡ν©λλ€.
+- λ€λ₯Έ ννΈμΌλ‘λ λ§μ νλ‘μ νΈκ° 컀μ€ν
νλκ³ μ’
μ’
νΌλμ€λ¬μ΄ μν€ν
μ²λ‘ λλκ² λ©λλ€.
+
+μ΄ κΈμ μννΈμ¨μ΄ μν€ν
μ²μ 리μ‘νΈ μ±μ λν μ리μ¦μ μ¬λ λ²μ§Έ λΆλΆμΌλ‘, λ§μ μλͺ»λ κ΄νμ΄ μλ μ½λλ² μ΄μ€λ₯Ό λ¨κ³μ μΌλ‘ 리ν©ν λ§νλ κ³Όμ μ λ€λ£Ήλλ€.
+
+μ΄μ μ κΈμμλ,
+
+- [we created the initial API layer and extracted fetch functions](https://profy.dev/article/react-architecture-api-layer-and-fetch-functions)
+- [added data transformations](https://profy.dev/article/react-architecture-api-layer-and-data-transformations)
+- [separated domain entities and DTOs](https://profy.dev/article/react-architecture-domain-entities-and-dtos)
+- [introduced infrastructure services using dependency injection and](https://profy.dev/article/react-architecture-infrastructure-services-and-dependency-injection)
+- separated [business logic](https://profy.dev/article/react-architecture-business-logic-and-dependency-injection) and [domain logic](https://profy.dev/article/react-architecture-business-logic-and-dependency-injection) from the components.
+
+μ΄λ₯Ό ν΅ν΄ μ°λ¦¬λ UI μ½λλ₯Ό μλ²λ‘λΆν° λΆλ¦¬νκ³ , λΉμ¦λμ€ λ‘μ§μ UI νλ μμν¬μ λ
립μ μΌλ‘ λ§λ€λ©°, ν
μ€νΈ κ°λ₯μ±μ λμΌ μ μμμ΅λλ€.
+
+νμ§λ§ μ°λ¦¬λ μμ§ νλ‘λμ
리μ‘νΈ μ±μμ κ°μ₯ μ€μν λꡬ μ€ νλμΈ react-query λλ λ€λ₯Έ μλ² μν κ΄λ¦¬ λΌμ΄λΈλ¬λ¦¬λ₯Ό μκ°νμ§ μμμ΅λλ€.
+
+μ΄κ²μ΄ μ΄λ² κΈμ μ£Όμ μ
λλ€.
+
+μμνκΈ° μ μ κ°λ¨ν λ©λͺ¨: react-queryμ μ€μ λ°©λ²μ΄λ κΈ°λ₯μ μμΈν μ€λͺ
νμ§λ μκ² μ΅λλ€. μ΄μ μ΅μνλ€κ³ κ°μ ν©λλ€. κ·Έλ μ§ μλ€λ©΄ λ¬Έμλ λ§μ νν 리μΌμ μ°Έκ³ ν μ μμ΅λλ€.
+
+λν μ΄ μ리μ¦μ λ€λ₯Έ κΈμ μ½μ§ μμλ€λ©΄, κ³μνκΈ° μ μ μ½μ΄λ³΄λ κ²μ κΆμ₯ν©λλ€.
+
+## λͺ©μ°¨
+
+- λ¬Έμ κ° μλ μ½λ μμ 1: μλ² λ°μ΄ν°λ₯Ό μλμΌλ‘ κ΄λ¦¬
+
+ - λ¬Έμ : 보μΌλ¬νλ μ΄νΈ λ° μν κ΄λ¦¬
+ - ν΄κ²°μ±
: react-query ν
μμ±
+
+- λ¬Έμ κ° μλ μ½λ μμ 2: λΉμ¦λμ€ λ‘μ§κ³Ό react-query
+
+ - λ¬Έμ : μ€λ³΅ μμ²
+ - ν΄κ²°μ±
: react-query λ°μ΄ν°μ λ³μ΄ μ¬μ©
+
+- λ€μ 리ν©ν λ§ λ¨κ³
+
+[μ νλΈ λ§ν¬](https://youtu.be/A2FOOudW5GQ)
+
+## λ¬Έμ κ° μλ μ½λ μμ 1: μλ² λ°μ΄ν°λ₯Ό μλμΌλ‘ κ΄λ¦¬
+
+λ¬Έμ κ° μλ μ½λ μμλ₯Ό μ΄ν΄λ³΄κ² μ΅λλ€. λ€μμ νμ¬ λ‘κ·ΈμΈλ μ¬μ©μλ₯Ό κ°μ Έμ€κ³ , μ¬μ©μκ° λ©μμ§(aka shout)μ λ΅μ₯ν μ μλ νΌμ λ€μ΄μΌλ‘κ·Έμ λ λλ§νλ μ»΄ν¬λνΈμ
λλ€.
+
+[μ 체 μμ€ μ½λλ₯Ό ν¬ν¨ν λͺ¨λ λ³κ²½ μ¬νμ μ¬κΈ°μ μμ΅λλ€.](https://github.com/jkettmann/react-architecture/pull/15)
+
+```tsx
+import { useEffect, useState } from "react";
+
+import { isAuthenticated as isUserAuthenticated } from "@/domain/me";
+import UserService from "@/infrastructure/user";
+
+...
+
+export function ReplyDialog({
+ recipientHandle,
+ children,
+ shoutId,
+}: ReplyDialogProps) {
+ const [open, setOpen] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const [hasError, setHasError] = useState(false);
+
+ ...
+ // μμ λ λΆλΆ
+ useEffect(() => {
+ UserService.getMe()
+ .then(isUserAuthenticated)
+ .then(setIsAuthenticated)
+ .catch(() => setHasError(true))
+ .finally(() => setIsLoading(false));
+ }, []);
+ // μμ λ λΆλΆ
+
+ if (hasError || !isAuthenticated) {
+ return {children};
+ }
+
+ async function handleSubmit(event: React.FormEvent) {
+ // μ΄ λΆλΆμ λμ€μ λ€μ λ³Όκ²λλ€
+ }
+
+ return (
+
+ );
+}
+```
+
+## λ¬Έμ : 보μΌλ¬νλ μ΄νΈ λ° μν κ΄λ¦¬
+
+react-queryλ₯Ό μ¬μ©ν΄ λ³Έ μ μ΄ μλ€λ©΄ μλ§λ μ΄ λ¬Έμ λ₯Ό μκ³ μμ κ²μ
λλ€.
+
+- λ‘λ© λ° μ€λ₯ μνλ₯Ό μλμΌλ‘ κ΄λ¦¬νλ©΄ 보μΌλ¬νλ μ΄νΈ μ½λκ° λ°μν©λλ€.
+- λμμ, API μλ΅μ μΊμνμ§ μμμ μ€λ³΅ API μμ²μ΄ λ°μνκ² λ©λλ€.
+- μ΄ λ κ°μ§ λ¬Έμ λ₯Ό react-queryκ° ν΄κ²°ν΄ μ€ μ μμ΅λλ€.
+
+## ν΄κ²°μ±
: react-query ν
μμ±
+
+λ€μμ `useEffect`λ₯Ό λ체ν μ μλ query ν
μ
λλ€.
+
+```tsx
+import { useQuery } from '@tanstack/react-query';
+
+import UserService from '@/infrastructure/user';
+
+export function getQueryKey() {
+ return ['me'];
+}
+
+export function useGetMe() {
+ return useQuery({
+ queryKey: getQueryKey(),
+ queryFn: () => UserService.getMe(),
+ });
+}
+```
+
+쿼리 ν¨μλ λ³νλ API μλ΅μ λ°ννλ `UserService`([μ΄μ κΈμμ μμ±λ](https://profy.dev/article/react-architecture-domain-entities-and-dtos))λ₯Ό νΈμΆν©λλ€. μ΄ λΆλΆμ μ¬κΈ°μ ν¬κ² μ€μνμ§ μμ΅λλ€.
+
+μ΄μ μ»΄ν¬λνΈμμ `useEffect` λμ μ 쿼리 ν
μ κ°λ¨ν μ¬μ©ν μ μμ΅λλ€.
+
+```tsx
+import { useState } from "react";
+
+import { useGetMe } from "@/application/queries/get-me";
+import { useReplyToShout } from "@/application/reply-to-shout";
+import { isAuthenticated } from "@/domain/me";
+
+...
+
+export function ReplyDialog({
+ recipientHandle,
+ children,
+ shoutId,
+}: ReplyDialogProps) {
+ const [open, setOpen] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+ const [replyError, setReplyError] = useState();
+ const replyToShout = useReplyToShout();
+ const me = useGetMe(); // ν΄λΉ μ½λ μΆκ°
+
+ if (me.isError || !isAuthenticated(me.data)) { // ν΄λΉ μ½λ μΆκ°
+ return {children};
+ }
+
+ async function handleSubmit(event: React.FormEvent) {
+ // μ΄ λΆλΆμ 곧 λ€μ λ³Όκ²λλ€
+ }
+
+ return (
+
+ );
+}
+```
+
+μ°λ¦¬λ μ€λ₯ λ° λ‘λ© μν μ²λ¦¬μ κ°μ λ§μ 보μΌλ¬νλ μ΄νΈ μ½λλ₯Ό μ κ±°νμ λΏλ§ μλλΌ, μλ΅ μΊμ±, μ¬μλ λ± λ§μ κΈ°λ₯μ κΈ°λ³Έμ μΌλ‘ μ»μ μ μμ΅λλ€.
+
+μμ½νλ©΄, 쿼리 ν
μ μ»΄ν¬λνΈμ μλΉμ€ λ μ΄μ΄ μ¬μ΄μ μΌμ’
μ νλ‘μλ‘ μ¬μ©ν©λλ€.
+
+μ΄μ λ 볡μ‘ν μμ λ₯Ό μ΄ν΄λ³΄κ² μ΅λλ€.
+
+## λ¬Έμ κ° μλ μ½λ μμ 2: λΉμ¦λμ€ λ‘μ§κ³Ό react-query
+
+μ΄μ μ½λ μμ μμλ submit νΈλ€λ¬λ₯Ό μ΄ν΄λ³΄μ§ μμμ΅λλ€. μ¬κΈ°λ₯Ό 보면:
+
+```tsx
+import { useReplyToShout } from "@/application/reply-to-shout";
+
+...
+
+export function ReplyDialog({
+ recipientHandle,
+ children,
+ shoutId,
+}: ReplyDialogProps) {
+ ...
+
+ const replyToShout = useReplyToShout(); // μμ λ λΆλΆ
+
+ async function handleSubmit(event: React.FormEvent) {
+ event.preventDefault();
+ setIsLoading(true);
+
+ const message = event.currentTarget.elements.message.value;
+ const files = Array.from(event.currentTarget.elements.image.files ?? []);
+
+ // μμ λ λΆλΆ
+ const result = await replyToShout({
+ recipientHandle,
+ message,
+ files,
+ shoutId,
+ });
+ // μμ λ λΆλΆ
+
+ if (result.error) {
+ setReplyError(result.error);
+ } else {
+ setOpen(false);
+ }
+ setIsLoading(false);
+ }
+
+ return (
+
+ );
+}
+```
+
+[μ΄μ κΈ](https://profy.dev/article/react-architecture-business-logic-and-dependency-injection)μμ μ°λ¦¬λ submit νΈλ€λ¬μ λ§μ λΉμ¦λμ€ λ‘μ§μ μμμ κ°μ‘°ν `useReplyToShout` ν¨μλ‘ μΆμΆνμ΅λλ€.
+
+νμ¬ `useReplyToShout` ν
μ μμ‘΄μ± μ£Όμ
μ ν΅ν΄ `replyToShout` ν¨μμ λͺ κ°μ§ μλΉμ€ ν¨μλ₯Ό μ 곡ν©λλ€.
+
+```tsx
+import { useCallback } from "react";
+
+import { hasExceededShoutLimit } from "@/domain/me";
+import { hasBlockedUser } from "@/domain/user";
+import MediaService from "@/infrastructure/media";
+import ShoutService from "@/infrastructure/shout";
+import UserService from "@/infrastructure/user";
+
+...
+
+// μμ λ λΆλΆ
+const dependencies = {
+ getMe: UserService.getMe,
+ getUser: UserService.getUser,
+ saveImage: MediaService.saveImage,
+ createShout: ShoutService.createShout,
+ createReply: ShoutService.createReply,
+};
+// μμ λ λΆλΆ
+
+export async function replyToShout(
+ { recipientHandle, shoutId, message, files }: ReplyToShoutInput,
+ { getMe, getUser, saveImage, createReply, createShout }: typeof dependencies
+) {
+ const me = await getMe(); // μμ λ λΆλΆ
+ if (hasExceededShoutLimit(me)) {
+ return { error: ErrorMessages.TooManyShouts };
+ }
+
+ const recipient = await getUser(recipientHandle); // μμ λ λΆλΆ
+ if (!recipient) {
+ return { error: ErrorMessages.RecipientNotFound };
+ }
+ if (hasBlockedUser(recipient, me.id)) {
+ return { error: ErrorMessages.AuthorBlockedByRecipient };
+ }
+
+ try {
+ let image;
+ if (files?.length) {
+ image = await saveImage(files[0]); // μμ λ λΆλΆ
+ }
+
+ const newShout = await createShout({ // μμ λ λΆλΆ
+ message,
+ imageId: image?.id,
+ });
+
+ await createReply({ // μμ λ λΆλΆ
+ shoutId,
+ replyId: newShout.id,
+ });
+
+ return { error: undefined };
+ } catch {
+ return { error: ErrorMessages.UnknownError };
+ }
+}
+
+export function useReplyToShout() {
+ return useCallback(
+ (input: ReplyToShoutInput) => replyToShout(input, dependencies), // μμ λ λΆλΆ
+ []
+ );
+}
+```
+
+## λ¬Έμ : μ€λ³΅ μμ²
+
+`replyToShout` ν¨μλ μ¬λ¬ API μμ²μ 보λ
λλ€. κ·Έ μ€ `UserService.getMe(...)`μ `UserService.getUser(...)` νΈμΆμ΄ ν¬ν¨λ©λλ€. κ·Έλ¬λ μ΄ λ°μ΄ν°λ μ΄λ―Έ μ ν리μΌμ΄μ
μ λ€λ₯Έ λΆλΆμμ κ°μ Έμ react-query μΊμμ μ‘΄μ¬ν©λλ€.
+
+λν, λ€μ λ‘λ© μνλ₯Ό μλμΌλ‘ κ΄λ¦¬ν΄μΌ ν©λλ€.
+
+## ν΄κ²°μ±
: react-query λ°μ΄ν°μ mutation μ¬μ©
+
+μ΄μ μμ μμ `useGetMe` 쿼리 ν
μ λμ
νμ΅λλ€. μ΄μ μ¬μ©μμ νΈλ€μ κΈ°λ°μΌλ‘ μ¬μ©μλ₯Ό κ°μ Έμ€λ λ λ€λ₯Έ 쿼리 ν
μ μΆκ°ν΄ λ³΄κ² μ΅λλ€.
+
+```tsx
+import { useQuery } from '@tanstack/react-query';
+
+import UserService from '@/infrastructure/user';
+
+interface GetUserInput {
+ handle?: string;
+}
+
+export function getQueryKey(handle?: string) {
+ return ['user', handle];
+}
+
+export function useGetUser({ handle }: GetUserInput) {
+ return useQuery({
+ queryKey: getQueryKey(handle),
+ queryFn: () => UserService.getUser(handle),
+ });
+}
+```
+
+κ·Έλ° λ€μ νμν mutation ν
μ μμ±ν©λλ€. μ¬κΈ° shoutλ₯Ό μμ±νλ μμ ν
μ΄ μμ΅λλ€.
+
+```tsx
+import { useMutation } from '@tanstack/react-query';
+
+import ShoutService from '@/infrastructure/shout';
+
+interface CreateShoutInput {
+ message: string;
+ imageId?: string;
+}
+
+export function useCreateShout() {
+ return useMutation({
+ mutationFn: (input: CreateShoutInput) => ShoutService.createShout(input),
+ });
+}
+```
+
+μ΄μ μ΄λ¬ν ν
μ `useReplyToShout` ν
λ΄μμ μ¬μ©ν μ μμ΅λλ€.
+
+첫 λ²μ§Έ λ¨κ³λ‘, `dependencies` κ°μ²΄λ₯Ό TypeScript μΈν°νμ΄μ€λ‘ λ체νκ³ , `replyToShout` ν¨μλ₯Ό μ΄μ λ§κ² μ‘°μ ν©λλ€.
+
+```tsx
+import { Me, hasExceededShoutLimit, isAuthenticated } from "@/domain/me";
+import { Image } from "@/domain/media";
+import { Shout } from "@/domain/shout";
+import { User, hasBlockedUser } from "@/domain/user";
+
+import { useCreateShout } from "../mutations/create-shout";
+import { useCreateShoutReply } from "../mutations/create-shout-reply";
+import { useSaveImage } from "../mutations/save-image";
+import { useGetMe } from "../queries/get-me";
+import { useGetUser } from "../queries/get-user";
+
+...
+
+// μΆκ°ν λΆλΆ
+interface Dependencies {
+ me: ReturnType["data"];
+ recipient: ReturnType["data"];
+ saveImage: ReturnType["mutateAsync"];
+ createShout: ReturnType["mutateAsync"];
+ createReply: ReturnType["mutateAsync"];
+}
+
+export async function replyToShout(
+ { shoutId, message, files }: ReplyToShoutInput,
+ { me, recipient, saveImage, createReply, createShout }: Dependencies // μΆκ°ν λΆλΆ
+) {
+ if (!isAuthenticated(me)) { // μΆκ°ν λΆλΆ
+ return { error: ErrorMessages.NotAuthenticated };
+ }
+ if (hasExceededShoutLimit(me)) { // μΆκ°ν λΆλΆ
+ return { error: ErrorMessages.TooManyShouts };
+ }
+
+ if (!recipient) { // μΆκ°ν λΆλΆ
+ return { error: ErrorMessages.RecipientNotFound };
+ }
+ if (hasBlockedUser(recipient, me.id)) { // μΆκ°ν λΆλΆ
+ return { error: ErrorMessages.AuthorBlockedByRecipient };
+ }
+
+ try {
+ let image;
+ if (files?.length) {
+ image = await saveImage(files[0]);
+ }
+
+ const newShout = await createShout({
+ message,
+ imageId: image?.id,
+ });
+
+ await createReply({
+ shoutId,
+ replyId: newShout.id,
+ });
+
+ return { error: undefined };
+ } catch {
+ return { error: ErrorMessages.UnknownError };
+ }
+}
+```
+
+λ€μμΌλ‘, `useReplyToShout` ν
μ λ€μ μμ±ν΄μΌ ν©λλ€.
+
+λ¨μν dependencies κ°μ²΄λ₯Ό `replyToShout` ν¨μμ μ 곡νκ³ λ°ννλ λμ ,
+
+- 쿼리μ mutation ν
μ ν΅ν΄ λͺ¨λ μ’
μμ±μ μμ§ν©λλ€.
+- `mutateAsync` ν¨μλ₯Ό λ°νν©λλ€(μ΄λ¦μ μμμ΄μ§λ§ react-query mutation ν
κ³Ό API μΌκ΄μ±μ μ μ§ν©λλ€).
+- λͺ¨λ 쿼리μ mutation ν
μ λ‘λ© μνλ₯Ό λ³ν©(merge)ν©λλ€.
+- λ 쿼리 ν
μ μ€λ₯λ₯Ό λ³ν©ν©λλ€.
+
+```tsx
+interface UseReplyToShoutInput {
+ recipientHandle: string;
+}
+
+export function useReplyToShout({ recipientHandle }: UseReplyToShoutInput) {
+ const me = useGetMe();
+ const user = useGetUser({ handle: recipientHandle });
+ const saveImage = useSaveImage();
+ const createShout = useCreateShout();
+ const createReply = useCreateShoutReply();
+
+ return {
+ mutateAsync: (input: ReplyToShoutInput) =>
+ replyToShout(input, {
+ me: me.data,
+ recipient: user.data,
+ saveImage: saveImage.mutateAsync,
+ createShout: createShout.mutateAsync,
+ createReply: createReply.mutateAsync,
+ }),
+ isLoading:
+ me.isLoading ||
+ user.isLoading ||
+ saveImage.isPending ||
+ createShout.isPending ||
+ createReply.isPending,
+ isError: me.isError || user.isError,
+ };
+}
+```
+
+μ΄ λ³κ²½μΌλ‘ `ReplyDialog` μ»΄ν¬λνΈκ° λ λλ§λμλ§μ `me`μ `user`λ₯Ό μ»κΈ° μν API μμ²μ΄ μ μ‘λ©λλ€. λμμ, μ΄μ μ λ°μ΄ν°λ₯Ό κ°μ ΈμμΌλ©΄ λ μλ΅ λͺ¨λ μΊμμμ μ 곡λ μ μμ΅λλ€.
+
+μ¬μ©μκ° μ€μ°νΈμ λ΅μ₯ν λ μ΄λ¬ν μμ²μ κΈ°λ€λ¦΄ νμκ° μμ΄ μ λ°μ μΈ μ¬μ©μ κ²½νμ΄ κ°μ λ©λλ€.
+
+μ΄ μ κ·Ό λ°©μμ λ λ€λ₯Έ μ₯μ μ, μλ² λ°μ΄ν° κ΄λ¦¬λ₯Ό μν΄ react-queryλ₯Ό μ¬μ©νλ©΄μλ `replyToShout` ν¨μμ λͺ¨λ λΉμ¦λμ€ λ‘μ§μ κ°λ³μ μΌλ‘ ν
μ€νΈν μ μλ€λ κ²μ
λλ€.
+
+μ΄λ¬ν λ³κ²½ μ¬νμΌλ‘ `ReplyToDialog` μ»΄ν¬λνΈλ₯Ό κ°μνν μ μμ΅λλ€. μ‘°μ λ useReplyToShout ν
μμ `isLoading`κ³Ό `hasError` μνλ₯Ό μ 곡νλ―λ‘ λ μ΄μ νμνμ§ μμ΅λλ€.
+
+```tsx
+import { useState } from "react";
+
+import { useGetMe } from "@/application/queries/get-me";
+import { useReplyToShout } from "@/application/reply-to-shout";
+import { LoginDialog } from "@/components/login-dialog";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { isAuthenticated } from "@/domain/me";
+
+...
+
+export function ReplyDialog({
+ recipientHandle,
+ children,
+ shoutId,
+}: ReplyDialogProps) {
+ const [open, setOpen] = useState(false);
+ const [replyError, setReplyError] = useState();
+ const replyToShout = useReplyToShout({ recipientHandle });
+ const me = useGetMe();
+
+ if (me.isError || !isAuthenticated(me.data)) {
+ return {children};
+ }
+
+ async function handleSubmit(event: React.FormEvent) {
+ event.preventDefault();
+
+ const message = event.currentTarget.elements.message.value;
+ const files = Array.from(event.currentTarget.elements.image.files ?? []);
+
+ const result = await replyToShout.mutateAsync({ // μΆκ°λ λΆλΆ
+ recipientHandle,
+ message,
+ files,
+ shoutId,
+ });
+
+ if (result.error) {
+ setReplyError(result.error);
+ } else {
+ setOpen(false);
+ }
+ }
+
+ ...
+
+ return (
+
+ );
+}
+```
+
+λΉμ¦λμ€ λ‘μ§μ `useReplyToShout` ν
μΌλ‘ μΆμΆν λ λ€λ₯Έ μ₯μ μ΄ μ΄μ λͺ
νν΄μ§λλ€:
+
+μλ² λ°μ΄ν° κ΄λ¦¬μ κΈ°λ³Έ λ©μ»€λμ¦μ ν
λ΄λΆμμ μλΉν λ³κ²½νμ§λ§, μ»΄ν¬λνΈμμμ μ‘°μ μ μ΅μνμ΄μμ΅λλ€.
+
+## λ€μ 리ν©ν λ§ λ¨κ³
+
+μ΄λ² κΈμ μ¬κΈ°κΉμ§μ
λλ€. μ°λ¦¬λ react-queryλ₯Ό React μ ν리μΌμ΄μ
μν€ν
μ²μ μ±κ³΅μ μΌλ‘ ν΅ν©νμ΅λλ€. λ€μ λ²μλ λ μΌλ°μ μΈ κΈ°λ₯ μ€μ¬μ ν΄λ ꡬ쑰μ λ§μΆκΈ° μν΄ ν΄λ ꡬ쑰λ₯Ό 리ν©ν λ§ν κ²μ
λλ€.