Conversation
- 불필요한 useEffect 제거 - LIMIT_OPTIONS 상수 분리 - posts, author 병합로직 post entity에 추가
e8a6e69 to
d01d51d
Compare
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요, 정유열 님! 5주차 과제 잘 진행해주셨네요. 고생하셨습니다 👍🏻👍🏻
현재 남기는 코멘트는 GPT-5-mini model을 사용하여 AI가 남기는 피드백입니다 🤖
과제 피드백과는 별개이므로 참고해주세요!
1. 🏗️ FSD 아키텍처
💡 개념 정의
Feature-Sliced Design(FSD)는 관심사별로 코드를 물리적/논리적으로 분리해 협업과 변경에 유연하도록 만드는 아키텍처 원칙입니다. 권장된 의존성 방향은 app → pages → widgets → features → entities → shared(하위 계층 참조만 허용)이며, 각 slice는 public API(주로 index.ts)만 통해 외부에 노출해야 합니다.
⚡ 중요성
FSD 준수는 리팩토링, 아키텍처 변화(모노레포/마이크로프론트엔드), 기술 교체 시 영향 범위를 최소화합니다. 명확한 레이어 경계는 변경 시 수정 파일 수를 줄이고, 팀 단위 협업에서 안전한 리팩토링을 가능하게 합니다.
📊 현재 상황 분석
AS-IS: 대규모 단일 파일(초기 PostsManagerPage)에서 UI/비즈니스/상태/서버 호출이 섞여 있던 구조를 widgets/features/entities로 잘 분리했습니다. 그러나 코드베이스 내에서 import 경로 표기(@features vs @/features)가 혼재되어 있고, 일부 컴포넌트(예: features의 dialog 관련 컴포넌트)가 UI와 비즈니스 로직을 혼합한 채로 남아 있는 경우가 있습니다. 또한 public API(배럴)에 필요한 항목만 노출하는 규칙은 잘 지켜졌으나, 일부 내부 구현을 노출하지 않도록 stricter한 인덱스 관리가 필요합니다.
📝 상세 피드백
PR은 Feature-Sliced Design(FSD)을 의도적으로 적용한 좋은 사례입니다. layers (app → pages → widgets → features → entities → shared) 가 대부분 지켜지고 있으며, 각 slice가 배럴(index)로 외부 노출하는 패턴도 적용되어 있습니다. 다만 몇 가지 개선 포인트(의존성 표시의 일관성, public API 경계 강화)가 보입니다. 아래 항목에서 구체적으로 문제와 개선안을 제시합니다.
❌ 현재 구조 (AS-IS)
AS-IS: 초기 PostsManagerPage에 모든 로직(데이터 fetch, 상태, UI)이 결합되어 있었음. 예: src/pages/PostsManagerPage.tsx (원본 긴 버전) — fetch, setState, UI, dialog 컨트롤이 한 파일에 존재.✅ 권장 구조 (TO-BE)
TO-BE: PostsManagerPage는 위젯/feature를 조합만 하고, 각 관심사는 features/entities/shared로 분리됩니다. 예: 페이지는 <PostManagerHeader />, <PostFilters />, <PostTable /> 등 위젯만 렌더하고, 실제 비즈니스 로직은 features/hooks와 entities/api에 존재합니다.🔄 변경 시나리오별 영향도
- 만약 UI 라이브러리를 Material UI로 교체하면: 현재 shared/ui 내부 컴포넌트(약 12개 파일)를 재작성해야 함. shared/ui를 adapter 레이어로 추상화하면 수정 파일 수를 1~2개로 줄일 수 있음.
- 모노레포 전환 시: entities, features를 독립 패키지로 분리하려면 각 slice의 public API가 명확해야함. 현재 배럴 사용은 이 전환을 수월하게 함.
- 새로운 feature 추가(예: 북마크): feature로 추가할 컴포넌트와 entities로 추가할 API 경계가 명확하면 기존 코드 변경은 최소화됨.
🚀 개선 단계
- 1단계: 단기(책임 경계 강화, 1일): import alias 통일(@features vs @/features) 및 eslint/tsconfig 경로 규칙 고정. 배럴 index 파일에서 외부 노출 리스트 검토(불필요 노출 차단).
- 2단계: 중기(쿼리/공용 규칙 정립, 2-3일): 각 slice(entities/features/widgets) 별 Public API 체크리스트 작성(무엇을 export/비노출 할지).
- 3단계: 장기(아키텍처 확장성, 1주): shared adapter 레이어(디자인 시스템 어댑터, HTTP adapter) 추가로 외부 기술 스택 변경 시 영향 범위 최소화.
2. 🔄 TanStack Query
💡 개념 정의
TanStack Query는 서버 상태 관리 라이브러리로, 캐싱, 재검증, 낙관적 업데이트, 에러/로딩 상태 관리를 선언적으로 제공합니다. 핵심패턴은 체계적인 queryKey 관리, queryFn과 비즈니스 로직의 계층 분리(entities -> features)입니다.
⚡ 중요성
API 엔드포인트 변경, 새로운 데이터 소스 추가, 에러 핸들링/모니터링 도구 추가 등의 변화에 직면했을 때 수정 범위를 줄여줍니다. 일관된 쿼리 키는 invalidate/optimistic update의 정확도를 확보합니다.
📊 현재 상황 분석
AS-IS: queries와 mutations가 entities에 잘 모여 있음. 그러나 query key factory(예: shared/api/queryKeys)를 사용하지 않아 키가 각 파일에 하드코딩되어 있음(예: ['posts', 'list', { limit, skip }] vs ['comments', postId]). 또한 에러 처리 대부분이 console.error에 의존해 일관된 UX/로깅/모니터링으로 연결되지 않음. HTTP 호출이 fetch로 직접 구현되어 있어 HTTP 클라이언트 변경 시 여러 파일을 고쳐야 합니다(현재 약 3개 엔티티 인덱스 파일에 fetch 사용: post/index.ts, comment/index.ts, user/index.ts).
📝 상세 피드백
TanStack Query 사용이 entities 계층에서 일관되게 적용되어 있고, QueryProvider에 전역 기본옵션(staleTime 5분 등)을 설정한 점이 좋습니다. optimistic update 패턴(updateQueriesWithRollback)을 잘 활용하고 있으며, mutations와 queries가 entities에 모여 있어 책임 분리가 명확합니다. 개선할 점은 쿼리 키의 중앙화, 예외 처리 일관화, HTTP 추상화입니다.
❌ 현재 구조 (AS-IS)
AS-IS (entities/post/api/queries.ts):
useQuery({ queryKey: ['posts', 'list', { limit, skip, sortBy, sortOrder }], queryFn: () => fetchPosts(...) })
HTTP 호출은 fetch(createApiUrl(...))로 직접 구현되어 있음.✅ 권장 구조 (TO-BE)
TO-BE (권장):
// shared/api/queryKeys.ts
export const queryKeys = { posts: { list: (meta) => ['posts','list',meta] } }
// entities/post/api/queries.ts
useQuery({ queryKey: queryKeys.posts.list({ limit, skip }), queryFn: () => postApi.fetchPosts(...) })
// shared/lib/httpClient.ts (adapter)
export const http = { get: (url)=>fetch(url).then(r=>r.json()), post: (...) => ... }
// entities call http.get(createApiPath(...)) -> HTTP 라이브러리 교체 시 shared만 수정.🔄 변경 시나리오별 영향도
- API 클라이언트 변경 (fetch → axios): 현재 구조면 src/entities/*/api/index.ts 파일(대략 3개)을 수정해야 함. 개선하면 shared/http-client 한 곳만 수정(1파일)으로 끝남.
- API 경로/파라미터 규칙 변경: queryKey가 분산되어 있으면 여러 파일에서 key 변경 필요. queryKeys 팩토리 사용 시 변경은 queryKeys 한 곳에서만 처리.
- 에러 핸들링 정책 변경(예: Sentry 통합): 현재 console.error 분포가 많아 모든 위치를 바꿔야 하나, 중앙화된 onError 핸들러 또는 customFetch를 쓰면 한 곳에서 처리 가능.
🚀 개선 단계
- 1단계: 단기(반나절): queryKeys 팩토리 파일 생성 (shared/api/queryKeys.ts)로 기존 키들을 매핑. entities의 쿼리/뮤테이션에서 이 팩토리를 사용하도록 변경.
- 2단계: 단기(1일): shared/lib에 httpClient 추상화(예: http.get/post), entities에서 fetch 직접 사용 중인 파일(대략 3개)을 httpClient로 전환.
- 3단계: 중기(1-2일): 전역 에러 처리 정책 수립(onError hook, toast/monitoring 연동). mutation의 onError/onSettled 패턴을 표준화.
- 4단계: 장기(1주): 테스트용 mock http adapter를 도입해 E2E/통합 테스트에서 서버 상태 시나리오를 재현 가능하게 함.
3. 🎯 응집도 (Cohesion)
💡 개념 정의
응집도는 모듈/패키지 내부의 요소들이 얼마나 밀접하게 연관되어 있는지를 나타냅니다. 높은 응집도는 한 기능 변경 시 수정해야 할 파일 수를 줄여 유지보수를 쉽게 합니다.
⚡ 중요성
높은 응집도는 기능 단위로 모듈을 잘 떼어낼 수 있어 단위 배포, 패키지 분리, 리팩토링에 유리합니다.
📊 현재 상황 분석
AS-IS: 대부분의 변경은 관련 모듈 안에서 일어나게 구조화되어 있음(예: 댓글 관련 로직은 entities/comment, 댓글 UI는 features/edit-comment/add-comment). 그러나 일부 결정을 통해 '조합 위젯'과 '기능 위젯'을 혼용한 결과(특히 post-detail과 dialogs)로 인해 응집도가 약간 낮아지는 부분이 있습니다. 예를 들어 CommentFormDialog widget은 내부적으로 Add/Edit features를 직접 import해 구성합니다(조합은 좋지만 경계 규칙을 명확히 하면 더 좋음).
📝 상세 피드백
응집도(모듈 내부 관련성)는 전반적으로 양호합니다. 게시물 관련 로직이 entities/post에 모이고, UI/행동은 features와 widgets로 분리되어 있어 변경 시 수정 범위가 줄어듭니다. 다만 일부 위젯들이 내부적으로 너무 많은 책임(예: PostDetailDialog가 댓글 CRUD를 직접 소유하지 않음) 혹은 초기 monolith에서 이관된 잔존 코드가 있을 수 있습니다.
❌ 현재 구조 (AS-IS)
AS-IS: 한 파일(이전 PostsManagerPage)에서 검색, 태그, pagination, dialog 상태, fetch 로직이 모두 섞여 있었음.✅ 권장 구조 (TO-BE)
TO-BE: 게시물 관련 모든 로직이 entities/post로, UI는 widgets/post-table, widgets/post-filters, features/post-dialog로 각각 명확히 분리되어 있어 '댓글 모델 변경'은 entities/comment/*와 features/comment-dialog만 수정하면 됨.🔄 변경 시나리오별 영향도
- 댓글 모델 변경(예: likes 타입 변경) 시: 현재 구조면 entities/comment/model/types.ts 와 mutations/queries 파일들(약 4개)만 수정하면 되지만, widget 레벨에서 댓글 바인딩을 직접 처리한다면 widget 파일들도 영향을 받을 수 있음.
- 다국어(i18n) 추가 시: UI 텍스트가 여러 파일에 분산되어 있으면 수정 범위 증가. 통일된 locale 파일로 추출하면 수정은 locale 파일과 일부 컴포넌트만 변경.
- feature 승격(Widget → Feature) 발생 시: 응집도 높은 폴더 구조라면 승격은 새 폴더와 index만 추가하면 되지만, 응집도가 낮다면 여러 파일에 분산된 로직을 찾아 이동해야 함.
🚀 개선 단계
- 1단계: 단기(반나절): 파일별 책임 README(혹은 CODEOWNERS) 추가 — 각 디렉터리의 책임을 한 문장으로 명시.
- 2단계: 중기(1-2일): 애매한 위젯(예: CommentFormDialog)의 경계 재검토 — UI Shell(Widget) vs 행동/비즈니스(Feature)로 분리.
- 3단계: 장기(1주): 도메인별(entities) 완전한 응집도 측정을 위해 변경 시 수정되는 파일 수를 추적(간단한 스크립트 또는 git-changes 기반)하고 목표치 설정.
4. 🔗 결합도 (Coupling)
💡 개념 정의
결합도(Coupling)는 모듈 간의 의존성 강도를 말합니다. 낮은 결합도는 모듈 교체/테스트/재사용을 쉽게 만듭니다. 인터페이스/추상화(예: httpClient, queryKeys)를 통해 결합도를 낮춥니다.
⚡ 중요성
라이브러리 교체(axios/fetch), 상태관리 변경(zustand → recoil 등), 디자인 시스템 교체 시 최소한의 수정으로 전환 가능하도록 합니다.
📊 현재 상황 분석
AS-IS: 많은 훌륭한 추상화(entities <— features <— widgets)를 볼 수 있음. TO-DO: HTTP 클라이언트 직접 사용(fetch) 때문에 바꿀 때 여러 파일을 건드려야 하는 점, import 경로 표기 혼재가 refactor 시 실수 유발 가능성이 있음.
📝 상세 피드백
결합도는 전반적으로 낮은 편이며, entities(데이터)와 features(행동)를 잘 분리해 인터페이스 중심으로 의존하고 있습니다. 그러나 HTTP 호출을 fetch로 직접 사용하고 있고 일부 컴포넌트가 구체 구현(예: usePostDialogStore의 상태 접근 방식)에 의존해 교체 비용이 발생할 수 있습니다.
❌ 현재 구조 (AS-IS)
AS-IS: entities/post/api/index.ts 안에서 fetch(createApiUrl(...)) 직접 호출. 여러 엔티티 파일이 동일 패턴을 반복.✅ 권장 구조 (TO-BE)
TO-BE: entities는 httpClient abstraction 사용
// shared/lib/httpClient.ts
export const http = { get: (p) => fetch(p).then(r=>r.json()), post: (...) => ... }
// entities/post/api/index.ts
return http.get(createApiUrl(`posts?${query}`))🔄 변경 시나리오별 영향도
- HTTP 라이브러리 변경: 현재 약 3 엔티티 인덱스 파일 수정 필요 → httpClient로 추상화하면 1파일만 변경.
- 상태관리 라이브러리 변경(zustand → recoil): dialog store가 전역에 퍼져있으므로 store 추상화 레이어(예: interfaces + adapter)를 두면 변경 시 adapter만 교체 가능.
- 디자인 시스템 변경: shared/ui가 디자인 토큰/구현을 가지고 있어, adapter 패턴으로 wrapping 하면 변경 비용 1곳으로 축소.
🚀 개선 단계
- 1단계: 단기(반나절): shared/lib/httpClient.ts 생성 및 엔티티 파일 3곳에서 httpClient로 전환.
- 2단계: 중기(1-2일): 상태 store 접근을 캡슐화하는 작은 adapter 레이어(예: features/post-dialog/api.ts) 추가해 전역 store 직접 호출을 줄임.
- 3단계: 장기(1주): shared adapter 패턴을 문서화해 라이브러리 교체 시 영향 범위를 축소.
5. 🧹 Shared 레이어 순수성
💡 개념 정의
Shared 레이어 순수성은 shared가 특정 도메인(예: post, comment)에 의존하지 않고 재사용 가능한 형태로 존재하는 것을 의미합니다. 즉 shared는 구현 세부가 아닌 범용 API를 제공해야 합니다.
⚡ 중요성
도메인 변경 시 shared에 의존성이 섞여 있으면 불필요한 재작업이 발생합니다. 또한 다른 프로젝트에서 재사용성이 떨어집니다.
📊 현재 상황 분석
AS-IS: shared는 비교적 도메인 독립적입니다. TO-DO: shared/ui 내부에서 디자인 토큰(클래스명 등)이 하드코딩 되어 있어 디자인 시스템 변경 시 많은 컴포넌트 수정이 필요합니다. 또한 shared/lib/text-utils.tsx의 highlightText는 React 반환값을 포함하므로 "pure util"로서의 경계(렌더링 로직 vs 순수 함수)를 고민해볼 수 있습니다.
📝 상세 피드백
shared 레이어는 UI primitives(src/shared/ui/)과 공용 유틸(src/shared/lib/)로 잘 분리되어 있습니다. 도메인 의존성이 거의 없지만, 주의할 점은 shared/ui가 디자인 시스템 역할을 하면서 바뀔 때 파급 범위가 큽니다. shared는 도메인에 무관한 범용 기능만 갖도록 유지하세요.
❌ 현재 구조 (AS-IS)
AS-IS: src/shared/ui/Dialog.tsx는 Radix와 라우팅용 아이콘 등 구체 UI 구현을 포함. shared/lib/highlightText는 React 노드 반환.✅ 권장 구조 (TO-BE)
TO-BE: shared/ui는 design-system-adapter 레이어로 추상화
// shared/ui/Button.tsx -> 내부적으로 designSystem.Button을 호출
// designSystemAdapter/chakra/Button.ts -> ChakraButton 래핑
결과: 디자인 시스템 교체 시 adapter만 재구현.🔄 변경 시나리오별 영향도
- 디자인 시스템 변경(예: Tailwind → Chakra): shared/ui 대부분 파일(약 12개)을 재작성해야 함. adapter 추상화를 두면 한 레이어만 교체하면 됨.
- 새 프로젝트에서 shared 재사용: 도메인 의존성(없음) 덕분에 재사용 가능성이 높음.
- 공통 유틸 변경(예: API base 변경): createApiUrl만 바꾸면 됨.
🚀 개선 단계
- 1단계: 단기(반나절): shared/lib의 순수 유틸과 React UI 유틸을 분리(예: highlightText는 pure util로 분리, React 래핑은 shared/ui에서 담당).
- 2단계: 중기(1-2일): shared/ui에 디자인 시스템 adapter(간단한 wrapper) 추가해 외부 디자인 라이브러리 의존성을 캡슐화.
- 3단계: 장기(1주): shared 문서화(사용 규칙: 어떤 파일에 무엇을 넣는지)로 재사용성 확보.
6. 📐 추상화 레벨
💡 개념 정의
추상화는 복잡한 구현 세부사항을 숨기고 명확한 인터페이스를 노출하는 것 입니다. 좋은 추상화는 모듈 교체/리팩토링 시 다른 코드의 변경을 최소화합니다.
⚡ 중요성
추상화가 잘되어 있으면 기술 스택(HTTP 라이브러리, UI 라이브러리) 변경이나 아키텍처 전환 시 수정 범위를 줄일 수 있습니다.
📊 현재 상황 분석
AS-IS: form 훅은 UI에서 필요로 하는 인터페이스만 제공(좋음). 단, API 호출이 fetch로 직접 구현되어 있고 query key가 파일별로 하드코딩되어 있어 추상화 레벨을 높이면 더 유연해집니다.
📝 상세 피드백
비즈니스 로직(훅)과 UI(컴포넌트)가 분리되어 추상화 수준이 적절합니다(useAddPostForm, useEditPostForm 등). 개선 포인트는 API 호출 추상화와 쿼리 키 팩토리 도입으로 변경에 더 강해지도록 하는 것입니다.
❌ 현재 구조 (AS-IS)
AS-IS: entities/post/api/index.ts에서 fetch 사용 -> 직접 네트워크 세부 구현이 퍼짐.✅ 권장 구조 (TO-BE)
TO-BE: entities가 httpClient & queryKeys를 사용
postApi.fetchPosts(...) 내부는 http.get / http.post 호출만 사용
useGetPosts는 queryKeys.posts.list(meta)를 사용🔄 변경 시나리오별 영향도
- HTTP 클라이언트 변경 시: 추상화를 통해 entities 레벨에서만 변경되도록 개선 가능.
- 새로운 서버 인증(토큰 방식) 도입 시: httpClient에 interceptor를 추가하면 개별 엔티티 파일 수정 없이 적용 가능.
- UI 라이브러리 변경 시: shared/ui adapter만 수정하면 앱 전체에 반영 가능.
🚀 개선 단계
- 1단계: 단기(반나절): httpClient 추상화와 queryKeys 팩토리 추가.
- 2단계: 중기(1-2일): entities 내부에서 httpClient를 사용하도록 마이그레이션.
- 3단계: 장기(1주): contract(인터페이스) 기반으로 mock adapter를 만들어 테스트 시 의존성 주입이 가능하도록 함.
7. 🧪 테스트 용이성
💡 개념 정의
테스트 용이성은 코드가 단위 테스트/통합 테스트/E2E 테스트로 검증되기 얼마나 쉬운지를 말합니다. 순수 함수 분리, 의존성 주입, 사이드 이펙트 격리가 핵심입니다.
⚡ 중요성
외부 API 변경, 리팩토링 시 안정성과 회귀 방지를 위해 중요합니다. 테스트가 잘 설계되면 변경에 대한 자신감을 높여 점진적 개선을 촉진합니다.
📊 현재 상황 분석
AS-IS: 로직-UI 분리가 잘 되어 있어 컴포넌트 테스트와 훅 테스트를 분리해 쓸 수 있음. TO-DO: QueryClient mocking(React Query Testing Library)과 httpClient mock을 준비하면 엔드 투 엔드 수준 테스트 없이도 서버 상태 흐름을 검증할 수 있음. mutation.isPending 사용 등 React Query 버전 의존성을 검토해야 함(테스트 시 API 변화로 인한 assertion 깨짐 위험).
📝 상세 피드백
구조적으로 단위/통합 테스트 작성이 용이한 편입니다. 훅과 순수 유틸이 분리되어 있고 QueryProvider가 있어 통합 테스트 세팅이 수월합니다. 그러나 테스트 관련 설정/모킹 코드가 아직 없으므로 이를 보강하면 더 좋습니다.
❌ 현재 구조 (AS-IS)
AS-IS: useAddPostForm는 내부 상태(제목, 본문)를 가지고 mutate를 호출 — 이 훅은 단위 테스트 가능하나, 현재 프로젝트엔 테스트 파일 없음.✅ 권장 구조 (TO-BE)
TO-BE: useAddPostForm에 httpClient/쿼리 클라이언트를 주입하거나 mocking hook을 통해 테스트에서 mutate 동작을 제어하도록 구성.🔄 변경 시나리오별 영향도
- 새 외부 API 연동 시: httpClient mock으로 테스트 작성하면 빠르게 회귀를 검증 가능.
- 비즈니스 규칙 복잡화 시: 순수함수(entities/lib)에 유닛 테스트를 추가하면 비즈니스 로직 안정성 확보.
- 테스트 전략 변경(Jest → Vitest) 시: mocking 패턴과 테스트 유틸을 한 번에 정리 필요.
🚀 개선 단계
- 1단계: 단기(반나절): QueryProvider에 테스트용 쿼리 클라이언트 생성 유틸 추가 (테스트 환경에서 쿼리 무효화/캐시 제어).
- 2단계: 중기(1-2일): httpClient mock 유틸(jest/vi mock) 작성, entities API 함수들에 대한 단위 테스트 추가.
- 3단계: 장기(1주): 훅(useGetPosts, usePostPost 등)의 통합 테스트 작성(React Testing Library + MSW 사용).
8. ⚛️ 현대적 React 패턴
💡 개념 정의
현대적 React 패턴은 Suspense, Error Boundaries, custom hooks, declarative data fetching(React Query 활용) 등을 의미합니다. 관심사 분리와 선언적 UI가 핵심입니다.
⚡ 중요성
로딩/에러 UI 일관성 확보, 복잡한 비동기 흐름 단순화, 재사용 가능한 비즈니스 훅 작성에 유리합니다.
📊 현재 상황 분석
AS-IS: hooks와 providers 활용은 우수하나, 페이지 전반에 선언적 로딩/에러 처리(Suspense/ErrorBoundary) 적용 부족. 또한 일부 컴포넌트는 isLoading/isPending로 로컬 처리하고 있어 일관된 UX가 나타나지 않을 수 있음.
📝 상세 피드백
커스텀 훅 분리, QueryProvider 사용 등 현대적 React 패턴이 잘 적용되어 있습니다. 다만 Suspense와 Error Boundary는 사용하지 않았고, 일부 컴포넌트에서 로딩/에러 처리가 직접 이루어지는 부분이 남아있습니다. 선언적 에러/로딩 처리(Suspense + ErrorBoundary) 도입을 권장합니다.
❌ 현재 구조 (AS-IS)
AS-IS: PostTable에서 isLoadingPosts || isLoadingSearch || isLoadingTag 등으로 로딩 상태를 컴포넌트 내부에서 판단해 로딩 UI를 노출.✅ 권장 구조 (TO-BE)
TO-BE: <ErrorBoundary><Suspense fallback={<Skeleton />}><PostTable /></Suspense></ErrorBoundary> 와 같이 상위에서 선언적으로 처리.🔄 변경 시나리오별 영향도
- 로딩 전략 변경(스켈레톤 → 스피너 등): Suspense로 전환하면 하위 컴포넌트 변경 없이 상위에서 로딩 UX 일괄 적용 가능.
- 에러 처리 정책 변경: ErrorBoundary로 감싸면 개별 컴포넌트에서의 console.error 부하를 줄이고 일관된 fallback 제공 가능.
🚀 개선 단계
- 1단계: 단기(반나절): ErrorBoundary 구성요소와 Suspense fallback 컴포넌트(Skeleton)를 도입하고 문서화.
- 2단계: 중기(1-2일): 핵심 위젯(PostTable, PostDetailDialog 등)을 Suspense 호환 방식(useSuspenseQuery 또는 suspense 옵션)으로 조정.
- 3단계: 장기(1주): 에러 로깅(Sentry)과 ErrorBoundary 연동으로 프로덕션 에러 모니터링 체계 마련.
9. 🔧 확장성
💡 개념 정의
확장성은 새로운 요구사항(기능/비기능)을 수용할 때 기존 코드 변경을 최소화하며 기능을 추가할 수 있는 능력입니다. 모듈화, 추상화, 설정/코드 분리가 중요합니다.
⚡ 중요성
제품 성장에 따른 새로운 요구사항(다국어, A/B, 실시간)을 빠르게 도입하고, 팀 간 책임 경계를 지키면서 변경을 최소화합니다.
📊 현재 상황 분석
AS-IS: 새로운 서버 엔드포인트나 새로운 feature를 추가하는 흐름은 명확함. TO-DO: 디자인 시스템/HTTP adapter 부재로 외부 도구 변경시 여러 파일 수정 필요(디자인 시스템 약 12개 파일, HTTP: 3 엔티티 파일).
📝 상세 피드백
새 기능(다국어, A/B 테스트, 실시간 등)을 추가할 때의 확장성은 전반적으로 좋습니다. entities와 features가 분리되어 있고, dialog store 등 전역 UI 상태는 zustand로 캡슐화되어 있어 새로운 기능의 통합 경로가 명확합니다. 다만 shared adapter가 부족해 디자인 시스템이나 HTTP layer 교체 시 작업량이 큽니다.
❌ 현재 구조 (AS-IS)
AS-IS: 버튼/다이얼로그 텍스트가 컴포넌트 내부 하드코딩(예: '게시물 추가')되어 있어 i18n 적용시 많은 파일 교체 필요.✅ 권장 구조 (TO-BE)
TO-BE: 텍스트를 i18n.t('post.add') 형태로 추상화하고 shared/ui 컴포넌트는 locale을 전달받아 렌더링. 디자인 시스템 adapter로 스타일 변경 시 영향 최소화.🔄 변경 시나리오별 영향도
- 다국어(i18n) 추가: UI 텍스트가 컴포넌트에 분산되어 있으므로 locale 파일을 도입하고 컴포넌트의 텍스트를 대체하면 대부분의 컴포넌트 변경을 피할 수 있음.
- A/B 테스트 도입: 기능(Feature) 레벨에서 토글을 적용하면 widget/feature 경계 덕분에 영향 범위가 국한됨.
- 실시간 기능 추가(WebSocket): entities/api에서 실시간 데이터 소스 abstraction을 추가하면 기존 쿼리/뮤테이션을 보완하여 통합 가능.
🚀 개선 단계
- 1단계: 단기(반나절): UI 텍스트 하드코딩 제거 계획(핵심 컴포넌트부터 i18n 키 교체).
- 2단계: 중기(1-2일): httpClient, design system adapter 도입으로 외부 교체 비용 축소.
- 3단계: 장기(1주): 기능 토글/Feature Flag(예: LaunchDarkly 또는 간단한 토글 레이어) 도입으로 A/B 테스트 준비.
10. 📏 코드 일관성
💡 개념 정의
코드 일관성은 파일명, 네이밍 규칙, import/export 패턴, 코드 스타일(들여쓰기/따옴표 등)이 프로젝트 전반에서 일관되게 사용되는 정도입니다.
⚡ 중요성
일관성은 신규 개발자 온보딩, 코드 리뷰 속도, 자동화 도구(ESLint/Prettier/Codecov)와의 통합에 큰 영향을 줍니다.
📊 현재 상황 분석
AS-IS: 대부분 규칙을 잘 지키고 있으나 import alias 혼재가 유지보수 리스크입니다. 예: src/features/add-comment/ui/add-comment-form-dialog.tsx에서 import { useCommentDialogStore } from "@/features/comment-dialog" (슬래시 포함) — 다른 파일들에서는 "@features/..." 사용. 또한 몇몇 파일 끝에 newline 없음 등의 작은 스타일 불일치가 존재합니다.
📝 상세 피드백
전체적으로 네이밍과 파일 구조는 일관성이 높습니다(components/훅/타입 규칙 준수). 다만 import 경로 표기와 일부 네이밍/스타일 불일치가 관찰됩니다. 이를 정리하면 온보딩과 협업 효율이 더 올라갑니다.
🚀 개선 단계
- 1단계: 단기(반나절): tsconfig/webpack alias 규칙과 ESLint import/order 규칙을 확립하고 코드베이스에서 '@/features' vs '@features'를 하나로 통일(자동 replace 스크립트 사용).
- 2단계: 단기(반나절): Prettier/ESLint 규칙으로 newline, quotes, semi 등을 강제 적용해 스타일 불일치 제거.
- 3단계: 중기(1일): mutation/queries 상태 네이밍 컨벤션(isLoading/isPending) 표준 문서화 및 코드 일괄 수정.
🎯 일관성 체크포인트
파일명 규칙
- 대체로 PascalCase 사용: good. 하지만 일부 경로 사용이 혼재되어 있어(예: @widgets vs @/widgets) 규칙 통일 필요.
Import/Export 패턴
- export 패턴은 배럴 사용으로 정리되어 있음(좋음). 다만 import alias 표기(@/features vs @features) 혼재로 자동 리팩토링/검색 시 문제 발생 가능.
변수명 규칙
- 대부분 camelCase 사용: good. 그러나 React Query mutation state 사용(isPending)과 일반 관습(isLoading) 혼용 확인 필요.
코드 스타일
- 파일 끝 newline 누락, 일부 파일에서 console.error 사용 등 스타일 일관성(로깅/에러 처리 정책) 보완 필요.
11. 🗃️ 상태 관리
💡 개념 정의
데이터 흐름 및 상태 관리는 전역 상태, 지역 상태, 서버 상태를 어떤 도구로, 어디에 저장할지의 규칙을 의미합니다. 서버 상태는 TanStack Query, 클라이언트/UI 상태는 local state 또는 경량 store(zustand)가 권장됩니다.
⚡ 중요성
상태를 적절히 분리하면 디버깅/성능 최적화/오프라인/실시간 요구사항을 더 쉽게 만족시킬 수 있습니다.
📊 현재 상황 분석
AS-IS: 서버 상태와 클라이언트 상태 분리는 잘 지켜지고 있습니다. TO-DO: 전역 store(zustand)에 어떤 데이터를 둘지(visibility only vs data 포함) 규칙화(예: dialog store는 visibility만, payload는 entities에서 fetch하게)하면 혼합 상태 위험을 줄일 수 있습니다.
📝 상세 피드백
서버 상태는 TanStack Query, UI/로컬 상태는 Zustand와 훅(useAddPostForm 등)으로 분리되어 있어 서버/클라이언트 상태 분리가 잘 되어 있습니다. 이 구조는 실시간 또는 오프라인 지원으로 확장할 때 유리합니다. 다만 일부 UI 상태(예: 다이얼로그)를 전역 store에 두는 기준을 문서화하면 UX 예측 가능성이 높아집니다.
❌ 현재 구조 (AS-IS)
AS-IS: Dialog visibility는 zustand stores (usePostDialogStore, useCommentDialogStore)로 관리, 서버 데이터는 useGetPosts/useGetComments로 분리.✅ 권장 구조 (TO-BE)
TO-BE: 전역 store는 UI 상태(visibility, selected IDs)만 보유하고, 실제 데이터는 entities의 queries로 가져오는 규칙을 문서화하여 유지.🔄 변경 시나리오별 영향도
- 오프라인 지원 요구: 현재 TanStack Query의 캐시 전략을 확장하면 오프라인 경험을 일부 지원 가능(캐시 보존, revalidation).
- 실시간 동기화 요구: mutation의 optimistic 업데이트 로직은 이미 존재하므로, WebSocket이 추가되면 queryClient.setQueryData를 통해 실시간 반영을 연결하면 됨.
- 복잡한 폼 상태 증가: form 상태가 복잡해지면 훅에서 useReducer 또는 form 라이브러리(react-hook-form)로 전환 고려.
🚀 개선 단계
- 1단계: 단기(반나절): state 관리 가이드(전역 store는 UI 상태만, 데이터는 Query) 작성 및 README에 추가.
- 2단계: 중기(1-2일): dialog stores에서 payload(예: selectedPost 객체)를 id로 저장하고, 상세 데이터는 useGetPost(id)로 조회하도록 변경.
- 3단계: 장기(1주): 오프라인/실시간 요구에 대비한 캐시/재동기화 전략 문서화(staleTime, cacheTime, refetchOnReconnect 등).
🤔 질문과 답변
Q1: dialog를 통째로 분리할지, 액션 로직까지만 분리할지? → 권장 패턴: dialog Shell(렌더링/애니메이션/접근성)은 widgets(또는 shared/ui)로, 다이얼로그 내부의 비즈니스 로직(폼 상태, mutation, 성공 콜백)은 feature 훅으로 분리하세요. 즉 '통째로 분리' 대신 'Shell(Widget) + Feature 훅/컴포지션'을 권합니다. 장점: 디자인/애니메이션 변경은 Shell만, 행동 변경은 feature 훅만 수정하면 됨. 만약 다이얼로그 자체가 특별한 비즈니스 흐름(모달 자체가 중요한 UX flow)이라면 feature로 올리는 것도 합리적입니다. Q2: 여러 useQuery를 한 파일에 모아도 될까? → 작은 파일(<= ~200LOC) 수준에서는 묶어두는 것이 관리에 편합니다. 하지만 서로 다른 소비자(위젯/feature)에서 사용될 경우, 기능별로 분리하는 것이 재사용성과 tree-shaking에 유리합니다. 기준: 같은 도메인(예: posts)이고 관련 로직(검색/태그/목록)이면 하나의 queries 파일 유지, 다른 컨텍스트에서 재사용된다면 별도 파일로 분리하세요. Q3: 파일구조가 잘됐는지? → 전반적으로 훌륭합니다. 핵심 개선 권고: import alias 통일, queryKeys/ httpClient 중앙화, shared/ui adapter 도입, 코드 스타일(ESLint/Prettier) 강화. 이 네 가지만 개선해도 아키텍처 유연성이 크게 향상됩니다.
🎯 셀프 회고 & 제안
좋은 셀프회고와 학습 포인트가 잘 정리되어 있습니다. barrel export의 이점(캡슐화, 리팩토링 안전성)과 상대/절대 경로 혼용 규칙을 실무 규칙으로 명확히 정한 점이 인상적입니다. 또한 '점진적 승급 전략'을 실제로 적용하고 검증한 점은 현실적인 팀 환경에서 매우 유효한 접근입니다. 추가로 생각해볼 질문들: 1) 현재 '점진적 승급' 결정을 누가/어떤 기준으로 최종 확정할 것인가(팀 합의 프로세스)? 2) Widget → Feature 승격 시 이력(리팩토링 로그)을 어떻게 관리할 것인가(코드 리뷰 템플릿 또는 이슈 템플릿)? 3) 배럴 export의 버전 관리(외부 API 변경시 deprecation 전략)는 어떻게 설계할 것인가? 이러한 질문을 정리하면 팀 적용 시 더욱 견고한 규칙이 만들어질 것입니다.
추가 논의가 필요한 부분이 있다면 언제든 코멘트로 남겨주세요!
코드 리뷰를 통해 더 나은 아키텍처로 발전해 나가는 과정이 즐거웠습니다. 🚀
이 피드백이 도움이 되었다면 👍 를 눌러주세요!
unseoJang
left a comment
There was a problem hiding this comment.
안녕하세요 유열님
2-3 과제도 수고 많으셧습니다.
코드 전체에서 FSD, 관심사에 대해 고민을 많이 해보신 흔적이 나타나고 있네요
API분리, 낙관적 업데이트도 잘 하셧어요
@features/add-post/ui/add-post-form-dialog.tsx와 같은 컴포넌트는 피쳐로 분리할때 다이얼로그에서 액션 로직과 연결되어있는 부분까지만 분리할지, dialog를 통째로 분리할지 고민하다가 dialog를 통째로 분리했습니다. '모달이 표시된다'라는 현상 자체도 기능으로 보기로 결정했기 때문입니다. 하지만 이 dialog 안에도 add-post라는 기능이 있는데 이런 기능의 중첩 상황같은 경우 세부 분리를 더 진행하는게 좋을까요?
제 생각엔 과제에서는 최대한 해볼수 있는데 까지 해보는게 좋다고 실무에서는 세부분리를 더 할지 말지 팀원들과 논의해야된다고 생각이 듭니다.
나머지 코드리뷰는 준일 코치님의 GPT가 더 깔끔하게 진행해주신것같아 해당 리뷰를 참고해주시면 감사하겠습니다.
수고하셨습니다!
There was a problem hiding this comment.
전체 코드가 위에 정리하신데로 깔끔하고 좋네요
api 통신이지만 실무 처럼 대응안해도 무리 없다고 봅니다
| user: { id: c.userId, username: "You" }, | ||
| }) | ||
|
|
||
| export const usePostComment = () => { |
There was a problem hiding this comment.
아마 여기서 실제 서버 였으면 스켈레톤으로 onSuccess 들어오면 바로 치환해줫을거에요!
| onMutate: async (variables: { id: number; body: string; postId: number }) => { | ||
| await queryClient.cancelQueries({ queryKey: ["comments", variables.postId] }) | ||
| const pairs = queryClient.getQueriesData<CommentsListData>({ queryKey: ["comments", variables.postId] }) | ||
| const rollback = updateQueriesWithRollback(queryClient, pairs, (prev) => ({ |
There was a problem hiding this comment.
updateQueriesWithRollback의 제너릭을 명시하면 더 안전할것같아요
There was a problem hiding this comment.
onMutate의 의 반환 컨텍스트 타입을 명시하면 좋을 것 같아요!
|
|
||
| export const useGetComments = (postId: number | null) => { | ||
| return useQuery({ | ||
| queryKey: ["comments", postId], |
|
|
||
| export const usePatchCommentLikes = () => { | ||
| const queryClient = useQueryClient() | ||
| return useMutation({ |
There was a problem hiding this comment.
mutationKey를 넣으면 Devtools 추적이 좋아질거에요!
| const openEditComment = useCommentDialogStore((s) => s.openEditComment) | ||
| const postId = post?.id | ||
|
|
||
| const { data: commentsData } = useGetComments(postId ?? null) |
There was a problem hiding this comment.
postId ?? null 대신 undefined를 주고, 훅 내부에서 enabled: !!postId로 제어하는 쪽이 안전합니다. “없음”을 표현할 땐 undefined + enabled로 ‘아예 호출하지 않기’를 보장이 되니까요
| onOpenEditComment: (comment: Comment) => void | ||
| } | ||
|
|
||
| export const CommentItem: React.FC<CommentItemProps> = ({ comment, postId, searchQuery = "", onOpenEditComment }) => { |
There was a problem hiding this comment.
댓글 목록을 <ul>/<li>로 표현하면 스크린리더 접근성이 좋아져요!
| <Dialog open={open} onOpenChange={(o) => !o && close()}> | ||
| <DialogContent className="max-w-3xl"> | ||
| <DialogHeader> | ||
| <DialogTitle>{highlightText(post?.title || "", param.search || "")}</DialogTitle> |
There was a problem hiding this comment.
highlightText(post?.title, param.search) 같은 연산은 길이가 길면 비용이 커질 수 있으니 useMemo로 메모이즈를 고려하세요(검색어/본문이 바뀔 때만 재계산).
과제 링크
https://yuyeol.github.io/front_6th_chapter2-3
과제 체크포인트
기본과제
목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기
체크포인트
심화과제
목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기
체크포인트
최종과제
과제 셀프회고
이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.
배럴 익스포트를 하는 실용적인 이유를 알게되었습니다.
이때까지는 barrel export를 하는 방식이 존재한다는 것만 알고 사용해야 할 이유를 궁금해하지도, 적용해보지도 않았습니다.
그런데 FSD는 왜 굳이 barrel export를 사용해서 index.ts 파일만 늘려서 구조를 복잡하게 만드는 거지? 라는 의문이 들면서 이유를 찾아보게 되었습니다.
초기 의문:
실제 적용해보니 알게 된 실용적 이유들:
상대 경로와 절대경로를 혼용하는 이점에 대한 이해도 얻게됨
배럴 익스포트와 함께 언제 상대경로를, 언제 절대경로를 써야 하는지에 대해서도 규칙을 정해서 어기지 않도록 노력했습니다.
처음에는 모든 경로를 모조리 절대경로로 바꾸는게 좋겠다. 라고 생각했지만, FSD import rule 기반으로 AI와 토론을 거친 결과 상대경로와 절대경로를 혼용하는 것이 좋겠다고 판단했습니다.
./api,../model)@shared/ui,@entities/post)이렇게 했을때 좋은 점:
처음에는 이런 세세한 룰까지 신경써야 하나? 싶었는데, 적용해보면서 경험해보니 배럴과 경로 설정을 통해 코드의 구조로 의도를 표현해보는 연습이 되었던 점이 좋았습니다.
특히 팀 프로젝트에서는 이런 규칙이 잘 지켜진다면 업무 안정성이 높아질 것 같다는 생각이 들었습니다.
막연했거나 고민이 필요했던 부분을 적어주세요.
프로젝트에 FSD 아키텍처를 적용하면서 많이 고민했던 부분 중 하나는 Feature와 Widget의 경계를 정의하는 것이었습니다.
처음에는 직관적으로 CRUD 작업을 기준으로 분리해보자.
이 기준은 꽤 합리적이라고 느꼈지만, 실제 프로젝트에는 더 복잡한 경우들이 있었습니다.
생각보다 컴포넌트는 복잡한 것 같다...
상태 관리가 복잡한 경우:
post-dialog,comment-dialog같은 조회여도 다른 복잡도:
user-dialogvspost-detail-dialoguser-dialog: 단순 사용자 정보 표시post-detail-dialog: 댓글 CRUD, 좋아요 등 여러 기능 조합UI 제어도 기능일 수 있음:
like-comment3단계 기준에 따른 분석결과로 결론 도출 해보기
실제 적용 결과
결과적으로 실제 프로젝트의 모든 위젯을 체크리스트 기준으로 분석한 결과:
🟢 Feature로 분리한 9개 컴포넌트
🟡 Widget으로 유지한 애매한 1개 컴포넌트
post-filters
🔴 Widget으로 유지한 8개 명백한 조회 또는 단순 UI용 컴포넌트
Feature와 Widget의 분리 핵심 전략
"점진적 승급 전략이 유효했다."
일단은 모든 컴포넌트는 Widget으로 시작했습니다. 그리고 '이건 명백한 기능이야!' 싶은 것들은 스스로 판단하여 Feature로 승급시키는 전략이 유효했던 것 같습니다.
그리고 명백한 기능이라도 분리하기 복잡한 경우는 로직이 정리되는 동안에는 Widget에서 점진적 개선을 거쳐서 올리면 되지않을까 하는 생각이 직접 경험해보니 실제로 효과적이었던 것 같습니다.
이번에 배운 내용을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.
이번 과제 규모나 지금 회사 규모에서 FSD를 적용한다고 상상해보면, 상황이나 규모가 적절하지 않은 경우도 많겠다는 생각이 들엇습니다.
FSD 자체를 실무에 적용할 일이 잘 없을 것 같지만, FSD를 분리해보면서 컴포넌트를 기능 중심으로 본다라는 관점을 가져갈 수 있었던 점은 좋았던 것 같습니다.
평소에 컴포넌트를 그냥 별 생각없이 생성하고 무거워지면 분리하는 일차원적인 작업을 했다고 한다면, 이번에 기능이 있는 컴포넌트와 없는 컴포넌트를 구분해보면서 역할이나 레이어 분리에 대한 감각을 연습 해 볼 수 있었습니다.
기능이 있는 컴포넌트 (Feature):
기능이 없는 컴포넌트 (Widget):
이런 식으로 컴포넌트의 설계를 고려한 코드작성을 한다면, 코드리뷰를 받더라도 명쾌하고 납득이 되는 코드를 보여줄 수 있지 않을까 하는 생각이 듭니다.
결국 완벽한 아키텍처를 도입해야겠다 라는 생각 보다는, 이번에 얻은 관점을 실제 코드 구현에 적응해보도록 사고를 유지해 볼 수 있는 것이 더 의미가 컸습니다.
챕터 셀프회고
이번 챕터를 진행하면서 느낀점을 토대로 얻어간 것이 크게 두가지 있습니다.
소득1: 엔티티 구조를 가장 실용적으로 사용할 것 같습니다.
이번 챕터를 다 해보니 가장 바로 써먹을 수 있겠다 싶었던 건 엔티티 구조였습니다. 엔티티 > 순수함수 > 훅 > 컴포넌트 이런 흐름이 제게는 상당히 합리적이고 현실적인 구조라고 생각이 들었습니다.
저는 그동안 스토어를 만들어도 거대한 리듀서처럼 모든 로직을 스토어 안에 몰아넣었고, 커스텀 훅을 만들어도 "훅 자체가 분리된 모듈이니까 더 분리하는건 좋지 않을거같아" 라고 하며 그 안에서 다시 순수함수를 빼거나 하는 생각은 못했던 것 같습니다.
테오가 평일 Q&A 세션에서 말한 부분 중 "신경 쓰지 않아도 될 코드는 신경 쓰지 않을 곳에 구획을 만들어두고 시선을 두지 않는 것도 좋다"는 말이 인상깊었는데, 엔티티 구조를 사용하면서 비즈니스 로직은 따로 정리해두고, 필요할 때만 꺼내 쓰는 식으로 적용해보면 좋겠다는 생각이 들었습니다.
소득2: 점진적 개선의 중요성을 체감했습니다
회사에서는 제가 혼자 개발하다 보니 리팩토링을 할 때 손바닥 뒤집듯 구조를 갈아엎곤 했습니다. 어차피 저만 보는 코드이기도 해서 불편함도 크게 없었던 것 같습니다.
하지만 팀으로 일한다면 맥락을 공유해야 하는 거고, 팀원들이 따라올 수 있는 속도에 맞춰 점진적으로 개선해야겠다는 생각이 들었고, 구조를 크게 바꾼다면 그만한 합리성과 필요성이 따라와야 되겠구나 느끼게 되었습니다.
테오가 계속 점진적 개선을 강조하는 이유를 제 나름대로 이해한 것 같고, 앞으로는 이 부분을 좀 더 고려해서 리팩토링해보려고 합니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문
@features/add-post/ui/add-post-form-dialog.tsx와 같은 컴포넌트는 피쳐로 분리할때 다이얼로그에서 액션 로직과 연결되어있는 부분까지만 분리할지, dialog를 통째로 분리할지 고민하다가 dialog를 통째로 분리했습니다. '모달이 표시된다'라는 현상 자체도 기능으로 보기로 결정했기 때문입니다. 하지만 이 dialog 안에도 add-post라는 기능이 있는데 이런 기능의 중첩 상황같은 경우 세부 분리를 더 진행하는게 좋을까요?
@entities/post/api/queries.ts 파일처럼 부담스럽지 않은 코드라인 수준이라면 여러개의 useQuery를 굳이 파일분리를 진행시켜주지 않아도 되겠다 싶어 이번 프로젝트에서는 모아둔 로직들이 많은 것 같습니다. 합리적인 선택이었을까요? 아니면 제가 인지못한 불편함이 존재할까요?
항상 질문을 할 때 포괄적인 범위의 질문은 안된다고 전달받았으나, 이번 과제는 괜찮으시다면 파일구조를 잘 구성했는지를 봐주시면 감사하겠습니다. 제가 파일 구조를 잘 짰는지 정말로 궁급합니다.