thismeme-team/thismeme-web

RFC: FDA(Feature-Driven Architecture) 기반 폴더 구조 변경 및 react-query 구조 개선

Opened this issue · 0 comments

정보

  • 담당자 : @ojj1123
  • 개요 : react-query 구조 개선 및 비즈니스 로직 관심사 모으기

현재 상황

각 비즈니스 로직에 대한 관심사가 흩어져 있다

한 API 추가 시 건드려야 하는 파일이 5개정도다. (infra/api, hook/api, 쿼리키 파일, 타입 파일, mock handler 파일) 이는 응집도를 떨어뜨려 커지는 서비스 규모에 따라 개발 생산성 저하가 있을거라 생각한다.
아래 PR에서 그 문제점을 볼 수 있다.

  1. #28
    → 태그 검색 결과 API에 필드를 수정하기 위해 mock handler, infra/api, hook/api, type 파일 4개의 파일을 모두 수정해야 했다.
  2. #12
    → 태그 즐겨찾기 관련 API를 수정하기 위해 mock handler, infra/api, hook/api, querykey 파일 4개의 파일을 수정해야 했다.
    → 태그 즐겨찾기 관련 컴포넌트 (CategoryContent, TagBookmarkButton)도 추가로 수정해야했음

폴더구조가 복잡하다

비즈니스 로직이 여러 파일에 흩어져 있어 API 추가 시 여러 파일을 건드려야 해서 API수가 많아진다면 충분히 실수할 여지가 있다. 또한 API 수정/삭제 시 여러 파일을 바라봐야 해서 유지보수성과 생산성이 떨어진다. 따라서 아래와 같은 문제가 있다.

  1. 폴더구조가 복잡하고 depth가 많이 들어간다.

    • 우리가 소통할 때 applicationinfra 폴더를 많이 언급했는가? 개인적인 경험상 그러지 않았다
    • API 하나 추가/수정/삭제하더라도 여러 파일을 바라봐야한다 -> 유지보수성과 생산성 저하
  2. 같은 페이지 내 로직들이 흩어져 있다.

  • 프로젝트 규모가 커질 수록 어떤 기능과 연관된 코드를 탐색하는 과정이 느려지고 어려워진다. 정확한 의존관계에 놓여있는 코드를 탐색하기 위해 복잡한 구조의 폴더와 코드를 순차적으로 탐색해나가야 하기 때문이다.

리팩터링1. 폴더구조 개선

FDA(Feature-Driven Architecture)로 변경하고자 한다. FDA를 통해 코드의 역할과 책임을 나누고, 같은 관심사의 코드를 응집시켜 필요한 로직과 그 의존관계를 빠르게 탐색할 수 있도록 개선하고자 한다.

src
- pages -> 페이지 컴포넌트

- features -> ⭐️ 페이지 기준으로 feature를 나눔
  - page1
    - components
    - hooks
  - page2

- api -> API 요청/응답 관련 로직, react-query 커스텀 hook을 모아둔 폴더
  - domain1
    - useGetMemes.tsx
    - usePostMeme.tsx
  - domain2

- common -> 공통 hook, component, util
  - hooks
  - components
  - utils
  - types

리팩터링2. react-query 구조 개선

우성님 조언

  • 같은 관심사의 API가 너무 멀리 흩어져 있다. → 유지보수와 테스트하기 힘든 환경
  • 오히려 다른 관심사 API가 같은 파일에 존재해 있다.

react query maintainer도 쿼리키와 커스텀 훅 그리고 react query와 관련된 모든 것들을 같은 파일에 두는 것을(colocation) 권장하고 있다.
https://tkdodo.eu/blog/effective-react-query-keys#effective-react-query-keys
effective-react-query-keys

같은 쿼리키를 가지는 관심사(같은 API)는 최대한 같은 파일에 위치시킨다.

AS-IS
  • queryFn
// src/infra/api/tags/index.ts

export class TagApi {
  constructor(private api: AxiosInstance) {}

 ...

  getFavoriteTags = () => {
    return this.api.get<GetFavoriteTagsResponse>("/tags/favs").then((response) => response.data);
  };
}
  • Response Type
// src/infra/api/tags/types.ts
...
export type GetFavoriteTagsResponse = Pick<Category, "tags">;
...
  • React query custom hook
// src/application/hooks/api/tags/index.ts
...
export const useGetMemeTagsById = (id: string) => {
  const { data, ...rest } = useSuspendedQuery({
    queryKey: QUERY_KEYS.getMemeTagsById(id),
    queryFn: () => api.tags.getMemeTagsById(id),
    staleTime: Infinity,
  });
  return { ...data, ...rest };
};
...
  • queryKey
// src/application/hooks/api/tags/queryKey.ts

export const QUERY_KEYS = {
 ...
  getFavoriteTags: ["getFavoriteTags"],
  getMemeTagsById: (id: string) => ["getMemeTagsById", id],
 ...
} as const;
  • Mock handler
// mocks/handlers/tags/index.ts
...
export const getFavoriteTags = rest.get(
  `${process.env.NEXT_PUBLIC_API_URL}/tags/favs`,
  async (req, res, ctx) => {
    return res(
      ctx.delay(300),
      ctx.status(200),
      ctx.json({
        tags: MOCK_DATA.favoriteTags,
      }),
    );
  },
);
...
TO-BE
  • type, react query, queryFn, queryKey를 하나의 파일에서 관리할 수 있다.
// 응답 타입도 쿼리와 같은 파일에 묶어둔다.
type Response {
 property1: Type1;
 property2: Type2;
 ...
}

const useGetTags = () => {
 return useQuery({
  queryKey: useGetTags.queryKey(...);
  queryFn: useGetTags.queryFn
  ...
 })
}

// 함수도 객체이므로 같은 쿼리에 대한 관심사를
// 다음과 같은 컨벤션으로 묶어둘 수 있다.
useGetTags.queryKey = (params) => [params, ..., ...];

useGetTags.queryFn = () => {
//  return axios.get<Response>("/endpoint").then((response) => response.data);
//  return request<Response>({url: '/endpoint', method: 'GET' });
}

// 해당 쿼리키에 대한 추가적인 작업이 생길 경우
// 같은 리액트 쿼리 파일 내에 메서드를 추가해
// 같은 관심사끼리 묶어두기 용이하다
useGetTags.removeQueries = () => {
 queryClient.removeQueries(...)
}
  • (추가) axios 관련 모듈화
import { AxiosRequestConfig, AxiosResponse } from 'axios';

import axios from '@/shared/configs/axios';

export const request = <T,>(config: AxiosRequestConfig) =>
  axios(config).then((response: AxiosResponse<T>) => response.data);

우리 서비스에 좋아지는 것

  • 기능이 추가되고 서비스가 커짐에 따라 대처하기 용이해질 것이라 생각해요.
  • 비즈니스 로직 관심사가 한 곳에 모여서 테스트나 성능 파악, 수정 등이 이전보다 수월해질 것이라고 생각해요.

팀원들에게 도움되는 것

  • 서비스를 지속가능하게 유지하기 위해 고민해보고 서비스 유지보수 경험을 가져갈 수 있다고 생각해요
  • 바뀐 아키텍처, 컨벤션이 추가되는 기능(API, 기획 등)에 대응하는 것이 이전 컨벤션보다 편할 것이라 생각해요.

체크리스트

#54

  • 공통 컴포넌트 이동(components/common -> common/components)
  • 공통 Hook 이동(application/hooks/common -> common/hooks)
  • 공통 유틸 이동(application/util -> common/utils)
  • 서드파티 라이브러리 이동(infra/sdk -> common/libs)

#58

  • 페이지별 컴포넌트 이동(components/{domain,page..} -> feature/{page}/components)
  • 페이지별 hook 이동(application/hooks/domain -> feature/{page}/hooks)

#61

  • React query, 비동기 API 리팩터링