[React] 컴포넌트를 어떻게 분리할까?

Nine·2022년 8월 1일
4

React

목록 보기
19/22

UI가 비슷하면 재사용

초반에 개발을 진행할 때 재사용의 기준, 컴포넌트의 분리 기준은 대부분 UI였습니다.

"UI가 비슷하면 분명 재사용될 것이니 이 부분들을 묶어서 컴포넌트화하자!"

다음은 달록의 카테고리 목록을 조회하는 페이지입니다.

  • BE 공식일정 카테고리
  • 알록달록 팀 회의 카테고리

두 카테고리가 매우 흡사하게 생겼죠? 비슷하게 생겼으니 하나의 컴포넌트로 취급하여 사용했습니다. (CategoryItem)

function CategoryItem({ category, subscriptionId }: CategoryItemProps) {
  // ... 생략
  
  // ⚠️구독을 위한 react query
  const { mutate: postSubscription } = useMutation<
    AxiosResponse<Pick<SubscriptionType, 'color'>>,
    AxiosError,
    Pick<SubscriptionType, 'color'>,
    unknown
  >(() => subscriptionApi.post(accessToken, category.id, body), {
    onSuccess: () => {
      queryClient.invalidateQueries(CACHE_KEY.SUBSCRIPTIONS);
    },
  });

  // ⚠️구독 해제를 위한 react query
  const { mutate: deleteSubscription } = useMutation(
    () => subscriptionApi.delete(accessToken, subscriptionId),
    {
      onSuccess: () => {
        queryClient.invalidateQueries(CACHE_KEY.SUBSCRIPTIONS);
      },
    }
  );

  // ⚠️구독 해제 로직 
  const unsubscribe = () => {
    if (window.confirm(CONFIRM_MESSAGE.UNSUBSCRIBE)) {
      deleteSubscription();
    }
  };

  // ⚠️구독 버튼을 눌렀을 때 구독 여부에 따라 수행해야할 로직 변경
  const handleClickSubscribeButton = () => {
    subscriptionId > 0 ? unsubscribe() : postSubscription(body);
  };

  return (
    <div css={categoryItem(theme)}>
      <span css={item}>{category.createdAt.split('T')[0]}</span>
      <span css={item}>{category.name}</span>
      <div css={item}>
        // ⚠️구독 여부에 따른 버튼 스타일링 변경
        <SubscribeButton
          isSubscribing={subscriptionId > 0}
          handleClickSubscribeButton={handleClickSubscribeButton}
        ></SubscribeButton>
      </div>
    </div>
  );
}

export default CategoryItem;

구독 여부에 따라 버튼 스타일링만 다르게 하였고 실제로 큰 문제없이 의도한대로 렌더링되었습니다.


하지만.. 문제 발생

문제는 API를 연동했을 때 발생했습니다. (부끄럽게도 컴포넌트에 많은 고민과 설계 없이 구현한 대가를 치룬 것이죠.)

BE 공식일정은 이미 구독중인 상태여서 버튼을 누르면 구독해제 api가 실행되어야합니다.


알록달록 팀 회의는 버튼을 누르면 구독 api가 실행되어야합니다.

😢 문제는 바로 두 카테고리의 데이터 스키마가 다르다는 점이였습니다.

  • 구독 api는 구독을 위한 카테고리 id가 필요합니다. (애초에 구독을 안했기 때문에 구독 id가 존재할 수 없습니다.)

  • 구독 해제 api에서는 카테고리 id가 아닌 구독으로 새롭게 발급된 구독 id가 필요합니다.

구독 여부와 관계없이 하나의 컴포넌트로(CategoryItem) 묶었을 때에는 위 2가지 케이스로 인해 구독 id에서 문제가 발생합니다.

하나의 컴포넌트로 묶었기 때문에 구독id가 존재하든 존재하지 않든 subscriptionId라는 필드가 반드시 존재해야합니다.

😱 요리조리 머리를 굴려 구독하지 않았을 때에는 구독 id를 -1로 할당했습니다.
(아마 위의 코드를 읽으시면서 subscriptionId > 0인 경우 구독 중이라고 판단하는 로직을 어색하게 느끼셨을 겁니다.)

물론 실제 DB에서 구독 id에 -1이 할당되는 경우는 없기 때문에 큰 문제는 아닐 수 있습니다.


새로운 컴포넌트의 분리기준: 데이터 스키마, 모델

하지만 태초에 "이런 컴포넌트 설계가 맞을까?" 라는 생각이 들었고 팀 회의를 통해 컴포넌트의 분리 기준을 ✨데이터 스키마와 모델✨에 초점을 맞추는 방향으로 바꾸었습니다.

즉, 구독 중인 카테고리와 구독하지 않은 카테고리는 구독id의 존재 여부가 다르기 때문에 데이터 스키마가 다르다고 말할 수 있겠습니다.

따라서, 구독 중인 카테고리 컴포넌트와 구독하지 않은 카테고리 컴포넌트 2가지 컴포넌트로 분리하였습니다.


실제 코드를 볼까요?

👇 구독하지 않은 컴포넌트 (UnsubscribedCategoryItem)

  • 구독 id가 없습니다.
  • 구독 react query가 있습니다.
// ⚠️subscriptionId를 아예 받지 않음
function UnsubscribedCategoryItem({ category }: UnsubscribedCategoryItemProps) {
  // ... 생략

  // ⚠️구독을 위한 react query
  const { mutate } = useMutation<
    AxiosResponse<Pick<SubscriptionType, 'color'>>,
    AxiosError,
    Pick<SubscriptionType, 'color'>,
    unknown
  >(() => subscriptionApi.post(accessToken, category.id, body), {
    onSuccess: () => {
      queryClient.invalidateQueries(CACHE_KEY.SUBSCRIPTIONS);
    },
  });

  // ⚠️오직 한 가지 일만 담당하는 핸들러 함수
  const handleClickSubscribeButton = () => {
    mutate(body);
  };

  return (
    <div css={categoryItem}>
      <span css={item}>{category.createdAt.split('T')[0]}</span>
      <span css={item}>{category.name}</span>
      <div css={item}>
        <Button cssProp={subscribeButton(theme)} onClick={handleClickSubscribeButton}>
          구독
        </Button>
      </div>
    </div>
  );
}

👇 구독중인 컴포넌트 (SubscribedCategoryItem)

  • 구독 id가 있습니다.
  • 구독 해제 react query가 있습니다.
function SubscribedCategoryItem({ category, subscriptionId }: SubscribedCategoryItemProps) {
  // ... 생략
  
  // ⚠️구독 해제를 위한 react query
  const { mutate } = useMutation(() => subscriptionApi.delete(accessToken, subscriptionId), {
    onSuccess: () => {
      queryClient.invalidateQueries(CACHE_KEY.SUBSCRIPTIONS);
    },
  });

  // ⚠️오직 한 가지 일만하는 핸들러 함수
  const handleClickUnsubscribeButton = () => {
    if (window.confirm(CONFIRM_MESSAGE.UNSUBSCRIBE)) {
      mutate();
    }
  };

  return (
    <div css={categoryItem}>
      <span css={item}>{category.createdAt.split('T')[0]}</span>
      <span css={item}>{category.name}</span>
      <div css={item}>
        <Button cssProp={unsubscribeButton(theme)} onClick={handleClickUnsubscribeButton}>
          구독중
        </Button>
      </div>
    </div>
  );
}

마무리

어떤가요?

💪 subscriptionId가 필요하지 않은 경우에는 애초에 props로 들어가지 않기 때문에 subscriotionId가 -1이 되는 경우가 없습니다.

또한 react query와 관련된 로직들은 어떠한가요?

💪 관심사 분리가 적절히 이루어져 각 컴포넌트에서 알맞게 호출되고 있습니다.

UI를 기준으로 컴포넌트를 나누는 것도 좋지만 먼저 데이터 스키마에 따라 컴포넌트를 분리하는 것을 먼저 고려해보면 더 좋을 것이라고 이번 리팩토링을 통해 느끼게 되었습니다.

profile
함께 웃어야 행복한 개발자 장호영입니다😃

2개의 댓글

comment-user-thumbnail
2023년 2월 5일

게시글 잘 봤습니다. 다만 궁금한게 컴포넌트 2개로 가지 않고 동일 컴포넌트로 가되, props에 type를, isSubscribed 같이 받아와서 한개의 컨포넌트에 조건 분기를 하는게 좋지 않을까요?? 혹시 컴포넌트 따로 가는게 이슈를 줄이거나 다른 효과가 있을까요?? 단순 궁금해서 질문 달아 봅니다 ㅎ

답글 달기
comment-user-thumbnail
2023년 2월 5일

게시글 잘 봤습니다. 다만 궁금한게 컴포넌트 2개로 가지 않고 동일 컴포넌트로 가되, props에 type를, isSubscribed 같이 받아와서 한개의 컨포넌트에 조건 분기를 하는게 좋지 않을까요?? 혹시 컴포넌트 따로 가는게 이슈를 줄이거나 다른 효과가 있을까요?? 단순 궁금해서 질문 달아 봅니다 ㅎ

답글 달기