[3팀 이정우] Chapter 2-3. 관심사 분리와 폴더구조#36
Conversation
/api/... → https://dummyjson.com/... 로 치환하는 문자열 기반 로직을 넣었습니다. 새 글은 실제 DummyJSON 서버에 반영되지 않아서 clientPosts[] + 로컬스토리지로만 관리하고 있습니다. forwardRef 래퍼에 DOM 제네릭을 일부만 추가해 빌드 오류는 없지만, |
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요, 이정우 님! 5주차 과제 잘 진행해주셨네요. 고생하셨습니다 👍🏻👍🏻
현재 남기는 코멘트는 GPT-5-mini model을 사용하여 AI가 남기는 피드백입니다 🤖
과제 피드백과는 별개이므로 참고해주세요!
1. 🏗️ FSD 아키텍처
💡 개념 정의
Feature-Sliced Design(FSD)는 도메인(entities) 중심으로 폴더를 나누고, 책임에 따라 layers(app/pages/widgets/features/entities/shared)로 분리하여 의존성 방향을 단일 쪽(app→...→shared)으로 유지하는 아키텍처 패턴입니다. 각 slice는 자신의 public API만 외부에 노출해야 합니다.
⚡ 중요성
FSD를 준수하면 기능 추가/리팩터 시 변경 범위가 국소화되어 개발 생산성과 코드 재사용성이 향상됩니다. 아키텍처 변경(모노레포 전환, MFE 분리)이나 라이브러리 교체 시 영향도를 크게 줄입니다.
📊 현재 상황 분석
문제 지점:
- src/shared/api/posts.ts: 엔티티 타입(Post, User) 정의와 HTTP wrapper가 shared에 위치. (shared가 도메인 타입에 의존)
- src/entities/post/hooks.ts: TanStack Query 훅은 있지만 API 함수는 shared/api에 있어 entities ↔ shared 관계가 혼재.
- src/shared/stores/postsStore.ts: 동일 책임(데이터 fetching)을 zustand와 React Query가 중복 관리할 소지가 있음.
영향: 도메인 변경(예: Post 타입 추가 필드)이 발생하면 shared + entities + pages까지 동시에 수정해야 할 가능성이 큽니다.
📝 상세 피드백
전체적으로 FSD(Feature-Sliced Design) 관점이 잘 적용되어 있습니다. entities/post, shared/, pages/, features가 나뉘어 있고 index 구조로 컴포넌트를 노출하는 등 기본 원칙을 따릅니다. 다만 몇몇 사례에서 계층 의존성 방향과 Public API 노출 규칙이 완전히 지켜지지 않아 향후 기술/아키텍처 변화에 따른 수정 범위가 커질 수 있습니다.
예를 들어 src/shared/api/posts.ts에 타입 정의와 API 호출이 모여 있는데 타입은 entities/post/model로 분리하는 것이 바람직합니다. 또한 src/shared/stores/postsStore.ts는 내부적으로 fetch를 직접 호출하며 entities 대신 직접 API 경로를 사용하고 있어 entities → features → pages의 의존성 방향이 뒤섞일 소지가 있습니다.
권장 개선: shared는 UI/유틸/infra(플랫폼 추상)만 보유하고, 엔티티 관련 타입/모델/순수 API 호출은 entities/post로 이동시키세요. 또한 각 slice는 index.ts로 public API만 노출하도록 표준화하세요.
❌ 현재 구조 (AS-IS)
// AS-IS (현재 PR 발췌)
// src/shared/api/posts.ts
export interface Post { id:number; title:string; body:string; userId:number; ... }
export const getPosts = (skip=0, limit=10) => enrichPosts(jsonFetch(`/api/posts?limit=${limit}&skip=${skip}`))
// src/entities/post/hooks.ts
import { getPosts } from '../../shared/api/posts'
useQuery({ queryKey: postsKeys.list(params), queryFn: () => getPosts(skip, limit) })✅ 권장 구조 (TO-BE)
// TO-BE: 책임 분리 권장 예시
// src/entities/post/model/types.ts
export interface Post { id:number; title:string; body:string; userId:number; tags?:string[] }
// src/entities/post/api/postApi.ts (순수 CRUD)
export const postApi = { getPosts: (skip,limit)=> httpClient.get('/posts', {params:{skip,limit}}) }
// src/entities/post/hooks.ts (쿼리 훅만)
import { postApi } from './api/postApi'
useQuery({ queryKey: postsKeys.list(params), queryFn: () => postApi.getPosts(skip,limit) })
// src/shared/ui/Button/index.ts (public API)
export { Button } from './Button'
export type { ButtonProps } from './Button.types'🔄 변경 시나리오별 영향도
- 디자인 시스템을 컴포넌트 라이브러리로 분리(예: Material → Chakra)할 때 shared/ui의 범용성 및 노출 방식에 따라 수정 파일 수가 달라짐.
- API 응답 스키마 변경(Post에 tags 추가) 시 현재 구조: shared/api + entities/hooks + pages 컴포넌트 수정 필요 → 개선 구조: entities/model만 수정하면 상위 영향 최소화
- 모노레포로 전환 시 entities/post 패키지로 추출하면 의존성 정리가 쉬워짐(현재는 shared 에 혼재되어 어려움)
🚀 개선 단계
- 1단계: 단기(1-2일): src/shared/api/posts.ts에서 타입 정의(Post, User 등)를 entities/post/model/types.ts로 이동. shared/api는 infra wrapper만 유지.
- 2단계: 중기(2-4일): entities/post 디렉토리에 postApi(순수 CRUD)와 model(types)을 정리하고 entities/post/hooks.ts가 postApi만 소비하도록 변경.
- 3단계: 중장기(3~7일): 모든 slice에 index.ts public export 규칙 도입(리ント/PR 템플릿으로 강제). 리팩터 이후 의존성 그래프를 도구( madge 등)로 검증.
2. 🔄 TanStack Query
💡 개념 정의
TanStack Query는 서버 상태를 선언적으로 관리하는 라이브러리로, 쿼리 키(queryKey), queryFn, mutation, 캐싱, invalidation, optimistic update를 통해 서버와 클라이언트 상태를 일관성 있게 유지합니다.
⚡ 중요성
API 엔드포인트 변경, 새로운 데이터 소스 추가, 에러 정책 변경 등 변화에 대해 코드 수정 범위를 최소화하고, 성능(캐싱)·일관성(invalidations)을 확보하는 데 중요합니다.
📊 현재 상황 분석
이슈 요약:
- postsKeys.list에 params 객체를 그대로 넣음: 객체 동등성/직렬화 문제가 있음. (queryKey를 객체로 쓰면 캐시 접근 시 의도치 않은 미스가 발생할 수 있음)
- 일부 쿼리에 staleTime 설정이 있어 일관성 없음.
- optimistic update는 addPost에서 로컬 캐시 직접 수정으로 처리하나, 실패 rollback 처리가 보이지 않음.
영향: API 경로 변경 시 queryKey/쿼리 로직이 흩어져 있다면 전역 수정량이 커질 수 있음.
📝 상세 피드백
TanStack Query 활용은 전반적으로 잘 되어 있습니다. queryKeys 팩토리(src/shared/queryKeys.ts) 사용, entities/post/hooks.ts에서 useQuery/useMutation 분리, React Query Devtools 도입 등 훌륭한 적용사례가 보입니다. 다만 몇 가지 개선으로 변화에 더 유연해질 수 있습니다:
- queryKey에 객체를 그대로 넣는 패턴(postsKeys.list(params))은 직렬화/메모리 식별성 문제를 야기할 수 있습니다. params를 안정적(예: normalized string, tuple)으로 변환하세요.
- 캐싱 전략(staleTime, cacheTime)은 일부 쿼리에만 설정되어 있습니다(예: useTagsQuery에 staleTime). 리스트/디테일/태그 등에 명확한 정책을 문서화하세요.
- optimistic 업데이트와 에러 처리 패턴은 일부 구현되어 있으나 일관된 추상화(유틸 훅)를 도입하면 API 변경시 수정 범위를 줄일 수 있습니다.
❌ 현재 구조 (AS-IS)
// AS-IS (hooks 사용)
// src/shared/queryKeys.ts
export const postsKeys = { all: ['posts'], lists: () => [...postsKeys.all,'list'], list: (params={}) => [...postsKeys.lists(), params] }
// src/entities/post/hooks.ts
useQuery({ queryKey: postsKeys.list({ skip,limit,tag,search }), queryFn: () => getPosts(...) })✅ 권장 구조 (TO-BE)
// TO-BE: 안정적 쿼리키 예시
export const postsKeys = {
all: ['posts'] as const,
lists: () => [...postsKeys.all, 'list'] as const,
list: (skip=0,limit=10,tag='all',search='') => [...postsKeys.lists(), `skip:${skip}`, `limit:${limit}`, `tag:${tag}`, `search:${search}`] as const,
}
useQuery({ queryKey: postsKeys.list(skip,limit,selectedTag,searchQuery), queryFn: () => postApi.getPosts(...) })🔄 변경 시나리오별 영향도
- 백엔드가 게시물 리스트 반환 포맷을 변경(저장된 total 필드 제거)한다면 postsKeys와 post 훅, 페이지가 동시에 수정되어야 함.
- 새로운 실시간 소스(웹소켓)가 추가되어 리스트 동기화 방식이 바뀐다면 useQuery와 mutation 리팩터 필요.
- 쿼리 키 네임스페이스 변경(예: 'posts' → 'entities/posts') 시, 키를 중앙화해두면 영향 파일 수가 최소화됨.
🚀 개선 단계
- 1단계: 단기(반나절): postsKeys.list의 파라미터를 객체에서 안정적 문자열/튜플로 변환. 각 쿼리에 기본 staleTime 정책(예: tags:5min, lists:1min, detail:30s) 적용.
- 2단계: 중기(1~2일): 모든 mutation에 대해 optimistic update + rollback 패턴을 공통 유틸로 추상화(예: useOptimisticMutation).
- 3단계: 중장기(2~3일): Query 키 네임스페이스 가이드(도메인별 prefix, 파라미터 정규화)를 문서화하고 코드베이스에 적용.
3. 🎯 응집도 (Cohesion)
💡 개념 정의
응집도(Cohesion)는 모듈 내부 요소들이 얼마나 관련성이 높은지를 나타냅니다. 높은 응집도는 한 기능 변경 시 수정 파일이 집중되어 유지보수가 쉬워집니다.
⚡ 중요성
응집도가 높으면 온보딩·디버깅이 쉬워지고 모듈을 패키지로 분리(또는 마이크로프론트 전환)할 때 경계가 명확해집니다.
📊 현재 상황 분석
AS-IS: posts 관련 책임이 3곳에 분산되어 있어 단일 변경(예: author 필드 추가)에 대해 3개 이상의 파일을 수정해야 함.
TO-BE: types + api + hooks를 entities/post로 모아 응집도를 높이면 수정 범위가 좁아집니다.
📝 상세 피드백
응집도 측면에서 entities/post 관련 코드가 대체로 모여있어 변경 시 집중되는 편이지만, 같은 책임(데이터 fetching & enrichment)이 entities와 shared/stores에 중복되어 분산되어 있습니다. 특히 fetch/enrich 로직이 shared/api/posts.ts와 shared/stores/postsStore.ts, entities/post/hooks.ts에 흩어져 있어 한 기능을 변경할 때 수정 파일 수가 늘어납니다.
권장: 엔티티 관련 모든 타입·API·도메인 로직은 entities/post 아래로 모으고, shared는 UI/infra 유틸로 유지하세요. 이렇게 하면 'Post 타입 변경' 시 수정해야 할 파일 수를 현재(예: 4-6개)에서 1-2개로 줄일 수 있습니다.
❌ 현재 구조 (AS-IS)
// AS-IS: 분산된 응집성 문제
// src/shared/api/posts.ts: enrichPosts가 author 매핑을 담당
// src/shared/stores/postsStore.ts: fetchPosts도 author enrichment 수행
// src/entities/post/hooks.ts: queryFn이 shared/api.getPosts를 호출✅ 권장 구조 (TO-BE)
// TO-BE: 높은 응집도
// src/entities/post/model/types.ts (타입)
// src/entities/post/api/postApi.ts (enrich + CRUD)
// src/entities/post/hooks.ts (usePostsQuery -> postApi.getPosts 를 사용)
// shared는 단지 http client(wrapper)만 제공🔄 변경 시나리오별 영향도
- 새로운 필드(author.bio) 추가: 현재 구조 - shared/api + postsStore + entities/hooks + 페이지 수정 필요(약 4곳). 개선 구조 - entities/model + entities/api + pages(소비자)로 최소화(1-2곳).
- 검색 알고리즘 변경(서버→클라이언트 필터): 필터 로직 위치가 분산되면 여러 모듈을 건드려야 함.
🚀 개선 단계
- 1단계: 단기(1일): types를 entities/post/model로 이동하고, shared/api/posts.ts의 타입 의존 제거.
- 2단계: 중기(1~3일): postsStore에 남아있는 fetch/enrich 로직을 entities/post/api로 이동하고, store는 클라이언트 UI 상태만 관리하도록 축소.
- 3단계: 중장기: 모듈 경계가 잘 지켜지는지 자동 검사(의존성 그래프 검사)를 CI에 추가.
4. 🔗 결합도 (Coupling)
💡 개념 정의
결합도(Coupling)는 모듈 간 의존성의 강도를 의미하며, 낮을수록 한 모듈의 변경이 다른 모듈에 미치는 영향이 적습니다.
⚡ 중요성
낮은 결합도는 기술 스택 변경(HTTP 클라이언트 교체, 상태관리 변경)이나 아키텍처 전환 시 변경량을 최소화합니다.
📊 현재 상황 분석
문제 상황:
- 다수의 fetch 사용 지점 → HTTP client 변경 비용 큼
- postsStore와 entities/hooks가 서로 다른 방식으로 동일 API를 호출 → 일관성·모킹 어려움
권장: httpClient(wrapper) 하나로 통합하고 API는 entities/*에 한 곳으로 모아두세요.
📝 상세 피드백
결합도는 일부 영역에서 높은 편입니다. 구체적으로 shared/api, shared/stores, pages가 구체적 구현(fetch, API_BASE)을 직접 사용하고 있어 HTTP 클라이언트나 API 경로 변경 시 파급 범위가 큽니다. 결합도를 낮추려면 infra 계층(HTTP client, endpoints)을 추상화하고 의존성 주입(또는 wrapper API)을 사용하는 것이 좋습니다.
예: fetch가 코드 곳곳에 직접 사용되고 있습니다(src/shared/stores/postsStore.ts, PostsManagerPage.tsx). 만약 axios로 교체하면 최소 8-12개 지점에서 변경이 필요할 수 있습니다. 반면 httpClient 추상화를 만들면 한 파일만 수정하면 됩니다.
❌ 현재 구조 (AS-IS)
// AS-IS: 직접 fetch 사용
// src/shared/stores/postsStore.ts
const postsResp = await fetch(`${API_BASE}/posts?limit=${limit}&skip=${skip}`)
// src/shared/api/posts.ts
const res = await fetch(url, { headers: {...}, ...init })✅ 권장 구조 (TO-BE)
// TO-BE: httpClient 추상화
// src/shared/infra/httpClient.ts
export const httpClient = { get: (url, opts)=> fetch(url,opts).then(r=>r.json()), post: (...) }
// src/entities/post/api/postApi.ts
import { httpClient } from 'shared/infra/httpClient'
export const getPosts = (skip,limit) => httpClient.get(`/posts?limit=${limit}&skip=${skip}`)🔄 변경 시나리오별 영향도
- axios → fetch 변경: 현재 구조 - 모든 fetch 호출 지점을 수정해야 함(약 8-12곳). 추상화된 httpClient 사용 시: httpClient.ts 한 파일만 수정하면 됨.
- 상태관리 라이브러리 변경(zustand→recoil): store 인터페이스를 분리하면 페이징/UX 상태만 바뀌어 pages 영향 최소화 가능.
🚀 개선 단계
- 1단계: 단기(반나절): 프로젝트 전역의 fetch 호출을 찾아 httpClient 추상화 파일을 추가하고 점진적으로 교체.
- 2단계: 중기(1~2일): entities/api로 CRUD 함수를 통합하고 stores/pages는 이를 통해 접근하도록 리팩터.
- 3단계: 중장기: 의존성 주입 패턴을 도입하여 테스트 시 모킹이 쉬운 구조로 개선.
5. 🧹 Shared 레이어 순수성
💡 개념 정의
Shared 레이어 순수성은 shared 코드가 특정 도메인이나 비즈니스 로직에 의존하지 않는 상태를 의미합니다. 재사용성과 이식성을 높이는 핵심입니다.
⚡ 중요성
도메인 변화 시 shared 변경을 최소화해 다른 프로젝트나 팀으로의 재사용 가능성을 보장합니다. 디자인 시스템 분리/패키징 시에도 유리합니다.
📊 현재 상황 분석
문제: shared 내 도메인 타입 존재 → 다른 프로젝트에 shared 패키지를 재사용할 때 불필요한 도메인 의존이 전파됩니다.
해결: 타입과 도메인 유틸은 entities로 이동, shared의 api는 순수 http wrapper만 유지.
📝 상세 피드백
shared 레이어는 UI 컴포넌트(src/components/index.tsx), CSS 토큰(src/index.css), 인프라 래퍼(src/shared/api/posts.ts)이 혼재되어 있습니다. shared는 도메인 코드에 의존하지 않도록 유지되어야 하는데, 현재 shared/api/posts.ts에 Post/User 타입과 도메인 관련 병합 로직(enrichPosts)이 포함되어 있어 도메인 독립성이 훼손됩니다.
권장: shared는 범용 컴포넌트와 infra(HTTP client)만 포함시키고, 도메인 관련 로직은 entities로 이동하세요. UI 컴포넌트는 가능한 범용 props(variant, children)로 설계되어야 합니다.
❌ 현재 구조 (AS-IS)
// AS-IS (shared에 도메인 존재)
// src/shared/api/posts.ts
export interface Post { ... }
export const enrichPosts = async (promise) => { /* author mapping */ }
// 이 파일은 shared에 위치✅ 권장 구조 (TO-BE)
// TO-BE: shared는 infra만
// src/shared/infra/httpClient.ts (shared)
export const httpClient = { get: (p)=> fetch(p)... }
// src/entities/post/api/postApi.ts (entities)
import { httpClient } from 'shared/infra/httpClient'
export const postApi = { getPosts: ()=> httpClient.get('/posts') }
// src/entities/post/model/types.ts (entities)
export interface Post { ... }🔄 변경 시나리오별 영향도
-
새 프로젝트에서 shared 재사용: 현재는 도메인 타입 제거 작업 필요(추가 작업량 발생).
-
디자인 시스템을 외부 패키지로 분리: shared/ui가 범용이면 export만으로 가능하나, 도메인 의존이 있으면 리팩터 필요.
🚀 개선 단계
- 1단계: 단기(반나절): src/shared/api/posts.ts에서 타입 선언을 entities/post/model/types.ts로 이동.
- 2단계: 중기(1~2일): enrichPosts(저장소 병합/author 매핑) 로직을 entities/post/api로 이동하고 shared에는 httpClient만 남김.
- 3단계: 중장기: shared 패키지화(스타일 토큰, 컴포넌트)는 peerDependencies와 빌드 스텝을 마련해 독립 배포.
6. 📐 추상화 레벨
💡 개념 정의
추상화는 복잡한 구현 세부사항을 숨기고 재사용 가능한 인터페이스를 제공하는 수준을 말합니다. 좋은 추상화는 변경에 대한 영향범위를 줄입니다.
⚡ 중요성
추상화가 잘되어 있으면 기술 스택 변화(HTTP client, 에러 포맷) 시 코드 수정량이 감소하고 테스트 작성이 쉬워집니다.
📊 현재 상황 분석
문제: fetch/HTTP 로직이 여러 계층에 분산 -> 추상화 부족.
권장: postApi(entities)로 CRUD 추상화, UI는 hooks(entities)만 사용하도록.
📝 상세 피드백
추상화 수준은 부분적으로 잘 되어 있습니다(components에 제네릭 forwardRef 타입 적용, queryKeys 팩토리 사용). 하지만 API 호출과 상태 업데이트 로직이 페이지/스토어/엔티티에 분산되어 있어 추상화가 완전하지 않습니다. 핵심 비즈니스 로직(예: 게시물 생성/업데이트/삭제에 대한 캐시 동기화)은 entities/post/hooks.ts의 mutation에 집중시키고, 페이지는 오직 UI 상태·호출만 하도록 분리하세요.
추상화 이점: API 클라이언트 변경, 에러 처리 방침 변경, 인증 토큰 처리 등 기술 세부사항을 한 곳에서 변경하면 됩니다.
❌ 현재 구조 (AS-IS)
// AS-IS
// src/pages/PostsManagerPage.tsx에서 직접 fetch로 사용자 정보 조회
const resp = await fetch(`${API_BASE}/users/${post.author.id}`)
// 동시에 entities/hooks.ts에서 다른 방식으로 API 호출✅ 권장 구조 (TO-BE)
// TO-BE
// src/shared/infra/httpClient.ts
export const httpClient = { get: (url)=>fetch(url).then(r=>r.json()) }
// src/entities/user/api.ts
export const userApi = { getUser: (id)=> httpClient.get(`/users/${id}`) }
// src/pages -> userApi.getUser 사용🔄 변경 시나리오별 영향도
- 인증 방식 변경(Bearer token 추가): 모든 fetch 호출을 수정해야 하는 현재 구조 vs httpClient만 수정하면 되는 개선 구조.
- API endpoint 리팩터링: 추상화로 수정점 단일화 가능
🚀 개선 단계
- 1단계: 단기(반나절): httpClient 추상화 추가 및 페이지에서 직접 fetch 사용 지점 목록화.
- 2단계: 중기(1~2일): 모든 직접 fetch 사용 지점을 httpClient/postApi로 리팩터.
- 3단계: 중장기: 추상화 레벨에 대한 팀 가이드(어떤 책임을 entities에 둘지)를 문서화.
7. 🧪 테스트 용이성
💡 개념 정의
테스트 용이성은 코드가 단위/통합 테스트로 검증되기 얼마나 쉬운지를 나타냅니다. 순수 함수와 사이드이펙트 분리는 핵심 원칙입니다.
⚡ 중요성
신규 기능 추가나 API 변경 시 회귀를 방지하고 신속히 검증할 수 있어 유지보수 비용을 낮춥니다.
📊 현재 상황 분석
문제: 직접 fetch 사용 + zustand에 비즈니스 로직 포함 → 단위 테스트의 모킹 포인트가 늘어남.
해결: httpClient/Api 층을 모킹 가능한 인터페이스로 만들고, UI는 props로 상태를 주입해 단위테스트가 가능하도록 구성.
📝 상세 피드백
테스트 용이성 측면에서 개선 여지가 있습니다. 좋은 점은 API가 postApi와 hooks로 분리되어 있고 components가 순수 UI로 구성된 사례가 있다는 점입니다. 반면 직접 fetch 호출과 전역 zustand store, React Query가 혼재되어 있어 단위 테스트/통합 테스트에서 모킹과 경계설정이 복잡할 수 있습니다.
권장: 비즈니스 로직(데이터 변환, 캐시 갱신)은 순수 함수로 추출하고, 네트워크는 httpClient로 모킹하세요. React Query와 zustand는 각각의 레이어를 테스트할 수 있도록 인터페이스를 제공하세요.
❌ 현재 구조 (AS-IS)
// AS-IS: PostsManagerPage는 fetch를 직접 호출해 사용자 데이터 조회 -> 테스트 시 fetch 모킹 필요
onClick -> fetch(`${API_BASE}/users/${id}`)
// pages와 store가 결합되어 있어 통합 테스트 설계가 복잡✅ 권장 구조 (TO-BE)
// TO-BE: httpClient 모킹
// tests: jest.mock('shared/infra/httpClient')
// pages: userApi.getUser -> mockResolvedValue
// UI 컴포넌트는 props(user, isLoading)로 분리되어 단위 테스트 가능🔄 변경 시나리오별 영향도
- 외부 API 계약이 변경되어 테스트 케이스를 수정해야 하는 경우: 모킹 경계가 명확하면 테스트 수정 범위가 작아짐.
- E2E에서 React Query Devtools나 zustand 상태 확인용 hook을 사용하면 디버깅이 쉬워짐.
🚀 개선 단계
- 1단계: 단기(반나절): httpClient를 모킹 가능한 형태로 분리하고 간단한 unit test 하나 작성.
- 2단계: 중기(1~2일): entities의 순수 함수(데이터 가공)를 분리하여 단위 테스트 추가.
- 3단계: 중장기: React Query 훅과 zustand 스토어에 대한 통합 테스트 전략(모킹, 테스트 utils)을 문서화.
8. ⚛️ 현대적 React 패턴
💡 개념 정의
현대적 React 패턴은 Suspense, Error Boundary, 커스텀 훅, forwardRef와 타입 제네릭 등을 의미합니다. 이들은 UI/비즈니스 로직 분리, 선언적 로딩/에러 처리를 돕습니다.
⚡ 중요성
패턴을 적용하면 로딩·에러 UI 변경이 한곳에서 가능하고 컴포넌트 재사용성이 좋아집니다. 또한 요구사항 변화(예: 스켈레톤 변경)에 대한 수정 범위가 작아집니다.
📊 현재 상황 분석
현재: useQuery 사용은 잘 되어 있지만 Suspense 모드나 ErrorBoundary를 통해 선언적 처리하지 않음. 결과적으로 각 컴포넌트가 loading/error 처리를 직접 담당하고 있어 UI 전략 변경 시 영향 범위 큼.
📝 상세 피드백
현대적 React 패턴 일부가 적용되어 있습니다(ForwardRef 타입 명시, React Query, Devtools). 그러나 Suspense와 Error Boundary 사용, 데이터 로딩/에러 처리를 상위에서 선언적으로 다루는 패턴은 아직 적용되지 않았습니다. 또한 PostsManagerPage가 여전히 꽤 많은 로직을 포함하고 있어 커스텀 훅을 통해 더 분리하면 변화에 대한 적응력이 향상됩니다.
권장: 데이터 로딩/에러 처리를 상위 레이어(예: ErrorBoundary + Suspense)에서 처리하고, PostsManager 로직은 usePosts 훅으로 추출하세요.
❌ 현재 구조 (AS-IS)
// AS-IS
const Dashboard = () => { const { data, isLoading, error } = useTagsQuery(); if(isLoading) return <div>Loading</div>; if(error) return <div>Error</div> }
// 각 컴포넌트가 직접 처리✅ 권장 구조 (TO-BE)
// TO-BE
<ErrorBoundary fallback={<ErrorFallback/>}>
<Suspense fallback={<Skeleton/>}>
<Dashboard/>
</Suspense>
</ErrorBoundary>
// Dashboard는 data 처리를 단순화🔄 변경 시나리오별 영향도
- 로딩 UX 전략 변경(스켈레톤 추가): Suspense를 사용하면 상위 레이아웃만 수정하면 됨.
- 에러 UI를 통합하려면 ErrorBoundary로 래핑하면 각 컴포넌트 수정 불필요.
🚀 개선 단계
- 1단계: 단기(반나절): PostsManagerPage의 데이터 로직을 usePosts/usePostMutations 같은 커스텀 훅으로 분리.
- 2단계: 중기(1~2일): App 레벨 또는 상위 페이지에 ErrorBoundary/Suspense 패턴 도입(문서화 포함).
- 3단계: 중장기: 스켈레톤/에러 컴포넌트를 디자인 시스템에 등록해 일관된 UX 제공.
9. 🔧 확장성
💡 개념 정의
확장성(Extensibility)은 새로운 기능이나 변경 요구가 들어왔을 때 시스템을 쉽게 확장할 수 있는 능력입니다.
⚡ 중요성
비즈니스 요구 변화(다국어, A/B 테스트, 실시간 등)에 대비하려면 초기 구조에서 확장 포인트를 마련해야 합니다.
📊 현재 상황 분석
좋은 점: React Query 훅과 QueryClientProvider 도입으로 서버 상태 확장에 유리.
개선 포인트: infra 추상화 부재로 실시간 소스나 오프라인 처리 도입 시 추가 변경이 필요.
📝 상세 피드백
확장성은 비교적 양호합니다. entities/post/hooks와 queryKeys로 분리하여 새로운 기능(태그/검색/낙관적 업데이트)을 수용할 준비가 되어 있습니다. 다만 중복된 fetch 코드와 shared 레이어의 도메인 의존성은 새로운 기능(다국어, 실시간, 오프라인) 도입 시 리팩터 비용을 증가시킵니다.
권장: real-time/오프라인 같은 요구를 고려해 data layer를 명확히 하고, 서비스 어댑터(REST/WebSocket)를 추상화하세요.
❌ 현재 구조 (AS-IS)
// AS-IS: fetch가 곳곳에 직접 존재 -> 실시간 소스 추가 시 모든 fetch 호출을 재고해야 함
// TO-BE: postApi 어댑터 패턴으로 교체하면 WebSocket 어댑터로 대체 가능✅ 권장 구조 (TO-BE)
// TO-BE: adapter 패턴
export const createPostService = (adapter) => ({ getPosts: adapter.getPosts, addPost: adapter.addPost })
// adapter: restAdapter, websocketAdapter 등으로 교체 가능🔄 변경 시나리오별 영향도
- 다국어 지원: UI/텍스트는 shared/ui에서 토큰화하고 i18n 적용. 현재 구조면 페이지별로 분산 변경이 필요할 수 있음.
- 실시간 기능 추가: postApi 추상화가 있으면 WebSocket 어댑터로 교체만으로 가능; 현재는 fetch 분산으로 리팩터 필요.
🚀 개선 단계
- 1단계: 단기(반나절): postApi 추상화와 httpClient를 먼저 정리해 어댑터 교체 지점을 만들기.
- 2단계: 중기(1~3일): 실시간/오프라인을 고려해 caching/invalidations 전략 문서화 및 adapter 패턴 도입.
- 3단계: 장기: 실제 실시간 알림/동기화 모듈을 도입하고 regression 테스트 추가.
10. 📏 코드 일관성
💡 개념 정의
코드 일관성은 네이밍, 파일명, import/export 패턴, 코드 스타일(들여쓰기·따옴표 등)의 일관성을 의미합니다.
⚡ 중요성
일관성은 팀 단위 협업에서 학습 곡선과 병합 충돌을 줄이며 자동화 도구 적용(리네이밍, 코드 분석)에 유리합니다.
📊 현재 상황 분석
규칙 실행 방안: ESLint(prettier integration), import/order 플러그인, 컴포넌트 export 규칙 문서화.
📝 상세 피드백
전반적으로 코드 스타일과 네이밍이 일관적입니다. components/index.tsx에서 제네릭 타이핑을 적용한 점은 우수합니다. 다만 몇 군데에서 따옴표 스타일(' vs ")와 파일명 케이스(페이지 파일은 PascalCase로 잘 통일됨)가 혼재하지는 않지만, import 경로 사용(절대/상대) 규칙을 명확히 하면 더 좋습니다.
특히 import/export 패턴은 components가 named exports로 잘 구성되어 있으나 일부 파일(예: Header/Footer)은 default export를 사용 중일 수 있으니 규칙 통일(컴포넌트는 named export 권장)을 제안합니다.
❌ 현재 구조 (AS-IS)
// AS-IS
import { BrowserRouter as Router } from "react-router-dom"
// 일부 파일은 ' 사용, 일부는 " 사용 — 포맷터로 통일 필요✅ 권장 구조 (TO-BE)
// TO-BE
// Prettier/ESLint 적용 후 모든 파일이 single-quote, import order 규칙 준수
import React from 'react'
import { useQuery } from '@tanstack/react-query'
import { Button } from 'shared/ui'
import { highlightText } from './utils'🔄 변경 시나리오별 영향도
- 다른 팀에서 코드베이스 합치기: 규칙 불일치 시 엄청난 리팩터 필요(자동화로 일부 해결 가능).
- 자동 린트/포맷 도구 도입 시 초기 변경량 발생하지만 장기적으로 편의성 증가.
🚀 개선 단계
- 1단계: 단기(half day): Prettier + ESLint 설정 파일 추가 및 CI에서 포맷 검사 적용.
- 2단계: 중기(1일): import/order 규칙과 컴포넌트 export(명명/파일) 컨벤션 문서화.
- 3단계: 장기: Git hook(pre-commit)으로 자동 포맷 및 lint 실행.
🎯 일관성 체크포인트
Import/Export 패턴
- 몇몇 컴포넌트가 default export일 가능성(검토 필요) — 팀 규칙으로 통일 권장
코드 스타일
- 따옴표 스타일이 파일에서 혼재된 흔적(패치에서 일부 파일 변환됨) — Prettier/ESLint로 고정 권장
11. 🗃️ 상태 관리
💡 개념 정의
데이터 흐름 및 상태 관리는 전역 상태(클라이언트)와 서버 상태(원격 데이터)를 어떻게 분리·관리하는지를 뜻합니다. 서버 상태는 React Query 같은 툴로, UI 상태는 로컬 스토어로 분리하는 것이 일반적입니다.
⚡ 중요성
명확한 분리는 데이터 중복, 일관성 문제를 줄이고 테스트·디버깅을 단순화합니다. 또한 낙관적 업데이트 등 서버 상태 전략 도입이 쉬워집니다.
📊 현재 상황 분석
문제: postsStore와 usePostsQuery의 중복 책임(데이터 fetching/enrich). 이는 상태 동기화 이슈와 디버깅 복잡도를 야기합니다.
권장: postsStore는 pagination, UI flags, cached client-only changes 등으로 축소하고, 서버 데이터는 React Query로 단일화하세요.
📝 상세 피드백
서버 상태(React Query)와 클라이언트 상태(zustand)를 분리하려는 시도는 명확하고 잘 수행되었습니다. usePreferencesStore는 로컬 퍼시스트를 적절히 처리합니다. 문제는 posts 관련 책임이 두 군데(zustand store + React Query hooks)에 중복되어 있다는 점입니다. 한 시스템(React Query)을 서버 상태에, zustand는 UI/로컬 상태(모달 open/selected item/pagination)를 담당하도록 역할을 더 엄격히 분리하세요.
예: 현재 postsStore는 서버 fetch, tags, comments 등을 직접 관리합니다. entities/post/hooks.ts와 중복되므로 하나로 통합하면 상태 충돌과 업데이트 불일치 위험을 줄일 수 있습니다.
❌ 현재 구조 (AS-IS)
// AS-IS
// src/shared/stores/postsStore.ts: fetchPosts(), fetchPostsByTag(), searchPosts() 등 서버 fetch 직접 수행
// src/entities/post/hooks.ts: usePostsQuery()가 동일한 종류의 데이터를 가져옴
// Pages consume both layers -> 동기화 고민 발생✅ 권장 구조 (TO-BE)
// TO-BE
// zustand: pagination, selectedTag, UI flags 만 보유
// entities/hooks.ts: usePostsQuery({skip,limit,tag,search}) -> 서버 데이터 단일 소스
// Pages: zustand에서 pagination 읽고 usePostsQuery 호출🔄 변경 시나리오별 영향도
- 오프라인/동기화 기능 추가: React Query의 페칭/캐시 전략과 zustand의 로컬 큐를 명확히 분리해야 구현 비용을 줄일 수 있음.
- 실시간 동기화 추가: 서버 상태를 React Query로 유지하고, 실시간 이벤트를 queryClient.setQueryData로 반영하는 패턴 권장.
🚀 개선 단계
- 1단계: 단기(반나절): postsStore에서 fetch* 관련 메서드(서버 fetch)를 주석 처리하거나 deprecated 표시하고, usePostsQuery로 전환하도록 페이지 수정.
- 2단계: 중기(1~2일): postsStore 리팩터링 - 서버 관련 메서드 제거, UI 상태만 유지. CI 테스트로 동기화 검증.
- 3단계: 장기: 오프라인·실시간 전략을 문서화하고 Query/Store 협업 패턴을 팀 규칙으로 제정.
🤔 질문과 답변
- 대규모 프로젝트에서 Query 캐시 키 관리 방법
- 중앙화된 queryKeys 팩토리: 도메인별로 queryKeys 파일을 만들고 함수(또는 튜플)으로 키를 생성하세요. 예: postsKeys.list(skip,limit,tag,search)처럼 파라미터를 안정적(primitive string/number)으로 변환한 튜플을 반환합니다.
- 파라미터 정규화: 객체를 그대로 넣지 말고 순서가 보장되는 문자열/튜플로 변환하세요(예:
['posts','list',skip,limit, tag ?? 'all', search ?? '']). - 네임스페이스와 버전: 큰 변경 시 캐시 충돌을 막기 위해 키에 버전(v1)을 포함하기도 합니다.
- 키 설계 규칙: list/detail, entity/{id}, meta(tags)로 계층화하고 쿼리단위로 책임을 분명히.
- 운영 팁: queryClient.invalidateQueries vs setQueryData를 상황에 맞게 사용(대량 invalidation 대신 세분화된 setQueryData 권장).
- 디자인 시스템을 별도 패키지로 뺄 때 팁
- 모노레포 툴(pnpm workspace, yarn workspace)을 사용해 components/tokens/styles 패키지를 분리하세요.
- 빌드: rollup/tsup으로 ESM+CJS 번들 제공, types (.d.ts) 포함.
- 의존성: react, react-dom는 peerDependencies로 지정.
- 테마 토큰(색상, spacing)은 CSS variables로 추상화하면 런타임 테마 전환(예: dark/light)과 호환성이 좋습니다.
- 문서·스토리북: Storybook + Chromatic으로 UI 변경 이력을 관리하고 시각적 회귀를 방지하세요.
- 버전 전략: semantic versioning(major 변경 시 breaking changes)과 migration 가이드(CHANGELOG)를 제공하세요.
- FSD에서 '공통 vs 기능' 경계 기준
- 재사용성: 여러 feature가 동일하게 사용하면 shared에 둡니다(예: Button, Input, Badge).
- 도메인 지식: 도메인 개념(예: PostStatusBadge)은 feature/entities 쪽에 둡니다.
- API 표면: small, stable API는 shared에, 자주 변경되는 비즈니스 규칙은 feature/entities에 둡니다.
- Ownership: 어느 팀이 책임질지 기준으로 경계 설정—공통은 플랫폼팀, 도메인은 도메인팀.
- 테스트: 공통 컴포넌트가 feature 로직을 포함하지 않아야 단독 테스트가 가능합니다.
간단한 체크리스트: (1) 이 모듈을 다른 도메인에서 쓰는가? (2) 도메인 지식을 포함하는가? (3) 자주 바뀔 가능성이 있는가? Yes/No로 분류하세요.
🎯 셀프 회고 & 제안
귀하의 셀프회고에서 보이는 핵심 인사이트는 '도메인 우선 분리(entities-first) → 역할 분리' 의 실무 적용과 'React Query로 서버 상태를 깔끔하게 처리'에 대한 경험입니다. 아래 몇 가지 추가로 생각해볼 질문을 남깁니다:
- 폴더/모듈 경계에서 '타입의 거주지'를 어떻게 결정하셨나요? 타입을 entities에 두면 어떤 장단점이 있나요? 반대로 shared에 두었을 때의 편리함은 무엇인가요?
- Zustand와 React Query의 책임 경계를 팀 컨벤션으로 문서화한다면 어떤 항목들을 최소 규칙으로 삼을 것인지요? (예: 'Zustand는 UI/세션 상태만', 'React Query는 모든 원격 데이터')
- queryKey 설계 시 파라미터 직렬화 전략에 대해 더 고민해보면 어떤 edge case(예: 객체 정렬, 빈값 처리)가 있나요?
제안: 현재의 경험을 바탕으로 '작은 실험'을 진행해보세요. 예를 들어 한 엔티티(Post)에 대해 types→api→hooks를 완전히 entities로 옮기고 pages가 그 public API만 사용하도록 리팩터한 뒤, 변경 전/후의 수정 파일 수와 테스트 커버리지를 비교해보면 인사이트가 더 명확해집니다.
추가 논의가 필요한 부분이 있다면 언제든 코멘트로 남겨주세요!
코드 리뷰를 통해 더 나은 아키텍처로 발전해 나가는 과정이 즐거웠습니다. 🚀
이 피드백이 도움이 되었다면 👍 를 눌러주세요!
과제 체크포인트
jungwoo0203.github.io/front_6th_chapter2-3/
기본과제
목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기
체크포인트
심화과제
목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기
체크포인트
최종과제
과제 셀프회고
이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.
본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?
아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.
이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.
챕터 셀프회고
to be continued..
클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기
결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리
응집도 높이기: 서버상태관리, 폴더 구조
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문