[9팀 박상수] Chapter 2-3. 관심사 분리와 폴더구조#50
Conversation
There was a problem hiding this comment.
안녕하세요! AI 코드 리뷰어입니다. 🤖
게시물 관리자 리팩터링 PR(게시물 FSD 분리 + Jotai 도입)에 대한 리뷰를 완료했어요. 함께 코드를 더 발전시켜 볼까요?
Note
✨ 이번 PR 한 줄 요약
FSD 폴더 구조와 Jotai 전역 상태 도입, UI 컴포넌트 분리 시도가 잘 보이지만 — 잘못된 import/경로, API 함수/응답 처리 불일치와 타입/네이밍 문제로 런타임 에러·버그가 발생할 가능성이 큽니다. 또한 심화 과제(루틴: TanStack Query 도입)는 아직 반영되지 않았습니다.
🎯 핵심 리뷰 요약
가장 중요한 개선 포인트부터 빠르게 확인해 보세요.
- P1: 잘못된 import 경로와 중복/충돌하는 훅/함수명(usePost 등) → 빌드/런타임 에러 유발
- P1: API 함수(공통 api)와 훅에서의 응답 처리 불일치(Response vs parsed JSON) → 데이터 사용 불가
- P1: features <-> entities 간 파일 위치/임포트 불일치 (예: CommentListDialog 경로) → 모듈 경로 오류
- P2: atoms/상태의 타입이 없음(타입 안정성 부족) 및 Jotai 전역 atom 설계 검토 필요(응집도/결합도)
- P2: TanStack Query 미도입 — 심화 요구사항 미충족, 서버 상태 관리 전략 부재
- P3: FSD 적용 시 일부 모듈(태그/포스트 훅 등) 명명/중복으로 아키텍처 일관성 저하
🔍 상세 리뷰
1. Import/경로 및 네이밍 충돌 문제 (P1)
- 👍 좋은 점:
- components → widgets/ui로 이동 등 FSD 구조 반영 시도는 적절합니다.
- 💡 개선 제안:
- PostsManager에서 import한 컴포넌트 경로와 실제 파일 위치가 불일치합니다.
- 예: PostsManager.tsx가
import { CommentListDialog } from "../entities/comment/ui/CommentListDialog"를 사용하지만 실제 파일은 src/features/post/comment/ui/CommentListDialog.tsx 입니다.
- 예: PostsManager.tsx가
- 훅/유틸 함수의 이름 충돌을 제거하세요. (예: features/post/tag/hooks/useTag.ts 에서 export한 훅 이름이 usePost로 되어 있음 -> 의미에 맞는 고유명 사용: useTagFeature 등)
- PostsManager에서 import한 컴포넌트 경로와 실제 파일 위치가 불일치합니다.
- 🤔 이렇게 제안하는 이유:
- 잘못된 경로는 빌드타임/런타임 에러를 바로 발생시키며, 같은 이름의 훅이 여러곳에 존재하면 유지보수와 자동 import에 혼란을 줍니다.
- 💻 코드 예시 (AS-IS vs TO-BE)
AS-ISTO-BE// src/features/post/tag/hooks/useTag.ts (현재) export const usePost = () => { ... } // 이름 충돌export const useTagFeature = () => { ... } // 그리고 PostsManager에서는 import { CommentListDialog } from "../features/post/comment/ui/CommentListDialog"
2. API 함수와 훅의 응답 처리 불일치 — Response vs JSON (P1)
- 👍 좋은 점:
- API 함수들을 각 도메인별 파일로 분리하려 한 시도는 좋습니다.
- 💡 개선 제안:
- API 함수는 가능한 한 일관된 계약을 가져야 합니다. (리턴: 항상 parsed JSON 또는 에러 throw)
- 현재 일부 api는 Response 객체를 반환(getPostApi 등)하고, 훅에서는 .json()을 기대하거나 반대로 .json()을 이미 하는 경우가 섞여 있습니다.
- addPostApi/updatePostApi 등은 parsed JSON을 반환하도록 수정하세요.
- 🤔 이렇게 제안하는 이유:
- 호출부가 .json()을 기대하느냐 아니냐에 따라 코드가 깨지고, 타입 안정성도 떨어집니다.
- 💻 코드 예시 (AS-IS vs TO-BE)
AS-IS (post-api.ts)TO-BEexport const getPostApi = async (limit, skip) => { const response = await fetch(`/api/posts?limit=${limit}&skip=${skip}`) if (!response.ok) throw new Error(...) return response }export const getPostApi = async (limit, skip) => { const res = await fetch(`/api/posts?limit=${limit}&skip=${skip}`) if (!res.ok) throw new Error(...) return res.json() // 항상 parsed JSON 반환 } // 또는 // return { posts: ..., total: ... } 형태를 보장
3. usePost 훅(entities)와 features 훅의 충돌 및 잘못된 의존 (P1/P2)
- 문제 포인트:
- src/entities/post/hooks/usePost.ts에서
import { getTagApi, getPostsByTagApi, ... } from "../api/post-api"를 하고 있지만, 해당 함수들은 post-api에 정의되어 있지 않습니다(태그 API는 features/.../tag-api.ts에 정의됨). 또한 searchPostApi 시그니처 불일치 등. - addPost/updatePost 사용 시 API의 반환값을 parsed JSON으로 기대하는데 실제 post-api는 Response를 반환하는 상태(또는 반대) — 앞서 언급한 응답 불일치와 결합되어 에러 발생.
- src/entities/post/hooks/usePost.ts에서
- 권장 개선:
- 훅은 도메인 별로 명확히 하나의 책임만 갖도록 하세요(서버 통신과 로컬 상태 업데이트의 책임 분리).
- API 호출은 domain/api/* 에서만 하고, 훅에서는 해당 함수의 반환 계약에 의존.
- 함수명을 명확히 해 충돌 방지(usePostEntity, usePostFeature 등).
- 이유:
- 응집도(Cohesion)를 높이고 결합도(Coupling)를 낮추면 변경시 파급 범위가 줄어듭니다.
4. 타입 안정성 및 Jotai atom 정의 개선 (P2)
- 문제 포인트:
- atoms가 대부분 any 타입으로 생성되어 타입 안정성이 없습니다. (예: postsAtom = atom([]))
- selectedUserAtom = atom<SelectedUser>(null) 은 좋지만 다른 atom들은 제너릭 타입이 없음.
- 개선 제안:
- 각 atom에 제네릭 타입을 명확히 선언하세요.
- postsAtom = atom<Post[]>([])
- totalPostsAtom = atom<number>(0)
- 가능한 경우 atomWithStorage, atomFamily 등을 사용해 명확히 분리.
- 각 atom에 제네릭 타입을 명확히 선언하세요.
- 근거(기술적 배경):
- 타입이 명확하면 자동완성·리팩토링·버그 예방에 유리합니다.
5. FSD(Feature-Sliced Design) 적용 관찰 & 개선(아키텍처) (P2/P3)
-
👍 좋은 점:
- entities/post, features/post/comment, widgets/ui, shared/* 로 분리하려는 시도는 FSD 원칙에 맞습니다.
-
개선 제안(AS-IS vs TO-BE)
- AS-IS: 파일들이 대체로 분리되어 있으나 일부 파일이 잘못된 위치에 있거나 import 경로가 꼬여 있음. 훅과 컴포넌트의 책임이 혼재됨.
- TO-BE: 아래처럼 좀 더 엄격한 구조와 네이밍 규칙을 추천합니다.
제안 폴더 구조 (알파벳 정렬 포함)
src/ ├─ base/ (shared lowest-level lib) ├─ entities/ │ ├─ post/ │ │ ├─ api/ │ │ ├─ model/ │ │ ├─ ui/ // 단순 presentation 컴포넌트 │ │ └─ hooks/ // entity-level hooks (usePostEntity) │ └─ user/ ├─ features/ │ └─ post/\ │ ├─ comment/ │ │ ├─ api/ │ │ ├─ ui/ │ │ └─ hooks/ // feature-level logic (useCommentFeature) │ └─ tag/ ├─ modules/ (widgets) ├─ pages/ └─ shared/ -
이유:
- entities는 데이터(타입·model) 중심, features는 사용자 행동(유스케이스) 중심으로 나누면 응집도가 높아집니다.
6. TanStack Query(심화과제) — 미적용 상태 & 도입 제안 (P2)
- 현재 상태:
- PR은 Jotai를 클라이언트 상태로 잘 도입했으나, TanStack Query는 사용되지 않았습니다 → 심화 체크포인트 미충족
- 권장 접근:
- 서버상태(게시물/댓글/유저/태그) 관리는 TanStack Query로 이관하세요. 로컬 UI 상태(다이얼로그 열림/선택된 아이템 등)는 Jotai로 유지.
- Query Key Factory 패턴 도입으로 쿼리 키를 중앙화하면 캐시 관리가 쉬워집니다.
- 예시: Query Key Factory + useQuery + useMutation (요약)
// src/shared/lib/queryKeys.ts export const postsKeys = { all: ['posts'] as const, lists: (filters) => [...postsKeys.all, 'list', filters] as const, details: (id) => [...postsKeys.all, 'detail', id] as const, } // usePosts.ts (hook) const usePosts = (filters) => { return useQuery(postsKeys.lists(filters), () => api.getPosts(filters)) } // add post optimistic update const useAddPost = () => { const qc = useQueryClient() return useMutation(api.addPost, { onMutate: async (newPost) => { await qc.cancelQueries(postsKeys.all) const previous = qc.getQueryData(postsKeys.all) qc.setQueryData(postsKeys.all, (old: any) => ({...old, posts: [newPost, ...old.posts]})) return { previous } }, onError: (err, newPost, ctx) => qc.setQueryData(postsKeys.all, ctx.previous), onSettled: () => qc.invalidateQueries(postsKeys.all), }) } - 이유:
- TanStack Query를 통해 캐시/리페치/낙관적 업데이트 관리가 쉬워지고 서버 상태가 분명해집니다.
7. DummyJson 라이브러리 한계 관련 조언 (P2)
- 문제:
- DummyJson(예: /api/posts/add 등)이 실제로 서버에 영구 반영하지 않을 경우, 지금 같은 fetch 기반 추가/수정/삭제는 클라이언트 상태만 변경하는 임시 로직이 필요합니다.
- 권장 대안:
- 대안 1: TanStack Query의 캐시 직접 수정 (위 optimistic update 예시). 서버 응답이 변하지 않더라도 클라이언트에서 일관된 상태 유지 가능.
- 대안 2: MSW(Mock Service Worker) 도입 — 로컬에서 CRUD 동작을 모킹하고, 테스트·개발에서 실제 백엔드처럼 동작하도록 구성.
- 예시: MSW 기본 핸들러
// handlers.ts rest.post('/api/posts/add', (req, res, ctx) => { const newPost = { id: ++idCounter, ...req.body } postsDB.unshift(newPost) return res(ctx.status(201), ctx.json(newPost)) }) - 이유:
- 테스트·개발 환경에서 클라이언트 로직(낙관적 업데이트, 에러 복구 등)을 더 현실적으로 검증할 수 있습니다.
8. 코드 일관성(스타일) & 라이브러리 버전 확인 (P3)
- 네이밍/컨벤션:
- import 순서/형식 통일: 외부 라이브러리 -> 절대/alias import -> 상대 import 순으로 정렬 권장.
- 컴포넌트/파일명 규칙은 PascalCase로 유지(잘 지켜짐).
- 스타일 가이드:
- Airbnb 스타일과 Toss Frontend Fundamentals에 부합하도록 일부 화살표 함수의 반환 타입 명시, any 지양 권장.
- 라이브러리 버전:
- React ^19.1.1, Jotai ^2.13.1 사용중 — 현재 코드에서 사용하는 Jotai atom API는 호환성 문제 없음. (React 19 관련 deprecated API 없음 특별한 주의사항 없음)
- TanStack Query 도입 예정이라면 최신 버전에 맞춰 useQueryClient/Mutation API 사용하세요.
🗺️ 전체 개선 로드맵 (우선순위 가이드)
- P1 (Must-Fix) 🚨:
- 잘못된 import 경로 수정 (PostsManager 및 기타 파일) — 빌드 실패 방지.
- API 함수의 반환 계약 통일 (Response vs JSON) 및 훅에서의 사용 일치화.
- 중복/충돌 훅명(usePost 등) 고유명으로 변경.
- P2 (Recommended)
⚠️ :- TanStack Query 도입 및 Query Key Factory로 서버 상태 관리 전환.
- atoms에 명시적인 타입 적용 및 일부 atom 책임 재검토.
- DummyJson 한계에 대한 대응 (낙관적 업데이트 또는 MSW).
- P3 (Optional) 💡:
- 폴더 구조 정리(위 TO-BE 구조) 및 문서화.
- 코드 스타일 정리(ESLint/Prettier 규칙 강제).
- 테스트(단위/통합) 추가 — MSW 활용 권장.
🤔 한 걸음 더: 코드와 의도 돌아보기
PR 본문에서 의도(전역 상태 관리, FSD 적용)가 잘 드러났습니다. 몇 가지 스스로 질문해보세요.
- 목표와 구현의 연결고리: “전역 상태 분리는 entities와 features 중 어디에 넣어야 했는가?” — posts의 서버 상태는 features(서버유지)로, UI 상태(다이얼로그 등)는 entities나 shared로 나눌 수 있는지 검토해보셨나요?
- 숨겨진 의도 파악: Post 관련 api 함수를 entities/post/api에 둔 이유는 무엇인가요? tag/ comment api는 features 쪽에 남겨둔 이유가 있었나요?
- 스스로에게 던지는 질문:
- 이번 PR에서 가장 자랑하고 싶은 변경은 무엇이며 왜인가요?
- 훅(usePost 등)의 추상화 수준은 적절한가요? (하나의 훅에 너무 많은 책임이 있진 않나요?)
- Jotai와 TanStack Query의 책임(클라이언트 상태 vs 서버 상태) 분리가 명확한가요?
- 동료가 코드를 볼 때 "왜 getPostApi가 Response를 반환하는가?" 같은 의문을 갖지 않도록 API 계약을 문서화했나요?
📚 참고 자료
- Toss Frontend Fundamentals — 코드 품질 관련 권장사항
https://frontend-fundamentals.com/code-quality/code/ - Airbnb JavaScript 스타일 가이드 — 코드 스타일 표준
https://github.com/airbnb/javascript - Feature-Sliced Design (FSD) — 구조 가이드
https://feature-sliced.design/kr/docs/get-started/overview - TanStack Query (React Query) — 공식 문서 (쿼리 키, optimistic update)
https://tanstack.com/query/latest - MSW (Mock Service Worker) — API mocking
https://mswjs.io/
이번 리뷰가 코드 품질 향상과 아키텍처 정립에 도움이 되었으면 합니다. 다음주도 화이팅 하세요! ✨
끝.
과제 체크포인트
기본과제
목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기
체크포인트
심화과제
목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기
체크포인트
최종과제
과제 셀프회고
이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.
본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?
아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.
이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.
챕터 셀프회고
클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기
결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리
응집도 높이기: 서버상태관리, 폴더 구조
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문