Conversation
- Button, Card, Dialog, Input, Select, Table, Textarea
- type 변수명 오탈자 수정
- axios에서 likeComment를 위한 patch 타입 추가
There was a problem hiding this comment.
안녕하세요! AI 코드 리뷰어입니다. 🤖
FSD 적용 + 댓글 도메인 TanStack Query 도입 Pull Request에 대한 리뷰를 완료했어요. 함께 코드를 더 발전시켜 볼까요?
Note
✨ 이번 PR 한 줄 요약
댓글 도메인에 TanStack Query와 Query Key Factory를 도입하고, FSD 레이어로 UI를 분리한 좋은 방향성이 보입니다. 다만, 캐시 업데이트 키 사용 버그, Provider/엔트리 중복, 타입/의존성 관리 불일치 등 Must-Fix 이슈가 있어 안정화가 먼저 필요합니다.
🎯 핵심 리뷰 요약
가장 중요한 개선 포인트부터 빠르게 확인해 보세요.
- [버그] useUpdateCommentMutation에서 잘못된 쿼리 키 사용으로 캐시 업데이트가 적용되지 않음
- [구조] index.tsx와 main.tsx에 Provider가 이중 정의됨(엔트리/Provider 충돌 가능성)
- [타입] Comments vs CommentsResponse 이중 정의와 혼용으로 타입 일관성 깨짐
- [버전/의존성] pnpm을 dependencies에 추가, zustand는 devDependencies에 추가되었으나 미사용
- [선언적 서버 상태] Posts/users/tags는 여전히 fetch/로컬 상태로 관리 (과제 심화 목표 미충족)
- [키 일관성] 태그 항목 key로 tag.url 사용(옵셔널) → slug 사용 권장
- [FSD 적용] features/entities 분리와 Query Key Factory 도입은 좋음. posts/users 도메인에도 확장 필요
- [DummyJson 한계 대응] 낙관적 업데이트 일부만 적용(좋아요). create/update/delete에도 전략 통일 필요
🔍 상세 리뷰
각 항목에 대한 구체적인 설명과 코드 예시를 준비했어요.
-
댓글 업데이트 캐시 갱신 버그: 쿼리 키 오사용
-
👍 좋은 점: 댓글 도메인에 Query Key Factory(commentQueryKeys)를 도입했고, setQueryData로 부분 캐시 갱신을 시도한 점은 매우 좋습니다.
-
💡 개선 제안: useUpdateCommentMutation onSuccess에서 list 키로 commentId를 사용하고 있어 캐시가 갱신되지 않습니다. postId 기반 키를 사용해야 합니다.
-
🤔 이렇게 제안하는 이유: 결합도/응집도 관점에서 “댓글 리스트”는 postId에 응집됩니다. 쿼리 키는 “데이터의 소유자(postId)”에 결합되어야 하며, commentId는 리스트 키가 아닙니다.
-
💻 코드 예시 (AS-IS vs TO-BE)
AS-ISonSuccess: (response, { id }) => { queryClient.setQueryData<Comments>(commentQueryKeys.list(id), (old) => { if (!old) return old; return { ...old, comments: old.comments.map((comment) => comment.id === response.id ? response : comment ), } }) }TO-BE
onSuccess: (response) => { if (!response.postId) return queryClient.setQueryData<CommentsResponse>( commentQueryKeys.list(response.postId), (old) => { if (!old) return old return { ...old, comments: old.comments.map((c) => (c.id === response.id ? response : c)), } } ) }또는, variables에 postId를 포함해 의도를 더 명확히 만들 수 있습니다.
mutationFn: (payload: { id: number; body: string; postId: number }) => updateComment(payload.id, { body: payload.body }), onSuccess: (response, { postId }) => { queryClient.setQueryData(commentQueryKeys.list(postId), ...) }
-
-
Provider/엔트리 중복 정의: AppProviders vs QueryProvider
- 👍 좋은 점: React Query Devtools를 포함한 AppProviders를 추가해 DevX를 높였습니다.
- 💡 개선 제안: index.tsx와 main.tsx가 동시에 존재하며, 각각 다른 Provider(AppProviders vs QueryProvider)를 사용하고 있습니다. Vite의 엔트리 포인트가 하나로 일치해야 하며, Provider 계층도 단일화해야 합니다.
- 🤔 이렇게 제안하는 이유: 이중 마운트 및 상태 중복/파편화는 디버깅 난이도를 높이고 예측 불가능한 버그를 유발합니다(결합도 증가).
- 💻 코드 예시 (AS-IS vs TO-BE)
AS-ISTO-BE// index.tsx <Router> <AppProviders client={queryClient}> <App /> </AppProviders> </Router> // main.tsx <QueryProvider> <App /> </QueryProvider>// main.tsx (단일 엔트리) import { AppProviders } from './providers/AppProviders' createRoot(document.getElementById('root')!).render( <StrictMode> <Router> <AppProviders> <App /> </AppProviders> </Router> </StrictMode> )- QueryProviders 파일은 제거하고 AppProviders만 유지
- index.tsx 파일은 제거(또는 main.tsx로 통합)
-
타입 일관성: Comments vs CommentsResponse 혼용
- 👍 좋은 점: 타입을 도메인 중심으로 분리했습니다.
- 💡 개선 제안: Comments와 CommentsResponse가 동일한 구조로 중복 정의되어 혼용되고 있습니다. 단일 타입으로 통일하고, API 응답 스키마와 도메인 모델이 다르다면 명확히 구분하세요.
- 🤔 이렇게 제안하는 이유: 타입 중복은 유지보수 비용을 늘리고, setQueryData 제네릭 인수와 반환값 불일치로 인해 런타임 버그를 유발합니다(응집도 저하).
- 💻 코드 예시 (AS-IS vs TO-BE)
AS-ISTO-BE// setQueryData<Comments> ... setQueryData<CommentsResponse> 혼용export interface CommentsList { // 단일 타입 comments: Comment[] limit: number skip: number total: number } // setQueryData<CommentsList>로 일원화
-
의존성/버전 관리: pnpm, zustand
- 👍 좋은 점: 최신 스택(React 19, TanStack Query v5)을 사용합니다.
- 💡 개선 제안:
- pnpm은 패키지 매니저로, 프로젝트 runtime dependencies에 포함되면 안 됩니다. 제거하세요.
- zustand는 devDependencies에 추가되어 있으나 사용되지 않습니다. 미사용 패키지는 제거하거나 실제 사용 시 dependencies로 옮기세요.
- 🤔 이렇게 제안하는 이유: 불필요한 의존성은 번들 크기와 공격 면적을 증가시키며, lockfile/빌드 최적화에도 악영향을 줍니다.
-
서버 상태 선언화(심화 목표): posts/users/tags는 여전히 fetch/로컬 상태
- 👍 좋은 점: 댓글 도메인은 TanStack Query로 잘 추상화했습니다. Query Key Factory를 도입한 점도 훌륭합니다.
- 💡 개선 제안:
- posts, users, tags도 각각 entities/post|user|tag에 query/mutation 레이어를 만들고 tanstack-query로 통일하세요.
- 현재 pages/PostsManagerPage가 너무 많은 서버상태/이벤트를 소유합니다. features 레이어의 model 훅으로 추상화하면 응집도가 올라갑니다.
- 🤔 이렇게 제안하는 이유: “서버 상태는 선언적으로”라는 심화 과제 목표와 직결됩니다. fetch + useState 패턴은 캐싱/리트라이/에러 경계/동시성 제어가 어렵고 결합도가 높습니다.
- 💻 코드 예시 (TO-BE)
// entities/post/model/queryKeys.ts export const postQueryKeys = { all: ['posts'] as const, list: (params: { limit: number; skip: number }) => [...postQueryKeys.all, 'list', params] as const, detail: (id: number) => [...postQueryKeys.all, 'detail', id] as const, } // entities/post/model/queries.ts export const usePostsQuery = (params) => useQuery({ queryKey: postQueryKeys.list(params), queryFn: () => getPosts(params.limit, params.skip), staleTime: 30_000, gcTime: 5 * 60_000, })
-
낙관적 업데이트 전략 통일 + DummyJson 한계 대응
- 👍 좋은 점: 댓글 좋아요에 onMutate 기반 낙관적 업데이트를 적용했습니다.
- 💡 개선 제안:
- create/update/delete에도 낙관적 업데이트를 적용하거나, onSettled invalidate로 일관성을 확보하세요.
- DummyJson은 CUD가 서버에 영속되지 않습니다. 데모 일관성을 위해 아래 대안을 적용하세요.
- 대안 1: tanstack-query 캐시 직접 수정(현재 방향) + onError 롤백 철저히 관리
- 대안 2: MSW로 API 모킹하여 CUD를 로컬 DB(state)로 반영
- 🤔 이렇게 제안하는 이유: 서버-클라이언트 상태 분리가 심화 목표의 핵심입니다. 낙관적 업데이트 일관성은 UX와 데이터 정합성 모두에 중요합니다.
- 💻 코드 예시 (TO-BE)
// onMutate → onError 롤백 → onSettled invalidate onMutate: async (payload) => { ...; return { key, previous } } onError: (_err, _vars, ctx) => { if (ctx?.previous) queryClient.setQueryData(ctx.key, ctx.previous) } onSettled: (_d, _e, vars) => queryClient.invalidateQueries({ queryKey: commentQueryKeys.list(vars.postId) })
-
태그 렌더링: key와 value에 slug 사용
- 👍 좋은 점: Tag 타입을 분리하고 필터 UI를 컴포넌트로 추출했습니다.
- 💡 개선 제안: PostsFilter에서 key={tag.url}은 옵셔널 필드에 의존합니다. key와 value 모두 slug에 통일하세요.
- 🤔 이렇게 제안하는 이유: React key는 안정적인 식별자여야 하며, url 옵셔널 사용은 예기치 않은 re-mount를 유발할 수 있습니다.
- 💻 코드 예시
AS-ISTO-BE<SelectItem key={tag.url} value={tag.slug}> {tag.slug} </SelectItem><SelectItem key={tag.slug} value={tag.slug}> {tag.slug} </SelectItem>
-
타입 안전성: useComment(postId) null 전달, 포스트 상세 댓글 로딩 트리거
- 👍 좋은 점: PostDetail 열릴 때 postId에 연동해 댓글을 불러오는 구조입니다.
- 💡 개선 제안:
- useComment의 인수는 number | undefined로 제한하고, 호출부에서 null을 undefined로 정규화하세요.
- enabled 옵션과 함께, Dialog open 시점에만 쿼리를 enable하는 패턴을 고려해 네트워크 비용을 줄일 수 있습니다.
- 🤔 이렇게 제안하는 이유: null/undefined 혼용은 타입 안정성을 떨어뜨리고 런타임 분기 오류를 유발합니다.
- 💻 코드 예시
// 호출부 const postId = selectedPost?.id ?? undefined const { ... } = useComment(postId) // query useQuery({ ..., enabled: open && !!postId })
🗺️ 전체 개선 로드맵 (우선순위 가이드)
-
P1 (Must-Fix) 🚨
- useUpdateCommentMutation onSuccess 키 오사용 수정(postId 기반)
- 엔트리/Provider 단일화(main.tsx만 유지, AppProviders로 통합)
- 타입 정리: Comments vs CommentsResponse 통일
- package.json에서 pnpm 제거, 미사용 zustand 정리(사용 시 dependencies로 이동)
- PostsFilter SelectItem key를 slug로 교체
-
P2 (Recommended)
⚠️ - posts/users/tags API를 tanstack-query로 전환, Query Key Factory 확장
- 낙관적 업데이트 전략 통일(onMutate/onError/onSettled)
- fetch 대신 shared/api/axios로 통일
- PostDetail/CommentForm 등 다이얼로그 open 상태 기반 enabled 제어로 네트워크 최적화
- AirBnB import 순서/그룹화, 네이밍 컨벤션 정리
-
P3 (Optional) 💡
- MSW 도입으로 DummyJson CUD 영속화 시뮬레이션
- React 19 JSX 자동 런타임 사용 시 파일 상단의 import React 제거하여 보일러플레이트 축소
- posts도메인 usePosts 훅으로 pages의 상태/이벤트 이관(응집도 향상)
- Query key와 API 응답 타입에 zod 등으로 런타임 검증 도입
아키텍처 유연성 평가 및 AS-IS → TO-BE 제안
변경 요구사항 시뮬레이션
- 시나리오 A: 새로운 디자인 시스템 도입
- AS-IS: shared/ui를 통일해서 사용하는 방향은 좋음. 컴포넌트 추상화 수준이 적절해 교체 용이.
- TO-BE: 디자인 토큰/테마를 shared/theme로 분리하고, ui 컴포넌트는 토큰만 의존하도록 축소.
- 시나리오 B: 새로운 핵심 기능 추가(예: 게시물 반응/북마크)
- AS-IS: pages/PostsManagerPage가 많은 상태와 이벤트를 소유. 기능 추가 시 pages가 비대해질 가능성.
- TO-BE: features/post/model/usePosts 훅으로 서버 상태와 이벤트를 이동. pages는 orchestration만 담당.
- 시나리오 C: 테스트 코드 작성
- AS-IS: 데이터 패칭 로직이 pages에 산재하면 훅 단위 테스트가 어려움.
- TO-BE: entities/*/model의 queries/mutations를 순수 훅으로 분리하면 Vitest/RTL로 손쉬운 유닛 테스트 가능.
폴더 구조 제안(변형된 FSD, 알파벳 정렬)
src/
app/ # 엔트리/루트 레이아웃
entities/
comment/
api/
model/
queryKeys.ts
queries.ts
mutations.ts
types.ts
ui/ # 아주 얇은 컴포넌트만
post/
api/
model/
ui/
user/
...
features/
comment/
model/useComment.ts # 페이지 독립적인 도메인 훅
ui/CommentForm.tsx
post/
model/usePosts.ts # NEW: posts 관리 훅
ui/...
pages/
posts-manager/ # page-specific 조립만
index.tsx
shared/
api/axios.ts
lib/queryClient.ts
theme/
ui/ # DS 추상화
widgets/
Header.tsx
Footer.tsx
데이터 흐름(설계 의도 강조)
flowchart LR
Page[PostsManagerPage] -->|uses| FeaturePost[features/post/model/usePosts]
Page -->|uses| FeatureComment[features/comment/model/useComment]
FeaturePost -->|queries/mutations| EntityPost[entities/post/model]
FeatureComment -->|queries/mutations| EntityComment[entities/comment/model]
EntityPost --> Axios[shared/api/axios]
EntityComment --> Axios
핵심 개념 적용
- 응집도(Cohesion): 도메인별(entities) 데이터 계약/키/타입을 한 곳에 모아 높은 응집도를 달성합니다.
- 결합도(Coupling): pages는 features의 퍼사드 훅만 의존해 결합도를 낮춥니다. api/axios, query key 구현 세부는 pages에서 숨겨집니다.
- 추상화(Abstraction): Query Key Factory로 키 생성을 중앙화하면, 쿼리 무효화/캐시 직접 수정의 추상화 수준이 상승합니다.
코드 일관성 검토
- 네이밍/폴더
- features/comment/model/useComment, entities/comment/model/* 일관성 좋습니다.
- posts/users/tags에도 동일한 queryKeys/queries/mutations 패턴을 확장하세요.
- import/export
- 외부 → 절대경로(혹은 alias) → 상대경로 순으로 그룹화 권장(Airbnb 가이드).
- index re-export(features/index.ts)는 잘 하고 있습니다. entities에도 index.ts 배치해 import 경로를 얕게 유지하세요.
- 컴포넌트/함수 네이밍
- 이벤트 핸들러 onXxx, 명령형 함수는 동사 우선(updatePost, deletePost) 일관성 좋음.
- Toss Frontend Fundamentals
- “결합도 낮추기”: pages에서 서버 상태를 features로 이동하면 더 준수하게 됩니다.
- “읽기 좋은 코드”: PostForm/CommentForm로 폼 UI를 공통화한 점 좋습니다.
- Airbnb JavaScript 스타일
- Optional chaining과 nullish 병용 시 타입 명확화 권장.
- React 17+ JSX 자동 런타임 사용 시 import React 생략 가능(프로젝트 tsconfig 확인 후 일괄 정리).
라이브러리 버전 확인
- React 19.1.1: JSX 자동 런타임가 기본입니다. import React는 필요 없습니다. forwardRef는 여전히 유효합니다.
- @tanstack/react-query v5.85.3: 현재 사용 방식 적합합니다. cancelQueries/setQueryData/optimistic update API 정상.
- @tanstack/react-query-devtools v5: AppProviders에서 올바르게 사용.
- zustand v5: 현재 코드에서 미사용. 사용 의도 없다면 제거 권장.
- pnpm: 의존성에 추가할 대상이 아님. 제거 권장.
과제 특수성 반영: DummyJson 한계와 대안
- 현재 좋아요에 낙관적 업데이트(onMutate) 적용은 적절합니다. 동일 전략을 create/update/delete에도 확장하세요.
- 대안 1: tanstack-query 캐시 직접 수정
- 장점: 즉시 반응, 서버 비영속 문제를 UX로 가림
- 주의: onError 롤백, onSettled invalidate 설계 필수
- 대안 2: MSW 도입
- 장점: API를 로컬에서 진짜처럼 영속화/변형 가능, 테스트 용이
- 제안: src/mocks/handlers.ts에 posts/comments/users 핸들러 정의 → worker.start()로 개발 모드에만 활성화
기술적 인사이트
- 변형된 FSD 아키텍처의 장점
- 알파벳 정렬/레이어 구분은 멘탈 모델 구축과 탐색 용이성 향상에 도움.
- entities는 데이터 계약의 단일 출처(SSOT), features는 사용자 행동의 단일 출처가 됩니다.
- Query Key Factory의 효과
- 쿼리 키 중앙화로 invalidateQueries/setQueryData 사용성을 높이고, 타입 안정성이 보장됩니다.
- 키 변경 시 전역 refactor 범위 최소화.
- posts/users에도 Key Factory 확장
export const userQueryKeys = { all: ['users'] as const, list: (params?: { limit?: number }) => [...userQueryKeys.all, 'list', params ?? {}] as const, detail: (id: number) => [...userQueryKeys.all, 'detail', id] as const, }
🤔 한 걸음 더: 코드와 의도 돌아보기
PR 본문을 통해 개발자님의 깊은 고민을 엿볼 수 있었어요. 코드와 PR 본문을 비교하며 몇 가지 질문을 준비했습니다. 스스로에게 답해보며 코드를 더 깊게 이해하는 시간을 가져보는 건 어떨까요?
- 목표와 구현의 연결고리: 본문에 작성된 ‘서버 상태를 선언적으로 다루기’ 목표는 댓글 도메인에 잘 반영되었네요. 혹시 posts/users/tags도 Query로 전환하는 과정에서 고민했던 다른 접근(예: SWR, RTK Query, Zustand + fetch 조합)은 무엇이었나요?
- 숨겨진 의도 파악: PostsManagerPage에서 UI를 features로 추출한 변화가 보입니다. “페이지는 오케스트레이션만 담당하도록” 리팩토링을 더 진행할 계획이 있으신가요?
- 스스로에게 던지는 질문:
- "이번 PR에서 내가 가장 자랑하고 싶은 코드 한 줄은 무엇이고, 그 이유는 뭘까?"
- "컴포넌트의 추상화 수준은 적절했나요? (과도한 추상화나 불필요한 반복은 없었나요?)"
- "Toss Frontend Fundamental의 결합도(Coupling) 원칙처럼, 컴포넌트 간 의존성을 낮추기 위해 어떤 고민을 했나요?"
- "이 코드를 처음 보는 동료는 어떤 부분을 가장 이해하기 어려워할까?"
- "이번에 적용한 ‘낙관적 업데이트’가 DummyJson의 비영속 환경 외 실제 서버 에러/경합 상황에서도 잘 동작하도록 설계되었을까?"
📚 참고 자료
📄 내부 문서
- [스타일 가이드] Toss Frontend Fundamentals: https://frontend-fundamentals.com/code-quality/code/
- [관련 기술 문서] FSD 공식 문서(한글): https://feature-sliced.design/kr/docs/get-started/overview
🌐 추천 아티클
- [TanStack Query v5 – Queries](https://tanstack.com/query/latest/docs/framework/react/guides/queries)
- 선언적 데이터 패칭의 핵심인 쿼리 구조와 키 관리, 캐시 라이프사이클 이해에 도움
- [TanStack Query – Optimistic Updates](https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates)
- onMutate/onError/onSettled 패턴으로 낙관적 업데이트 설계 방법
- [MSW 공식 문서](https://mswjs.io/)
- API 모킹으로 DummyJson 한계를 극복하고 테스트 친화적 환경 구축
- [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript)
- 일관된 코드 스타일과 import 정렬/네이밍 컨벤션 참고
이번 리뷰가 개발자님의 성장에 도움이 되었으면 좋겠습니다. 다음주도 화이팅 하세요! ✨
끝.
- QueryProvider 감싸지 않은 부분 수정 - queries 괄호 체크
과제 체크포인트
기본과제
목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기
체크포인트
심화과제
목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기
체크포인트
최종과제
과제 셀프회고
이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.
본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?
아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.
이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.
챕터 셀프회고
클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기
결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리
응집도 높이기: 서버상태관리, 폴더 구조
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문