[8팀 박창준] Chapter 2-3. 관심사 분리와 폴더구조🧦#24
Conversation
… integrate Zod for validation
…tity to use types instead of schemas
… keys with corresponding hooks
…rch functionality
…r undefined 처리 제거거
…ent query parameter handling across post operations
…ve type safety in useBaseQueryParams
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요, 박창준 님! 5주차 과제 잘 진행해주셨네요. 고생하셨습니다 👍🏻👍🏻
현재 남기는 코멘트는 GPT-5-mini model을 사용하여 AI가 남기는 피드백입니다 🤖
과제 피드백과는 별개이므로 참고해주세요!
1. 🏗️ FSD 아키텍처
💡 개념 정의
Feature-Sliced Design(FSD)는 기능(Feature)을 중심으로 관심사를 분리하고, 계층별 책임(app → pages → widgets → features → entities → shared)과 공개 API(각 slice의 index)를 통해 모듈화하는 아키텍처 접근입니다.
⚡ 중요성
FSD는 대규모로 확장될 때 변경 영향도를 낮추고, 모듈 단위로 팀 분리 및 독립적 테스트/배포를 가능하게 합니다. 잘못 적용하면 오히려 파일 위치 파악 비용과 리팩토링 비용이 증가할 수 있습니다.
📊 현재 상황 분석
AS-IS: 구조는 FSD를 따르고 있으나 일부 규칙 불일치로 변화 대응성이 떨어질 수 있음. 예: AppProviders가 App 내부와 app/index에서 중복 래핑되어 Provider가 두 번 적용될 위험(부작용/성능 이슈). 배럴 파일이 일부 누락 또는 빈 파일이 있어 소비자가 잘못된 엔트리로 import할 가능성 있음. 또한 일부 slice가 index를 통해 외부에 일관되게 노출하지 않는 경우가 있어 교체/이동 시 영향 범위를 정확히 파악하기 어려움.
📝 상세 피드백
FSD(Feature-Sliced Design) 적용이 잘 보입니다: 디렉터리 계층(app, pages, widgets, features, entities, shared)이 명확히 분리되어 있고, entities의 model/api/types와 features의 model/ui가 분리되어 있습니다. 다만 몇 가지 의존성/공개 API 관점의 미세한 문제들이 변화에 대한 유연성에 영향을 줍니다.
주요 관찰: AppProviders 중복 적용, 일부 배럴(index) 파일 누락/중복, 계층간 의존성 방향과 Public API 규칙 점검 필요.
핵심 권고: 1) 각 slice(entities/features/widgets/shared)는 외부에 노출할 공개 API(index.ts/tsx)를 일관되게 유지하세요. 2) app 계층(Providers 등)은 단일 진입점에서만 적용되어야 합니다. 3) 의존성 방향을 정적 분석(ESLint 의존성 규칙)으로 자동화하세요.
❌ 현재 구조 (AS-IS)
src/App.tsx (중복 Provider 적용됨)
// src/App.tsx
import { AppProviders } from "@/app/providers"
...
const App = () => {
return (
<AppProviders>
<Router> ... </Router>
</AppProviders>
)
}
// src/app/index.tsx
const AppRoot = () => {
return (
<AppProviders>
<App />
</AppProviders>
)
}
=> 결과: AppProviders가 중복으로 래핑될 수 있음✅ 권장 구조 (TO-BE)
// 권장: Providers는 한 곳에서만 적용
// src/app/index.tsx (entry)
const AppRoot = () => {
return (
<AppProviders>
<Router>
<App />
</Router>
</AppProviders>
)
}
// src/App.tsx (프레젠테이션 레이어만)
const App = () => (
<div className="flex flex-col min-h-screen">
<Header />
<main>...</main>
<Footer />
</div>
)
=> Providers는 entry에서만 제공되어 중복/사이드이펙트 방지🔄 변경 시나리오별 영향도
- UI 라이브러리 변경(MUI → Chakra): shared/ui에 범용 컴포넌트만 남기면 변경 파일 수 최소화 가능하지만, 현재 app과 feature에 UI 세부가 섞여 있다면 더 많은 수정 필요
- 모노레포로 전환: 각 slice의 public API가 잘 정리되어 있어야 패키지 분리 시 의존성 체인을 얇게 유지할 수 있음
- features 재배치(도메인 이동): 배럴 export가 없거나 혼재되어 있으면 리팩토링 시 import 경로 전부 수정해야 함
🚀 개선 단계
- 1단계: 단기(1-2시간): AppProviders 중복 제거 — App.tsx에서 AppProviders를 제거하거나 app/index.tsx에서 제거하고 Entrypoint(main.tsx)에서만 wrapping 하도록 통일 (테스트 후 배포).
- 2단계: 단기(1일): 빈/잘못된 배럴 파일 확인 및 정리(src/shared/ui/dialog/index.ts 등). 각 slice가 하나의 index 파일로 외부 노출되는지 확인.
- 3단계: 중기(2-3일): 의존성 방향을 ESLint 규칙(예: eslint-plugin-boundaries 또는 custom rule)으로 자동화하여 하위 계층만 참조하도록 강제.
- 4단계: 중기(1주): 각 slice의 public API(배럴)를 문서화하고 변경 시 테스트 체크리스트에 포함시키기(리팩토링 가이드).
- 5단계: 장기(2주): CI에서 구조 규칙(의존성 방향, 배럴 존재 여부)을 검사하는 파이프라인 도입.
2. 🔄 TanStack Query
💡 개념 정의
TanStack Query는 서버 상태 캐싱/동기화 툴로, 쿼리 키(identifiers) 관리, 쿼리 함수 분리, 캐시 업데이트(optimistic/pessimistic), 에러/리페치 전략을 통해 서버 상태를 예측 가능하게 관리합니다.
⚡ 중요성
일관된 쿼리 키와 캐시 조작 전략은 API 변경, 새로운 데이터 소스 추가, 낙관적 업데이트/리패치 전략 변경 시 수정을 국소화하여 유지보수 비용을 줄여줍니다.
📊 현재 상황 분석
AS-IS: 쿼리 키 설계와 normalize 유틸은 재사용성/일관성 측면에서 우수합니다. 하지만 다음이 개선 필요합니다: 1) queryOptions라고 추정되는 헬퍼가 레포에 존재하지 않아 런타임 에러 가능성(또는 잘못된 import), 2) 낙관적 업데이트를 주장했으나 onMutate/onError 롤백 패턴이 없어서 네트워크 실패 시 데이터 불일치 가능성, 3) 일부 setQueriesData 호출이 정확한 쿼리 키(특히 normalize된 키)를 사용하지 않으면 캐시가 갱신되지 않을 위험.
📝 상세 피드백
TanStack Query의 기본 사용(QueryClientProvider, defaultOptions, Devtools, useQuery/useMutation)을 잘 적용하셨고, query key 체계화(POST_QK, commentKeys, TAG_QUERY_KEYS)와 쿼리 키 정규화(normalize)가 매우 인상적입니다. 그러나 몇 가지 실무적 개선 포인트가 있습니다: query option helper 사용 오류 가능성, 낙관적 업데이트(optimistic update) 패턴의 불완전성, 그리고 에러/롤백 전략 누락입니다.
❌ 현재 구조 (AS-IS)
AS-IS(낙관적 업데이트 부족)
// src/features/comment/create-comment/model/useCreateComment.ts
useMutation({
mutationFn: async (comment) => createComment(comment),
onSuccess: (newComment, variables) => {
queryClient.setQueriesData({ queryKey: commentKeys.listByPost(variables.postId) }, (old) => ({ ...old, comments: [...old.comments, newComment], total: old.total + 1 }))
}
})
문제: 네트워크 실패 시 롤백 로직이 없음.✅ 권장 구조 (TO-BE)
TO-BE(권장 낙관적 업데이트 패턴)
useMutation({
mutationFn: createComment,
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey: commentKeys.listByPost(variables.postId) })
const previous = queryClient.getQueryData(commentKeys.listByPost(variables.postId))
queryClient.setQueryData(commentKeys.listByPost(variables.postId), (old) => ({ ...old, comments: [...old.comments, { ...variables, id: 'optimistic-id' }], total: (old?.total ?? 0) + 1 }))
return { previous }
},
onError: (err, variables, context) => {
queryClient.setQueryData(commentKeys.listByPost(variables.postId), context.previous)
},
onSettled: () => {
queryClient.invalidateQueries(commentKeys.base())
}
})
=> 이렇게 하면 실패시 롤백이 가능하고, 성공시 정식 데이터로 교체/무효화 처리 가능.🔄 변경 시나리오별 영향도
- API 엔드포인트가 쿼리 파라미터를 변경(예: limit/search param 이름 변경)하면 normalize 및 buildApiQueryParams를 수정하면 대부분의 queryKey가 한 번에 대응 가능 — 좋은 설계
- 새로운 데이터 소스(예: 실시간 소켓)를 추가하면 기존 queryKey 패턴을 재사용하여 invalidate/merge 전략 구현이 쉬움
- 에러 핸들링/낙관적 업데이트 규칙 변경 시 onMutate/onError/onSettled를 일관되게 적용하면 변경은 useMutation을 정의한 파일들(10~15개)에만 집중될 수 있음
🚀 개선 단계
- 1단계: 단기(2-4시간):
queryOptions처럼 보이는 import들이 실제로 정의되어 있는지 확인하거나, useQuery 호출을 표준 형태(useQuery({queryKey, queryFn, enabled}))로 통일하여 불확실성 제거. - 2단계: 단기(半일): 모든 useMutation에 onMutate/onError/onSettled 패턴 추가 — 특히 create/update/delete 훅들 (features/*/model)에서 낙관적 업데이트를 안전하게 구현.
- 3단계: 중기(1-2일): queryKey 생성 규칙 문서화(정규화 규칙 포함) 및 테스트 케이스 추가(동일 파라미터 순서가 달라도 같은 키로 매칭되는지).
- 4단계: 중기(1주): 캐싱 정책(기본 staleTime, refetchOnWindowFocus 등)을 팀 합의로 문서화. 환경별(개발/스테이징/프로덕션) 설정을 분리하여 운영 유연성 확보.
- 5단계: 장기(2주): 에러 전역 처리 전략(Toast/Modal + Query retry/RetryDelay 정책)을 구축하고, Devtools/CI에서 쿼리 규칙을 검사하는 자동화 추가.
3. 🎯 응집도 (Cohesion)
💡 개념 정의
응집도(Cohesion)는 모듈 내부 요소들이 얼마나 같은 목적을 위해 함께 있는지를 나타냅니다. 높은 응집도면 한 기능 변경 시 수정 범위가 좁습니다.
⚡ 중요성
높은 응집도는 변경 시 영향을 국소화하여 버그/리팩토링 비용을 줄이고, 도메인 단위로 패키지 분리 시 이점을 제공합니다.
📊 현재 상황 분석
AS-IS: 엔티티별 타입/쿼리/API가 잘 모여있어 응집도가 높음. 그러나 일부 '데이터 조합' 책임(여러 엔티티를 동시에 호출해 하나의 반환 형식을 만드는 것)이 features단에서 수행되어 '응집도는 높으나 책임의 경계'가 애매해짐. 예: useCreatePost에서 새 포스트 생성 후 author 조회 로직은 entities 레이어(혹은 domain service)로 옮겨 재사용성을 높일 수 있음.
📝 상세 피드백
응집도는 전반적으로 높습니다. 도메인별(entities)로 타입/쿼리키/api를 모아두고, features 단위로 사용자를 위한 훅 및 UI를 두어 관심사 분리가 이루어졌습니다. 다만 일부 기능(예: post 작성 시 author 보강 로직)이 feature 훅 내부에서 추가 API 호출을 직접 수행하여 관심사가 조금 섞여있습니다(데이터 결합 로직 vs 순수 CRUD).
❌ 현재 구조 (AS-IS)
// src/features/post/create-post/model/useCreatePost.ts
const newPost = await createPostApi(data)
if (!data.author && newPost.userId) {
const author = await getUserById(newPost.userId)
return { ...newPost, author }
}
// 문제: posts feature hook이 users API를 직접 호출하여 author 조합 책임을 가짐✅ 권장 구조 (TO-BE)
// 이상적: 엔티티 또는 도메인 서비스에서 조합 제공
// src/entities/post/api/composePost.ts (new)
export const createPostWithAuthor = async (data) => {
const post = await createPostApi(data)
const author = await getUserById(post.userId)
return { ...post, author }
}
// features는 단순히 엔티티 서비스 호출
const useCreatePost = () => useMutation(createPostWithAuthor, ...)🔄 변경 시나리오별 영향도
- 새 엔티티(Post)에 속성(author 확장)이 추가되면: 현재는 features/* 훅과 entities/post/model/types.ts 모두를 수정해야 할 가능성이 있음 — 엔티티 변화 시 상위 레벨 문서화 되어 있지 않다면 파급 범위가 넓어짐
- 패키지 분리(entities를 별도 패키지로 추출) 시: 엔티티 내부에서만 필요한 조합 로직은 entities에 남기고 feature에서 호출만 하면 추출 시 수정량이 줄어듦
🚀 개선 단계
- 1단계: 단기(1-2일): feature 훅에서 다른 엔티티 API를 직접 호출하는 곳(예: useCreatePost)을 찾아 엔티티 레벨로 이동 가능한 조합 로직 분리.
- 2단계: 중기(2-4일): 도메인 서비스 계층(entities 안의 service 폴더)을 만든 뒤 공통적인 조합 로직(게시물+작성자 등)을 이곳으로 옮겨 여러 feature에서 재사용.
- 3단계: 장기(1주): 응집도 기준 체크리스트(한 모듈이 캡슐화해야 할 책임)를 문서화하고 코드리뷰 템플릿에 추가.
4. 🔗 결합도 (Coupling)
💡 개념 정의
결합도(Coupling)는 모듈 간 의존의 강도를 나타냅니다. 낮은 결합도는 한 모듈 변경이 다른 모듈에 미치는 영향을 줄입니다.
⚡ 중요성
낮은 결합도는 기술 스택(HTTP client, 상태관리 등) 변경 시 수정 범위를 최소화하고 테스트/모킹을 쉽게 합니다.
📊 현재 상황 분석
AS-IS: 현재 구조에서 axios -> fetch 같은 HTTP 클라이언트 교체 시 src/shared/api/* 파일(axios-client + http wrapper)만 수정하면 대부분 영향이 제한될 것 같음. 그러나 HttpClient가 static class 형태라 DI/테스트 대체시 일부 제약이 있습니다. 또한 직접 호출/임포트 패턴으로 인해 엔티티 간 의존성을 분리하는 유닛 테스트 작성을 더 어렵게 만들 수 있음.
📝 상세 피드백
결합도는 비교적 낮은 편입니다: HttpClient 추상화, entities의 API/타입 분리, features가 해당 엔티티의 API를 호출하는 형태로 잘 설계되어 있습니다. 그러나 몇 가지 결합 완화 포인트가 보입니다: HTTP 클라이언트(axios) 교체를 대비한 DI(의존성 주입) 미흡, 일부 훅이 구체적 impl(예: getUserById) 에 의존하는 부분.
❌ 현재 구조 (AS-IS)
// HttpClient: 정적 클래스 접근
export class HttpClient {
static async get<T>(url: string) { return this.request('get', url) }
}
// 사용
import { HttpClient } from '@/shared/api/http'
const posts = await HttpClient.get('/posts')
// 문제: 정적 클래스인 경우 런타임 대체/모킹 경로가 제한됨✅ 권장 구조 (TO-BE)
// DI-friendly 인스턴스화 접근
export const createHttpClient = (impl) => ({ get: impl.get, post: impl.post })
export const httpClient = createHttpClient(axiosInstance)
// 사용: import { httpClient } from '@/shared/api/http'
// 테스트: jest.mock('@/shared/api/http', () => ({ httpClient: mockClient }))🔄 변경 시나리오별 영향도
- HTTP 클라이언트 전환(axios→fetch): 영향을 받는 파일은 src/shared/api/axios-client.ts와 src/shared/api/http.ts 두 파일(대략 2개) — 잘 추상화되어 있어 수정량 작음. 다만 static class 대신 인스턴스 기반 DI로 변경하면 테스트/교체가 쉬움.
- 상태관리 라이브러리 교체(zustand → recoil 등): zustand 사용은 일부 features에서만 쓰이고 있음(useSelectedPostStore, useSelectedUserStore, useDialogStore) — 전환 시 각 store 파일(대략 3~4개)만 수정하면 될 가능성 높음.
🚀 개선 단계
- 1단계: 단기(반나절): HttpClient facade 문서화(어디에서 사용되는지, 교체 포인트 명시) 및 axiosInstance 설정(환경별) 검증.
- 2단계: 단기(1일): HttpClient를 인스턴스화하여 DI/모킹이 쉬운 형태로 리팩토링(예: createHttpClient 패턴).
- 3단계: 중기(2-3일): 엔티티간 직접 참조(예: features에서 다른 엔티티 API 호출)를 도메인 서비스로 추상화하여 결합 완화.
5. 🧹 Shared 레이어 순수성
💡 개념 정의
Shared 레이어 순수성은 shared 코드가 도메인(entities/features)에 의존하지 않고, 범용적으로 재사용 가능해야 한다는 원칙입니다.
⚡ 중요성
Shared가 도메인에 독립적이면 새로운 프로젝트로의 재사용성, 디자인 시스템 교체, 스타일 라이브러리 전환이 쉬워집니다.
📊 현재 상황 분석
AS-IS: 대부분의 shared 컴포넌트는 도메인 독립적으로 설계됨. 그러나 footer의 하드코딩 텍스트, 일부 디렉터리의 빈 배럴 파일이 재사용성/이해도를 낮춤. 또한 shared/model/useDialogStore는 UI dialog 전용이라 shared에 두는 것은 합리적이지만 '글로벌 상태'라는 점에서 프로젝트 규모 커졌을 때 분리 고려 필요.
📝 상세 피드백
shared 레이어에 UI 컴포넌트와 유틸들이 잘 모여 있습니다. 대부분 도메인에 의존하지 않는 범용 컴포넌트입니다. 다만 일부 작은 오염 소지(도메인별 로직 혼입 또는 빈 배럴 파일)가 있어 재사용성에 영향을 줄 수 있습니다.
❌ 현재 구조 (AS-IS)
// shared/footer 하드코딩
const Footer = () => (<p>© 2023 Post Management System. All rights reserved.</p>)
// 문제: 년도/텍스트가 하드코딩되어 재사용성 저하✅ 권장 구조 (TO-BE)
// 재사용 가능한 Footer
const Footer = ({ copyrightText = `© ${new Date().getFullYear()} My App` }) => (
<p>{copyrightText}</p>
)
// shared/ui/index.ts에서 export 해 재사용🔄 변경 시나리오별 영향도
- 디자인 시스템 교체(Material→Chakra): shared/ui가 범용적으로 구현돼 있으면 바꿔야 할 파일은 shared/ui 내부(약 10~20개 컴포넌트)로 국한될 수 있음. 도메인 로직이 shared에 섞여 있다면 수십개의 feature 파일까지 수정 필요.
- 새 프로젝트에서 재사용: shared/lib( normalize, queryParams )을 extract하면 많은 파일 수정 없이 재사용 가능.
🚀 개선 단계
- 1단계: 단기(반나절): 빈 배럴 파일 정리 및 하드코딩된 UI 텍스트(Footer 등)를 props/상수로 추출.
- 2단계: 중기(1-2일): shared 컴포넌트(특히 Dialog, Button, Input 등)에 Design Token/Theme 추상화 레이어 추가하여 디자인 시스템 교체 비용 최소화.
- 3단계: 장기(1주): shared 패키지화(예: 내부 패키지 또는 별도 레포) 전략 수립 — 재사용성과 독립성 증대.
6. 📐 추상화 레벨
💡 개념 정의
추상화는 복잡한 구현 세부사항을 감추고 재사용 가능한 인터페이스(함수/훅)를 제공하는 수준을 의미합니다.
⚡ 중요성
적절한 추상화는 기술 스택 교체 시 수정 지점을 줄이고, 테스트/모킹을 용이하게 합니다.
📊 현재 상황 분석
AS-IS: 많은 책임이 훅에 잘 캡슐화되어 있지만, 일부 유틸이 QueryClient를 직접 탐색(set/ getQueriesData 등)을 사용하여 내부 구현에 의존적입니다. 이는 TanStack Query의 내부 변화나 캐시 구조 변경 시 취약점이 될 수 있습니다.
📝 상세 피드백
추상화 수준은 적절합니다: entities에 순수한 API와 타입, features에 비즈니스 훅을 분리했습니다. 일부 로직(예: post + author 결합, getTotal의 QueryClient 직접 조회)이 feature/utility 경계에서 혼재되어 있어 추상화 개선 여지가 있습니다.
❌ 현재 구조 (AS-IS)
// src/widgets/pagination/lib/getTotal.ts가 queryClient.getQueryData/ getQueriesData를 직접 사용
// 문제: 내부 캐시 구조가 바뀌면 이 유틸이 깨질 위험이 있음✅ 권장 구조 (TO-BE)
// 권장: entities/post 레벨에서 'getTotalForParams' 같은 엔트리 제공
export const getPostsTotalFromCache = (queryClient, params) => { ... }
// widgets는 상위 유틸을 사용🔄 변경 시나리오별 영향도
- TanStack Query 버전 업그레이드(내부 API 변경): queryClient 내부 접근에 의존한 코드가 더 많은 수정이 필요함
- 서버 API 변경 시: POST_QK와 normalize를 통해 많은 훅이 적은 수정으로 대응 가능 — 추상화의 장점
🚀 개선 단계
- 1단계: 단기(반나절): getTotal 같은 유틸을 entities 쪽으로 이동시키거나 엔티티 전용 캐시 헬퍼로 래핑.
- 2단계: 중기(1-2일): feature 훅과 util 간의 공용 인터페이스(예: getCachedTotal(params))를 정의하여 내부 구현 변경시 영향을 국소화.
- 3단계: 장기(1주): 추상화 계층 문서화 및 인터페이스 계약 테스트 추가.
7. 🧪 테스트 용이성
💡 개념 정의
테스트 용이성은 단위·통합 테스트 작성이 얼마나 쉽고 의존성 모킹이 가능한지를 의미합니다.
⚡ 중요성
테스트가 쉬우면 요구사항 변화, 버그 수정 시 안정적으로 리팩토링할 수 있어 변화 대응성이 높아집니다.
📊 현재 상황 분석
AS-IS: 컴포넌트가 훅에 의존하고 훅이 외부 API를 호출하는 형태라 mocking만 해주면 테스트가 가능. 그러나 HttpClient를 wrapper 인스턴스로 바꾸면 테스트 스텁 주입이 쉬워짐. 또한 useQuery 훅들을 더 순수하게 만들고, side-effect (analytics, console.log 등)는 분리하면 테스트가 더 쉬워집니다.
📝 상세 피드백
테스트 용이성은 전반적으로 양호합니다: 데이터 로직(usePosts, useCreatePost 등)이 훅으로 분리되어 UI 컴포넌트를 순수하게 유지하고 있어 단위 테스트/모킹이 쉬움. 다만 HttpClient의 정적 클래스와 QueryClient의 전역 사용(특히 getTotal 유틸)이 테스트에서 모킹을 어렵게 할 수 있습니다.
❌ 현재 구조 (AS-IS)
// HttpClient 정적 클래스 -> 테스트에서 mocking이 번거롭다
jest.spyOn(HttpClient, 'get').mockResolvedValue(...)✅ 권장 구조 (TO-BE)
// DI-friendly httpClient
export const httpClient = createHttpClient(axiosInstance)
// 테스트: jest.mock('@/shared/api/http', () => ({ httpClient: mockClient }))🔄 변경 시나리오별 영향도
- 외부 API가 바뀌어 응답 모양이 바뀌면 hook 단위 목 설정만 변경하면 되나, HttpClient mocking을 중앙화하면 모든 테스트 변경량이 줄어듦
- E2E 테스트 도입: 현재 구조에서 widgets/pages 중심으로 시나리오 테스트 작성하기 용이
🚀 개선 단계
- 1단계: 단기(반나절): HttpClient를 모킹 가능한 인스턴스 패턴으로 변경하여 테스트에서 대체 가능하게 만듭니다.
- 2단계: 단기(1일): 핵심 훅(usePosts, useComments 등)에 대한 단위 테스트 템플릿 작성(react-query의 QueryClient를 테스트 유틸로 래핑).
- 3단계: 중기(1주): CI에 유닛/통합 테스트 추가 및 기본 커버리지 목표 설정.
8. ⚛️ 현대적 React 패턴
💡 개념 정의
Modern React Patterns는 Suspense, Error Boundaries, 커스텀 훅의 책임 분리, 선언적 데이터 로딩을 포함합니다.
⚡ 중요성
이 패턴들을 도입하면 로딩/에러 UI를 중앙에서 관리할 수 있어 UX 일관성 유지와 변경 시 수정 범위 최소화에 도움이 됩니다.
📊 현재 상황 분석
AS-IS: 훅 분리는 잘 되어 있으나 선언적 로딩/에러 처리를 더 활용하면 컴포넌트가 더 단순해짐. 예를 들어 Suspense + ErrorBoundary를 상위에 적용하면 개별 다이얼로그에서 반복되는 로딩/에러 처리를 제거 가능.
📝 상세 피드백
현대적 React 패턴(커스텀 훅 사용, 관심사 분리)은 잘 적용되어 있습니다. 다만 Suspense, ErrorBoundary 및 서버 컴포넌트 같은 선언적 전역 로딩/에러 처리 패턴은 사용되지 않았습니다. 로딩/에러 처리가 각 컴포넌트에 반복되어 있어 일관성/재사용성 측면에서 개선 여지가 있습니다.
❌ 현재 구조 (AS-IS)
// 개별 컴포넌트에서 로딩 처리
if (isLoading) return (<Dialog>로딩중</Dialog>)
// 반복되는 패턴✅ 권장 구조 (TO-BE)
// 상위에서 선언적 처리
<ErrorBoundary fallback={<ErrorFallback/>}>
<Suspense fallback={<Skeleton/>}>
<PostDetailDialog />
</Suspense>
</ErrorBoundary>
// 훅은 data만 제공🔄 변경 시나리오별 영향도
- 로딩 UX 변경(스크린 전환에서 로딩 스켈레톤으로 변경) 시 현재 구조에서는 각 다이얼로그 파일들을 수정해야 함. Suspense로 바꾸면 상위 레이어만 수정하면 됨
- 에러 처리 정책(전역 toast → 모달) 변경 시 각 컴포넌트에서 일괄 적용 필요 -> ErrorBoundary/공통 훅으로 대응 가능
🚀 개선 단계
- 1단계: 단기(1-2일): ErrorBoundary 컴포넌트와 Suspense fallback 컴포넌트 템플릿을 만들고, 반복되는 다이얼로그의 로딩/에러 코드를 제거하여 상위로 위임.
- 2단계: 중기(1주): react-query의 useIsFetching나 Suspense 모드를 시범 적용하여 선언적 패턴에 익숙해지기.
- 3단계: 장기: 팀 컨벤션으로 '로딩/에러는 상위에서 처리' 규칙을 문서화.
9. 🔧 확장성
💡 개념 정의
확장성은 새로운 기능을 추가하거나 요구사항을 변경할 때 코드 변경량과 리스크를 최소화하는 능력입니다.
⚡ 중요성
요구사항이 진화하는 환경에서 빠른 기능 추가와 안정적 운영을 가능하게 합니다.
📊 현재 상황 분석
AS-IS: 구조는 확장성 친화적. 그러나 디자인 시스템/국제화 도구(i18n)와의 통합을 고려하면 일부 shared/ui 컴포넌트에 텍스트를 props로 주입하거나 i18n 훅을 통합해야 함. 또한 A/B 테스트 도입 시 feature 토글을 위한 중앙 플래그(FeatureFlag) 레이어를 추가하면 변경 폭을 줄일 수 있음.
📝 상세 피드백
확장성 측면에서 좋은 설계 요소가 많습니다: query key 팩토리화(normalize 포함), features별 CRUD 훅 분리(useCreatePost, useUpdatePost 등), shared 컴포넌트 분리. 이러한 구조는 새로운 기능(예: 다국어, A/B 테스트, 실시간 기능) 추가 시 변경 범위를 좁혀 줍니다. 다만 다국어/접근성/SEO 같은 비기능 요구사항을 고려한 준비는 일부 보완이 필요합니다.
❌ 현재 구조 (AS-IS)
// 하드코딩 텍스트 예: Footer, DialogTitle의 한국어 문자열
// i18n 도입시 모든 문자열을 변경해야 함✅ 권장 구조 (TO-BE)
// i18n 통합 예
<DialogTitle>{t('post.edit.title')}</DialogTitle>
// 텍스트는 t() 호출로 대체되어 확장성 확보🔄 변경 시나리오별 영향도
- 다국어(i18n) 도입: shared/ui 컴포넌트가 하드코딩 텍스트를 가지고 있으면 많은 파일 수정 필요 — 텍스트를 키로 바꾸는 작업이 필요
- 오프라인/실시간 기능 추가: 현재 서버 상태는 tanstack query로 관리되어 일부 오프라인 전략(IndexedDB, background sync)을 추가하면 분리된 엔티티/feature 훅의 수정을 최소화 가능
🚀 개선 단계
- 1단계: 단기(1일): shared/ui 컴포넌트에서 하드코딩된 문자열을 props로 추출하거나 i18n key로 대체할 준비를 함.
- 2단계: 단기(1-2일): Feature Flag/Remote Config 계층 설계(간단한 zustand 또는 context 기반 플래그)로 A/B 테스트 도입 준비.
- 3단계: 중기(1주): 접근성(A11y) 점검 자동화(axe, eslint-plugin-jsx-a11y) 도입 및 컴포넌트 점검.
10. 📏 코드 일관성
💡 개념 정의
코드 일관성은 파일명, 네이밍, import/export 방식, 코드 스타일(따옴표, 세미콜론 등)을 팀 전체가 통일하는 것을 의미합니다.
⚡ 중요성
일관성이 높으면 신규 개발자가 빠르게 코드에 적응하고, 자동화 도구(린터/포맷터)로 대규모 리팩토링 위험을 줄일 수 있습니다.
📊 현재 상황 분석
AS-IS: 대부분 파일 네이밍은 PascalCase로 일관되어 있음. 그러나 export 방식(기본/명명된 export 혼용)과 import quote 스타일이 혼재되어 있어 코드 리뷰 시 스타일 논쟁 발생 가능. 또한 일부 배럴 파일의 존재 여부가 불분명하여 import 경로를 찾기 어렵게 함.
📝 상세 피드백
전반적인 코드 스타일은 깔끔하지만 일부 컨벤션 불일치가 관찰됩니다(Import/Quote 스타일, export 방식 등). 일관된 컨벤션을 자동화하면 온보딩/리뷰 비용과 병합 충돌을 줄일 수 있습니다.
❌ 현재 구조 (AS-IS)
파일명/Export 방식 혼재 예시:
- src/app/ui/header/Header.tsx => export default Header
- src/shared/ui/button/Button.tsx => export const Button
- src/app/ui/index.tsx => export { default as Header } from "@/app/ui/header/Header"
따옴표 혼재 예: import './index.css' vs import "./index.css"✅ 권장 구조 (TO-BE)
권장 일관성:
- 컴포넌트: PascalCase, named export (export const Header = ...; export { Header })
- 훅: usePrefix camelCase (usePosts)
- 파일 확장자/quote: 프로젝트 Prettier 설정으로 통일
- barrel index 파일은 반드시 내용 또는 재수출만 포함하고 빈 파일 제거🔄 변경 시나리오별 영향도
- 새 팀원 온보딩: 컨벤션 불일치가 많으면 학습 비용 증가
- 자동화 도구 적용(Prettier/ESLint): 현재 혼재된 스타일은 자동 포맷터 한 번으로 해결 가능
🚀 개선 단계
- 1단계: 단기(반나절): Prettier + ESLint(airbnb/standard 기반) 설정 추가 및 팀 합의(quote, semi, import/order 규칙 등) — .prettierrc와 .eslintrc 구성.
- 2단계: 단기(반나절): 코드베이스에 자동 포맷 적용(예: git-commit hook 혹은 CI)하여 혼재된 스타일 정리.
- 3단계: 중기(1일): export 규칙(컴포넌트는 named export 권장)을 팀 컨벤션으로 정하고 기존 코드에서 자동 변환 스크립트 적용.
- 4단계: 중기(1-2일): 빈/잘못된 배럴 파일 정리 및 배럴 사용 가이드 문서화.
🎯 일관성 체크포인트
파일명 규칙
- 대부분 PascalCase로 잘 되어 있으나 일부 엔트리 파일이 index.tsx/index.ts로 섞여 있음(문제는 아니나 컨벤션 문서 필요)
Import/Export 패턴
- 컴포넌트 일부가 default export(Header/Footer), 일부는 named export(Button)로 혼재 — 팀 규칙 부재
- 일부 배럴(index) 파일이 비어 있거나 중복되어 있어 import 경로 혼동 가능
변수명 규칙
- 전반적으로 camelCase를 따르나 일부 상수나 query key가 UPPER_SNAKE_CASE와 혼용되지 않도록 주의
코드 스타일
- 따옴표(single vs double) 혼재: main.tsx에서 single->double 변경 흔적
- 일부 파일에 console.log/debug 주석이 남아 있음 (normalize, HttpClient)
11. 🗃️ 상태 관리
💡 개념 정의
데이터 흐름 및 상태 관리는 서버 상태와 클라이언트 상태를 분리하고, 전역 상태는 최소화하여 예측 가능한 흐름을 만드는 것을 의미합니다.
⚡ 중요성
명확한 상태 소유권은 실시간/오프라인 기능 도입, 상태관리 라이브러리 변경 등에 대한 적응력을 제공합니다.
📊 현재 상황 분석
AS-IS: URL 기반의 페이지 상태 관리(useBaseQueryParams)는 공유/재현성 측면에서 우수. 다만 dialog 상태를 전역으로 두면서 특정 다이얼로그가 언제 열려야 하는지 로직이 분산될 수 있음. 예: 다이얼로그 종류가 늘면 useDialogStore가 비대해질 가능성.
📝 상세 피드백
서버 상태는 TanStack Query로, 클라이언트(UI 상태)는 zustand와 local state로 적절히 분리되어 있습니다(useSelectedPostStore, useDialogStore 등). 전역 dialog 상태와 선택 상태를 zustand로 관리한 점은 Props Drilling 감소에 긍정적입니다. 다만 상태 경계 문서화와 일부 상태의 소유권(entities vs feature)이 더 명확하면 좋습니다.
❌ 현재 구조 (AS-IS)
// useDialogStore: 전역 dialog 상태 관리
// 장점: Props Drilling 제거
// 단점: 모든 dialog를 한 store에서 관리하면 기능 추가 시 store가 비대해질 수 있음✅ 권장 구조 (TO-BE)
// 권장: dialog 종류가 많아지면 기능 단위로 dialog slice를 분리
// features/post/dialogs.ts (post 관련 다이얼로그만 관리)
// shared/useDialogStore는 최소한의 공용 기능만 유지🔄 변경 시나리오별 영향도
- 실시간 기능(WebSocket) 추가: server push를 TanStack Query의 invalidate/subscribe 패턴과 결합하면 용이함. 전역 상태가 명확하면 실시간 동기화 범위를 좁힐 수 있음
- 오프라인 모드 도입: 로컬 클라이언트 상태와 서버 상태의 경계를 명확히 해두면 회복 전략(offline -> sync) 구현이 쉬움
🚀 개선 단계
- 1단계: 단기(반나절): useDialogStore의 도메인 책임을 정리 — 공용 다이얼로그와 도메인 다이얼로그를 분리할지 결정.
- 2단계: 중기(1-2일): 상태 소유권 문서화(어떤 상태를 URL에 둘지, 전역 store에 둘지, local state에 둘지) 작성.
- 3단계: 장기(1주): 실시간/오프라인 시나리오를 고려한 상태 동기화 전략(서버 push 수신 시 query invalidation/merge 방법) 설계.
🤔 질문과 답변
질문: FSD를 언제 도입하는 것이 좋을까요? (초기 도입 vs 점진적 리팩토링)
답변 요약: 정답은 없습니다. 실무에서는 '프로젝트 규모, 팀 구성, 예상 성장성, 유지보수 비용'을 기준으로 실용적으로 접근합니다. 권장 전략은 다음과 같습니다:
-
소규모/초기 프로젝트: 빠른 개발이 필요하면 단순한 폴더 구조로 시작하세요(components/hooks/pages). 단, 초기부터 FSD의 핵심 원칙(관심사 분리, public API, query key 규칙)은 간단한 체크리스트로 적용하세요. 예: entities 폴더에 타입/CRUD만 모아두고, features는 UI와 훅을 묶는 정도로 시작.
-
성장 임계치 기반 전환: 코드베이스가 어느 정도 커지고(예: 파일 수 200+, 리뷰/병합 충돌 빈도 증가, 동일 도메인 책임 분산이 잦을 때) FSD로의 전환을 고려하세요. 이때 점진적 리팩토링을 권합니다 — 모듈 단위(도메인 단위)로 옮기고, 각 리팩토링 단계에 테스트와 CI 검증을 추가하세요.
-
점진적 리팩토링 방법(권장):
- 1단계: 규칙 정립(배럴 export, queryKey 규칙, Provider 위치 등) 및 CI lint 검사 도입.
- 2단계: 한 도메인(예: posts)만 FSD로 옮기기(entities/features/ui 분리). 서비스에 영향 적은 작은 도메인부터 시작.
- 3단계: 레거시 코드와 FSD 코드간 호환 레이어(compat layer)를 두어 점진적 전환 지원.
- 실무 팁:
- FSD는 인프라가 아닌 컨벤션입니다. 소규모 팀에서는 과도한 분리보다 '명확한 책임 규약'이 더 중요합니다.
- 리팩토링 비용을 줄이려면 import 경로를 절대경로로 통일하고, 배럴 파일을 통해 진입점을 제공하면 이동 시 영향 범위를 줄일 수 있습니다.
결론: 초기부터 완전한 FSD로 시작할 필요는 없습니다. 그러나 FSD의 핵심 원칙(관심사 분리, 명확한 public API, query key 규칙)은 초기에 도입하고, 프로젝트 성장에 따라 점진적으로 FSD 구조를 확대하는 하이브리드 접근을 권장합니다.
🎯 셀프 회고 & 제안
작성하신 셀프회고에서 느껴지는 장점: 개념을 단순히 따라한 것이 아니라 여러 자료를 비교하고 본인만의 기준(도메인+기능 1depth)을 세운 점이 매우 좋습니다. 또한 쿼리 키 정규화(normalize)와 select를 활용한 클라이언트 측 처리 시도는 실무에서 흔히 필요한 사고방식입니다. 몇 가지 더 고민해볼 질문을 남깁니다:
- 현재 '도메인 + 기능 1depth' 규칙을 채택하셨는데, 실제로 팀원이 추가되거나 다른 엔티티가 생겼을 때 이 규칙으로 폴더가 확장되는 모습을 상상해보셨나요? 확장 시 각 폴더가 얼마나 커질지(파일 수) 예측해보면 좋습니다.
- 낙관적 업데이트를 구현할 때 '실패 시 롤백' 시나리오와 사용자 경험(로딩/에러 메시지)을 같이 설계해보셨나요? 단순히 cache set만 하면 실패 케이스에서 UX가 혼란스러울 수 있습니다.
- FSD 적용 시점 관련해선 '코스트-베네핏' 분석(예: 초기 적용으로 인한 시간 손해 vs 추후 리팩토링 비용 절감)을 숫자로(예: 예상 파일 수정수, 시간) 계산해보시면 의사결정에 도움이 됩니다.
추가 인사이트 제안:
- 작은 실험 프로젝트에서 FSD 규칙(예: 배럴 패턴, queryKey 규칙, providers 위치) 템플릿을 만들어 두고 새로운 프로젝트 시작 시 체크리스트로 적용해 보세요. 이를 통해 '언제 적용할지' 문제의 정성적 불확실성을 줄일 수 있습니다.
- 낙관적 업데이트는 onMutate/onError 패턴과 함께 로컬 식별자(temporary id) 전략을 적용하면 실패 시 자연스러운 롤백이 가능합니다.
추가 논의가 필요한 부분이 있다면 언제든 코멘트로 남겨주세요!
코드 리뷰를 통해 더 나은 아키텍처로 발전해 나가는 과정이 즐거웠습니다. 🚀
이 피드백이 도움이 되었다면 👍 를 눌러주세요!
unseoJang
left a comment
There was a problem hiding this comment.
안녕하세요 창준님
2-3 과제도 수고가 많았습니다.
전체적으로 고민을 많이 하신 흔적들이 곳곳에 눈에 띄네요 기준이 있는 과제가 아니라 내 생각이 확실히 들어간 관심사로 분리해본 경험이 저도 5기 과제할떄가 처음이었어요
초기부터 FSD를 도입하면 프로젝트 규모가 작을 때는 오버엔지니어링이 될 수 있음. 러닝커브, 팀원들 컨벤션 맞추고 하는것도 비용임. → 그래서 그냥 일단 빠르게 프로젝트를 시작하는게 더 좋을 수도 있다고 생각했습니다.
라고 하셧는데 저도 동의 합니다, 대규모 프로젝트가 진행되고 있는 회사나 프로젝트에 투입되는게 아니라면 분명 소규모 스타트업, 작은 단위의 프로젝트에 투입이 될텐데 회사입장에서는 AB테스트나 사용자의 반응을 바로 확인하기 위해 관심사 분리는 나중에 하고 최대한 빠르게 프로덕트를 만드는게 중요하다고 생각이 듭니다. 추후에 안되면 버리는 프로젝트들도 무궁무진 하니까요
이외에 나머지 리뷰는 준일코치님의 GPT가 더 자세히 남겨준것같아서 그걸 참고하면 될것 같습니다.
수고하셨습니다!
| import { ReactQueryDevtools } from "@tanstack/react-query-devtools" | ||
| import { ReactNode } from "react" | ||
|
|
||
| const queryClient = new QueryClient({ |
There was a problem hiding this comment.
리 렌더시 재생안되게 Singleton QueryClient: 모듈 스코프에서 생성한거 신기하네요.
There was a problem hiding this comment.
api 잘 정리 해줫네요 특히 재활용 가능하게 httpClinet 써주신거랑 재활용가능한 형태가 너무 좋네요
| import { normalize } from "@/shared/lib/normalizeParams" | ||
|
|
||
| export const commentKeys = { | ||
| base: () => ["comments"] as const, |
| const { data, isLoading, error } = useQuery({ | ||
| queryKey: POST_QK.list({ ...baseQueryParams }), | ||
| queryFn: async () => { | ||
| const { search: searchQuery, tag: selectedTag, ...otherFilters } = baseQueryParams |
There was a problem hiding this comment.
searchQuery, tag는 반드시 encodeURIComponent로 감싸세요. 현재는 공백/한글/특수문자에서 깨질 수 있어요.
예) /posts/search?q=${encodeURIComponent(searchQuery)}, /posts/tag/${encodeURIComponent(tag)}.
There was a problem hiding this comment.
오 그런가요????? 좋은 꿀팁 알아갑니다..!
| }, [data]) | ||
|
|
||
| const handleUpdate = () => { | ||
| console.log(editingPost) |
There was a problem hiding this comment.
console.log 데이터 확인용이라면 과제할떈 굳이 안지워도 된다고 생각합니다 대신 주석만 달아주면 좋을 것 같아요
|
|
||
| export const useUserProfile = (id: number, enabled: boolean = true) => { | ||
| return useQuery({ | ||
| queryKey: USER_QUERY_KEYS.detail(id), |
There was a problem hiding this comment.
5개 메서드에 try/catch와 impl.request 호출이 반복됩니다. 공통 request 헬퍼로 일관성 있게 가는게 좋다고 생각이 들어요!
| ...config, | ||
| }) | ||
|
|
||
| console.log("[DEBUG] response", response) |
|
|
||
| return response.data | ||
| } catch (error) { | ||
| console.error(`HTTP request failed for ${url}:`, error) |
There was a problem hiding this comment.
원래 에러 로딩할땐 .error로 로깅하는게 습관인지라 ㅎㅎ 딱히 이유는 없습니다!
과제 체크포인트
배포링크
https://devchangjun.github.io/front_6th_chapter2-3/
기본과제
목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기
체크포인트
심화과제
목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기
체크포인트
최종과제
과제 셀프회고
이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.
이전에 FSD의 개념을 들어는 봤는데 글로만 봤다보니 명확하게 이해하지 못했습니다. 이번 과제를 통해 실제 프로젝트에 적용해보면서 단순히 폴더를 잘 나눈다기 보단 어떤 관점을 가지고 기능 단위로 책임을 명확하게 분리하는 설계를 해본 좋은 경험이였습니다.
본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?
폴더 구조에 대한 고민
entities와 features의 개념이 처음엔 매우 헷갈렸습니다. 많은 블로그 글과 유튜브 영상을 찾아봤지만, 각자 하는 이야기가 조금씩 다르고 주관적이어서 결국 테오 말씀대로 정해진 정답은 없구나 하는 생각이 들었습니다.
그래서 이번엔 여러 자료를 참고하되 내 관점과 주관적인 판단을 기준으로 폴더를 설계해봤습니다.
한 가지 더 고민했던 부분은 features 폴더를 나누는 관점에 있어서 동사 중심일지 도메인 중심일지 고민했습니다. 만약 동사 중심으로 설계한다하면 엔티티 하나당 최소 4개의 CRUD 동작이 있기때문에 폴더가 너무 많아지는 문제가 발생할 수 있을 것 같았습니다.
예를들어 features/create-post, features/update-post, features/delete-post, features/read-post
이런식으로 엔티티 하나당 동사 4개가 추가되어 폴더가 커질 수 있는 문제가 생겼습니다.
반대로 명사(도메인) 중심으로 설계를 하면 그 폴더 내부에도 결국 똑같은 문제가 발생한다고 생각했습니다. 예를들어 features/post/ui 안에 create,update,delete,read를 담당하는 모든 컴포넌트가 모여있습니다.
결국 저는 이 두 관점을 합쳐보기로했고 관심사에 대한 관점을 도메인 + 기능 중심으로 봤습니다. (도메인 + 기능이 1 Depth)
예를들어
post/create-post,post/update-post처럼 도메인(post) 아래에 관련 기능들을 배치하는 식으로 설계했고, 이렇게 하니 관련 기능이 한 곳에 모여 관리가 편리하고 기능별로 구조가 잘 분리되었습니다.완벽한 정답은 아니지만 여러 관점을 고민해보며 나름 합리적인 절충안을 찾은 것 같아 만족스럽습니다.
최종 폴더 구조
쿼리 키 관리
이번 과제에서 URL의 QueryParams로 업데이트되는 키들을 쿼리 키로 어떻게 관리할지가 큰 고민이었습니다.
처음에는 단순히 쿼리 키를 하드코딩하는 방식으로 접근했는데, 재사용성 문제와 누락 또는 오타로 인해 캐시가 제대로 업데이트되지 않는 문제가 발생했습니다. 게다가 쿼리 키가 페이지 번호, 검색어, 필터링, 정렬 등 URL SearchParams 기반으로 생성되고 여러 곳에서 사용되고 있어서, 하드코딩 방식으로는 관리가 쉽지 않았습니다.
그래서 위와 같이 QueryParams를 관리할 수 있는 중앙 훅을 만들었습니다.
또한 과제에서는 더미 데이터를 사용했기 때문에 실제 데이터가 반영되지 않았고, 이로 인해 낙관적 업데이트를 적용할 때 캐시 안의 데이터를 직접 갱신해야 했습니다. 이를 위해
qc.setQueryData를 사용하여 쿼리 캐시 안의 데이터를 바로 업데이트하는 방식을 시도했습니다.그러나 동적으로 변하는 쿼리 키를 안정적으로 관리하기 위해 중앙에서 쿼리 키를 관리하고, 정규화(normalize) 하는 방법을 적용했습니다. 정규화 함수는 빈 값(
undefined,null,"")을 제거하고, 키를 정렬하여 순서가 달라도 동일하게 인식되도록 하며, 모든 값을 원시 타입으로 단정하여 참조 불일치 문제를 방지합니다.이 방식을 통해 다양한 페이지 상태와 검색/필터 조건을 가진 쿼리 키를 안정적으로 관리할 수 있었고, 낙관적 업데이트 시 여러 곳에서 일관되게 캐시를 갱신할 수 있었습니다.
동작하지 않는 API를 클라이언트에서 구현하기
이번에 TanStack Query의 select 옵션을 활용하여 클라이언트 단에서 데이터를 가공하고 변환하는 기능을 구현해 보았습니다. 특히 좋아요 정렬이나 오름차순/내림차순 정렬과 같은 기능은 실제로 동작하지 않았는데 백엔드 API 요청 없이도 클라이언트에서 처리하면 되지 않을까? 싶어서 직접 구현해봤습니다.
구현 과정에서 알게 된 점은, select 함수가 서버에서 받아온 원본 데이터를 화면에 전달하기 전에 후처리할 수 있는 역할을 한다는 것이었습니다.
특히 정렬이나 검색, 태그 필터링처럼 사용자 상호작용이 빈번한 기능은 클라이언트에서 처리하는 편이 훨씬 자연스럽고 빠르게 한다고 생각합니다. 서버에 매번 요청을 보내지 않으니 사용자 경험(UX)도 개선되고, 성능적인 측면에서도 이점이 있었습니다.
이 경험을 통해, "어떤 부분은 서버에 맡기고, 어떤 부분은 클라이언트에서 처리하는 것이 더 효율적인가?"라는 주제에 대해서도 얕게 고민할 수 있어서 좋았어요
아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.
아직은 FSD를 언제, 어떻게 적용하는 것이 적절한지에 대해 확신이 서지 않습니다.
구조적으로 장점이 뚜렷하지만 진입 장벽이 높아 처음 접하는 사람은 적응하는 데 시간이 많이 걸린다고 느꼈습니다. 특히 index.ts를 통한 배럴 익스포트 방식은 파일을 직접 찾아보는 제 습관과 맞지 않아 불편했고, 단일 파일과 배럴 파일을 매번 따로 만드는 과정이 효율적인지에 대한 의문도 남았습니다.
무엇보다도 고민되는 부분은 FSD의 도입 시점입니다.
작은 규모에서 초기부터 도입하면 오버엔지니어링이 될 수 있고, 팀원들과의 컨벤션 정립에도 비용이 많이 듭니다. 반대로 프로젝트가 어느 정도 커진 뒤에 전환하려 하면, 기존 구조를 FSD로 리팩토링하는 데 큰 비용과 리스크가 발생할 수밖에 없습니다.
따라서 FSD는 “무조건 좋은 구조”라기보다는, 프로젝트의 규모, 팀원의 경험, 컨벤션 정립 상황 등을 종합적으로 고려해 유연하게 적용해야 한다는 결론을 내렸습니다.
이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.
이번 과제에서 FSD 구조를 체험해본 덕분에 앞으로 실제 프로젝트에서는 몇 가지 방식으로 활용해보고 싶다는 생각이 들었습니다.
우선 도메인 단위로 기능을 묶는 방식을 적용해보고 싶습니다. 실제로 과제를 해보면서 도메인 단위로 묶는게 굉장히 편리했는데 이렇게 하면 기능이 흩어지지 않고 관련 코드가 한 곳에 모여 있어서 유지보수가 편리할 것 같다고 생각했습니다. 또한 프로젝트 전반에서 공용으로 쓰이는 유틸이나 컴포넌트는 shared 폴더에 따로 모아 관리하면 중복을 줄이고 일관성을 높일 수 있을 것 같습니다.
그리고 QnA 시간에 테오가 말씀해주신 것처럼 API 호출을 useQuery와 같은 훅으로 바라보는 관점도 기억에 남았습니다. 앞으로는 API 레이어와 상태 관리 로직을 단순히 데이터 패칭 훅으로 다룬다면, 코드의 가독성과 일관성을 동시에 챙길 수 있을 것 같아 적극적으로 활용해보고 싶습니다.
또한 폴더 구조를 어떻게 설계하느냐에 따라 협업 효율성과 유지보수성이 크게 달라질 수 있다는 점을 배웠습니다. 특히 FSD 구조를 직접 체험하면서, 단순히 “정답이 있는 구조”가 아니라 상황에 맞게 절충하고 적용하는 사고 방식이 더 중요하다는 것을 알게되었습니다
챕터 셀프회고
클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기
첫 클린코드 과제를 할때가 생각이 나네요.. 엊그제 같은데 벌써 2주전이라니 시간이 참 빠른것 같습니다.
더티코드를 처음 접했을땐 정말 막막하긴 했던거 같아요. 어디서부터 정리를 시작해야 할지 감도 잡히지 않고, 전체 구조가 뒤엉켜 있어서 손대는 게 부담스러웠습니다. 하지만 어지럽혀진것을 정리하는거에 있어서 쾌감을 느끼고 만족을 하는 성격인데 아마 이런 성향이 작용했던 것 같아서 재밌게 과제를 진행했습니다.
우선 클린한 코드를 짠다는건 계속 깨끗한 코드를 봐야한다는 말이고 그 말은 계속해서 그 프로젝트의 코드를 짜야한다는 말인거겠죠. 이건 단순히 코드를 예쁘게 짠다는게아니라 품질을 높여 지속적으로 관리하는 습관을 갖는다라고 생각합니다. 즉 프로젝트를 계속 개발하고 유지보수할 수 있는 환경을 만드는거겠죠.
읽기 좋은 코드란 동료들이 봤을때도 아무 스트레스가 없는 코드? 의문이 없는 코드? 라고 생각합니다. 이거 왜이렇게 짰어요? 이거 무슨 코드에요? 라는 노이즈가 나오지 않고 바로 구조와 흐름을 직관적으로 잘 이해할 수 있도록 설계된 코드라고 생각합니다. 결국 읽기 좋은 코드는 커뮤니케이션을 코드로 대신하는거라고 생각합니다.
결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리
큰 로봇을 작은 부품 단위로 쪼개는 느낌이었고, 나중에 하나씩 조립하는 과정이 재미있었습니다. 하지만 각 컴포넌트에서 이벤트 핸들러를 어떻게 일관성 있게 관리할지 고민이 많았는데, 테오 QnA 시간에 좋은 방향을 찾을 수 있었습니다.
핵심은 핸들러도 컴포넌트의 책임이라는 점이었습니다. 즉, 이벤트 처리 로직을 컴포넌트 내부에서 관리하면서, 필요한 경우만 외부로 전달하는 구조를 만들면 각 컴포넌트가 독립성을 유지하면서도 일관된 방식으로 동작할 수 있다는 깨달음을 얻었습니다.
응집도 높이기: 서버상태관리, 폴더 구조
처음에는 create, update, remove 같은 mutations를 하나의 훅(usePostMutation 같은)에서 통합해서 관리하려고 고민했어요. 이렇게 하면 코드가 한 곳에 모여서 관리하기는 편하지만, 동시에 관심사가 섞이게 되었습니다. 즉, “생성”과 “삭제”가 같은 훅 안에 섞이면, 나중에 각각의 동작을 독립적으로 관리하거나 재사용하기 어려워진다는 문제점이 발생한다고 생각했습니다
그래서 최종적으로 선택한 방식은 각 mutation을 개별 훅으로 분리하는 것 (useCreatePost, useUpdatePost, useDeletePost)입니다. 이렇게 하면 각 훅이 자기 역할만 담당하고, 관심사 분리가 명확해진다고 생각했습니다.
TanStack Query의 사용경험은 음.. 해방감이라기보단 약간의 답답함이 컸던거같아요. 더미데이터를 관리하다보니 실제 데이터가 반영이 안되고 캐시를 업데이트 하는 식으로 구현했어야 했습니다. 그러다보니 FSD에 고민하는 시간보다 이 더미데이터를 어떻게 잘 처리해서 실제 기능이 되는것 처럼 보이게하지? 라는 고민도 많이 했던거같아요ㅎㅎ
하지만 이런 기능을 구현하면서 몰랐던 TanStack Query의 기능에 대해서도 배울 수 있어서 좋았습니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문
Q1. 아까 회고에도 언급했던 FSD에 대한 적용 시점이 궁금합니다. 제가 고민했던 내용은 아래와 같아요
코치님이 생각하시기엔 초기부터 FSD를 도입하는게 좋을지, 반대로 점진적으로 리팩토링하면서 FSD를 적용하면 좋을지 궁금합니다.
Q2. 제가 설계한 폴더 구조에서 좀더 개선하거나 보완했으면 좋을것 같은 부분이 있을까요?
Q3. 이번 과제를 진행하면서 6기 동기들과도 굉장히 많은 토론(?)을 했는데, 각자 생각이 모두 달라서 하나의 기준을 정하기가 쉽지 않았습니다. 실제 현업에서도 팀원마다 의견이 다를 수 있을 것 같은데요. 이런 경우 보통 어떤 과정을 통해 합의를 이끌어내는지, 그리고 코치님이 경험하셨던 좋은 사례나 방법이 있다면 궁금합니다.