RFC: FDA(Feature-Driven Architecture) 기반 폴더 구조 변경 및 react-query 구조 개선
Opened this issue · 0 comments
ojj1123 commented
정보
- 담당자 : @ojj1123
- 개요 : react-query 구조 개선 및 비즈니스 로직 관심사 모으기
현재 상황
각 비즈니스 로직에 대한 관심사가 흩어져 있다
한 API 추가 시 건드려야 하는 파일이 5개정도다. (infra/api
, hook/api
, 쿼리키 파일
, 타입 파일
, mock handler 파일
) 이는 응집도를 떨어뜨려 커지는 서비스 규모에 따라 개발 생산성 저하가 있을거라 생각한다.
아래 PR에서 그 문제점을 볼 수 있다.
- #28
→ 태그 검색 결과 API에 필드를 수정하기 위해mock handler
,infra/api
,hook/api
,type
파일 4개의 파일을 모두 수정해야 했다. - #12
→ 태그 즐겨찾기 관련 API를 수정하기 위해mock handler
,infra/api
,hook/api
,querykey
파일 4개의 파일을 수정해야 했다.
→ 태그 즐겨찾기 관련 컴포넌트 (CategoryContent
,TagBookmarkButton
)도 추가로 수정해야했음
폴더구조가 복잡하다
비즈니스 로직이 여러 파일에 흩어져 있어 API 추가 시 여러 파일을 건드려야 해서 API수가 많아진다면 충분히 실수할 여지가 있다. 또한 API 수정/삭제 시 여러 파일을 바라봐야 해서 유지보수성과 생산성이 떨어진다. 따라서 아래와 같은 문제가 있다.
-
폴더구조가 복잡하고 depth가 많이 들어간다.
- 우리가 소통할 때
application
과infra
폴더를 많이 언급했는가? 개인적인 경험상 그러지 않았다 - API 하나 추가/수정/삭제하더라도 여러 파일을 바라봐야한다 -> 유지보수성과 생산성 저하
- 우리가 소통할 때
-
같은 페이지 내 로직들이 흩어져 있다.
- 프로젝트 규모가 커질 수록 어떤 기능과 연관된 코드를 탐색하는 과정이 느려지고 어려워진다. 정확한 의존관계에 놓여있는 코드를 탐색하기 위해 복잡한 구조의 폴더와 코드를 순차적으로 탐색해나가야 하기 때문이다.
리팩터링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
같은 쿼리키를 가지는 관심사(같은 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, 기획 등)에 대응하는 것이 이전 컨벤션보다 편할 것이라 생각해요.
체크리스트
- 공통 컴포넌트 이동(components/common -> common/components)
- 공통 Hook 이동(application/hooks/common -> common/hooks)
- 공통 유틸 이동(application/util -> common/utils)
- 서드파티 라이브러리 이동(infra/sdk -> common/libs)
- 페이지별 컴포넌트 이동(components/{domain,page..} -> feature/{page}/components)
- 페이지별 hook 이동(application/hooks/domain -> feature/{page}/hooks)
- React query, 비동기 API 리팩터링