[Refactoring] Chapter3. 관심사분리 & useQuery hook을 사용해보자 (feat. SOLID 원칙)

rlorxl·2024년 8월 26일
0

회고

목록 보기
5/7
post-thumbnail

관심사 분리

리팩토링에서 중요도가 높았던것은 관심사의 분리이다.

관심사 분리는 관심사를 쪼개 한 번에 한 가지의 관심사만 갖도록 코드를 분리하는 것이다. 관심사 분리를 잘 하면 가독성이 좋아지고, 재사용하기가 쉽고, 유지보수에 용이하고, 결과적으로 테스트코드를 작성하기 좋은 코드가 될 수 있다.

이것을 염두에 두면서 컴포넌트를 쪼개고 함수를 쪼개고 각 컴포넌트에서 최대한 한가지의 관심사만 가질 수 있도록 코드를 수정해갔다.

대표적으로 컴포넌트에서 특정 도메인 전용 로직을 커스텀 훅으로 분리하는 리팩토링을 많이 했는데 그 중에 인상깊었던 과정을 써보려고 한다.

커스텀 훅으로 도메인 로직분리

프로젝트에는 두 개의 페이지에서 각각 포스팅을 할 수 있는 기능이 있었고 버튼 클릭시submit을 하는데 각 컴포넌트의 로직이 거의 동일했기 때문에 컴포넌트에서 관련 로직을 묶어 하나의 커스텀훅으로 분리하는 리팩토링을 했다.

수정된 코드

// useCreatePost.tsx
const useCreatePost = ({ type, userData }: useCreatePostProps) => {
  // ... 다른 코드 생략
  
  const createRequest = async (requestData: MutatePayload) => {
    const { data, imageUrlList, tagIdList, options } = requestData;
    const payload =
      type === "market"
        ? { data, imageUrlList, options, userId: userData?.id ?? 1 }
        : { data, imageUrlList, tagIdList, userId: userData?.id ?? 1 };
    const response = type === "market" ? await apiPost.CREATE_ITEM(payload) : await apiPost.CREATE_POST(payload);
    return response;
  };

  const { mutate, isLoading } = useMutation(createRequest, {
    onSuccess: ({ message }) => {
      const toastMessage = message ?? "등록이 완료되었습니다.";
      setToast(toastMessage, false);
    },
    onError: ({ response }) => {
      setToast(response.data.message, true);
    },
  });

  const submit = (submitData: MutatePayload) => {
    const imageUrlList: string[] = [];
    imgsrc.forEach(item => {
      const imageurl = createImageUrl(item.file, type);
      imageUrlList.push(imageurl);
    });

    mutate({ ...submitData, imageUrlList });
  };

  return {
    submit,
    status: isLoading,
    ...
  };
};

// market.tsx
const Market = () => {

const { submit, isLoading } = useCreatePost({ type: "market", userData: userData.contents });

// ... 다른 코드 생략
}

useCreatePost에서는 submit, mutate에 사용되는 로직을 공통으로 사용할 수 있고 type과 유저정보를 실제 사용하는 컴포넌트에서 넘겨받는다.

처음엔 공통된 로직을 커스텀 훅으로 분리하고 컴포넌트에서 재사용이 가능하게 리팩토링했다고 생각해 이렇게 두었다가 나중에 useCreatePost에서 type에 의존하고 있는 부분이 잘못된것을 알게되었다. 이유는 여러가지가 있지만 SOLID원칙에서 OCP원칙과 연관된 부분이 많기에 이 부분을 설명해보겠다.

OCP(Open/Closed Principle)원칙 - 개방/폐쇄 원칙

💡 OCP(Open/Closed Principle, 개방/폐쇄 원칙)은 객체 지향 설계의 중요한 원칙 중 하나로, 소프트웨어 모듈이나 클래스가 확장에는 열려(Open) 있고, 수정에는 닫혀(Closed) 있어야 한다는 것을 의미한다.

OCP 원칙의 핵심

  • 확장에는 열려 있음 (Open for extension):
    • 소프트웨어 모듈은 새로운 기능이나 요구사항이 추가될 때 그 모듈을 확장할 수 있어야 한다.
    • 이는 기존의 코드에 영향을 주지 않으면서 새로운 기능을 추가하거나 변경할 수 있어야 한다는 의미.
  • 수정에는 닫혀 있음 (Closed for modification):
    • 소프트웨어 모듈은 기존의 기능을 변경하거나 수정할 필요가 없도록 설계되어야 한다.
    • 즉, 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있어야 한다.

하나의 함수의 기능이 여러가지 옵션들로 인해 내부에서 분기가 많이 발생하고 있다면 OCP와 SRP(단일책임)의 원칙에 위배된다.

useCreatePost에서 type을 넘겨받고 createRequest는 type(’market’)이 무엇인지에 따라 payload를 다르게 구성하고 분기 처리를 통해 API콜을 하고 있다. 이는 OCP 원칙을 위반한다고 볼 수 있다.

이런 구조는 새로운 type이 추가되거나 기존 type에 대한 로직이 변경될 때마다 기존 코드를 수정해야 한다는 문제를 일으킬 수 있고 의존성을 강화하여 유지보수를 어렵게 만든다.

😥 그럼 여기서 어떻게 다시 리팩토링 해야할까?


OCP 원칙을 준수하려면 type에 따른 분기 로직을 제거하고, 각각의 type에 대응되는 함수들을 생성해 커스텀 훅에 type을 넘기는 대신 명시적 함수를 호출하도록 수정해야한다.

useCreatePost라는 커스텀 훅 내부에서 행해지던 API콜을 각 컴포넌트에서 바로 API콜을 할 수 있도록 밖으로 꺼낸다.

그러면 더 이상 useCreatePost 커스텀 훅은 존재의미가 없어져 제거할 수 있다.

API콜이 아닌 생략된 다른 코드들은 setToastcreateImageUrl 을 위해 또 다른 커스텀 훅을 내부에서 호출하는 코드였는데 이 코드들도 useCreatePost 내부에서 종속되지 않고 각 컴포넌트마다 따로 호출할 수 있도록 해주었다.

useQuery훅 한단계 추상화하기

다음 단계는 useQuery훅을 추상화하는 단계이다. 나는 보통 useQuery훅을 컴포넌트에서 직접적으로 호출해 사용하곤 했다.

왜냐면 리액트 훅을 사용하는것과 같이 useQuery훅이 이미 그것만으로도 충분하다고 생각했기 때문인데 위의 코드 수정 후 컴포넌트에서 useMutation훅으로 api호출하는 코드를 추상화해 리팩토링하면 더 나은 리팩토링 결과를 얻을 수 있다.

페이지 컴포넌트에서 useMutation을 직접 사용하는 대신 각각의 래퍼함수를 따로 만들어 로직을 감싸고 컴포넌트에서 호출하게 코드를 수정했다.

useCreateMarket / useCreateStyleFeed

// create.ts
export const useCreateMarket = () => {
  return useMutation({
    mutationKey: CREATE_KEY.MARKET,
    mutationFn: async (payload: MarketPayload) => await apiPost.CREATE_ITEM(payload),
  });
};

export const useCreateStyleFeed = () => {
  return useMutation({
    mutationKey: CREATE_KEY.STYLE_FEED,
    mutationFn: async (payload: StyleFeedPayload) => await apiPost.CREATE_POST(payload),
  });
};

// style-feed.tsx
  const { mutate: mutateStyleFeed, isLoading, isSuccess, isError } = useCreateStyleFeed();

쿼리 훅을 커스텀 훅(래퍼 함수)으로 만들면 컴포넌트는 직접 useMutation에 의존하지 않고 useCreateMarket과 같은 함수에 의존하게 되는데 이렇게 함으로써 컴포넌트는 useMutation의 세부 구현에 대해서는 알 필요 없이 추상화된 함수에만 의존하게 된다.

useQuery/useMutation를 추상화 하는것의 장점

  1. 컴포넌트 내에서 api호출관련 로직이 단순해진다.
  2. 해당 로직(useCreateMarket)을 여러 컴포넌트에서 재사용할 수 있다.
  3. api호출하는 함수에 이름이 붙어 더 직관적이고 가독성이 좋아진다.
  4. query key값 등을 관리, 수정하기 쉬워져 유지보수에 용이하다.
  5. 함수를 확장하거나 다른 기능과 결합하기가 좋다.
  6. 테스트 코드에서 일관된 방식으로 훅을 호출할 수 있다.

정리

이번에는 단순히 useMutation을 래퍼 함수로만 감싸는 방식으로 리팩토링했는데 관련 쿼리 함수들을 객체로 묶거나 한 단계 더 캡슐화 하는 방법으로 리팩토링하는등의 더 나은 방법들이 있을 것이다. 하지만 이렇게 한단계만 추상화했을 때도 확실히 전보다 선언적이고 유지보수하기 편해진것을 느꼈다.

코드를 수정하고나니 기존에 관심사의 분리 차원에서 만들었던 커스텀 훅이 비즈니스 로직을 분리하는 리팩토링은 됐지만 커스텀 훅에서 다시 분기처리를 하면서 오히려 책임도 모호해지고 로직을 묶어 보이지 않게 숨기는 것 이상의 역할이 없었던 것 같다는 생각이 든다.

은탄환은 없다는 말처럼 무조건적으로 적용되는 이론과 원칙도 없을 것이다. 그때그때 상황에 맞는 최적의 방법을 고민하고, 실용적인 선택을 통해 코드를 개선하는 것이 최선이고 앞으로도 다양한 관점에서 코드를 고민해야될 것 같다.

0개의 댓글