[4팀 김지혜] Chapter 2-3. 관심사 분리와 폴더구조#35
Conversation
- Button, Input, Card, Select 컴포넌트를 src/shared/ui로 이동 - BaseProps, BasePropsWithChildren 타입 정의로 className 공통화
- Dialog, Table 컴포넌트 정의 제거 및 import 구조 변경 - Textarea 타입 정확성 개선
- Post, User, Comment, Tag 도메인 모델링 - 비즈니스 로직과 API 로직 분리 - TypeScript 타입 추가
- Post 관련 API 함수들을 entities/post/api로 분리 - Post 타입 정의를 entities/post/types로 분리 - highlightText 함수를 entities/post/model로 분리 - PostsManagerPage에서 중복 코드 제거 및 엔티티 함수 사용 - FSD 아키텍처 적용
- Entities, Features, Widgets, Shared 계층으로 코드 분리 - TypeScript 타입 안전성 개선
- Header/Footer를 widgets 레이어로 이동 - components 폴더 정리 및 불필요한 타입 제거
- User 타입을 entities에서 shared로 이동하여 중복 제거 - TypeScript 타입 에러 해결
- Post API addPost에서 사용자 정보 가져와 author 필드 추가 - User Feature를 useState에서 Zustand로 전환
- 디버깅 로그 제거 및 프로덕션 코드 정리 - entities 함수 활용으로 중복 코드 제거 - shared/utils 정리 및 중복 로직 통합 - 타입 오류 수정 (UserBasic → User)
- entities/api: 상태 관리 로직 제거, 순수 API 호출만 유지 - features/api: 상태 관리와 결합된 API 로직 구현 - feature hooks: 새로운 API 사용으로 전환 - UI 컴포넌트: Dialog들을 전용 컴포넌트로 교체 - 타입 오류: Comment 타입 호환성 문제 해결
- entities에 비즈니스 로직 함수 추가 - PostsManagerPage를 PostManager widget으로 리팩토링
- useQuery와 useMutation으로 모든 API 호출 대체 - 쿼리 키 전략 및 캐싱 전략 구현 - 낙관적 업데이트로 즉시 UI 반영 - 서버 상태와 클라이언트 상태 명확히 분리 - 에러 핸들링 및 로딩 상태 관리 - TanStack Query Devtools 설정 완료
- useCommentQueries 훅 생성 (useComments, useAddComment, useUpdateComment, useDeleteComment, useLikeComment) - useCommentFeature를 TanStack Query 기반으로 리팩토링 - CommentList 컴포넌트에서 TanStack Query 직접 사용 - setQueryData를 활용한 낙관적 업데이트로 즉시 화면 반영 - 서버 상태(TanStack Query)와 클라이언트 상태(Zustand) 명확히 분리
Comment 엔티티: - 불필요한 import, 빈 함수, 디버깅 로그 제거 - postId 매개변수 정리 및 try-catch 구조 개선 Post 엔티티: - 사용하지 않는 변수, 빈 함수, 불필요한 로직 제거 - useEffect 로직 단순화 User 엔티티: - TanStack Query 완전 적용 (useUser, UserProfile) - 로딩/에러 상태 처리 추가
- 댓글 CRUD를 TanStack Query로 교체 - 사용하지 않는 Old 코드 정리 - Dialog 컴포넌트 타입 에러 수정 - 코드 최적화 및 정리
- 댓글 추가는 실제 API 호출로 사용자 정보 가져오기 - 댓글 수정/삭제/좋아요는 로컬에서만 작동
- Old 파일 정리 및 코드 정리 - CSS 모듈 타입 지원 추가 - Select 컴포넌트 타입 에러 해결 - 빌드 성공 확인
- base URL을 /front_6th_chapter2-3/로 설정 - @ alias 추가로 절대 경로 import 지원 - 프로덕션 빌드 최적화 (sourcemap 비활성화, esbuild minify) - @types/node 의존성 추가
- 환경별 API 분기를 위한 httpClient 생성 - 개발/프로덕션 환경 자동 감지 - GitHub Pages 배포 시 404 에러 해결 - 모든 API 함수를 httpClient 사용하도록 통일
- Tag API와 Post Utils에서 fetch 직접 사용 제거
- Post, User, Comment, Tag API 함수에 기능 설명 추가 - httpClient 디버깅 로그 및 불필요한 주석 정리 - postUtils 비즈니스 로직 함수 주석 개선
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요, 김지혜 님! 5주차 과제 잘 진행해주셨네요. 고생하셨습니다 👍🏻👍🏻
현재 남기는 코멘트는 GPT-5-mini model을 사용하여 AI가 남기는 피드백입니다 🤖
과제 피드백과는 별개이므로 참고해주세요!
1. 🏗️ FSD 아키텍처
💡 개념 정의
Feature-Sliced Design(FSD)은 관심사(Feature)와 역할(Slice)에 따라 코드를 분리하여 계층간의 의존 방향을 명확히 하는 아키텍처 패턴입니다. 하위 레이어는 상위 레이어를 참조하지 않습니다.
⚡ 중요성
FSD를 지키면 도메인 변경, 아키텍처 변화(모노레포, 패키지화), UI 라이브러리 변경 시 영향도를 최소화할 수 있습니다. 특히 entities는 도메인/데이터 모델과 순수 API만 가지면 재사용성이 커집니다.
📊 현재 상황 분석
엔티티에 UI setState와 콜백이 섞여있음 (src/entities/post/model/postUtils.ts, src/entities/tag/api/index.ts 등). features 레이어에도 상태 결합 로직이 남아있어 책임이 혼재되어 있음. 또한 일부 유틸은 entities에 있지만 구현상 UI와 결합될 소지가 있음.
📝 상세 피드백
개념 정의
Feature-Sliced Design(FSD)은 애플리케이션을 역할(계층) 단위로 나누어 책임을 분리하는 아키텍처입니다. 일반적으로 의존성 방향은 app → pages → widgets → features → entities → shared 순이며 하위 계층만 참조해야 합니다. 각 slice는 외부에 공개할 API를 index.ts로 한정하여 구현 세부사항을 숨깁니다.
중요성
FSD를 올바르게 적용하면 도메인 변경이나 UI 라이브러리 변경 등의 요구사항 변화 시 영향을 받는 파일 수가 줄어들고, 모듈 분리·패키지 독립화가 쉬워집니다.
근거(Evidence)
- Feature-Sliced Design 공식 가이드
- Clean Architecture의 계층적 책임 분리 원칙
현재 코드 분석 (PR 근거)
- entities 계층에 UI 상태/콜백이 섞여 있음:
- src/entities/post/model/postUtils.ts: setLoading, setPosts, setTotal, handleFetchPosts 같은 UI 상태 콜백을 파라미터로 받음.
- src/entities/tag/api/index.ts: fetchTags(setTags) — 엔티티 레이어가 setState를 직접 호출.
이로 인해 entities를 재사용하거나 테스트할 때 UI 의존성이 커짐.
- features 레이어에서도 상태와 API 로직이 혼재:
- src/features/post/api/index.ts, src/features/comment/api/index.ts: "상태 관리와 결합된 API 로직" 주석과 실제 setState 사용.
- 일부 utilities(예: openPostDetailWithComments)는 entities/model에 있으면서 features에서 호출하는 구조는 적절하나 entities 내부에서 UI를 직접 다루면 위계가 깨짐.
변화 시나리오
- 만약 UI 프레임워크(예: Dialog 컴포넌트)가 바뀐다면: entities에 UI 콜백을 전달하는 모든 호출부(약 6
8곳)를 점검해야 함. entities가 순수하면 영향은 12곳으로 축소됩니다. - 모노레포로 분리해 패키지화할 때: entities에 UI 의존성이 있으면 패키지화 불가 혹은 추가 의존성 주입 필요.
AS-IS 예시
- entities/post/model/postUtils.ts (발견됨):
export const fetchPostsByTag = async (setLoading, setPosts, setTotal, tag, handleFetchPosts) => { ... }
// 엔티티 단위에서 setLoading/setPosts를 직접 호출
TO-BE 예시
- entities/post/api.ts (순수 API)
export const fetchPostsByTag = async (params: { tag: string }) => {
const res = await httpClient.get(/api/posts/tag/${params.tag});
return res.json();
}; - features/post/hooks/usePostFeature.tsx (비즈니스/상태 연결)
const { data } = useQuery(['posts','tag', tag], () => fetchPostsByTag({ tag }));
setPosts(data.posts); // UI 상태는 feature/hooks에서 처리
개선 단계별 제안
- 단기(1-2일): entities 레이어에서 setState 파라미터를 제거하는 리팩토링. API는 항상 데이터를 반환하도록 수정.
- 중기(2-3일): features/*/api에 있는 '상태 결합' 함수들을 제거하고 TanStack Query 훅(usePosts, useComments 등)으로 대체.
- 장기(3-5일): entities를 패키지화 가능한 순수 API/타입 모듈로 정리, index.ts를 통한 Public API 표준화.
예상 효과
- UI 라이브러리 변경 시 수정 파일 수: AS-IS 약 8
12 파일 → TO-BE 약 23 파일
❌ 현재 구조 (AS-IS)
src/entities/post/model/postUtils.ts
export const fetchPostsByTag = async (setLoading, setPosts, setTotal, tag, handleFetchPosts) => { /* httpClient 호출 후 setPosts/setTotal 호출 */ }✅ 권장 구조 (TO-BE)
src/entities/post/api.ts
export const fetchPostsByTag = async (tag: string) => { const res = await httpClient.get(`/api/posts/tag/${tag}`); return res.json(); }
// features/post/hooks/usePostQueries.ts
export const usePostsByTag = (tag: string) => useQuery(['posts','tag',tag], () => fetchPostsByTag(tag));🔄 변경 시나리오별 영향도
- 디자인 시스템이 Material-UI → Chakra UI로 바뀌면, entities 레이어에 전달되는 setState를 사용하는 모든 호출부(약 8~12개)를 수정해야 함.
- 모노레포로 entities를 npm 패키지화하면 entities가 UI 의존성을 가지므로 별도 추상화 레이어를 만들어야 함.
- 새로운 비동기 API가 추가되어 응답 구조가 바뀌면 엔티티가 순수하면 features 훅 몇 개만 수정하면 되지만, 현재 구조는 여러 feature와 widget에서 setState 콜을 검사해야 함.
🚀 개선 단계
- 1단계: 단기(1~2일): entities 함수에서 UI setState 파라미터 제거. 모든 엔티티 API는 데이터를 반환하도록 변경.
- 2단계: 단기(1~2일): features 측에서 useQuery/useMutation로 server-state를 관리하고, 기존 setState 기반 호출부를 점진적으로 교체.
- 3단계: 중기(2~3일): features/*/api의 '상태 결합' 함수 제거 및 해당 로직을 hooks로 이동.
- 4단계: 장기(3~5일): entities를 독립 패키지로 추출하기 위한 index API 정리 및 Public API 문서화.
2. 🔄 TanStack Query
💡 개념 정의
TanStack Query는 서버 상태를 선언적으로 관리하는 라이브러리로, 쿼리 키에 따라 캐시를 식별하고 재사용/무효화/갱신을 처리합니다.
⚡ 중요성
쿼리 키 설계와 API 계층 분리는 새로운 필터/정렬/페이지네이션 추가 시 수정 범위를 줄이고, 캐싱 전략으로 UX를 개선합니다.
📊 현재 상황 분석
QueryClient 설정과 Devtools 포함 등 기본은 잘 갖춰졌습니다. 다만 queryKey의 일관적 관리(팩토리 없음), 일부 mutation의 가짜 응답 가정/범용 getQueriesData 사용, 낙관 업데이트 표준화 미비가 발견됩니다.
📝 상세 피드백
개념 정의
TanStack Query는 서버 상태를 관리하는 라이브러리로, 캐싱·중복 요청 방지·비동기 상태 관리·낙관적 업데이트 등을 제공합니다. 핵심은 쿼리 키(queryKey)와 쿼리 함수(queryFn)를 명확히 관리하는 것입니다.
중요성
쿼리 키 설계와 API 계층 분리는 API 변경 또는 필터/정렬 옵션 추가 시 수정 범위를 최소화하고, 캐싱 전략(staleTime, cacheTime)을 통해 성능과 UX를 조절할 수 있습니다.
근거(Evidence)
- TanStack Query 공식 문서의 Query Keys & Mutations 가이드
현재 코드 분석 (PR 근거)
- 좋은 점:
- 루트에 QueryClient를 생성하고 defaultOptions 설정(staleTime 5분 등) 및 ReactQueryDevtools 추가(src/App.tsx).
- 각 feature에 useQuery/useMutation 훅이 존재하여 선언적 패턴을 따름 (src/features/post/hooks/usePostQueries.ts, src/features/comment/hooks/useCommentQueries.ts 등).
- 개선 포인트:
- 쿼리 키 관리 일관성 부족: 현재 키는 ['posts', limit, skip], ['posts','search',query], ['comments',postId] 등으로 분산되어 있음. 필터/정렬/페이지네이션 옵션이 늘어나면 키 관리가 복잡해짐.
- 쿼리 키 팩토리 미비: queryKeys factory가 없어서 복잡 키를 재사용하기 어렵고 오타/일관성 문제가 발생할 수 있음.
- onSuccess에서 가짜 응답을 가정하거나 postId가 null인 케이스를 많이 다루고 있어(예: comment update/delete mutation에서 return {id, postId: null}) 향후 API 표준이 달라지면 복잡한 수정 필요.
- 낙관적 업데이트는 구현되어 있으나 일부 mutation은 서버 응답에 의존하지 않고 클라이언트에서 가정한 값을 사용함(리스크 존재).
변화 시나리오
- API 엔드포인트가 { posts: [...], total } → { data: { posts, total } }로 바뀌면, 현재 fetch 함수와 useQueries를 모두 수정해야 함. queryKeys 팩토리를 도입하면 변경 지점을 훨씬 줄일 수 있음.
- 필터/정렬 추가 시: 현재 구조는 ['posts', limit, skip] → ['posts', {limit, skip, sort, filter}] 형태로 키가 길어지고 오타 가능성 증가.
AS-IS 예시
- usePosts: queryKey ['posts', limit, skip]
- useSearchPosts: queryKey ['posts', 'search', query]
- useAddPost.onSuccess: queryClient.getQueriesData({ queryKey: ['posts'], exact: false }) 로 모든 posts 관련 쿼리를 찾아 변경
TO-BE 예시 (쿼리 키 팩토리)
- shared/api/queryKeys.ts
export const queryKeys = {
posts: (params?: { limit?: number; skip?: number; search?: string; tag?: string; sortBy?: string; sortOrder?: string }) => ['posts', params || {}] as const,
post: (id: number) => [...queryKeys.posts(), id] as const,
comments: (postId: number) => ['comments', postId] as const,
};
// usePosts
useQuery({ queryKey: queryKeys.posts({ limit, skip }), queryFn: () => postApi.fetchPosts({limit, skip}) })
개선 단계
- queryKeys 팩토리 추가(shared/api/queryKeys.ts) — (1일)
- 모든 useQuery/useMutation를 queryKeys로 대체 — (1~2일)
- mutation의 onSuccess에서는 getQueriesData 대신 queryClient.setQueryData(queryKeys.posts(...)) 또는 invalidateQueries(queryKeys.posts(...))를 사용하여 범위를 좁힘 — (1일)
- 낙관적 업데이트는 mutation의 onMutate/rollback 패턴으로 표준화 — (1~2일)
정량적 예상 효과
- 쿼리 키 변경 시 수정 파일 수: 현재 구조(직접 리터럴 사용) 시 약 10
15 파일 수정 필요 → queryKeys 적용 시 12곳(팩토리)만 수정하면 전체 적용 가능.
❌ 현재 구조 (AS-IS)
src/features/post/hooks/usePostQueries.ts
useQuery({ queryKey: ['posts', limit, skip], queryFn: async () => { ... } })
// mutation onSuccess에서 모든 posts 쿼리 탐색
const queries = queryClient.getQueriesData({ queryKey: ['posts'], exact: false });✅ 권장 구조 (TO-BE)
shared/api/queryKeys.ts
export const queryKeys = { posts: (params={}) => ['posts', params] as const, post: (id) => ['posts', 'item', id] as const }
useQuery({ queryKey: queryKeys.posts({limit,skip,tag}), queryFn: () => postApi.fetchPosts({limit,skip,tag}) })
useMutation({ onSuccess: (newPost) => queryClient.setQueryData(queryKeys.posts({}), old => ({ ...old, posts: [newPost, ...old.posts] })) })🔄 변경 시나리오별 영향도
- 필터/정렬/페이지 파라미터 확장: 단순 리터럴 키(['posts',limit,skip]) 방식이면 확장 시 모든 쿼리 호출부 수정 필요 → queryKeys 팩토리 도입으로 영향 최소화.
- API 응답 구조 변경: entities/api에서 반환 형식을 표준화하면 useQuery 측 변경이 줄어듭니다.
- 다중 데이터 소스(캐시 분리) 추가: queryKeys가 없으면 충돌/무효화 범위 지정이 어려움.
🚀 개선 단계
- 1단계: 단기(1일): shared/api/queryKeys.ts 파일을 추가하여 모든 쿼리/뮤테이션에서 사용하도록 전환.
- 2단계: 단기(1~2일): mutation의 onSuccess에서 getQueriesData({queryKey:['posts'],exact:false}) 패턴을 제거하고 queryKeys에 따른 setQueryData 또는 invalidateQueries로 범위를 좁힘.
- 3단계: 중기(1~2일): 낙관적 업데이트를 onMutate/onError/rollback 패턴으로 일관되게 구현.
- 4단계: 장기(2일): 복잡한 필터·정렬 파라미터는 객체 형태로 쿼리 키를 구성하고, stringify 대신 안정적 오브젝트 키 정책을 적용.
3. 🎯 응집도 (Cohesion)
💡 개념 정의
응집도는 모듈 내부 요소들이 얼마나 관련 있는지를 측정하는 지표로, 높은 응집도는 변경 시 수정 범위가 좁음을 의미합니다.
⚡ 중요성
응집도가 높으면 유지보수성이 좋아지고 패키지 분리 시 경계가 명확해집니다. 또한 버그 발생 시 원인 추적이 쉬워집니다.
📊 현재 상황 분석
위젯으로의 분해는 잘 되어 있으나, 댓글/게시물 관련 로직(데이터 결합·상태 관리)이 entities, features, widgets 사이에 중복되고 분산되어 있음. 결과적으로 댓글 변경 시 여러 위치를 건드려야 함.
📝 상세 피드백
개념 정의
응집도(Cohesion)는 한 모듈 내 요소들이 얼마나 강하게 연관되어 있는지를 나타냅니다. 높은 응집도는 관련 기능이 한 곳에 모여 변경 범위가 좁음을 의미합니다.
중요성
높은 응집도는 기능 추가·수정 시 영향을 받는 파일 수를 줄이고, 모듈을 패키지로 분리할 때 경계가 명확해집니다.
근거(Evidence)
- Clean Architecture, 모듈화 원칙
현재 코드 분석 (PR 근거)
- 긍정적 측면: PostsManagerPage가 widget/feature로 분해되어 PostManager, PostTable, SearchBar, FilterBar 등으로 응집도가 개선됨.
- 문제점:
- 댓글 관련 로직이 세 곳에 분산됨: src/features/comment/(hooks, api, store, ui), src/widgets/Comments, src/entities/comment/model/commentUtils.ts. 이는 댓글 기능을 이해/수정할 때 여러 위치를 살펴봐야 함.
- entities/post/model.postUtils와 features/post/api.fetchPostsWithUsers 등에서 유사한 데이터 조합(게시물 + 사용자 합치기) 로직이 중복되어 있음.
변화 시나리오
- 댓글 관련 UX 변경(예: 댓글 정렬, 페이징) 시: 현재는 최소 3개 모듈(entities, features, widgets)에서 변경될 가능성이 있어 약 8
12 파일 수정 필요. 높은 응집도일 경우 23 파일로 줄어듦.
AS-IS 예시
- 댓글 추가 로직: src/features/comment/api.addCommentWithState (setState 사용), src/entities/comment/model/commentUtils.handleAddCommentWithData (또 다른 위치) — 비슷한 책임이 중복.
TO-BE 예시
- 하나의 comment feature 폴더 안에: api (순수 호출), hooks (useComments, useAddComment), ui (CommentList, CommentForm), store (UI 상태). entities는 타입/API만 보유.
개선 단계
- 댓글 기능을 단일 feature 패키지로 정리 (1~2일).
- 중복된 사용자 결합 로직(게시물 + author) 유틸화(shared/utils/dataMerge) (0.5~1일).
- entities는 타입+순수API, feature는 비즈니스 로직+UI 상태, widget은 재사용 가능한 UI만 갖도록 재조정.
❌ 현재 구조 (AS-IS)
src/features/post/api.fetchPostsWithUsers 와 src/features/post/hooks/usePosts 의 사용자 결합 로직이 유사하게 중복됨.✅ 권장 구조 (TO-BE)
shared/utils/mergeAuthor.ts
export const attachAuthorsToPosts = (posts, users) => posts.map(p => ({ ...p, author: users.find(u => u.id === p.userId) }));
// features에서 재사용🔄 변경 시나리오별 영향도
- 댓글 페이징이나 정렬이 추가될 때: 현재 분산 구조에서는 6
10파일 수정 가능 → 응집도 개선 시 23파일로 감소. - 게시물 리스트에 author 메타데이터가 추가될 때: 중복 로직이 많으면 동일한 변형을 여러 곳에 적용해야 함.
🚀 개선 단계
- 1단계: 단기(0.5~1일): 게시물+사용자 결합 로직을 shared 유틸로 추출하여 중복 제거.
- 2단계: 단기(1~2일): 댓글 관련 파일을 한 feature 폴더로 정리(ui/hooks/api/store 모두 동일한 feature로 묶기).
- 3단계: 중기(1~2일): entities는 타입/순수API만 소유하고 feature가 비즈니스 로직/상태 관리를 전담하도록 재배치.
4. 🔗 결합도 (Coupling)
💡 개념 정의
결합도는 모듈 간의 의존성 강도를 나타내며, 낮출수록 한 모듈 변경이 다른 모듈에 미치는 영향이 작아집니다.
⚡ 중요성
낮은 결합도는 라이브러리 교체(HTTP 클라이언트, 상태관리 등)와 아키텍처 전환 시 수정을 최소화합니다.
📊 현재 상황 분석
httpClient 중앙화는 장점이지만 엔티티가 직접 httpClient에 의존하고 있어 HTTP 클라이언트 변경 시 약 8~11개 파일의 수정을 요구합니다. 또한 entities가 UI setState를 받는 부분은 결합도를 높입니다.
📝 상세 피드백
개념 정의
결합도(Coupling)는 모듈 간 의존성의 강도를 의미하며, 낮은 결합도는 한 모듈의 변경이 다른 모듈에 미치는 영향을 줄입니다.
중요성
낮은 결합도는 기술 스택(HTTP 클라이언트, 상태관리 등) 변경 시 수정 범위를 줄이고, 테스트와 모킹을 쉽게 해 줍니다.
근거(Evidence)
- 의존성 역전 원칙, 인터페이스 기반 설계 권장사항
현재 코드 분석 (PR 근거)
- 좋은 점: httpClient를 중앙화(src/shared/config/httpClient.ts). TanStack Query와 Zustand의 역할 분리가 시도됨.
- 문제점:
- entities/*/api가 httpClient 인스턴스에 직접 의존 (export const fetchPosts = async (...) => httpClient.get(...)). HTTP 클라이언트 교체(예: fetch → axios) 시 entities의 API 파일 약 8~10개를 수정해야 함.
- features 코드는 httpClient를 직접 사용하지 않지만, 일부 entities/model은 UI setState를 받음으로써 결합도가 올라감.
- shared 레이어에 도메인 의존성은 적으나 일부 shared/ui 컴포넌트(버튼 등)가 도메인 타입을 참조하지는 않음 — 양호.
변화 시나리오
- HTTP 클라이언트 라이브러리 변경(예: fetch 기반 httpClient → axios): 영향을 받는 files: entities/post/api, entities/comment/api, entities/tag/api, entities/user/api, shared/config/httpClient (1개), entities/post/model/postUtils 등 — 약 8~11 파일.
- 상태관리 라이브러리 교체(Zustand → Recoil): features/*/store와 usePostStore/useCommentStore 등 모든 zustand store 파일(약 4~6 파일) 수정 필요.
AS-IS 예시
- entities/post/api/index.ts
import { httpClient } from '../../../shared/config/httpClient';
export const fetchPosts = async (limit, skip) => { const response = await httpClient.get(/api/posts?limit=${limit}&skip=${skip}); return response.json(); }
TO-BE 예시
- entities/post/api.ts (의존성 주입)
export const createPostApi = (client: HttpClient) => ({ fetchPosts: async (params) => { const res = await client.get(/api/posts?...); return res.json(); } });
// app 초기화
const postApi = createPostApi(httpClient); export { postApi };
개선 단계
- 단기(1~2일): httpClient 래퍼를 유지하되 entities의 API가 client 인스턴스를 인자로 받는 팩토리 함수 형태로 리팩토링하여 DI 가능하게 만듦.
- 중기(1~2일): Zustand store를 server-state와 혼합 사용하지 않도록 분리(서버 데이터는 Query가 소유). store에서만 사용되는 함수만 유지.
- 장기(2~3일): DI 패턴을 적용해 테스트용 목 클라이언트 주입을 쉽게 함.
❌ 현재 구조 (AS-IS)
src/entities/comment/api/index.ts 에서 httpClient에 직접 의존: import { httpClient } from '../../../shared/config/httpClient';
export const fetchComments = async (postId) => { const response = await httpClient.get(`/api/comments/post/${postId}`); return response.json(); }✅ 권장 구조 (TO-BE)
src/entities/comment/api/factory.ts
export const createCommentApi = (client) => ({ fetchComments: (postId) => client.get(`/api/comments/post/${postId}`).then(r=>r.json()), addComment: (c) => client.post('/api/comments/add', c).then(r=>r.json()) });
// app 초기화에서 주입
export const commentApi = createCommentApi(httpClient);🔄 변경 시나리오별 영향도
- axios로 HTTP 클라이언트 교체 시: entities/api 파일 8
11개, post model 등 총 약 1012 파일 변경 필요(현재) → DI 적용 시 1~2곳 변경으로 축소. - Zustand→다른 상태관리로 변경 시: store 파일(약 4~6개)을 추상화/어댑터로 대체하면 변경범위 축소 가능.
🚀 개선 단계
- 1단계: 단기(1~2일): entities API를 팩토리(의존성 주입) 패턴으로 변경해 httpClient를 주입할 수 있게 함.
- 2단계: 단기(1일): features에서 server-state는 TanStack Query로, client-side UI state만 Zustand로 분리하여 결합도 완화.
- 3단계: 중기(2~3일): 상태관리 라이브러리 변경을 위한 어댑터 추상층을 추가하여 교체 비용을 낮춤.
5. 🧹 Shared 레이어 순수성
💡 개념 정의
Shared 레이어는 여러 도메인에서 재사용 가능한 UI·유틸·타입을 제공하며 도메인 로직에 의존하면 안 됩니다.
⚡ 중요성
도메인 중립성을 유지하면 다른 프로젝트나 패키지에 재사용하기 쉬워지고 디자인 시스템 변경 시 영향 범위가 좁습니다.
📊 현재 상황 분석
shared/ui와 shared/utils는 범용으로 잘 구성되어 있습니다. 다만 entities가 shared의 타입(User)을 사용하고 있어 순방향 의존성(entities→shared)은 괜찮으나 shared→entities 의존이 생기지 않도록 주의해야 합니다.
📝 상세 피드백
개념 정의
Shared 레이어는 여러 도메인에 재사용 가능한 UI 구성요소, 유틸리티, 타입 등을 제공해야 하며 특정 도메인 로직에 의존하면 안 됩니다.
중요성
Shared가 도메인에 의존하면 재사용성이 떨어지고 패키지화/설계 변경 시 문제를 일으킵니다.
근거(Evidence)
- 재사용 가능한 디자인 시스템 원칙
현재 코드 분석 (PR 근거)
- shared/ui는 Button, Input, Dialog 등 범용 컴포넌트를 잘 모아두었음(src/shared/ui/*).
- shared/utils에는 highlightText, urlUtils 등 UI/유틸 중심의 함수가 있음 — 적절함.
- 문제점: src/entities/post/types/index.ts가 shared/types/common의 User 타입을 import 하고 있음. (entities가 shared에 의존하는 것은 허용되지만 반대는 위험)
- overall: shared 레이어는 비교적 도메인-중립적으로 잘 유지되고 있음.
변화 시나리오
- 디자인 시스템을 바꾸면 shared/ui 컴포넌트만 수정하면 되지만, 만약 shared에 도메인 로직이 있으면 각 도메인에 영향 확산.
AS-IS 예시
- shared/ui/Button은 범용으로 사용되고 있음.
TO-BE 예시
- 현재 상태를 유지하되 shared는 strictly domain-independent 하게 유지 (도메인 로직 제거).
개선 단계
- 검토(0.5일): shared 내부에서 domain 타입/로직을 참조하는 파일 검색. domain 의존성 발견 시 refactor 권장.
- 리팩토링(1~2일): domain 관련 포맷팅이나 Badge 같은 컴포넌트는 domain-agnostic props(variant, label)로 변경하고 도메인 로직은 feature에서 담당.
❌ 현재 구조 (AS-IS)
src/shared/ui/Button/Button.tsx — 범용 컴포넌트로 적절히 설계됨.✅ 권장 구조 (TO-BE)
Badge 같은 UI는 variant/children 정도만 받도록 하고, 도메인에 특화된 상태 계산은 feature에서 수행 후 Badge에 전달.🔄 변경 시나리오별 영향도
- 디자인 시스템 변경(예: Tailwind→CSS-in-JS): shared/ui만 수정하면 되나, 만약 shared에 도메인 의존성이 있으면 feature별 변경이 필요.
- 다른 프로젝트에서 shared 재사용 시: domain 의존성이 없으면 바로 사용 가능.
🚀 개선 단계
- 1단계: 단기(0.5일): shared 내부에서 도메인 타입/로직 참조 여부 스캔.
- 2단계: 단기(0.5~1일): 발견 시 domain-agnostic API로 변경(Props 통한 전달).
- 3단계: 중기(1일): shared 레이어 문서화(사용 가이드, props 계약)로 재사용성 확보.
6. 📐 추상화 레벨
💡 개념 정의
추상화 수준은 비즈니스 개념과 구현 세부사항을 얼마나 분리했는지를 의미합니다. 좋은 추상화는 변경 대비 유연성을 높입니다.
⚡ 중요성
추상화가 잘되어 있으면 기술 스택 변화(HTTP 클라이언트, 상태관리)나 아키텍처 변화 시 수정해야 할 범위가 작아집니다.
📊 현재 상황 분석
entities에서 UI 콜백을 받거나 httpClient에 직접 의존하는 곳은 추상화가 낮아 기술 교체/패키지화 시 비용이 커집니다.
📝 상세 피드백
개념 정의
추상화 수준은 구현 세부사항을 숨기고 비즈니스 개념을 명확히 노출하는 정도입니다. 좋은 추상화는 재사용성과 테스트 용이성을 높입니다.
중요성
비즈니스 로직과 HTTP/프레임워크 코드를 분리하면 구현체 변경(HTTP 클라이언트, 라우터 등) 시 영향 범위를 줄일 수 있습니다.
근거(Evidence)
- 인터페이스 기반 설계, SRP(단일 책임 원칙)
현재 코드 분석 (PR 근거)
- 장점: entities와 features로 역할을 나누려는 시도(타입/모델/비즈니스 분리). shared 유틸/컴포넌트로 공통 기능을 추출함.
- 문제점: entities/model/postUtils.ts 등에서 httpClient와 UI 세팅(setLoading 등)을 혼재시켜 추상화 수준이 낮음. features/*/api는 상태 결합 로직을 포함하고 있어 추상화가 덜 되어 있음.
변화 시나리오
- API 호출 로직 추상화 미흡 → HTTP 클라이언트 변경 시 많은 파일에서 변경 필요.
AS-IS 예시
- entities/post/model/postUtils.ts: httpClient 사용 + setState 콜
TO-BE 예시
- entities/post/api.ts: 순수 fetchPosts 함수만 제공
- features/post/hooks/usePostQueries.ts: fetchPosts를 호출하여 결과를 가공(비즈니스)하고 컴포넌트에 전달
개선 단계
- 단기(1~2일): entities는 순수 API + 타입만 노출하도록 리팩토링.
- 중기(1~2일): features는 비즈니스 룰(낙관 업데이트/캐싱 정책)을 담당하도록 분리.
- 장기(2~3일): 공통 패턴(쿼리 키, api factory, error handling)을 shared로 추출하여 일관성 확보.
❌ 현재 구조 (AS-IS)
src/entities/post/model/postUtils.ts (httpClient 활용 + setLoading 호출)✅ 권장 구조 (TO-BE)
entities/post/api.fetchPosts만 순수 반환 → feature훅(usePosts)에서 캐싱/에러/로딩 처리 담당🔄 변경 시나리오별 영향도
- HTTP 클라이언트 변경: 현재 구조는 entities 파일 다수 변경 필요 → DI 적용 시 1~2곳으로 축소.
- 에러 처리 정책 변경: 공통 error handler 추출하면 mutation/useQuery의 onError를 일관되게 적용 가능.
🚀 개선 단계
- 1단계: 단기(1~2일): entities의 API를 순수 반환 함수로 변경하고, httpClient 주입/팩토리 형태로 전환.
- 2단계: 중기(1~2일): features 레이어에 비즈니스 로직을 집중시키고 공통 error/transform 유틸을 작성.
- 3단계: 장기(2~3일): 공통 API 어댑터(예: apiFactory, queryKeys, errorMapper)를 shared로 추출.
7. 🧪 테스트 용이성
💡 개념 정의
테스트 용이성은 코드가 모킹·유닛 테스트·통합 테스트로 검증되기 쉬운 정도입니다. 순수 함수와 DI가 테스트 용이성을 높입니다.
⚡ 중요성
테스트가 쉬우면 리팩토링과 요구사항 변화 대응 비용이 줄어듭니다.
📊 현재 상황 분석
entities와 features에 UI setState가 섞여 있어 유닛 테스트 시 많은 mocking과 환경 구성이 필요합니다. 반면 entities의 순수 API 함수(일부)는 테스트하기 쉬움.
📝 상세 피드백
개념 정의
테스트 용이성은 코드가 단위/통합 테스트로 검증되기 쉬운 정도입니다. 순수 함수와 사이드이펙트 분리는 테스트 작성을 용이하게 합니다.
중요성
테스트 가능한 구조는 변경 시 안정성을 보장하고 리팩토링 비용을 낮춥니다.
근거(Evidence)
- 의존성 주입(DI), 순수 함수 권장
현재 코드 분석 (PR 근거)
- 장점: entities의 API 함수가 대부분 httpClient 호출 후 JSON 반환 형태로 작성되어 있어 mocking이 가능함.
- 문제점:
- entities 함수에 setState 콜백이 들어가 있는 경우(예: fetchPostsByTag(setLoading, setPosts,...))는 유닛 테스트가 어려움.
- features/*/api 모듈들이 setState를 직접 조작하므로 테스트 대상이 UI 상태에 강하게 의존.
- 일부 mutation 훅에서 가짜 응답을 생성(return {id, postId: null})하는 부분이 있어 실제 API와의 차이로 오탐 가능.
변화 시나리오
- 외부 API 계약이 바뀔 때, 현재는 여러 레이어의 mocking 코드를 수정해야 할 수 있음.
AS-IS 예시
- src/entities/post/model/postUtils.ts: setLoading 등의 콜백 사용으로 단위 테스트에서 많은 mocking 필요.
TO-BE 예시
- entities/post/api.fetchPosts => 순수 반환 함수 (mocking 쉬움)
- features/hooks/usePosts => useQuery로 래핑, queryFn을 모킹하여 테스트 가능
개선 단계
- 단기(1~2일): entities 함수에서 setState 제거 -> 순수 API로 전환.
- 중기(1~2일): use*Queries 훅에 dependency injection(예: api 모듈 주입) 추가하여 테스트에서 목 주입 가능.
- 장기(1~2일): 주요 feature에 대해 단위/통합 테스트(React Testing Library + msw)를 작성하여 보호막 확보.
❌ 현재 구조 (AS-IS)
src/entities/post/model/postUtils.ts: setLoading, setPosts를 인자받아서 직접 UI를 조작함 → 단위 테스트 어려움✅ 권장 구조 (TO-BE)
entities/post/api.fetchPosts (순수), usePosts hook (useQuery로 래핑) → useQuery의 queryFn을 모킹하여 테스트 가능🔄 변경 시나리오별 영향도
- 새 외부 API 도입: 순수 API면 msw로 쉽게 시뮬레이션 가능, 현재 구조는 UI 상태 콜백 때문에 테스트 복잡도가 상승.
- 낙관적 업데이트 로직 변경: onMutate/rollback 패턴이 있으면 테스트 가능한 경로가 명확해짐.
🚀 개선 단계
- 1단계: 단기(1~2일): entities에서 UI 콜백 제거(순수 API로 전환)하여 모킹을 단순화.
- 2단계: 단기(1~2일): use*Queries 훅에 api 모듈 주입 가능하도록 refactor (테스트에서 목 주입).
- 3단계: 중기(1~2일): MSW를 이용한 통합 테스트 시나리오(게시물 리스트, 댓글 추가/삭제) 작성.
8. ⚛️ 현대적 React 패턴
💡 개념 정의
현대적 React 패턴은 Suspense, ErrorBoundary, 커스텀 훅 및 선언적 데이터 패칭을 통해 관심사 분리를 강화하는 방법들입니다.
⚡ 중요성
로딩/에러 처리를 중앙화하면 컴포넌트가 단순해지고 UX 변경 시 수정 범위를 줄일 수 있습니다.
📊 현재 상황 분석
useQuery/useMutation과 custom hook 분리는 잘 되어 있으나 Suspense/ErrorBoundary 활용은 아직 적용되지 않아 로딩/에러 UX 변경 시 수정 범위가 분산될 수 있습니다.
📝 상세 피드백
개념 정의
현대적 React 패턴은 Suspense, Error Boundary, 커스텀 훅, 선언적 데이터 패칭(React Query) 등을 포함합니다.
중요성
이 패턴들은 로딩/에러 처리를 선언적으로 만들고, 관심사 분리를 강화하여 컴포넌트 코드를 단순화합니다.
근거(Evidence)
- React 공식 문서(Suspense, Error Boundaries)
- TanStack Query + Suspense 통합 가이드
현재 코드 분석 (PR 근거)
- 장점:
- TanStack Query 사용, QueryClientProvider와 Devtools 추가 — modern pattern 적용.
- 커스텀 훅(usePostFeature, useCommentFeature, useUserFeature)로 비즈니스 로직을 캡슐화함.
- 개선 포인트:
- Suspense와 ErrorBoundary 미사용: useQuery 훅이 local loading/error 상태를 컴포넌트에서 처리하고 있어 선언적 로딩 처리를 더 활용할 수 있음.
- 일부 컴포넌트가 여전히 imperative한 상태 관리(예: setShowAddDialog 등)를 가지고 있으나 이는 UI state로 허용됨.
변화 시나리오
- 로딩 UX 개선을 위해 Suspense를 도입하면 개별 컴포넌트의 로딩 표시 코드를 줄이고 fallback 컴포넌트를 중앙에서 관리 가능.
AS-IS 예시
- CommentList에서 isLoading/ error를 사용해 직접 렌더링 분기 처리.
TO-BE 예시
- <ErrorBoundary fallback={}><Suspense fallback={}>
개선 단계
- 단기(1일): 중요 UI 경로(게시물 리스트, 상세)의 loading fallback을 Suspense로 전환(쿼리 옵션 useSuspenseQuery 사용 또는 suspense:true).
- 중기(1~2일): ErrorBoundary 컴포넌트 도입 및 공통 에러 UI 제공.
❌ 현재 구조 (AS-IS)
src/features/comment/ui/CommentList.tsx에서 isLoading과 error로 직접 분기 처리함.✅ 권장 구조 (TO-BE)
<ErrorBoundary fallback={<ErrorFallback/>}><Suspense fallback={<CommentListSkeleton/>}><CommentList postId={id} /></Suspense></ErrorBoundary>🔄 변경 시나리오별 영향도
- 로딩 UX 변경(스켈레톤 디자인 적용): Suspense 사용 시 훨씬 적은 변경으로 전체 UX 업데이트 가능.
- 에러 처리 정책 변경: ErrorBoundary 적용 시 한 곳에서 일관된 에러 UI 제공 가능.
🚀 개선 단계
- 1단계: 단기(1일): 주요 리스트/상세 컴포넌트에 Suspense fallback을 적용해 선언적 로딩 처리 실험.
- 2단계: 단기(0.5~1일): ErrorBoundary 컴포넌트 도입 및 공통 에러 UI 구성.
- 3단계: 중기(1~2일): TanStack Query의 suspense 옵션을 도입하고, 에러 로깅(예: Sentry) hook 통합.
9. 🔧 확장성
💡 개념 정의
확장성은 새로운 기능(다국어, 실시간, 오프라인 등)을 도입할 때 시스템이 얼마나 쉽게 적응하는지를 나타냅니다.
⚡ 중요성
확장성 높은 구조는 요구사항 변화 시 수정 비용과 위험을 줄입니다.
📊 현재 상황 분석
FSD 기반 구조와 Query+Zustand 조합은 좋은 출발이나, server-state와 client-state 중복 저장(comments in Zustand vs Query)이 확장성 저해요인입니다. queryKey도 정리 필요.
📝 상세 피드백
개념 정의
확장성은 새로운 기능이나 비기능 요구사항(다국어, A/B 테스트, 오프라인 등)을 도입할 때 기존 코드 변경량과 복잡도를 의미합니다.
중요성
확장성 높은 구조는 새로운 요구사항 추가 시 위험·비용을 줄여줍니다.
근거(Evidence)
- 모듈화, 인터페이스 기반 확장성 원칙
현재 코드 분석 (PR 근거)
- 장점: FSD 적용 의도, features/widgets/ shared 분리 등으로 확장성 기본을 갖춤.
- 개선 포인트:
- Post vs Comment 도메인 분리 질문이 의미하는 것처럼 엔티티 간 강한 연관(댓글이 게시물에 의존)이 있어 도메인 경계 설계가 중요합니다.
- queryKey가 단순 리터럴이라 필터/정렬/페이지가 증가하면 복잡해짐.
- comments 서버 상태를 일부 Zustand에 중복 저장하고 있어 실시간/오프라인 요구사항 구현 시 충돌 가능.
변화 시나리오
- 다국어(i18n) 추가: UI 텍스트는 shared로 모아두면 적은 파일만 수정하면 되지만 컴포넌트 내부 하드코딩 문구가 많으면 광범위 수정 필요.
- 오프라인/실시간 추가: server-state를 Query가 담당하면 오프라인 전략(TanStack Query + IndexedDB 캐쉬)을 적용하기 쉬움. Zustand에 서버 데이터를 중복 저장하면 복잡성 증가.
AS-IS 예시
- comments 데이터가 Zustand(comments)와 TanStack Query(comments query) 둘 다에 존재하는 케이스(중복 보유) — 확장 시 동기화 문제 발생 가능.
TO-BE 예시
- 서버 상태는 TanStack Query 단일 소유로 일원화하고 UI 상태만 Zustand에서 관리.
개선 단계
- 단기(1일): comments 같은 서버 데이터는 store에서 제거하고 Query만 사용(레거시 호환 어댑터 제공).
- 중기(1~2일): queryKeys 팩토리와 API 팩토리를 도입하여 필터/정렬/페이징 확장에 유연하게 대응.
- 장기(2~3일): 다국어, 실시간, 오프라인 전략에 대한 정책 문서와 샘플 구현 추가.
❌ 현재 구조 (AS-IS)
src/features/comment/store/commentStore.ts 에 comments: Record<number, Comment[]> 필드 존재. 동시에 useComments useQuery가 있는데 두 가지 상태 소유로 중복됨.✅ 권장 구조 (TO-BE)
Remove comments from Zustand store. useComments(query)만 서버 데이터를 소유하고, UI 상태(모달 오픈 등)만 Zustand로 유지.🔄 변경 시나리오별 영향도
- 다국어 추가: shared에 텍스트 추출/관리 구조가 있으면 최소 변경으로 적용 가능. 하드코딩이면 많은 파일 수정 필요.
- 오프라인 지원: 서버 상태를 TanStack Query로 일원화하면 캐시 기반 전략 적용이 쉬움.
🚀 개선 단계
- 1단계: 단기(1일): comments 등 서버 데이터의 Zustand 저장 제거 및 feature 훅에서 Query 결과를 직접 사용하도록 전환.
- 2단계: 단기(1~2일): queryKeys 및 apiFactory 도입으로 필터/정렬 확장에 유연성 확보.
- 3단계: 중기(2~3일): 오프라인/실시간 전략(예: TanStack Query + BroadcastChannel/IndexedDB) 검토 및 PoC 작성.
10. 📏 코드 일관성
💡 개념 정의
코드 일관성은 네이밍, 파일명, import/export 패턴, 스타일 규칙이 통일되어 있는 정도를 의미합니다.
⚡ 중요성
일관성은 신규 개발자 온보딩과 병합시 충돌을 줄이고 자동화 도구 적용을 가능하게 합니다.
📊 현재 상황 분석
대부분 규칙을 잘 지키고 있으나 default vs named export 혼용, queryKey 사용 방식에 따른 일관성 이슈가 보임.
📝 상세 피드백
개념 정의
코드 일관성은 네이밍·파일 구조·import/export 패턴·스타일 가이드가 통일되어 있는지를 의미합니다.
중요성
일관성은 온보딩 시간 단축 및 코드리뷰·자동화 도구 적용 시 편의성을 제공합니다.
근거(Evidence)
- 팀 컨벤션, ESLint/Prettier와 같은 자동화 도구 권장
현재 코드 분석 (PR 근거)
- 긍정적 사항: 대부분 파일명이 PascalCase(컴포넌트), 훅은 use*-형태, shared/ui 컴포넌트 구조 일관.
- 발견된 이슈(구체적):
- Import/Export 패턴 혼재 가능성: widgets/Header/Footer는 default export를 사용(원본 파일에서 default로 내보냄)하고 widgets index는 named export로 재노출(export { Header } from './Header'). 코드베이스 내에서 default vs named export 혼용이 있음.
- 파일 확장자 혼재 없음(대부분 .ts/.tsx) — 괜찮음.
- 일부 변수/함수 네이밍: query 키에서 리터럴 배열을 많이 쓰는 점(일관된 queryKeys 팩토리 미비).
- 코드 스타일: PR에서 세미콜론 표준화가 이루어짐(일관성 향상).
변화 시나리오
- 여러 팀이 병합하는 경우: export 스타일(디폴트 vs named) 불일치는 리팩토링 비용을 증가시킬 수 있음.
AS-IS 예시
- widgets/Header/index.ts: export { default as Header } from './Header';
- features/post/ui/PostTable.tsx: export const PostTable = (...) — named export
TO-BE 권장
- 컴포넌트: 항상 named export (export const Header = ...; export { Header } from './Header')
- 훅: use* 파일명과 named exports 유지
- shared/api/queryKeys 등 공통 규칙 문서화
개선 단계
- 단기(0.5~1일): Export 규칙(컴포넌트는 named export) 문서화 및 간단한 lint 룰 적용.
- 중기(1~2일): queryKeys 규칙 도입 및 코드베이스에 적용(자동 리팩토링 스크립트 활용 가능).
❌ 현재 구조 (AS-IS)
widgets/Header/index.ts: export { default as Header } from './Header';
features/post/ui/PostTable.tsx: export const PostTable = ... (named export)
// default/named 혼용✅ 권장 구조 (TO-BE)
컴포넌트는 모두 named export로 통일:
// Header.tsx
export const Header = () => { ... }
// widgets/Header/index.ts
export { Header } from './Header';🔄 변경 시나리오별 영향도
- 새 팀과 코드 병합 시: export 스타일 불일치로 인한 리팩토링 비용 증가.
- 정적 분석 도구 도입 시: 일관된 naming/exports가 있으면 적용이 쉬움.
🚀 개선 단계
- 1단계: 단기(0.5~1일): Export 스타일 가이드(컴포넌트: named export)를 문서화하고 코어 컴포넌트에 적용.
- 2단계: 단기(0.5~1일): ESLint/Prettier 설정 공유 및 자동 적용 스크립트 구성.
- 3단계: 중기(1~2일): queryKey와 API export 컨벤션을 shared 문서로 정리.
🎯 일관성 체크포인트
Import/Export 패턴
- widgets/Header, widgets/Footer는 default export를 사용한 뒤 index에서 named로 재노출 — default vs named export 혼용으로 일관성 부족
변수명 규칙
- queryKey 리터럴 사용이 산재(예: ['posts', limit, skip], ['posts','search',query]) — queryKeys 팩토리 사용 권장
코드 스타일
- 대체로 통일되어 있으나 일부 파일에서 과거 스타일 잔재 가능성 있음(이미 통일 작업이 진행된 것으로 보임)
11. 🗃️ 상태 관리
💡 개념 정의
서버 상태와 클라이언트 상태를 명확히 분리: 서버 상태는 TanStack Query, 로컬 UI 상태는 Zustand처럼 가볍고 캡슐화된 저장소가 담당해야 합니다.
⚡ 중요성
중복 상태를 피하면 동기화/버그/테스트 복잡도가 줄고 실시간/오프라인 요구사항을 도입하기 쉬워집니다.
📊 현재 상황 분석
Zustand로 UI 상태 관리는 적절하나 commentStore에 서버 데이터(comments)가 중복되어 존재함. 이는 상태 불일치와 유지보수 비용 증가의 원인입니다.
📝 상세 피드백
개념 정의
상태관리는 클라이언트(UI) 상태와 서버 상태를 명확히 분리하는 것을 의미합니다. 서버 상태는 TanStack Query가, 로컬 UI 상태는 Zustand 같은 경량 스토어가 담당하는 것이 권장됩니다.
중요성
명확한 역할 분리는 데이터 동기화 문제를 줄이고 테스트/확장성/성능을 향상시킵니다.
근거(Evidence)
- TanStack Query 공식 문서 권장 패턴: server-state는 Query, client-state는 local store
현재 코드 분석 (PR 근거)
- 장점: Zustand는 UI 상태(selectedPost, dialogs 등)에 사용되고 TanStack Query는 posts/comments/users fetching에 적용되어 있음.
- 문제점: commentStore에 comments: Record<number, Comment[]>가 포함되어 있어 서버 상태가 Zustand와 Query(또는 엔티티 상태) 사이에 중복 저장됨(src/features/comment/store/commentStore.ts). 중복 상태는 동기화 문제와 버그의 원인이 됩니다.
- features/comment/hooks/useCommentFeature.tsx는 Zustand 상태를 사용하고 useCommentQueries의 mutations을 호출하는 패턴으로 적절히 분리되어 있음. 다만 comments 자체를 Zustand에 보관하는 부분을 제거하면 더 명확해집니다.
변화 시나리오
- 실시간 동기화(웹소켓) 추가: 서버 상태가 Query에만 있으면 서버-클라이언트 동기화는 queryClient.setQueryData로 일원화 가능. Zustand에 중복 저장시 동기화 로직 두 곳 필요.
AS-IS 예시
- src/features/comment/store/commentStore.ts: comments: Record<number, Comment[]> 상태 포함.
- src/features/comment/hooks/useCommentFeature.tsx: useComments(query)로 데이터를 가져오지만, 동시에 Zustand의 comments를 사용하는 코드 일부 존재.
TO-BE 예시
- Zustand store는 UI state(모달, 폼 입력, 선택된 항목)만 소유.
- 서버 데이터는 useComments/usePosts 등 TanStack Query 훅만 소유.
개선 단계
- 단기(1일): commentStore의 comments 필드를 제거하고, UI는 useComments 쿼리 데이터를 바로 사용하도록 변경.
- 중기(1~2일): 모든 feature에서 "서버 상태는 Query, 클라이언트 상태는 Zustand" 규칙을 문서화하고 적용.
- 장기(2일): 실시간/오프라인 기능을 위해 queryClient를 기반으로 한 동기화 전략(예: onMessage -> setQueryData)을 설계.
❌ 현재 구조 (AS-IS)
src/features/comment/store/commentStore.ts: comments: Record<number, Comment[]> 존재 (server-state가 store에 있음)✅ 권장 구조 (TO-BE)
commentStore는 showAddCommentDialog, showEditCommentDialog, selectedComment, newComment 등의 UI 상태만 소유. comments 데이터는 useComments(postId)로 조회.🔄 변경 시나리오별 영향도
- 실시간 댓글 업데이트 추가: Query 중심이면 websocket 수신 시 queryClient.setQueryData만 업데이트하면 되지만, Zustand에 중복 보관하면 두곳 동기화 필요.
- 오프라인 모드 추가: Query 기반 캐시 전략을 활용하는 편이 쉬움.
🚀 개선 단계
- 1단계: 단기(1일): commentStore의 서버 데이터(comments) 제거. UI는 useComments 쿼리 결과를 직접 사용.
- 2단계: 단기(1~2일): 프로젝트 문서에 'server-state: TanStack Query / client-state: Zustand' 규칙을 명시하고 코드베이스에 적용.
- 3단계: 중기(2일): 실시간/오프라인 계획 수립 시 queryClient 기반 전략(예: websocket -> setQueryData)을 우선 고려.
🤔 질문과 답변
- Post와 Comment 도메인 분리 관련
인사이트 요약: Post와 Comment를 별도 entities로 분리한 결정은 도메인 관점에서 합리적입니다. 댓글은 독립적인 CRUD(작성, 수정, 삭제, 좋아요)를 가지므로 재사용성과 테스트 관점에서 분리하는 것이 좋습니다. 그러나 Post와 Comment의 관계(댓글이 게시물에 종속)는 feature 레벨에서 명확히 연계되어야 합니다.
권장 구조:
- entities/post: Post 타입, post API
- entities/comment: Comment 타입, comment API
- features/post: 게시물 관련 비즈니스(게시물 목록/상세, 댓글 표시 트리거) — 게시물 상세 열기 시 댓글 쿼리(useComments(postId))를 호출
- features/comment: 댓글 작성/수정/삭제와 관련된 훅과 UI
왜? Post와 Comment를 통합하면 재사용성과 변경 시 영향범위가 커질 수 있습니다. 다만 두 도메인의 일관된 트랜잭션(예: 댓글 추가 후 Post의 댓글Count 즉시 반영)이 필요하면 feature 레벨에서 queryClient.setQueryData나 invalidateQueries를 통해 동기화합니다.
추가 질문(고민거리): - 댓글이 항상 Post와 1:1 관계로만 사용되는가(다른 리소스와도 연동될 가능성)? 만약 댓글이 다른 엔티티에도 붙는다면 별도 entities로 유지하는 편이 낫습니다.
- TanStack Query 쿼리 키 관리 전략
권장 패턴:
- queryKeys 팩토리 도입(shared/api/queryKeys.ts) — 키를 생성하는 함수를 통해 일관성 보장.
- 키에 옵션 객체를 사용하되, 안정적인 직렬화 규칙을 적용(예: stable-json-stringify 사용 또는 객체 자체를 배열 두 번째 요소로 사용).
예시:
export const queryKeys = {
posts: (params = {}) => ['posts', params] as const,
post: (id) => ['posts', 'item', id] as const,
comments: (postId) => ['comments', postId] as const,
};
이유: - filter/sort/limit/skip가 늘어나면 키 생성 로직만 바꾸면 되고 오타로 인한 버그도 줄어듭니다.
실무 팁: - 불필요한 전체 invalidate 대신 setQueryData로 국지적 업데이트 권장(성능/UX 관점).
- 복잡한 필터(예: 다양한 조합)는 params 객체를 키로 두고 exact:false로 invalidate할 때 범위를 좁히는 전략을 혼합 사용.
- Zustand와 TanStack Query 조합 패턴(특히 onSuccess에서 queryClient.setQueryData 사용)
현재 PR에서의 패턴(요약):
- UI 상태: Zustand
- 서버 상태: TanStack Query
- mutation의 onSuccess에서 queryClient.setQueryData로 캐시를 직접 갱신
평가 및 권장: - 이 패턴은 합리적이며 실무에서 널리 사용됩니다. 다만 주의할 점:
- mutation이 서버에서 반환하는 응답 shape을 신뢰해야 함. 가짜 응답을 리턴하거나 postId가 null인 경우(onSuccess에서 전역 쿼리를 순회하는 패턴)는 API 변경 시 취약함.
- 낙관적 업데이트는 onMutate/onError/rollback 패턴을 사용해 일관성 있게 처리해야 합니다.
권장 구현 방식(예):
useMutation({
mutationFn: api.addComment,
onMutate: async (newComment) => {
await queryClient.cancelQueries(queryKeys.comments(newComment.postId));
const previous = queryClient.getQueryData(queryKeys.comments(newComment.postId));
queryClient.setQueryData(queryKeys.comments(newComment.postId), old => ({ comments: [...(old?.comments||[]), tempComment] }));
return { previous };
},
onError: (err, variables, context) => {
queryClient.setQueryData(queryKeys.comments(variables.postId), context.previous);
},
onSettled: (data, err, variables) => {
queryClient.invalidateQueries(queryKeys.comments(variables.postId));
}
});
추가 권장사항:
- 가능한 경우 mutationFn이 반환하는 실제 서버 응답을 사용하여 setQueryData를 업데이트(가짜 응답 사용 지양).
- 전체 쿼리 탐색(getQueriesData with exact:false) 대신 queryKeys 팩토리로 명확한 대상만 업데이트.
🎯 셀프 회고 & 제안
읽어본 감상 및 인사이트
당신의 회고에서 보이는 핵심은 '개념을 접하고 적용하는 과정에서의 불안감'과 '구조화하려는 의지'입니다. FSD와 TanStack Query를 함께 도입한 시도는 매우 좋은 출발입니다. 중요한 점은 "적용의 깊이"입니다. 현재 PR은 개념을 적용하려는 의도가 분명하지만, 일부 계층에서 책임이 혼재되어 있어(entities에서 UI 콜백 또는 상태 관리) 개념의 본질(계층 분리, server/client state 분리)이 흐려질 수 있습니다.
추가로 생각해볼 질문들
- FSD의 'features' 계층을 더 세분화한다고 했을 때, 어떤 기준으로 분리할 것인가요? (행동 단위? 화면 단위? 재사용 가능 단위?)
- TanStack Query의 "낙관적 업데이트"를 안전하게 적용하기 위해 어떤 실패/롤백 시나리오를 고려하고 있는가요? 실패 시 사용자 경험을 어떻게 보장할지 시나리오를 하나 적어보세요.
- 현재 entities에 남아있는 유틸리티들은 정말 "도메인 순수"한가요? 만약 UI 콜백을 제거한다면 해당 유틸을 어디로 옮길지(entities vs features vs shared)를 명확히 정해보세요.
구체적 제안
- "작은 리팩토링의 반복"을 추천합니다: 한 번에 모든 것을 바꾸려 하기보다는 'entities에서 setState 제거' → 'queryKeys 도입' → 'comments 서버 상태 제거' 식으로 단계별로 진행하세요. 이렇게 하면 성취감을 지속적으로 얻으면서 큰 구조도 개선할 수 있습니다.
추가 논의가 필요한 부분이 있다면 언제든 코멘트로 남겨주세요!
코드 리뷰를 통해 더 나은 아키텍처로 발전해 나가는 과정이 즐거웠습니다. 🚀
이 피드백이 도움이 되었다면 👍 를 눌러주세요!
과제 링크
https://adds9810.github.io/front_6th_chapter2-3/
과제 체크포인트
기본과제
목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기
체크포인트
심화과제
목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기
체크포인트
최종과제
과제 셀프회고
컨디선 난조로 언제나 괴발새발이었지만 더 괴발새발 했던 한주였습니다.ㅜ 매 회차 새로운걸 알게 되는 것 좋은데 기한 안에 이해하면서 적용이란 쉽지 않은 것 같습니다. 질문은 못하고 다른분들 코드나 토론하는거 들으면서 이렇게 생각하면서 짤 수 있구나를 많이 알게된 한 주였습니다.(항해 끝나면 꼭 나왔던 것들 정리하고 해보자 다짐도...)
이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.
FSD(Feature-Sliced Design) 아키텍처를 통해 대규모 애플리케이션에서 코드를 체계적으로 구조화하는 방법에 대해 알게 되었습니다. 이런 개념에 대해 몰라 작업시 일관성 없이 코드를 모아두곤 했는데 shared → entities → features → widgets → pages 순서로 계층을 나누면서 각 부분의 책임이 명확해지고 코드의 위치를 예측하기 쉬워졌습니다.(그렇지만 FSD의 개념 이해 미흡과 도메인, ui기준으로 나누던 습관이 있어 좀 불편...)
또한 Zustand와 TanStack Query를 함께 사용하여 클라이언트 상태와 서버 상태를 효과적으로 분리하는 방법을 경험했습니다. Zustand로 UI 상태를 관리하고, TanStack Query로 서버 데이터를 관리하면서 각 도구의 장점을 최대한 활용할 수 있다는 것을 깨달았습니다. 특히 TanStack Query의 캐싱과 낙관적 업데이트 기능을 통해 사용자 경험을 크게 향상시킬 수 있다는 점이 새로웠습니다.
본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?
FSD(Feature-Sliced Design) 아키텍처 원칙을 지키며 코드를 구조화하는 데 가장 많은 노력을 기울였습니다. shared → entities → features → widgets → pages 순서로 체계적으로 진행하면서, 각 계층별로 적절한 책임을 분리하고 코드의 응집도를 높이고 결합도를 낮추려 꾸준히 리팩토링했습니다. 각 엔티티별로 API 로직과 모델을 분리하며, 마지막에
PostManager위젯을 더 작은 단위의 위젯(FilterBar, SearchBar, PostTable 등)으로 분리한 것이 가장 큰 성과였습니다.아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.
FSD 아키텍처에서
features계층을 더 세분화하는 방법이 막연합니다. 현재는 도메인별로만 나누어져 있는데,features/post안에search,create,edit등 기능별로 더 나눴어야 하지 않았나 싶습니다. 시간이 부족해서 현재에 만족하지만, 실제로는 어느 정도까지 세분화해야 하는지에 대한 기준이 명확히 해서 더 나눌수 있으면 나눠봐야겟다 생각했습니다.또한, TanStack Query의 낙관적 업데이트(Optimistic Updates) 적용이 아직은 막연하게 느껴집니다. 이번 과제에서는 기본적인
useMutation의onSuccess콜백에서 쿼리를 무효화하는 방식으로 데이터 동기화를 처리했지만, 사용자 경험을 즉각적으로 개선할 수 있는 낙관적 업데이트를 실제 프로덕션 환경에서 자신 있게 적용하려면 더 깊은 이해와 연습이 필요할 것 같습니다.이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.
이번 과제를 통해 UI 기준(페이지별)으로만 구현하던 기존 방식에서 벗어나, FSD의 명확한 구분 기준을 어느 정도 적용할 수 있게 되었습니다. 완전한 적용은 아니지만, 이제 어느 정도 구분을 갖고 코드를 나눌 수 있는 기준을 갖게 된 것 같습니다.
또한 AI의 도움을 받아 TanStack Query의 캐싱 전략 최적화나 suspense와 결합한 로딩 UX 구현 같은 고급 기능들이 있다는 것을 알게 되었습니다. 아직 직접 구현해보지는 못했지만, 이런 기술들이 존재한다는 것을 알게 된 것만으로도 큰 수확이었다고 생각합니다.
챕터 셀프회고
클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기
3주간 진행했던 클린코드 과제는 마치 복잡한 실타래를 푸는 과정 같았습니다. 처음 마주했을 때는 막막했지만, 변수 하나를 잘못 건드려 연쇄적으로 에러가 발생하는 경험을 통해 클린코드의 중요성을 깊이 체감했습니다.
더티코드를 무작정 고치지 않고, 문제를 분석하고 단계별로 리팩토링하며 기능을 유지하는 점진적 개선의 효과를 배웠습니다. 특히 모호한 변수명과 전역 변수 남용은 코드 의존성을 파악하기 어렵게 만들어, 의미 있는 네이밍과 일관된 컨벤션의 필요성을 절실히 느꼈습니다.
또한 코드 리뷰를 통해 혼자서는 놓치던 부분을 발견하며, 협업을 위한 소통과 배려가 읽기 좋은 코드만큼 중요하다는 것을 알게 되었습니다. 이번 과제는 단순한 리팩토링을 넘어, 읽기 쉽고 유지보수하기 좋은 코드가 곧 협업의 기본이라는 점을 배운 경험이었습니다.
결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리
처음 마주했을 때는 복잡한 기계 설계도를 억지로 해석하는 기분이었습니다. 모든 기능이 한 덩어리에 뒤섞여 있어 어디서부터 손대야 할지 막막했고, 지난주에 겪었던 연쇄 에러 경험은 작은(?) 수정조차 전체에 영향을 줄까 하는 두려움으로 이어졌습니다.
하지만 상태 관리와 디자인 패턴을 적용하고, 로직을 순수 함수로 분리하면서 입력과 출력이 명확해졌습니다. 그 결과 테스트와 재사용이 쉬워지고, 거대한 코드 일부가 깔끔하게 정리되는 변화를 체감할 수 있었습니다.
이 과정에서 컴포넌트들은 점차 독립적인 책임을 갖게 되었고, 이해하기 쉬우며 수정이나 기능 추가도 한결 안전해졌습니다. 결국 하나의 덩어리 같던 코드가 여러 개의 작은 부품으로 나뉘는 과정을 보며, 유지보수하기 좋은 코드의 의미를 몸소 배울 수 있었습니다.
응집도 높이기: 서버상태관리, 폴더 구조
처음에는 “이 코드를 어디에 둬야 하지?”라는 고민이 많았지만, **FSD(Feature-Sliced Design)**를 적용하면서 코드 위치가 예측 가능해지고 책임이 명확해졌습니다. 도메인과 UI 기준으로만 나누던 습관 때문에 초반엔 불편했지만, entities 중심으로 모델과 API를 분리하고 widgets를 쪼개면서 점차 응집도가 높아졌습니다. 또한 TanStack Query를 도입해 서버 상태를 분리하자, 로딩, 에러, 캐싱을 직접 관리하던 복잡함에서 벗어나 선언적인 코드 작성이 가능해졌습니다. 특히 캐싱과 낙관적 업데이트는 사용자 경험을 개선할 수 있는 가능성을 보여줬고, 클라이언트 상태와 서버 상태를 명확히 구분할 수 있었습니다. 이번 경험을 통해 폴더 구조와 상태 관리의 정리는 단순한 형식이 아니라, 코드를 가볍고 예측 가능하게 만드는 핵심 전략이라는 걸 깨달았습니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문
['posts'],['comments', postId]와 같이 단순한 형태로 쿼리 키를 사용하고 있는데, 필터링이나 정렬이 추가될 경우['posts', { limit: 10, skip: 0, tag: 'react' }]같은 복잡한 쿼리 키가 필요할 것 같습니다. 이런 경우를 대비한 쿼리 키 팩토리 함수나 구조화된 쿼리 키 패턴에 대한 조언을 구합니다.onSuccess에서queryClient.setQueryData로 직접 캐시를 조작하는 현재 방식이 올바른 패턴인지, 그리고 더 나은 방법이 있는지 리뷰받고 싶습니다.