비즈니스 로직 분리 방황기

이동현·2022년 10월 27일
0

React

목록 보기
16/16

서론

SPA 프론트엔드 프로젝트 개발을 하다보면 라우팅 레이어를 만들 수 밖에 없다.
라우터 레이어에서 해주는 역할은 각기 다른 path 에 각기 다른 페이지 컴포넌트를 렌더해주는 역할이다.
그렇다면 각각의 페이지에서 가장 큰 컴포넌트는 페이지 컴포넌트일 것이다.

페이지 컴포넌트 하위로 재사용 목적, 혹은 유지보수를 수월하게 하기 위한 목적으로 세부 컴포넌트로 나눈다.
이를 컨테이너 컴포넌트라고 해보자. 중간 사이즈인 컨테이너 컴포넌트 내부에도 세부 컴포넌트들을 포함할 수 있다.
예를 들어, 버튼이나 텍스트 등을 아주 작은 단위의 컴포넌트로 구성한다면 재사용성이 높은 컴포넌트로 개발하여 생산성을 향상할 수 있을 것이다.

이 상황에서 여러 뷰와 상호작용하며 상태를 관리할 것이고 해당 상태의 상당 부분은 서버 상태일 것이다.
프로젝트에서 react-query 라이브러리를 사용하여 서버 상태를 관리해주고 있기 때문에 useQuery 등의 훅을 사용해서 서버 데이터를 가져와서 뷰에 보여준다.
사용자 인터렉션에 의해 서버 데이터의 수정이 필요할 때는 useMutation 훅을 사용해서 서버 상태를 변경해줄 것이다.

문제

이런 로직들을 비즈니스 로직이라고 하면 이런 로직들을 편하게 관리할 수 있는 방법에 대해 고민하기 시작했다.
프로젝트 초기 페이지 컴포넌트 하위에 컨테이너 컴포넌트를 구성했다. /container 디렉토리 하위에 각각의 컨테이너 컴포넌트를 위치시켰다.

컨테이너 컴포넌트에서는 위와 같은 비즈니스 로직을 포함하고 페이지 컴포넌트에서도 필요하다면 비즈니스 로직을 포함했다.

하지만, 이렇게 구조를 짜고 프로젝트를 진행하면서 문제가 생겼다. 디버그 및 유지보수를 할 때 어느 부분에서 문제가 발생했는지 추적하는데 비용이 크다는 것이다.
문제가 발생한 페이지에서 컨테이너 컴포넌트를 찾고 컨테이너 컴포넌트 하위의 하위의 하위의 컴포넌트 안에 있는 비즈니스 로직을 찾아야 한다면 너무 힘든 일이 될 것이라고 생각했다.

해결

이러한 문제를 해결하기 위해서 모든 비즈니스 로직을 페이지 컴포넌트에 통합하고 하위 컴포넌트는 뷰만 책임지도록 했다.
리액트에서 로직 분리를 쉽게 할 수 있는 커스텀 훅을 사용해서 페이지 안에 코드가 비대해지는 것도 방지할 수 있다.

이렇게 했을 경우 디버그를 하는 과정 또는 리팩토링을 하는 과정에서 하위 컨테이너 어디서 문제가 발생했는지 일일이 찾아볼 필요없이 페이지단에서 문제를 빠르게 찾을 수 있다.

page-컴포넌트.tsx

function ReviewOverViewPage() {
  const navigate = useNavigate();
  const snackbar = useSnackbar();

  const { reviewFormCode = '', displayMode: displayModeParams = '' } = useParams();
...
  const pageQueries = useReviewOverviewPage(reviewFormCode, displayMode);

  if (!pageQueries) return <>{/* Error Boundary, Suspense Used */}</>;

  const {
    infiniteScrollContainerRef,
    reviewsLikeStack,
    reviewMutations,
    reviews,
    reviewForm,
    reviewsOptimisticUpdater,
    isReviewsFetching,
    isFormLoading,
    addFetch,
  } = pageQueries;

/* 사용자 인터렉션에 따른 각종 handler들이 전부 여기에 위치하고 아래 뷰 컴포넌트에 prop으로 내려줌 이 */

  return (
    ...
    <Questions.EditButtons
      isVisible={info.isSelf}
      onClickEdit={handleEditAnswer(id)}
      onClickDelete={handleDeleteAnswer(id)}
    ></Questions.EditButtons>
    ...
  );
}

위와 같이 모든 비즈니스 로직은 페이지 컴포넌트에 커스텀 훅을 활용하여 통합돼있으며 하위 뷰 컴포넌트에 prop으로 핸들러를 넘겨준다.
해당 페이지단에서 핸들러를 하위 컴포넌트에 prop으로 전달해줌으로써 어떤 하위 뷰 컴포넌트에서 해당 로직이 사용되는지를 페이지만 봐도 쉽게 찾을 수 있으니 이런 전략으로 프로젝트를 구성했다.

다시 문제

네트워크 워터폴로 인한 로딩 속도 지연

이런 전략에서 문제점을 발견했다. 네트워크 워터폴이 발생한다는 것이다.

우리 프로젝트에서는 Suspense 를 페이지단에 적용하여 페이지 전체에 Loading UI를 보여주고 있었다.
스크린샷 2022-10-27 오후 3 18 16

위 UI 가 Suspense를 활용하여 페이지 이동간 Loading UI 를 보여주는 모습이다.

위와 같이 하나의 Suspense를 사용하고 있는데 그 안에서 다수의 useQuery를 통해 데이터를 불러오면 네트워크 요청이 동시에 처리되지 않고 차례대로 처리되는 문제점이 있다.
자세한 내용은 다음 아티클을 참고하면 좋다.

스크린샷 2022-10-27 오후 3 21 21

하나의 페이지에서 모든 비즈니스 로직을 갖게 됨으로써 발생하는 문제이다. 다수의 useQuery를 실행하면서 위 사진과 같이 network waterfall 이 발생하여 로딩 시간이 길어지는 문제점이 있다.

또 다른 문제는?

이 문제로 인해서 여러 가지 본질적인 문제점이 있다는 사실을 인지했다.

지금 프로젝트 같이 한 페이지에서 보여주는 정보가 많지 않다면 네트워크 워터폴 문제나 유지보수 등에서 큰 문제가 없겠지만 대량의 정보를 포함하는 UI 라면 이런 구조는 적절치 않다.

  1. 리렌더링 이슈
    페이지에서 모든 서버 상태를 관리하고 해당 상태를 통해 하위 뷰를 보여주고 있는 상태라면 독립적으로 리렌더링 될 수도 있는 뷰 컴포넌트가 있어도 페이지 전체를 리렌더링 하게 된다.

  2. 페이지 컴포넌트 비대화
    커스텀훅을 사용해 로직을 분리하는 것도 한계가 있을 것 같다. 모든 비즈니스 로직의 부하를 페이지 컴포넌트에 일임하는 것은 컴포넌트 비대화의 원인이 될 것이다.

해결

관리하는 서버 상태가 컴포넌트별로 독립적이라면 하위 컴포넌트에 비즈니스 로직을 위임해서 처리하는 방법이 더 낫다라는 결론을 냈다.
유지보수를 위한 관점에서 시도해본 전략이었지만 여러 방면에서 문제가 발생한 것을 경험했다. 하지만 하위 컴포넌트에 핸들러를 넘겨서 하위 컴포넌트는 뷰를 보여주는 역할에 충실하게
하는 패턴도 나름 유지보수에 상당한 이점을 준다. 하지만 이 전략의 문제는 단편의 문제를 해결하기 위해서 극단적인 전략을 취했다는 것이다.

이 계기로 여러 방면으로 생각하게 되고 다양한 방법을 균형있게 사용해야 겠다는 생각을 한다.

참고

profile
Dom Hardy : 멋쟁이 개발자 되기 인생 프로젝트 진행중

0개의 댓글