본문 바로가기
  • 프론트엔드 개발자 세오세오 | Frontend dev Seo
Learn to Code

[디자인패턴] React + Tanstack Query 서비스 레이어 패턴으로 구조짜기

by CEOSEO 2024. 6. 3.
728x90
반응형

현 회사 프로젝트는 React + Next.js (v.13, app routing)으로 구성해놨다. Tanstack Query도 주요하게 사용하는데, 비즈니스 로직과 UI 컴포넌트, 그리고 그 중간 어딘가에 관여하는 코드들을 어떻게 정리하면 좋을까 열심히 찾아보다가 서비스 레이어 패턴(Service Layer Pattern)에 근접하게 정착하게 되었다.

 

서비스 레이어 패턴이란?

서비스 레이어 패턴(Service Layer Pattern)이란 앱의 데이터 처리와 관련된 로직을 여러 레이어로 분리하여 구조화한 패턴이다. 이를 통해 코드의 재사용성과 유지보수성을 높이고, 각 레이어가 특정한 책임을 갖도록 만든다.

 

리액트에 적용하기: 최대 4개의 레이어

1. Layer 1: 순수 네트워크 호출

  • 이 레이어는 네트워크 호출을 실행하고, 자신을 호출한 친구에게 데이터를 리턴하는 것에만 관여한다.
  • HTTP 응답에 대한 추가적인 로직을 수행하는 경우도 있을 수 있다. 예를 들어, 복잡한 객체를 프론트엔드에서 좀 더 사용하기 좋은 버전으로 바꾼다거나 아니면 에러를 일반화한다거나와 같은 경우 이곳에 관련 로직이 들어갈 수 있다.
export const fetchById= (id: string) => {
  try {
    const data = await apiClient.getById(id);
    return mapBadAPIContractToGoodClientContract(data)
  } catch (e: unknown) {
    throw ServiceErrorFactory.create(e)
  }
};

 

 

2. Layer 2: API 상태 (Custom Hook)

  • 이 레이어는 첫번째 레이어의 네트워크 호출을 실행하고, 이를 react-query에 캐싱하는 일에만 관여한다.
  • 여기선 옵션들을 넣어주고, stale time을 바꾸고, react-query가 허용하는 일들을 뭐든 할 수 있지만, onSuccess 또는 onError에 모든걸 기대하면 안된다.
    • 모든 호출에 대한 데이터 수정이 필요하다면 레이어 1
    • 특정 호출에 대한 데이터 수정이 필요하다면 레이어 3
    • 의미있는 응답을 위해 체인 호출이 항상 필요하다면 레이어 1에서 → 아니면 react-query가 각각 응답을 개별적으로 캐싱하게 한다음, dependent-queries 패턴을 따른다
    • API 결과에 대한 응답을 노티해야한다면, 레이어 3 또는 레이어 4 진행
export const useFetchById= (id: string) => {
 const cacheKey = [CacheKey.FetchById, id]

  return useQuery<FetchByIdResponse, ServiceError>(
    cacheKey,
    () => fetchById(id), 
    {
      enabled: Boolean(id)
    },
  )
}

 

 

3. Layer 3: 페이지 훅 (Custom Hook, optional)

  •  필수가 아닌 페이지 훅으로, 두번째 레이어를 감싸는 레이어다. UI를 만드는 로직과 비즈니스 로직을 분리하기 위해 필요에 따라 사용한다.
export const usePageWithId= (id: string) => {
  const { 
    data, 
    error, 
    isLoading, 
    isSuccess, 
    isError 
} = useFetchById(id)

  /**
   * [선택사항] 네트워크 에러를 리액트 컴포넌트와 연결
   */
  let errorFallbackComponent: ReactNode | undefined = undefined
  
  if (isError) {
    errorFallbackComponent = mapErrorToFallbackComponent(error?.code)
  }

  /**
   * [선택사항] 컴포넌트 페이지 prop과 데이터 응답을 연결
   */
  let props: PageWithIdProps| undefined = undefined
  
  if (data) {
    props = mapMyDataToMyComponentProps(data)
  }

  /**
   * [선택사항] API 호출이 성공했을 때, 무언가를 실행하고 싶으면 함
   */
  useEffect(() => {
    if (isSucces) {
      showToastNotification('Success!') 
    }
  }, [isSuccess])

  /**
   * [선택사항] API 호출이 실패했을 때, 그에 맞는 액션을 하셈
   */
  useEffect(() => {
    if (isError) {
      showToastNotification('Error!') 
    }
  }, [isError])

  return { props, isLoading, isError, errorFallbackComponent }
}

 

 

4. Layer 4: UI 페이지

  • 레이어 3의 페이지 훅을 사용하거나, 아니면 레이어 3이 없을 경우 레이어 2의 useQuery 훅을 호출하는 컴포넌트
export const PageWithId= ({id}: {id: string}) => {
  const {
    props,
    isError,
    isLoading,  
    errorFallbackComponent,
  } = usePageWithId(id);

// 레이어 3(=usePageWithId)을 만들지 않았을 경우엔 레이어 2(=useFetchById)를 사용
//  const { 
//    data, 
//    error, 
//    isLoading, 
//    isSuccess, 
//    isError 
//  } = useFetchById(id)

  if (isLoading) {
    return <Loading />;
  }

  if (isError) {
    return errorFallbackComponent;
  }

  return <PageTemplate {...props} />;
};

 

 

 

플로우 요약

fetchById() ← [커스텀훅] useFetchById() ← [커스텀훅] usePageWithId() ← [컴포넌트] PageWithId()

 

 

 

 

 

 

 

728x90
반응형

댓글