[8팀 김민지] Chapter 2-3. 관심사 분리와 폴더구조#32
Conversation
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 (하위 계층만 상위 계층을 참조하지 않음) 입니다.
⚡ 중요성
의존성 방향을 지키면 한 계층을 바꾸거나 패키지화할 때 수정 파일 수가 최소화됩니다. shared가 feature에 의존하면 shared를 여러 프로젝트에서 재사용할 수 없고, 모노레포나 패키지 독립화 시 비용이 크게 증가합니다.
📊 현재 상황 분석
AS-IS: 대부분의 기능을 feature/와 entities/로 잘 나눴음. 그러나 shared에서 feature를 import하는 역의존이 존재합니다. 또 일부 index.ts로 Public API를 제공하는 것은 잘 되어 있으나, 파일/폴더 명칭(FSD 권장 명칭: features vs feature)에서 표준화가 일부 미흡합니다.
📝 상세 피드백
FSD 계층(앱→pages→widgets→feature(s)→entities→shared) 관점에서 전체 구조를 잘 분리하려는 시도가 보입니다. pages에서 PostsManager를 노출하고 widgets(Header/Footer)을 분리한 점, entities와 feature의 책임 분리가 명확해졌습니다. 다만 shared 계층과 feature 계층 간의 의존성 역전 위반 사례가 존재하여 FSD 의존성 방향 규칙(app→pages→widgets→features→entities→shared)을 일부 위반하고 있습니다. 특히 shared/hooks/useURL.ts가 feature/post/model/store의 useSearchQueryStore를 직접 import하고 있어 shared → feature 의 역방향 의존이 발생합니다. 이로 인해 shared를 다른 프로젝트에 재사용하거나 feature를 독립 패키지로 떼어내기 어려워집니다.
❌ 현재 구조 (AS-IS)
src/shared/hook/useURL.ts
import { useSearchQueryStore } from "../../feature/post/model/store"
// shared 훅이 feature store를 직접 사용 -> shared가 feature를 참조함✅ 권장 구조 (TO-BE)
// TO-BE: shared는 feature에 의존하지 않음
// 1) shared/useURL.ts는 URL 파싱/조작만 제공
export const parseQuery = (search) => { /* ... */ }
export const buildQuery = (params) => { /* ... */ }
// 2) feature에서는 parse/build를 사용하고 자체 store와 결합
import { parseQuery, buildQuery } from 'shared/hook'
import { useSearchQueryStore } from 'feature/post/model/store'
useEffect(() => {
const q = parseQuery(location.search)
setSearchQuery(q.search)
}, [])🔄 변경 시나리오별 영향도
- 만약 프로젝트를 모노레포에서 여러 패키지로 분리할 경우: shared가 feature를 참조하면 shared 패키지를 떼어내지 못해 리팩토링 비용이 큼.
- UI 라이브러리(Material→Chakra) 변경 시: shared/ui 계층만 수정하면 되지만 shared가 feature에 의존하면 feature들에서도 수정이 필요할 수 있음.
- widget과 feature의 경계 재정의(예: widget을 feature에서 더 많이 사용) 시: 역의존이 있으면 경계 변경 비용이 증가함.
🚀 개선 단계
- 1단계: 단기(1
2일): shared/hooks/useURL.ts에서 feature 의존 제거 — URL 파싱/조작 로직만 shared로 이동하고 store 연동은 feature에서 수행하도록 변경 (1인 작업: 24시간). - 2단계: 중기(1~2일): 프로젝트 전반의 import 경로 점검 및 Public API(index.ts) 표준화(예: features vs feature 네이밍 통일).
- 3단계: 장기(1주): 아키텍처 문서화(FSD 레이어 정의, 허용/비허용 의존성 목록) 및 CI에서 의존성 그래프 검증 도구(예: depcruise) 도입.
2. 🔄 TanStack Query
💡 개념 정의
TanStack Query는 서버 상태 관리 라이브러리로, 쿼리 키(queryKey)를 통해 캐시를 식별하고 useQuery/useMutation 등의 선언적 API로 비동기 상태를 관리합니다. 쿼리 키는 계층화된(factory) 패턴으로 정의해 변경에 안전하게 만들어야 합니다.
⚡ 중요성
일관된 쿼리 키와 하나의 QueryClient 구성은 캐시 전략(캐싱, staleTime, 낙관적 업데이트) 동작의 정확성을 보장합니다. 쿼리 키 불일치는 invalidate/garbage collection 실패, 중복 fetch, 또는 stale UI를 초래합니다.
📊 현재 상황 분석
AS-IS: useQuery/useMutation 사용과 Devtools 포함으로 작업하기 쉬운 구조. 그러나 쿼리 키 혼재(리터럴 vs QUERY_KEYS 팩토리), QueryClient 중복 생성, 일부 쿼리의 enabled/staleTime 설정 미세 조정 필요(예: getTags의 staleTime 0 설정).
📝 상세 피드백
React Query(TanStack Query)를 광범위하게 도입하여 서버 상태를 선언적으로 관리하고 낙관적 업데이트를 적용한 점이 훌륭합니다. 그러나 쿼리 키 사용의 일관성 문제와 QueryClient 구성의 중복(Providers.tsx의 QueryClient vs shared/constants/query.ts의 client)으로 인해 캐시 무효화/갱신이 누락되거나 중복 동작할 가능성이 있습니다. 또한 일부 mutation에서 invalidate/cancel에 문자열 리터럴(['getPosts'])을 사용하고, 다른 곳에서는 QUERY_KEYS 헬퍼(QUERY_KEYS.getPosts(...))를 사용하는 혼재가 존재합니다. 파라미터가 있는 쿼리의 경우 리터럴 키로 invalidate하면 기대한 쿼리들이 무효화되지 않습니다.
❌ 현재 구조 (AS-IS)
// AS-IS: 혼재된 쿼리키 사용
queryClient.cancelQueries({ queryKey: ["getPosts"] })
// vs
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.getPosts(limit, skip, sortBy, sortOrder) })✅ 권장 구조 (TO-BE)
// TO-BE: 통일된 QUERY_KEYS 사용
// shared/constants/query.ts 에 QUERY_KEYS만 사용
queryClient.cancelQueries({ queryKey: QUERY_KEYS.getPosts(limit, skip, sortBy, sortOrder) })
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.getPosts(limit, skip, sortBy, sortOrder) })
// 그리고 QueryClient는 Providers에서 하나만 생성하거나 shared에서 export한 client를 사용하도록 통일🔄 변경 시나리오별 영향도
- API 엔드포인트가 페이로드/파라미터를 바꿀 경우: 쿼리 키 객체 구조가 바뀌면 invalidate 코드를 전역적으로 수정해야 함(현재는 혼재로 범위 파악 어려움).
- 새로운 데이터 소스(예: 소셜 피드 실시간) 추가 시: 일관된 쿼리 키 정책이 없으면 캐시 병합/무효화 로직 복잡도 증가.
- Error handling 전략 변경(글로벌 에러 바 또는 toast로 통합) 시: 현재 각 mutation의 onError 핸들링을 일관된 방식으로 중앙화해야 함.
🚀 개선 단계
- 1단계: 단기(반나절): 프로젝트에서 쿼리 키 사용 정책 정의(파라미터는 객체로, 항상 QUERY_KEYS 팩토리 사용). 코드베이스에서 문자열 리터럴 키(['getPosts'])를 전부 FIND/REPLACE로 교체 (예상 소요: 2~4시간).
- 2단계: 단기(1일): Providers.tsx와 shared/constants/query.ts의 QueryClient 중복 문제 해결 — 하나의 client를 만들고 모든 곳에서 동일한 인스턴스를 사용하도록 통일.
- 3단계: 중기(1~2일): QUERY_KEYS 문서화 및 테스트 케이스 작성(예: key equality 테스트) 및 쿼리 key lint 규칙 도입.
- 4단계: 장기(1주): 에러/로딩/리프레시 정책(전역감시) 설계: 공통 onError 핸들러와 retry 정책 통일.
3. 🎯 응집도 (Cohesion)
💡 개념 정의
응집도(Cohesion)는 관련 기능이 한 모듈/디렉토리에 얼마나 모여있는지를 나타냅니다. 높은 응집도는 변경 시 수정 파일 수를 줄이고 모듈을 독립적으로 이해하기 쉽게 합니다.
⚡ 중요성
변경(새 필드 추가, UX 변경 등) 시 수정 범위를 좁혀 유지보수 비용을 낮춥니다. 또한 패키지화(도메인 분리) 시 간편하게 떼어낼 수 있습니다.
📊 현재 상황 분석
AS-IS: Post 관련 기능은 높은 응집도를 보임. URL/state sync 로직과 검색 관련 상태가 shared와 feature에 나뉘어 있어 검색 동작을 이해하기 위해 여러 폴더를 봐야 합니다.
📝 상세 피드백
응집도 측면에서는 feature 단위로 UI, model, type을 잘 묶어둔 모습이 많아 높은 응집도를 달성하려는 시도가 보입니다. PostsManagerPage가 작은 pieces(Feature 컴포넌트들)로 분리되어 PostList, PostHeader 등으로 재구성된 점은 긍정적입니다. 다만 일부 공통 로직(예: URL 처리)이 shared와 feature 사이에 섞여 있어 관련 코드가 분산되는 문제가 있습니다.
❌ 현재 구조 (AS-IS)
AS-IS: 검색 상태는 useSearchQueryStore(feature/post/model/store.ts)와 shared/useURL.ts가 혼용되어 관리됨 -> 관련 코드가 분산✅ 권장 구조 (TO-BE)
TO-BE: 검색/URL sync는 feature 레이어에서 책임지고 shared는 pure parsing/util 함수만 제공
// shared
export const buildQuery = (params) => {...}
export const parseQuery = (search) => {...}
// feature
const { setSearchQuery } = useSearchQueryStore()
useEffect(() => { const q = parseQuery(location.search); setSearchQuery(q.search) }, [])🔄 변경 시나리오별 영향도
- 새로운 필터(예: 작성일) 추가 시: 응집도가 높으면 feature/post 내부에서만 수정 가능. 현재 구조에서는 shared/useURL와 feature/post 둘 다 수정 필요할 수 있음.
- 다국어 지원 추가 시: shared/ui 문자열 처리만 수정하면 되지만 feature에 문자열이 하드코딩되어 있다면 여러 위치를 수정해야 함.
🚀 개선 단계
- 1단계: 단기: shared/useURL를 pure util로 분리하고 feature에서 store와 결합하도록 변경 (2~4시간).
- 2단계: 중기: feature 레이어 내부의 상태ful 로직과 순수 util을 명확히 주석/폴더로 구분.
- 3단계: 장기: 도메인별 응집도 측정을 CI에 도입(예: 변경 시 참조되는 파일 수(metric) 계산).
4. 🔗 결합도 (Coupling)
💡 개념 정의
결합도(Coupling)는 모듈 간 의존성의 강도를 말하며 낮은 결합도는 모듈 교체/리팩토링 시 영향을 최소화합니다. 인터페이스(추상화)를 통해 구체 구현을 숨기면 결합도를 낮출 수 있습니다.
⚡ 중요성
라이브러리 교체(axios↔fetch, zustand↔redux)나 아키텍처 변경(모노레포로 패키지 분리) 시 수정 범위를 좁혀 빠른 적용과 안전한 롤백을 가능하게 합니다.
📊 현재 상황 분석
AS-IS: API 계층의 추상화(requestApi)로 HTTP 클라이언트 변경 비용은 낮음(1파일). 반면 client-state 구현(zustand stores)은 여러 모듈에 분산되어 있어 상태관리 라이브러리 변경 비용이 상대적으로 높음(약 6곳).
📝 상세 피드백
결합도는 비교적 낮추려는 시도가 보입니다(entities는 requestApi 사용, feature는 entities API를 사용). 하지만 몇 가지 강한 결합 포인트가 존재합니다: shared가 feature store를 직접 참조(shared/hooks/useURL), QueryClient 중복 생성(Providers.tsx vs shared/constants/query.ts), 그리고 mutation에서 쿼리 키 리터럴 사용으로 인해 쿼리 캐시 대응이 약화됩니다. 또한 Zustand store 타입과 사용 위치가 feature 내부에 분산되어 있어 상태관리 라이브러리를 교체할 때 수정 범위가 feature별로 발생합니다.
❌ 현재 구조 (AS-IS)
// AS-IS: shared 훅이 feature store를 직접 참조
// src/shared/hook/useURL.ts
import { useSearchQueryStore } from "../../feature/post/model/store"
// 상태관리 라이브러리 변경 시 수정 영역이 커짐
// requestApi는 중앙화되어 있음
// src/shared/lib/api.ts -> 하나만 수정하면 됨✅ 권장 구조 (TO-BE)
// TO-BE: low coupling
// 1) shared는 순수 util만 제공
export const parseQuery = (s) => {...}
// 2) feature는 이를 조합해 store와 결합
import { parseQuery } from 'shared/hook'
import { useSearchQueryStore } from 'feature/post'
useEffect(()=>{ setSearchQuery(parseQuery(location.search).search) }, [])🔄 변경 시나리오별 영향도
- HTTP 클라이언트 변경(예: fetch→axios): 영향 범위는 requestApi 한 곳(1파일)으로 국한되어 빠르게 적용 가능.
- 상태관리 라이브러리 변경(zustand→redux): 영향 범위는 feature별 store 파일(현재 약 2곳의 store 파일, plus 이를 참조하는 훅/컴포넌트)으로 예상되며 약 8~12개 파일 수정 필요 가능.
- QueryClient 설정 변경(전역 옵션 변경): Providers.tsx와 shared/constants/query.ts 두 곳에서 수정 필요 — 중복 제거 필요.
🚀 개선 단계
- 1단계: 단기: shared → feature 역의존 제거(1일).
- 2단계: 중기: Zustand stores에 대한 adapter 레이어 도입 — stores를 추상화하는 인터페이스를 만들어 상태관리 라이브러리 교체를 단순화(2~3일).
- 3단계: 장기: 의존성 그래프 검증 툴 도입 및 규칙(enforce) 적용.
5. 🧹 Shared 레이어 순수성
💡 개념 정의
Shared 레이어 순수성은 shared 코드가 도메인(entities/feature)에 의존하지 않고 범용적으로 재사용 가능한지를 의미합니다. 공유 유틸/컴포넌트는 도메인 독립적이어야 합니다.
⚡ 중요성
shared가 도메인에 의존하면 다른 프로젝트에서 재사용 불가, 디자인 시스템 또는 UI 라이브러리를 교체할 때 큰 비용 발생.
📊 현재 상황 분석
AS-IS: 대부분의 shared 컴포넌트는 도메인 독립적이고 범용적으로 설계됨. 그러나 일부 shared 파일이 feature에 의존함으로써 shared 재사용성이 떨어짐.
📝 상세 피드백
shared 레이어에 UI 컴포넌트, 유틸(requestApi, HighlightText), 타입(PaginationInfo, Author) 등이 잘 모여 있어 재사용성이 높습니다. 다만 shared/hooks/useURL가 feature를 참조하는 문제로 인해 shared 순수성(purity)이 훼손되고 있습니다. 또한 shared/constants/query.ts에서 QueryClient를 생성해 export하는 패턴과 Providers.tsx에서 새 QueryClient를 생성하는 중복이 있어 shared가 전역 설정의 single source of truth로 동작하지 못하고 있습니다.
❌ 현재 구조 (AS-IS)
// AS-IS: shared 훅이 feature store를 참조
// src/shared/hook/useURL.ts
import { useSearchQueryStore } from "../../feature/post/model/store"✅ 권장 구조 (TO-BE)
// TO-BE: shared는 순수 util만 제공
export const parseQuery = (search) => { /* pure parsing */ }
export const buildQuery = (params) => { /* pure build */ }
// feature 내부에서 parse/build를 사용하여 store와 결합🔄 변경 시나리오별 영향도
- 다른 프로젝트에서 shared 재사용 시: shared/useURL가 feature를 참조하면 재사용 불가 -> 제거 필요.
- 디자인 시스템 교체 시: shared/ui 컴포넌트만 수정하면 되지만 domain-specific 로직이 섞여 있다면 feature도 수정 필요.
🚀 개선 단계
- 1단계: 즉시(반나절): shared/hooks/useURL의 feature 참조 제거 — parse/build 기능만 남기고 store 연동을 feature로 이전.
- 2단계: 단기(1일): Providers에서 사용하는 QueryClient를 shared/constants/query.ts의 client로 통일하거나, shared에서 client를 제거하고 Providers에서만 생성하도록 합의.
- 3단계: 중기: shared 레이어의 의존성 규칙을 문서화(허용/비허용 import 목록) 및 PR 템플릿에 체크리스트 추가.
6. 📐 추상화 레벨
💡 개념 정의
추상화는 구현 세부사항을 숨기고 명확한 인터페이스(계약)를 통해 상호작용하도록 하는 것입니다. 잘 설계된 추상화는 구현 교체(예: HTTP client, 데이터 포맷) 시 파급을 줄입니다.
⚡ 중요성
API 변경, 외부 서비스(예: 인증 제공자) 전환, 또는 테스트에서의 mocking이 쉬워집니다.
📊 현재 상황 분석
AS-IS: requestApi로 HTTP 구현을 추상화한 것은 매우 좋음. 그러나 데이터 조합/가공(특히 author 결합)을 별도 adapter로 추출하지 않아 동일 로직을 테스트/재사용하기 어려움.
📝 상세 피드백
비즈니스 로직(데이터 조합, author 병합 등)을 feature 레이어에서 처리하고 entities 레이어는 순수 CRUD(api functions) 역할을 담당하려 한 점은 좋은 추상화입니다. 다만 일부 훅에서는 fetch 결과를 가공하는 로직이 복합적으로 섞여 있어(예: userPostInfo 내부에서 getPosts와 getUsers를 병합) 테스트용 추상화 포인트(예: 데이터 어댑터)를 추가하면 더 유리합니다. 또한 requestApi의 return 형태({result, data})를 전제한 코드가 곳곳에 있어, 통일된 에러/데이터 형태에 대한 계약을 문서로 남겨두면 안전합니다.
❌ 현재 구조 (AS-IS)
// AS-IS: feature에서 직접 병합
const [postResult, userResult] = await Promise.all([getPosts(...), getUsers()])
const postsWithUsers = postResult.data.posts.map(post => ({ ...post, author: users.find(u => u.id === post.userId) }))✅ 권장 구조 (TO-BE)
// TO-BE: adapter 사용
// entities/posts/adapter.ts
export const enrichPostsWithUsers = (posts, users) => posts.map(p => ({...p, author: users.find(u=>u.id===p.userId)}))
// feature에서 사용
const postsWithUsers = enrichPostsWithUsers(postResult.data.posts, userResult.data.users)🔄 변경 시나리오별 영향도
- API 응답 구조가 변경되면: entities의 api 함수와 feature의 조합로직을 모두 업데이트해야 할 수 있음 — adapter를 두면 영향 최소화.
- 외부 데이터 소스가 추가되면: adapter 패턴으로 확장성 확보 가능.
🚀 개선 단계
- 1단계: 단기: posts+users 병합 로직을 feature 내부의 유틸/adapter로 추출(2~4시간).
- 2단계: 중기: requestApi의 반환 타입 계약을 문서화하고 전역 에러 핸들러(통일된 error shape) 정의.
- 3단계: 장기: adapter/mapper 패턴을 전역적으로 적용하여 외부 API 변경에 대비.
7. 🧪 테스트 용이성
💡 개념 정의
테스트 용이성은 단위/통합/엔드투엔드 테스트를 작성하고 유지보수하기 쉬운 정도입니다. 순수 함수, 의존성 주입, 사이드 이펙트 분리는 핵심 원칙입니다.
⚡ 중요성
새 API 연동, 복잡한 비즈니스 로직 추가, 리팩토링 시 회귀를 빠르게 잡아낼 수 있어 개발 속도와 안정성을 높입니다.
📊 현재 상황 분석
AS-IS: 대부분의 로직이 훅/모델로 분리되어 있어 단위 테스트 대상이 많음. 그러나 store와 훅의 결합으로 일부 테스트에서 전역 상태 초기화가 필요하여 테스트가 덜 독립적일 수 있음.
📝 상세 피드백
테스트 용이성 측면에서 긍정적 요소(비즈니스 로직 분리, requestApi 중앙화, react-query 사용)와 개선 포인트(zustand store 직접 생성으로 인해 mocking 어려움, shared에서 feature 참조로 인한 단위 테스트 범위 확장)가 혼재합니다. requestApi를 mock하면 엔티티 레벨 API 테스트가 쉬우며, feature 훅은 useQuery를 사용해 단위/테스트할 수 있습니다. 다만 useComment/usePost 훅들이 내부에서 store를 직접 사용하거나 상태 설정(side effect)을 포함해 일부 테스트에서 mocking이 필요합니다.
❌ 현재 구조 (AS-IS)
// 현재: useComment 훅 내에서 useCommentStore를 직접 사용
const { newComment } = useCommentStore()
// 테스트 시 전역 상태 리셋이 필요함✅ 권장 구조 (TO-BE)
// 개선: 의존성 주입으로 테스트 용이성 확보
export const useComment = (deps = { commentStore: useCommentStore }) => {
const { newComment } = deps.commentStore()
}
// 테스트에서는 mock store를 주입 가능🔄 변경 시나리오별 영향도
- 새 외부 API 연동 시: requestApi mock으로 엔티티 레벨 테스트만 작성하면 빠르게 검증 가능.
- 상태관리 라이브러리 변경 시: 현재 store에 의존하는 훅 테스트가 영향을 받음(테스트 리팩토링 필요).
🚀 개선 단계
- 1단계: 단기: 주요 훅(useComment, usePostForm)에 대해 의존성 주입 가능하게 리팩토링(1~2일).
- 2단계: 단기: requestApi의 mock helper(test util) 작성으로 엔티티 API 테스트 자동화(반나절).
- 3단계: 중기: zustand store 초기화/테스트 헬퍼 도입(전역 상태 재설정 함수 제공).
8. ⚛️ 현대적 React 패턴
💡 개념 정의
현대적 React 패턴은 Suspense, Error Boundary, 커스텀 훅, 훅을 통한 관심사 분리 등을 의미하며, UI는 가능한 한 순수 컴포넌트로 유지합니다.
⚡ 중요성
로딩/에러 처리의 일관성 유지, 컴포넌트 책임 분리, 사용자 경험 개선에 기여합니다.
📊 현재 상황 분석
AS-IS: useQuery 및 custom hooks 활용으로 관심사 분리 수준은 높음. TO-BE: Suspense + ErrorBoundary 적용으로 feature 컴포넌트에서 로딩/에러 코드를 제거하고 상위에서 처리하도록 개선 가능.
📝 상세 피드백
현대적 React 패턴(커스텀 훅, react-query, Devtools)은 잘 활용하고 있습니다. Suspense나 Error Boundary를 통한 선언적 로딩/에러 처리는 아직 도입되지 않았습니다. useQuery에서 suspense 옵션을 켜고 상위 컴포넌트에서 Suspense/ErrorBoundary로 감싸면 로딩/에러 처리의 일관성과 컴포넌트 단순화에 도움이 됩니다.
❌ 현재 구조 (AS-IS)
// AS-IS: 개별 컴포넌트 내부에서 로딩/에러 처리
if (loading) return <div>로딩 중...</div>
if (error) return <div>Error</div>
// TO-BE: Suspense/ErrorBoundary 상위에서 처리✅ 권장 구조 (TO-BE)
// TO-BE 예시
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Skeleton />}>
<PostList />
</Suspense>
</ErrorBoundary>🔄 변경 시나리오별 영향도
- 로딩 UX 변경(글로벌 스켈레톤 적용): Suspense 사용 시 적용 범위가 작아짐.
- 에러 처리 정책 변경(글로벌 fallback component): ErrorBoundary 도입으로 중앙화 가능.
🚀 개선 단계
- 1단계: 단기: 주요 목록 로딩(useQuery)을 suspense 옵션으로 변경 가능한지 검토(반나절).
- 2단계: 단기: 전역 ErrorBoundary 컴포넌트 추가(하루).
- 3단계: 중기: Suspense 적용 범위를 넓혀 스켈레톤 컴포넌트를 도입.
9. 🔧 확장성
💡 개념 정의
확장성(Extensibility)은 새로운 기능 추가나 비기능 요구사항 변경 시 기존 코드의 변경량과 난이도를 의미합니다.
⚡ 중요성
새로운 결제 수단, 다국어, 실시간 기능 등 요구사항 변화에 빠르게 대응할 수 있어야 합니다.
📊 현재 상황 분석
AS-IS: UI 교체는 shared/ui 계층 수정으로 대부분 해결 가능. 상태관리 라이브러리 변경이나 전역 QueryClient 변경은 현재 구조에서 여러 파일을 건드려야 함.
📝 상세 피드백
기능 확장(다국어, A/B 테스트, 새로운 인증 방식 등)을 고려한 구조적 준비는 잘 되어 있습니다. entities와 feature의 분리, shared UI/유틸의 존재가 확장성을 돕습니다. 그러나 쿼리 키 혼재와 shared↔feature 역의존, Zustand store의 분산은 확장 시 리스크가 될 수 있습니다. 예를 들어 다국어 적용 시 shared/ui 컴포넌트에 문자열 리소스를 연결하면 대부분의 UI 변경은 shared 레이어에서 처리되지만, feature 내 하드코딩된 문자열은 별도 작업이 필요합니다.
❌ 현재 구조 (AS-IS)
// requestApi 하나로 HTTP 전환 용이 (1파일 변경)
// Zustand store 분산은 상태관리 도구 변경 시 N파일(예: 2 store 파일 + 이를 소비하는 훅/컴포넌트) 수정 필요✅ 권장 구조 (TO-BE)
// TO-BE: 상태 추상화 레이어 도입
// state/adapters/postState.ts -> 인터페이스 제공
export const createPostState = (impl) => ({ useSelectedPostStore: impl })
// 교체 시 impl만 바꾸면 됨🔄 변경 시나리오별 영향도
- 다국어(i18n) 도입: shared/ui 컴포넌트가 문자열 prop을 사용하면 shared에서 처리 가능 — feature 내부 하드코딩 문자열을 refactor 필요.
- 새로운 인증 방식(OAuth) 도입: requestApi 중앙화로 토큰 처리 추가는 requestApi 한 곳에서 가능.
- A/B 테스트 도입: feature 내부 로직으로 분리되어 있으면 실험 분기가 쉽지만 shared에 기능 로직이 있으면 영향 범위 증가.
🚀 개선 단계
- 1단계: 단기: QUERY_KEYS/QueryClient 통일 및 shared↔feature 역의존 제거 (1~2일).
- 2단계: 중기: 상태관리 추상화 레이어(interface/adapter)를 도입하여 교체 비용 감소(2~4일).
- 3단계: 장기: 확장 시나리오별(다국어, A/B, 실시간) 템플릿 및 가이드 작성.
10. 📏 코드 일관성
💡 개념 정의
코드 일관성(위와 동일)
⚡ 중요성
위와 동일
📊 현재 상황 분석
위와 동일
📝 상세 피드백
코드 컨벤션은 비교적 일관적입니다(대부분 double quotes, PascalCase 컴포넌트, hooks는 use prefix 등). 좋은 점은 많은 index.ts를 통해 Public API를 구성한 점입니다. 다만 몇 가지 일관성 이슈가 관찰됩니다: 폴더명 'feature' vs FSD 문서의 'features' 불일치, Providers 파일명 케이스(Providers.tsx)와 파일내 주석 불일치, 일부 파일에서 세미콜론/따옴표 스타일 혼재가 미약하게 존재합니다. 또한 QUERY_KEYS의 함수명 오타(getSeachPosts) 등 네이밍 오류가 있어 일관성/가독성에 영향을 줍니다.
❌ 현재 구조 (AS-IS)
src/shared/constants/query.ts
export const QUERY_KEYS = {
getSeachPosts: (searchQuery: string) => ["getSeachPosts", { searchQuery }] as const,
}✅ 권장 구조 (TO-BE)
수정 예시:
export const QUERY_KEYS = {
getSearchPosts: (searchQuery: string) => ["getSearchPosts", { searchQuery }] as const,
}🔄 변경 시나리오별 영향도
- 새 팀원이 합류하거나 다른 팀과 코드 병합 시 일관성 부재는 온보딩 비용을 증가시킴.
🚀 개선 단계
- 1단계: 오타 수정 및 패턴 통일(위와 동일).
🎯 일관성 체크포인트
파일명 규칙
- 프로젝트 폴더명이 'feature' (singular)로 되어 있음 — FSD 문서에서는 'features' 권장
- Providers.tsx 파일명과 내부 주석/경로 표기가 일부 다름(대소문자/경로 표기 확인 필요)
Import/Export 패턴
- 일부 모듈에서 default export 사용 (Providers default), 대부분은 named export — export pattern 통일 필요
변수명 규칙
- QUERY_KEYS.getSeachPosts 오타 (getSeachPosts → getSearchPosts)
- 일부 타입/변수에 PascalCase/camelCase 혼용 미약하게 존재
코드 스타일
- 프로젝트 전반적으로 double quotes를 쓰지만 일부 legacy 파일에 single quotes가 남아있음 — Prettier/Eslint로 통일 필요
11. 🗃️ 상태 관리
💡 개념 정의
데이터 흐름 및 상태 관리는 서버 상태와 클라이언트(로컬) 상태를 분리하여 각 책임을 명확히 하는 것입니다. 서버 상태는 캐싱, 리프레시, 낙관적 업데이트 등에 집중하고 클라이언트 상태는 UI 관련 상태(모달 열림, 폼 데이터 등)를 다룹니다.
⚡ 중요성
실시간 동기화, 오프라인 모드, 복잡한 폼/상호작용 등 요구사항 추가 시 예측 가능한 상태 변화와 디버깅 용이성을 확보합니다.
📊 현재 상황 분석
AS-IS: 서버/클라이언트 상태 역할 분리는 잘 되어 있음. TO-BE: shared의 순수성 회복과 상태 추상화(adapter) 도입으로 더 튼튼한 구조 가능.
📝 상세 피드백
서버 상태(TanStack Query)와 클라이언트 상태(Zustand)를 명확히 분리하려는 의도가 잘 반영되어 있습니다. TanStack Query는 서버 상태(게시물/댓글/사용자)를 담당하고, Zustand는 UI 모달/폼 상태를 담당합니다. 그러나 shared/hooks/useURL가 feature store를 참조하는 문제와 Zustand store가 feature 내부에 분산되어 있다는 점은 상태 관리 전략의 약한 결합을 유발합니다. 이상적으로는 client-state의 shape와 action을 추상화한 인터페이스를 두어 UI 라이브러리나 상태관리 라이브러리 변경 시 영향을 최소화해야 합니다.
❌ 현재 구조 (AS-IS)
// AS-IS: zustand stores 직접 생성
export const useSelectedPostStore = create<...>((set)=>({...}))
// shared 훅이 feature store를 직접 참조 (역의존)
import { useSearchQueryStore } from "../../feature/post/model/store"✅ 권장 구조 (TO-BE)
// TO-BE: 상태 추상화
// state/adapters/postState.ts
export type PostStateApi = { useSelectedPostStore: () => ... }
export const createPostState = (impl): PostStateApi => impl
// feature는 adapter를 주입받아 사용
const { useSelectedPostStore } = createPostState(defaultImpl)🔄 변경 시나리오별 영향도
- 실시간 기능(웹소켓) 추가 시: 서버 상태를 TanStack Query에서 관리할지 전용 실시간 store로 분리할지 정책 필요.
- 오프라인 지원 추가 시: client-state와 서버 상태 동기화 전략 정립 필요.
🚀 개선 단계
- 1단계: 단기: shared/useURL 분리(위 섹션 참고).
- 2단계: 중기: state adapter/port 패턴 도입으로 상태관리 라이브러리 교체 비용 최소화(2~4일).
- 3단계: 장기: 실시간/오프라인 정책 문서화 및 테스트 시나리오 작성.
🤔 질문과 답변
Q: "widget과 feature의 경계가 애매한데 어떤 기준으로 나눠야 할까요?"
A: Widget은 재사용 가능한 UI 조각(Stateless or minimal state)로 생각하세요. Feature는 사용자의 행위(유저 여정)와 비즈니스 로직을 포함합니다. 기준 정리:
- 재사용성: 여러 feature에서 같은 UI를 쓴다면 widget.
- 상태 소유권: UI 자체만의 로컬 상태라면 widget, 여러 영역에서 공유되는 상태나 비즈니스 로직이면 feature.
- 책임 크기: 한 화면의 흐름(예: Post 상세 보기 + 댓글 작성)은 feature. 댓글 리스트 자체(재사용 가능)는 widget.
Q: "엔티티에 UI를 넣어도 되나요?"
A: 최소화하세요. 엔티티는 타입/CRUD/API 책임에 집중해야 합니다. "읽기 전용(readonly) UI"처럼 정말 도메인 전반에서 동일하게 쓰이고 도메인 특화된 뷰라면 예외적으로 엔티티에 둘 수 있으나, 대부분 feature/ui 또는 widgets에 두는 것이 유연성에 유리합니다.
Q: "react-query 키 관리 방법이 궁금합니다."
A: QUERY_KEYS 팩토리(현재 구현)는 좋은 접근입니다. 반드시 모든 쿼리/뮤테이션에서 이 팩토리만 사용하도록 규칙을 강제하세요. 문자열 리터럴 키(['getPosts']) 사용을 제거하면 파라미터 기반 캐시 무효화가 안전해집니다.
🎯 셀프 회고 & 제안
당신의 회고에서 읽히는 점: 아키텍처와 폴더구조에 대해 고민을 깊게 하셨고, 특히 dialog와 엔티티의 경계 문제에 대해 진지하게 생각하셨습니다. 그 관점은 매우 옳습니다. 지금 코드에서 얻을 수 있는 인사이트와 추가 질문:
- "엔티티는 정말로 UI를 가져야 하는가"에 대한 당신의 판단(엔티티에 UI를 넣지 않음)은 보수적이고 안전한 선택입니다. 한 가지 더 생각해볼 점: 만약 엔티티 레벨에서 특정 '읽기 전용' 렌더러를 제공한다면, 그것이 진짜로 여러 feature에서 재사용되는지(그리고 디자인 시스템과 충돌하지 않는지)를 검증하세요.
- dialog의 소유권 결정(댓글이 post에 속하느냐 comment 도메인에 속하느냐)은 역으로 "어떤 변경이 더 자주 일어날까?"를 기준으로 삼아보세요. 댓글의 비즈니스 규칙이 자주 바뀌면 comment feature에 두는 것이 유리합니다.
추가 사고를 유도하는 질문:
- widget을 "재사용 가능한 UI"로 볼 때, 얼마나 범용적으로 재사용되어야 widget으로 승격시키겠는가? (예: 동일 UI가 최소 2군데 이상 사용되는가?)
- shared 레이어를 완전히 도메인 독립적으로 만들려면 어떤 규칙(허용 import 경로)을 팀 룰로 삼을 것인가?
- 프로젝트가 성장(팀/패키지 수 증가)했을 때 지금의 'feature' 폴더명 단수형이 혼란을 줄 가능성이 있는가? 팀 합의로 표준화할 필요는 없는가?
추가 논의가 필요한 부분이 있다면 언제든 코멘트로 남겨주세요!
코드 리뷰를 통해 더 나은 아키텍처로 발전해 나가는 과정이 즐거웠습니다. 🚀
이 피드백이 도움이 되었다면 👍 를 눌러주세요!
unseoJang
left a comment
There was a problem hiding this comment.
민지님 2-3과제도 고생이 많으셧어요
확실히 저번주차 과제의 디렉토리 구성보다 이번주차 과제의 디렉토리 구성이 훨씬 짜임세가 있네요
준일코치님이 해주신 GPT를 사용한 코드리뷰가 너무 좋아서 저도 그걸 보고 상세히 공부가 됐네요
React도 그렇고 vue 에서도 같은 개념을 적용할수 있으니 그 부분 기억해주시면 좋을 것 같아요
현재 naver의 vibe 그리고 테오코치가 작업하고있는 naver cloud 도 이런식의 fsd를 적용하고 있는 것으로 알고 있어요
- 의존성 방향만 유지된다면 shared -> entities -> widgets -> features -> pages로 가도 될까요? FSD 원칙에 위반돼서 안되는 것일지 궁금합니다. 예를 들면 widgets에서 pagination을 구현해서 features의 ui에서 갖다 쓰는거죠.
제 생각에는 import 방향성만 잘 지켜준다면 괜찮다고 생각이 듭니다.
이런 경우만 금지된다고 보면 될것 같아요
// features/like-post/index.ts
import { PostList } from "@/pages/post-list"; // 상위 레이어로 역참조 금지더 자세한건 준일 코치님의 GPT가 잘 남겨주셔서 그걸 보면 되지 않을까 싶어요!
배포 링크
https://annkimm.github.io/front_6th_chapter2-3/
과제 체크포인트
기본과제
목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기
체크포인트
심화과제
목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기
체크포인트
최종과제
과제 셀프회고
이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.
본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?
이번에 세운 폴더구조

아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.
이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.
챕터 셀프회고
클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기
어쨌든 핵심이 그 하나만 보면 되니까요. 하지만 단일 원칙을 따르지 않는다면 아무래도 분산이 되어 있어서 알아보기 힘든 코드가 될꺼라는 생각이 들었습니다.
결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리
응집도 높이기: 서버상태관리, 폴더 구조
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문