현재 EditorTextArea 컴포넌트는 크게 3가지 역할을 한다.

  1. Create Thread
  2. Patch Thread
  3. Create Comment

각각의 행동을 할 때 UI는 비슷하지만(1, 3번은 완전 동일) 안에 필요한 props값들과 비즈니스로직이 다르게 작동한다.

내가 하고 싶은 것은 사용하는 쪽에서 EditorTextArea를 사용하면서 각각의 로직 작동을 모르고 선언적으로 사용할 수 있도록 필요한 props만 넣어주면 알아서 동작하도록 하고싶었다.

예를들어 prevContent를 넣어주면 PatchThread 로직이 실행되고, postId를 넣어주면 Create Comment가 실행되고 이런식으로 props에 따라서 다른 로직이 동작하는 방식을 구현해보고자 했다.

이를 위해서 먼저 비즈니스로직을 바깥으로 분리해야했다.

비즈니스 로직 커스텀 훅으로 분리

  • useCreateThread

    // useCreateThread
    
    interface Props {
      nickname: string | undefined;
      channelId: string;
      mentionedList?: UserDBProps[];
    }
    const useCreateThread = ({ nickname, channelId, mentionedList }: Props) => {
      const { mutateAsync: createThreadMutate } = usePostThread(channelId);
      const { mentionNotification } = useMentionNotification({ mentionedList });
      const { sendMessageBySlackBot } = usePostSlackMessage();
      const uploadThread = async (formValues: FormValues) => {
        if (!formValues) return;
    
        const threadRequest = {
          title: formJSONStringify({ formValues, nickname, mentionedList }),
          image: null,
          channelId,
        };
    
        const threadResponse = await createThreadMutate(threadRequest);
    
        if (!mentionedList) return;
    
        mentionNotification({
          content: formValues.content,
          postId: threadResponse._id,
          channelName: threadResponse.channel.name,
        });
    
        sendMessageBySlackBot({ mentionedList });
      };
      return { uploadThread };
    };
    
    export default useCreateThread;
    

    요약 : post 생성 후 생성 된 post._id를 사용해 mention 유저가 있을 경우 mentionNotificationsendMessageBySlackBot 호출

  • useChangeThread

    import usePutThread from "@/apis/thread/usePutThread.ts";
    import { FormValues } from "@/components/common/EditorTextArea.tsx";
    import { formJSONStringify } from "@/lib/editorContent.ts";
    import { UserDBProps } from "@/hooks/api/useUserListByDB.ts";
    import useMentionNotification from "@/hooks/api/useMentionNotification.ts";
    import usePostSlackMessage from "@/apis/slackBot/usePostSlackMessage.ts";
    
    interface Props {
      nickname: string | undefined;
      postId: string;
      channelId: string;
      mentionedList?: UserDBProps[];
    }
    const useChangeThread = ({
      nickname,
      postId,
      channelId,
      mentionedList,
    }: Props) => {
      const { mutateAsync: patchThreadMutate } = usePutThread();
      const { mentionNotification } = useMentionNotification({ mentionedList });
      const { sendMessageBySlackBot } = usePostSlackMessage();
    
      const changeThread = async (formValues: FormValues) => {
        if (!formValues) return;
    
        const threadRequest = {
          title: formJSONStringify({ formValues, nickname, mentionedList }),
          image: null,
          postId,
          channelId,
        };
    
        const ThreadResponse = await patchThreadMutate(threadRequest);
    
        if (!mentionedList) return;
    
        mentionNotification({
          content: formValues.content,
          postId: ThreadResponse._id,
          channelName: ThreadResponse.channel.name,
        });
    
        sendMessageBySlackBot({ mentionedList });
      };
      return { changeThread };
    };
    
    export default useChangeThread;
    

    요약 : post 수정 후 post._id를 사용해 mention 유저가 있을 경우 mentionNotificationsendMessageBySlackBot 호출

  • useCreateComment

    import { usePostComment } from "@/apis/comment/usePostComment.ts";
    import { usePostNotification } from "@/apis/notification/usePostNotification.ts";
    import { FormValues } from "@/components/common/EditorTextArea.tsx";
    import { formJSONStringify } from "@/lib/editorContent.ts";
    import { UserDBProps } from "@/hooks/api/useUserListByDB.ts";
    import useMentionNotification from "@/hooks/api/useMentionNotification.ts";
    import { NOTIFICATION_TYPES } from "@/constants/notification";
    import usePostSlackMessage from "@/apis/slackBot/usePostSlackMessage.ts";
    
    interface Props {
      nickname: string | undefined;
      postId: string;
      channelId: string;
      channelName: string;
      mentionedList?: UserDBProps[];
      postAuthorId: string;
    }
    
    const useUploadComment = ({
      nickname,
      postId,
      channelId,
      channelName,
      mentionedList,
      postAuthorId,
    }: Props) => {
      const { mutateAsync: commentMutate } = usePostComment(channelId);
      const { mutate: notificationMutate } = usePostNotification();
      const { mentionNotification } = useMentionNotification({ mentionedList });
      const { sendMessageBySlackBot } = usePostSlackMessage();
    
      const uploadComment = async (formValues: FormValues) => {
        if (!formValues) return;
    
        const commentRequest = {
          comment: formJSONStringify({ formValues, nickname, mentionedList }),
          postId,
        };
    
        const commentResponse = await commentMutate(commentRequest);
    
        const notificationRequest = {
          notificationType: NOTIFICATION_TYPES.COMMENT,
          notificationTypeId: commentResponse._id,
          userId: postAuthorId,
          postId,
        };
    
        notificationMutate(notificationRequest);
    
        if (!mentionedList) return;
    
        mentionNotification({
          content: formValues.content,
          postId,
          channelName,
        });
    
        sendMessageBySlackBot({ mentionedList });
      };
    
      return { uploadComment };
    };
    
    export default useUploadComment;
    

    요약 : comment 생성 후 comment._id를 사용해 알림 생성 및 mention 유저가 있을 경우 mentionNotificationsendMessageBySlackBot 호출

로 따로 커스텀 훅을 만들고 각각의 비즈니스 로직을 분리하였고 모든 로직에 공통으로 들어가는 부분을 다시 커스텀 훅으로 분리해서 호출했다.

  • useMentionNotification

    import { usePostNotification } from "@/apis/notification/usePostNotification.ts";
    import { usePostMention } from "@/apis/mention/usePostMention.ts";
    import { UserDBProps } from "@/hooks/api/useUserListByDB.ts";
    import { NOTIFICATION_TYPES } from "@/constants/notification";
    
    interface MentionNotificationProps {
      content: string;
      postId: string;
      channelName: string;
    }
    
    interface Props {
      mentionedList?: UserDBProps[];
    }
    const useMentionNotification = ({ mentionedList }: Props) => {
      const { mutateAsync: mentionMutate } = usePostMention();
      const { mutate: notificationMutate } = usePostNotification();
    
      const mentionNotification = ({
        content,
        postId,
        channelName,
      }: MentionNotificationProps) => {
        if (!mentionedList) return;
    
        mentionedList.forEach(async (mentionUser) => {
          const mentionRequest = {
            message: JSON.stringify({
              channelName,
              postId,
              content,
              receiverName: mentionUser.name,
            }),
            receiver: mentionUser.userId,
          };
    
          const mentionResponse = await mentionMutate(mentionRequest);
    
          const notificationRequest = {
            notificationType: NOTIFICATION_TYPES.MESSAGE,
            notificationTypeId: mentionResponse._id,
            userId: mentionResponse.sender._id,
            postId,
          };
    
          notificationMutate(notificationRequest);
        });
      };
    
      return { mentionNotification };
    };
    
    export default useMentionNotification;
    

    요약: 멘션기능이 API 상에 없기 때문에 message API를 커스텀해서 필요한 정보를 넘겨주는 식으로 사용하였고, 이를 위해 멘션 대상별로 message를 생성 후 notification을 만들었다

  • usePostSlackMessage

    import { useMutation } from "@tanstack/react-query";
    
    import { postMessageSlackBot } from "@/apis/slackBot/queryFn.ts";
    
    const usePostSlackMessage = () => {
      const { mutate, ...rest } = useMutation({
        mutationFn: postMessageSlackBot,
      });
    
      return {
        sendMessageBySlackBot: mutate,
        ...rest,
      };
    };
    
    export default usePostSlackMessage;
    

이제 각 로직들을 커스텀 훅으로 분리 되었으니 EditorTextArea를 사용하는 곳에서 props로 보내주는 값에 따라서 해당 비즈니스 로직을 동작하게 해주고 싶었다.

이를 위해 중간에 분기처리를 해주는 useEditorLogicByProps 라는 커스텀 훅을 사용했다.

이 커스텀 훅이 하는 일은 props로 넘어온 값에 따라서 해당 커스텀 훅을 사용하도록 만들어주는 중간다리 역할이다.

Props에 따라 해당 로직 선택하기

3가지의 로직으로 넘겨주는 Props값들이 서로 다른 값들이 있기때문에 커스텀 타입가드 함수를 사용하면 특정지을 수 있을 것이라고 생각했다.

export type EditorProps = CreateThreadProps | PatchThreadProps | CommentProps;

const isPatchThreadProps = (props: EditorProps): props is PatchThreadProps => {
  return "prevContent" in props;
};

const isCommentProps = (props: EditorProps): props is CommentProps => {
  return "channelName" in props;
};

따라서 Props로 넘겨주는 값에 따라서 커스텀 타입가드가 true인 값에 해당하는 비즈니스 로직이 state에 저장되어 반환된다.

const useEditorLogicByProps = ({ editorProps, nickname, mentionedList }: Props) => {
  const [upload, setUpload] = useState<UploadHooksProps>(() => () => {});

  const { uploadThread } = useCreateThread({...});
  const { changeThread } = useChangeThread({...});
  const { uploadComment } = useUploadComment({...});

  useEffect(() => {
    if (isPatchThreadProps(editorProps)) {
      setUpload(() => changeThread);
      return;
    }
    if (isCommentProps(editorProps)) {
      setUpload(() => uploadComment);
      return;
    }

    setUpload(() => uploadThread);
  }, [editorProps, mentionedList]);

  return { upload };
};

로직의 흐름을 요약하면 다음과 같다

Untitled

이를 통해 사용하는 곳에서 넘겨주는 Props에 따라서 해당 로직이 작동하게 구현하였다.

하지만…

어렵게 구현했지만 효과는 미미했다.

  1. 사용하는 입장에서 props로 어떤 값이 필요한지 한눈에 파악하기 어렵다.
  2. 중간에 분기처리를하는 useEditorLogicByProps를 추가함으로써 props가 변경될 때 사용하는 곳, useEditorLogicByProps, 각 해당 커스텀 훅 이렇게 세곳의 수정이 필요하게 되었다.

    ⇒ 즉, 의존성이 높다.

이러한 문제점으로 인해 멘토님께 질문과 함께 리액트 패턴 중 container-presentational 패턴을 말씀해 주셨고 딱 필요하던 패턴이었다.

  • 3가지의 ‘뷰’는 (거의) 동일하다 ⇒ persentational에서 뷰 재사용
  • 눌렀을 때의 ‘동작(이벤트 핸들링)’은 다르다 ⇒ container에서 동작 주입

따라서 뷰는 재사용하고 이벤트 핸들링은 주입하는 container-presentational를 사용해서 리팩토링 할 수 있다.

이번 주간에 여기를 리팩토링 할 예정이다!

⇒ 리팩토링 결과는 여기서 확인 가능하다!


참고

FE 패턴들

댓글남기기