[1팀 이의찬] Chapter 2-3. 관심사 분리와 폴더구조#38
Conversation
- forwardRef에서 일반 함수형 컴포넌트로 변경 - 모든 컴포넌트에 TypeScript 인터페이스 추가 - HTMLTableHeaderCellElement deprecated 타입 수정
- shared/ui 하위로 Button/Card/Dialog/Input/Select/Table/Textarea 구성 - 기존 components/index.tsx 제거 및 경로 정리
no-relative-imports 규칙의 경우 segment끼리도 절대경로를 강제하여 사용하지 않도록 설정
- fetch → tanstack query + http wrapper로 서버 상태 관리 최적화
public api를 사용했음에도 에러가 발생해서 제거
- 기존 통합 쿼리를 조건별 독립 쿼리로 완전 분리 * 기본 게시물, 검색, 태그별 쿼리 각각 독립적 관리 * 사용자 데이터 별도 쿼리로 분리하여 불필요한 재요청 방지 - 조건부 쿼리 실행으로 필요한 데이터만 요청 - useMemo를 통한 데이터 조합 로직 최적화 - entities API 함수 완전 적용으로 코드 일관성 확보
hooks를 사용했음에도 에러가 발생해서 제거
각 widget을 lib, model, ui 세그먼트로 세분화하여 관심사 분리
TanStack Query v5 호환성을 위해 QueryCache/MutationCache 사용
| user: Pick<IUser, 'id' | 'username'> & { image?: string } | ||
| } |
There was a problem hiding this comment.
Pick<IUser, 'id' | 'username'> & { image?: string}이 IUser와 동일하지 않나요? 이렇게 하신 이유가 궁금합니다.
There was a problem hiding this comment.
아 이거 기존의 IUser가 IUserDetail로 바뀌었는데 이 부분이 수정이 되지 않았네요;; 그냥 놓친 부분입니당
| export const http = { | ||
| get: <T>(url: string, config?: AxiosRequestConfig) => httpClient.get<T>(url, config), | ||
| post: <T>(url: string, data?: unknown) => httpClient.post<T>(url, data), | ||
| put: <T>(url: string, data?: unknown) => httpClient.put<T>(url, data), | ||
| patch: <T>(url: string, data?: unknown) => httpClient.patch<T>(url, data), | ||
| delete: <T>(url: string) => httpClient.delete<T>(url), | ||
| } |
There was a problem hiding this comment.
이렇게 HttpClient 구현부랑 유틸로 실제 사용하는 부분이랑 분리하는게 깔끔해서 좋네요.
There was a problem hiding this comment.
사실 영서님 코드 보고 깔끔해보여서 쌔비지 했습니다.
There was a problem hiding this comment.
features 하위에 index.ts도 동일하게 배럴 역할 하는 파일로 사용하는게 일관성 있을 것 같아요. 배럴 index.ts만 있어서 뭐지..?했는데 쿼리 코드가 있어서 깜짝 놀랐네요..!
| const postId = (data as unknown as { postId: number }).postId | ||
| if (postId) { |
There was a problem hiding this comment.
api 함수 정의할 때 반환값도 미리 정의해주면 탄스탁 쿼리가 알아서 data 타입을 추론해줍니다. 혹은 useMutataion<{ postId: number}, Error> 이런 식으로 useQuery, useMutation에 제네릭 타입으로 반환 데이터 타입, 에러 타입 등을 정의해줄 수 있어요.
// api 응답 데이터 정의
export interface CreatedComment {
body: string;
postId: number;
userId: number;
}
// api 함수에 반환값 정의
export const addComment = async (comment: ICommentRequest): Promise<CreatedComment> => {
const response = await http.post('/comments/add', comment)
return response
}
There was a problem hiding this comment.
추가로 as 타입 단언은 타입 강제이기 때문에 웬만하면 안 쓰는 게 좋습니다!!
There was a problem hiding this comment.
전체적으로 API 응답 데이터에 대한 타입 정의가 추가되면 좋겠어요!
| queryClient.setQueryData(['comments', context.postId], context.previousComments) | ||
| } | ||
| }, | ||
| onSettled: (data, error, variables) => { |
There was a problem hiding this comment.
안 쓰는 변수는 _, __(언더바 두개)로 처리해주면 unused-vars 린트 에러를 스킵시킬 수 있습니다.
| return useMutation({ | ||
| mutationFn: async ({ id, postId }: { id: number; postId: number }) => { | ||
| await deleteComment(id) | ||
| return { id, postId } |
There was a problem hiding this comment.
여기서 deleteComment 응답값을 그대로 반환해주지 않고 id, postId를 묶어서 전달해주는지 궁금합니다. 이런 예외 사항이 많아지면 코드가 예측하기 어려워질 것 같아요. 탄스탁쿼리에 사용되는 API 호출 함수들은 일관적으로 그 응답데이터를 반환하게 하는게 좋을 것 같아요.
| export function usePostsByTag(params: { tag: string; enabled?: boolean }) { | ||
| const { tag, enabled = true } = params | ||
| return useQuery({ | ||
| queryKey: ['posts', 'tag', { tag }], | ||
| queryFn: () => fetchPostsByTag(tag), | ||
| enabled, | ||
| }) | ||
| } |
There was a problem hiding this comment.
살짝 드는 생각은 usePostsByTag 를 쓰나 외부에서 useQuery를 그대로 쓰나 차이점은 fetch 함수가 내장되어 있냐 아니냐인 것 같은데, 이정도면 그냥 useQuery 를 그대로 쓰고 fetch 함수만 잘 주입하면 좋을 것 같다는 개인적인 생각이 듭니다.
| import { usePostsUIStore } from './store' | ||
|
|
||
| export function usePostsUI() { | ||
| return usePostsUIStore() | ||
| } |
There was a problem hiding this comment.
어떤 의미가 있는 코드인지 모르겠어요, 네이밍이 문제라면 차라리 이건 어떠신지?
| import { usePostsUIStore } from './store' | |
| export function usePostsUI() { | |
| return usePostsUIStore() | |
| } | |
| export { usePostsUIStore as usePostsUI } from './store' |
| return { | ||
| // State | ||
| selectedPostId, | ||
| selectedPost, | ||
| isPostLoading, | ||
| selectedComment, | ||
| isCommentLoading, | ||
| searchQuery, | ||
| newComment, | ||
| setNewComment, | ||
| editingComment, | ||
| setEditingComment, | ||
| showAddCommentDialog, | ||
| showEditCommentDialog, | ||
| showPostDetailDialog, | ||
|
|
||
| // Actions | ||
| setShowAddCommentDialog, | ||
| setShowEditCommentDialog, | ||
| setShowPostDetailDialog, | ||
| setSelectedCommentId, | ||
|
|
||
| // Mutations | ||
| addCommentMutation, | ||
| updateCommentMutation, | ||
|
|
||
| // Handlers | ||
| handleAddComment, | ||
| handleUpdateComment, | ||
| } |
There was a problem hiding this comment.
약간 이렇게 좀 묶어서 내보내면 밖에서 쓰기 편할 것 같아요
| return { | |
| // State | |
| selectedPostId, | |
| selectedPost, | |
| isPostLoading, | |
| selectedComment, | |
| isCommentLoading, | |
| searchQuery, | |
| newComment, | |
| setNewComment, | |
| editingComment, | |
| setEditingComment, | |
| showAddCommentDialog, | |
| showEditCommentDialog, | |
| showPostDetailDialog, | |
| // Actions | |
| setShowAddCommentDialog, | |
| setShowEditCommentDialog, | |
| setShowPostDetailDialog, | |
| setSelectedCommentId, | |
| // Mutations | |
| addCommentMutation, | |
| updateCommentMutation, | |
| // Handlers | |
| handleAddComment, | |
| handleUpdateComment, | |
| } | |
| return { | |
| post: { | |
| selectedPostId, | |
| selectedPost, | |
| isPostLoading, | |
| }, | |
| postDetailDialog: { | |
| isOpen: showPostDetailDialog, | |
| open: () => setShowPostDetailDialog(true), | |
| close: () => setShowPostDetailDialog(false), | |
| }, | |
| comment: { | |
| selectedComment, | |
| } | |
| ... | |
| } as const; |
| isLoading, | ||
| showUserModal, | ||
| handleCloseModal, | ||
| } |
There was a problem hiding this comment.
as const 를 붙여야 타입이 자동으로 잘 완성되더라구여
| } | |
| } as const |
CreatiCoding
left a comment
There was a problem hiding this comment.
전체적으로 코드가 깔끔하게 짜여져서 보기 편했습니다, 6주차 고생하셨어요~!!
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요, 이의찬 님! 5주차 과제 잘 진행해주셨네요. 고생하셨습니다 👍🏻👍🏻
현재 남기는 코멘트는 GPT-5-mini model을 사용하여 AI가 남기는 피드백입니다 🤖
과제 피드백과는 별개이므로 참고해주세요!
1. 🏗️ FSD 아키텍처
💡 개념 정의
Feature-Sliced Design(FSD)는 기능(Feature) 중심으로 레이어(앱, 페이지, 위젯, 기능, 엔티티, 공유)를 나누어 관심사를 분리하고 계층 간 의존성을 하향(상위 → 하위)으로 제한하는 아키텍처입니다. 각 slice는 index를 통해 public API만 노출해야 합니다.
⚡ 중요성
FSD를 올바르게 적용하면 요구사항 변화(도메인 변경, 라이브러리 교체, 모듈 분리)에 따른 파급 범위를 최소화할 수 있습니다. 특히 대팀/장기간 유지보수 환경에서 코드 안정성과 확장성에 직접적인 영향을 줍니다.
📊 현재 상황 분석
AS-IS: 대부분 FSD 규칙을 따르나 일부 feature가 entities를 통과하지 않고 http를 직접 사용합니다. Public API 통일(entities index 사용)과 의존성 방향은 대체로 준수됨. 문제는 '일관성 없는 접근' 때문에 하위 변경(예: http 클라이언트 교체)에 feature 쪽 파일이 여전히 산발적으로 변경될 위험이 존재합니다.
📝 상세 피드백
전체 폴더 구조와 import 패턴을 보면 Feature-Sliced Design(FSD)의 기본 원칙(계층 분리, slice의 public API 노출)은 잘 지켜지고 있습니다. app → pages → widgets → features → entities → shared 방향으로 의존성이 흐르고 있으며 entities의 index.ts를 통한 타입/함수 노출도 존재합니다. 다만 일부 feature 훅들이 entities를 거치지 않고 shared/http-client를 직접 호출하는 경우가 있어(예: features//get-single- 파일들에서 http.get 사용) FSD의 '하위 계층만 참조' 원칙과 public API 통일성에 위배될 소지가 있습니다. 이로 인해 요구사항 변화(예: API 응답 구조 변경, HTTP 클라이언트 라이브러리 교체) 시 수정 범위가 불필요하게 넓어질 수 있습니다.
❌ 현재 구조 (AS-IS)
// features/post/get-single-post/index.ts
export const usePostQuery = (postId) => {
return useQuery({
queryKey: ['post', postId],
queryFn: () => http.get(`/posts/${postId}`), // entities를 우회함
})
}✅ 권장 구조 (TO-BE)
// entities/post/api.ts
export const getPostById = (id: number) => http.get(`/posts/${id}`)
// features/post/get-single-post/index.ts
import { getPostById } from '@entities/post'
export const usePostQuery = (postId) => useQuery({
queryKey: ['post', postId],
queryFn: () => getPostById(postId),
})🔄 변경 시나리오별 영향도
- HTTP 클라이언트 교체(axios → fetch): 현재 구조에서는 shared/lib/http-client와 entities/*/api만 수정하면 된다면 이상적이지만, PR처럼 일부 feature가 http를 직접 사용하면 feature 파일도 수정해야 함.
- 엔티티 응답 구조 변경(예: posts.posts → items): entities/* 파일과 entities를 소비하는 feature들(entities public API를 통해만 접근 시)을 수정하면 3~6개 파일만 변경. 직접 http 호출이 섞여있으면 15+ 파일이 변경될 수 있음.
- 모노레포로 패키지 분리: 각 slice가 public API(index.ts)를 통해 노출된다면 패키지 독립화가 용이함.
🚀 개선 단계
- 1단계: 1) 단기(1일): features에서 직접 http 호출하는 케이스 목록화(예: usePostQuery, useUserQuery, useCommentQuery) — 3분 이내 파일 검색으로 파악 가능.
- 2단계: 2) 단기(1
2일): entities에 'getById' 같은 단일 책임 API를 추가하고 feature 훅이 이를 사용하도록 리팩토링. (수정 파일: 해당 feature 파일들만, 보통 36개) - 3단계: 3) 중기(1~2일): entities의 public API 사용을 강제하는 lint 규칙(예: import 경로 규칙) 추가하여 재발 방지.
- 4단계: 4) 장기(2~5일): 레이어별 의존성 테스트(의존성 그래프 생성)를 CI에 추가하여 계층 위반을 자동 감지.
2. 🔄 TanStack Query
💡 개념 정의
TanStack Query는 서버(원격) 상태를 선언적으로 관리하는 라이브러리로, 쿼리 키의 일관된 관리, API와 비즈니스 로직의 분리, 캐싱 전략, 낙관적 업데이트/롤백이 핵심 패턴입니다.
⚡ 중요성
일관된 쿼리 키와 캐싱 전략은 네트워크 비용 절감, 사용자 경험(빠른 응답, 일관된 캐시) 개선, 버그 감소에 직접 영향합니다. API 변경 시 수정 범위를 최소화하기 위해 API 호출과 쿼리 훅을 분리하는 것이 필수적입니다.
📊 현재 상황 분석
AS-IS: React Query를 적극 사용하여 fetch/useQuery/useMutation로 구성. 그러나 queryKeys 중앙화 미비, staleTime 미설정, 일부 feature 훅이 http 직접 사용(entities 우회)으로 query-API 분리가 완전하지 않음.
📝 상세 피드백
TanStack Query 사용은 전반적으로 적절하며, QueryClient 설정(shared/providers)에서 에러 핸들러, retry 전략을 중앙화한 점이 매우 긍정적입니다. 또한 댓글 좋아요/삭제에서 낙관적 업데이트(onMutate)와 rollback(onError) 패턴을 잘 구현했습니다. 그러나 쿼리 키 관리가 분산적이고 중앙화된 queryKeys 팩토리가 없어 일관된 쿼리 키 전략(중복·오타 방지)이 부족합니다. 또한 staleTime/cacheTime 설정이 보편적으로 빠져 있어 빈번한 refetch 발생 가능성(특히 목록 쿼리)에 주의가 필요합니다.
❌ 현재 구조 (AS-IS)
// query key 분산 예시
useQuery({ queryKey: ['posts','basic',{limit,skip}], ... })
useQuery({ queryKey: ['posts','tag',{tag}], ... })✅ 권장 구조 (TO-BE)
// 중앙화된 queryKeys
export const queryKeys = {
posts: { all: ['posts'] as const, basic: (p:{limit:number,skip:number}) => ['posts','basic',p] as const, byTag: (t:string) => ['posts','tag',t] as const }
useQuery({ queryKey: queryKeys.posts.basic({limit,skip}), queryFn: () => fetchPosts(...) })🔄 변경 시나리오별 영향도
- API 엔드포인트 경로 변경: entities api 사용 시 수정 범위 = entities/* 파일(약 10
15개)만. 직접 http 호출이 섞이면 feature 훅(약 510개)까지 수정 필요. - 새로운 데이터 소스(예: GraphQL) 추가: http-client만 추상화로 교체하면 entities 레이어에서 적절히 전환 가능(entities 10~15개). 분산된 http 호출이 많으면 작업량이 증가.
- 에러 처리 방식 변경(예: 전역 toast vs 전역 modal): providers의 QueryCache/MutationCache에서 처리하면 중앙에서 변경 가능.
🚀 개선 단계
- 1단계: 1) 단기(0.5
1일): src/shared/api/queryKeys.ts 파일 생성 및 기존 쿼리 키를 이곳으로 이동(변경 범위: 모든 useQuery/useMutation 호출의 queryKey 참조 변경 — 자동 치환으로 1020참조). - 2단계: 2) 단기(0.5
1일): 주요 목록 쿼리(게시물, 댓글, 사용자)에 대해 적절한 staleTime(예: 30s2min)과 cacheTime 설정 적용. - 3단계: 3) 중기(1~2일): entities에 query hooks(예: usePostsApiQuery)를 제공하거나 features에서 entities API를 사용하도록 리팩토링하여 API/쿼리 분리를 강화.
- 4단계: 4) 장기(2~4일): React Query의 suspense 옵션/비동기 boundary 적용 검토 및 장점(선언적 로딩)을 부분 도입.
3. 🎯 응집도 (Cohesion)
💡 개념 정의
응집도(Cohesion)는 하나의 모듈이 얼마나 관련된 책임만 가지고 있는지를 나타내며, 높은 응집도는 변경 시 수정 범위를 국지화합니다.
⚡ 중요성
높은 응집도는 유지보수 비용 감소, 온보딩 속도 개선, 패키지화/분리 시 이점(모듈 독립성)을 제공합니다.
📊 현재 상황 분석
AS-IS: 기능/도메인별 응집이 대체로 양호. 중복으로 인한 낮은 응집(하이라이트), 일부 유틸이 widgets 내부에 남아 있음.
📝 상세 피드백
기능 단위 응집도는 전반적으로 높습니다. posts 관련 로직(검색, 테이블, 폼, 댓글 관리)이 widgets/features/entities로 분리되어 있어 한 기능 변경 시 수정 파일이 모여 있습니다. 그러나 텍스트 하이라이트 로직이 shared/lib/highlightText, widgets/posts-table/lib/textUtils, widgets/comment-management/lib/textHighlight 등 여러 곳에 중복으로 존재하는 점은 응집도를 떨어뜨립니다.
❌ 현재 구조 (AS-IS)
// 중복된 하이라이트 함수들
// shared/lib/highlightText.ts
export function highlightText(text, highlight){...}
// widgets/posts-table/lib/textUtils.ts (유사 중복)
export const highlightText = (text, highlight) => {...}✅ 권장 구조 (TO-BE)
// TO-BE: 한 곳으로 통합
// shared/lib/highlightText.ts (단일 소스)
export function highlightText(...) { ... }
// widgets에서 재사용
import { highlightText } from '@shared/lib/highlightText'🔄 변경 시나리오별 영향도
- 하이라이트 로직 변경(정규식 escape 개선) → 현재 구조: 3개 파일 수정 필요 (shared/lib/highlightText.ts, widgets/posts-table/lib/textUtils.ts, widgets/comment-management/lib/textHighlight.ts).
- 하나의 Feature(댓글)에 새로운 필드(mention) 추가 → feature 훅 + widget + entities api 수정으로 3~5파일 변경.
🚀 개선 단계
- 1단계: 1) 단기(0.5일): 중복 하이라이트 유틸을 shared/lib로 통합(파일 2
3개 제거, 사용처 24곳 변경). - 2단계: 2) 단기(0.5~1일): widgets 내부의 공통 유틸(예: url-utils, text-utils)을 shared로 옮기기(이식 후 테스트).
- 3단계: 3) 중기(1~2일): 도메인 폴더(entities) 내에 고유 책임(모델+CRUD)을 묶어 응집성 강화.
4. 🔗 결합도 (Coupling)
💡 개념 정의
결합도(Coupling)는 모듈 간 의존성의 강도로, 낮을수록 한 모듈 변경이 다른 모듈에 주는 영향이 작습니다. 추상화(인터페이스), 의존성 주입, 모듈 경계 정의가 중요합니다.
⚡ 중요성
낮은 결합도는 기술 스택 변화(예: axios→fetch, zustand→jotai) 또는 아키텍처 변화(마이크로프론트 등) 시 변경해야 할 파일 수를 줄여 유연성을 확보합니다.
📊 현재 상황 분석
AS-IS: 전반적 결합도 낮음. 핵심 추상화(HTTP, store hook)는 존재. 그러나 일부 직접 호출 및 데이터 결합 로직은 수정 시 파급이 커짐.
📝 상세 피드백
결합도는 낮게 유지하려는 노력이 보입니다. http-client를 wrapper로 만들고 entities 레이어에 API를 모아둔 점, zustand store를 usePostsUI 훅으로 래핑하여 타 모듈이 내부 구현에 의존하지 않도록 한 점이 좋습니다. 반면 features에서 http를 직접 호출하거나 일부 UI 컴포넌트가 default export 혼재로 사용되는 점은 결합도를 약간 증가시킵니다. 또한 widgets 내부에서 author lookup을 하는 방식(사용자 목록과 게시물 목록을 병합하는 로직)이 존재하여 users API 응답 구조 변경 시 영향 범위가 커질 수 있습니다.
❌ 현재 구조 (AS-IS)
// AS-IS: features/usePostQuery 직접 호출
queryFn: () => http.get(`/posts/${postId}`)
// 높은 결합도 예시: widgets/posts-table 모델이 users 구조에 직접 의존
usersData.users.find(u => u.id === post.userId)✅ 권장 구조 (TO-BE)
// TO-BE: 추상화 사용
// entities/post/api.ts
export const getPostById = (id) => httpClient.get(`/posts/${id}`)
// widgets는 entities의 public API로만 상호작용
const { posts } = await fetchPostsWithAuthors() // entities가 조합 책임🔄 변경 시나리오별 영향도
- axios → fetch 전환: 이상적 구조(모든 http 호출이 shared/http-client를 통해 이뤄지면) → 수정 범위 = shared/lib/http-client.ts (1파일). 현재 구조 → 추가로 feature 훅(3~5개) 수정 필요.
- 상태관리 라이브러리 전환(zustand → jotai): usePostsUI 훅만 유지하면 변경 범위 = shared/store/posts-ui/* (4파일)로 제한 가능.
🚀 개선 단계
- 1단계: 1) 단기(0.5
1일): feature 훅에서 직접 http 호출하는 케이스를 entities API로 대체(파일 36개). - 2단계: 2) 단기(0.5일): widgets에서 사용자 조합 로직을 entities 또는 한 곳으로 추출하여 결합점 축소.
- 3단계: 3) 중기(1~2일): 주요 추상화(HTTP, store)를 문서화하고 변경 시나리오(예: axios→fetch) 테스트 케이스 준비.
5. 🧹 Shared 레이어 순수성
💡 개념 정의
Shared 레이어 순수성은 공유 모듈이 도메인(entities/features)에 의존하지 않고 범용적으로 재사용 가능한지를 의미합니다. UI/유틸은 도메인 로직과 분리되어야 합니다.
⚡ 중요성
shared 레이어가 순수하면 디자인 시스템 변경, 신규 프로젝트 재사용, 팀 간 공유가 쉬워집니다. 반대로 도메인 의존이 있으면 재사용 비용이 매우 커집니다.
📊 현재 상황 분석
AS-IS: shared 레이어의 구성은 훌륭. 중복 유틸을 제거하고 export 패턴을 일관화하면 재사용성이 더 좋아짐.
📝 상세 피드백
shared 레이어에 UI 구성요소(Button, Card, Dialog, Select, Table, Input, Textarea), HTTP 클라이언트, 유틸(highlightText, error-handler) 등이 잘 정리되어 있고 도메인 의존성이 낮습니다. 이는 다른 프로젝트로 재사용하거나 디자인 시스템 변경 시 이 레이어만 교체하면 되는 이상적인 구조입니다. 다만 widgets 내부에 유틸 중복과 일부 default export(Header/Footer) 혼재로 재사용성을 저해하는 부분이 있습니다.
❌ 현재 구조 (AS-IS)
// 좋은 사례: shared/lib/http-client.ts 캡슐화
// 중복 문제: widgets/posts-table/lib/textUtils.ts 와 shared/lib/highlightText.ts 유사 구현 존재✅ 권장 구조 (TO-BE)
// TO-BE: 모든 하이라이트/텍스트 유틸을 shared로 통합
import { highlightText } from '@shared/lib/highlightText'
// UI 컴포넌트는 named export로 통일
export { Button } from '@shared/ui/Button'🔄 변경 시나리오별 영향도
- 디자인 시스템(Material-UI→Chakra) 변경 시: shared/ui/* (약 18개 파일)만 수정하면 되는 이상적 상황. 하지만 widget들이 자체 스타일/클래스를 직접 사용하면 widget 쪽도 수정 필요 (추정 추가 파일 10개).
- 새 프로젝트로 shared 재사용: shared 레이어가 도메인 독립이면 파일 복사/패키지화만으로 가능.
🚀 개선 단계
- 1단계: 1) 단기(0.5일): widgets 내부 중복 유틸을 찾아 shared로 이동(약 2~4 파일 통합).
- 2단계: 2) 단기(0.5일): Header/Footer 같은 레이아웃 컴포넌트도 named export 정책으로 통일(2파일).
- 3단계: 3) 중기(1~2일): shared 패키지를 npm/private registry로 분리 가능하도록 준비(번들 설정, peerDeps 명시).
6. 📐 추상화 레벨
💡 개념 정의
추상화는 구현 세부사항(HTTP 클라이언트, 데이터 파싱 등)을 숨기고 명확한 인터페이스(엔티티 API, 훅)를 제공하는 정도입니다.
⚡ 중요성
추상화가 높으면 기술 스택 변경, API 변경시 영향 범위를 줄이고 재사용 가능한 인터페이스를 통해 기능 확장성이 좋아집니다.
📊 현재 상황 분석
AS-IS: 좋은 추상화가 자리잡고 있으나 일부 케이스가 이를 우회함. API 반환 데이터의 타입 명확성(예: addComment의 반환 타입을 any로 간주한 주석)이 부족해 호출부에서 타입 추론이 약해질 수 있음.
📝 상세 피드백
엔티티(api/model)와 feature(비즈니스 훅) 분리가 잘 되어 있어 추상화 수준은 양호합니다. entities/* 에 model 및 CRUD api가 있고 features/* 에 비즈니스 훅(usePostsBasic, useCommentsQuery 등)이 존재합니다. 다만 일부 feature 훅이 http를 직접 호출하여 추상화 계층을 우회하는 부분(중복된 데이터 변환 코드 포함)이 있어 개선 여지가 있습니다.
❌ 현재 구조 (AS-IS)
// AS-IS: addComment의 반환 타입 불명확
const postId = (data as unknown as { postId: number }).postId
// 문제: feature에서 타입 단언을 계속 사용해야 함✅ 권장 구조 (TO-BE)
// TO-BE: 명확한 반환 타입 정의
// entities/comment/api.ts
export const addComment = async (payload: ICommentRequest): Promise<IAddCommentResponse> => { ... }
// features는 안전하게 사용
const { postId } = await addComment(payload)🔄 변경 시나리오별 영향도
- API 반환 스키마가 바뀌면: entities에서 파싱/적응 레이어를 두면 feature는 변경 불필요. 현재 우회 호출이 있으면 추가 수정이 발생.
- 폼 검증 로직을 공통화하려면 현재 각 dialog에서 중복되므로 shared/form으로 추출 필요.
🚀 개선 단계
- 1단계: 1) 단기(1일): entities API에 모든 반환 타입을 명시적으로 선언(entities/*.ts 파일, 약 10~15파일).
- 2단계: 2) 단기(0.5
1일): feature 훅이 entities public API만 사용하도록 리팩토링(36파일). - 3단계: 3) 중기(1~2일): 화면-비즈니스-데이터 계층을 문서화(README)하여 팀 합의를 도출.
7. 🧪 테스트 용이성
💡 개념 정의
테스트 용이성은 코드가 단위 테스트, 통합 테스트, E2E 테스트로 분리되어 손쉽게 검증 가능한 정도입니다. 순수 함수·의존성 주입·작은 책임 단위가 핵심입니다.
⚡ 중요성
자동화된 테스트는 요구사항 변화 시 회귀를 빠르게 발견하게 해주며, 대규모 리팩토링의 안전망이 됩니다.
📊 현재 상황 분석
AS-IS: 훅과 UI 분리가 잘 되어 있으며 모킹 지점이 명확. 개선 포인트는 훅의 외부 의존성(특히 usePostsUI 같은 전역 훅) 주입을 허용하여 테스트에서 더 쉽게 스텁/스파이를 넣을 수 있게 하는 것.
📝 상세 피드백
비즈니스 로직과 UI가 분리된 구조(커스텀 훅 + 순수 UI 컴포넌트)로 설계되어 있어 단위 테스트/훅 테스트가 용이합니다. usePostFormData, useCommentManagement, usePostsTableData 같은 훅은 외부 의존을 주입 가능한 형태로 개선하면 더 좋습니다. shared/http-client 인터페이스가 단일화되어 있어 네트워크 레이어를 모킹하기 수월합니다. 반면 일부 컴포넌트가 default export이거나 내부에서 직접 훅을 생성(예: usePostsUI 호출을 컴포넌트 내부에서 직접 참조)하면 테스트용 Mocking이 살짝 번거로울 수 있습니다.
❌ 현재 구조 (AS-IS)
// 테스트하기 좋은 구조
// Widget은 hook에서 값을 받아 순수 렌더링
const CommentManagementWidget = () => {
const { comments } = useCommentManagement()
return <CommentsSection comments={comments} />
}
// 개선: useCommentManagement에 의존성 주입 옵션 추가✅ 권장 구조 (TO-BE)
// to-be: 훅에 DI 옵션 추가
export const useCommentManagement = ({ postsUI = usePostsUI() } = {}) => { /* ... */ }
// 테스트에서 postsUI를 모킹하여 다양한 상태를 시나리오로 검증 가능🔄 변경 시나리오별 영향도
- 외부 API 연동 테스트: entities API를 msw로 모킹하면 features 훅/위젯 테스트는 거의 수정 없이 재현 가능.
- 상태관리 라이브러리 변경: usePostsUI를 래핑한 훅만 스텁하면 대부분의 테스트 변경은 최소화 가능(변경 파일 4개).
🚀 개선 단계
- 1단계: 1) 단기(0.5~1일): 훅에 선택적 DI 파라미터 추가(예: useCommentManagement({ postsUI }))로 테스트용 mocking을 쉽게 만듦.
- 2단계: 2) 단기(0.5일): shared/http-client를 msw 기반 테스트 시나리오로 문서화.
- 3단계: 3) 중기(1
2일): 주요 훅(usePostFormData, useCommentManagement 등)에 대한 unit/integration 테스트 작성(각 훅당 12시간).
8. ⚛️ 현대적 React 패턴
💡 개념 정의
현대적 React 패턴은 Suspense/ErrorBoundary, hooks 중심 설계, 훅으로 비즈니스 로직 캡슐화, 선언적 데이터 페칭 등을 의미합니다.
⚡ 중요성
Suspense/ErrorBoundary를 이용하면 로딩/에러 처리를 중앙화하여 UI 변경 시 파급을 줄이고 일관된 UX를 제공할 수 있습니다.
📊 현재 상황 분석
AS-IS: 현대적 훅 패턴과 React Query를 잘 사용. TO-BE: 부분적으로 Suspense+ErrorBoundary 적용을 검토하면 컴포넌트 내부의 로딩/error 관리 코드가 단순해집니다.
📝 상세 피드백
커스텀 훅, React Query, 컴포넌트 분리 등 현대적 패턴이 잘 적용되어 있습니다. QueryClientProvider와 Devtools를 사용하고 있고, UI는 순수 컴포넌트로 훅에서 데이터를 주입받는 구조입니다. 반면 Suspense 및 Error Boundary를 통한 선언적 로딩/에러 처리는 아직 보이지 않으며, 컴포넌트의 lazy loading이나 React.Suspense를 활용한 리소스 분리도 미도입 상태입니다.
❌ 현재 구조 (AS-IS)
// AS-IS: 로컬 loading state 사용
const { data, isLoading } = usePostsBasic(...)
if (isLoading) return <div>로딩 중...</div>
// TO-BE: Suspense 활용
<Suspense fallback={<PostSkeleton/>}>
<PostsTableWidget />
</Suspense>✅ 권장 구조 (TO-BE)
// ErrorBoundary 적용
<ErrorBoundary fallback={<ErrorFallback/>}>
<Suspense fallback={<Loader/>}>
<App />
</Suspense>
</ErrorBoundary>🔄 변경 시나리오별 영향도
- 로딩 전략 변경(컴포넌트 단위 → 전역 Suspense) : Suspense 적용 시 컴포넌트 내부 loading state 제거 가능, 변경 범위는 주로 루트 레이아웃(1~2파일)과 훅 옵션 조정.
- 에러 처리 방식 변경(toast→ErrorBoundary) : ErrorBoundary 컴포넌트 추가 후 래핑만으로 전역 적용 가능.
🚀 개선 단계
- 1단계: 1) 단기(0.5~1일): 특정 위젯(예: PostsTableWidget)에 Suspense 적용 시범(컴포넌트 내부 loading 조건 제거).
- 2단계: 2) 단기(0.5~1일): 전역 ErrorBoundary 추가 및 QueryClient의 onError와 연동하여 일관된 에러 UI 제공.
- 3단계: 3) 중기(1~2일): 주요 페이지에 점진적으로 Suspense 전환 및 검증.
9. 🔧 확장성
💡 개념 정의
확장성은 새로운 요구사항(기능·비기능)을 추가할 때 기존 코드를 최소한으로 변경하면서 기능을 붙일 수 있는 정도입니다.
⚡ 중요성
제품 성장 또는 아키텍처 변화(마이크로프론트·다국어·A/B테스트 등) 시 빠른 대응과 낮은 리스크를 제공합니다.
📊 현재 상황 분석
AS-IS: 기능 추가는 widgets/features 단위로 추가하면 되지만 i18n/디자인 시스템 변경의 경우 shared/ui 및 다수의 widget 파일에서 문자열/스타일을 수정해야 할 가능성이 높습니다.
📝 상세 피드백
entities/features/widgets 구조는 새로운 기능 추가(다국어, 오프라인, A/B 테스트 등)에 비교적 유연합니다. 서버 상태는 TanStack Query로 분리되어 있고 UI 상태는 zustand로 잘 분리되어 있어 확장 시 기능 추가 영역이 명확합니다. 그러나 하드코딩된 문자열(많은 한국어 텍스트)과 UI 컴포넌트 내부 텍스트 분산으로 i18n 적용 시 많은 파일을 건드려야 합니다.
❌ 현재 구조 (AS-IS)
// 하드코딩된 텍스트 예
<Button>게시물 추가</Button>
// i18n 적용 예
<Button>{t('posts.add')}</Button>✅ 권장 구조 (TO-BE)
// TO-BE: i18n 적용 후
// shared/i18n.ts
export const t = (key: string) => translations[currentLocale][key]
// component
<Button>{t('posts.add')}</Button>🔄 변경 시나리오별 영향도
- i18n(한국어→다국어) 적용: 현재 텍스트 분산 상태라면 약 25
35개 파일의 문자열 수정 필요. 공통 번역 함수를 사용하면 shared/ui와 widgets의 텍스트 포맷만 변경(35파일)하면 됨. - A/B 테스트 도입: UI 구성요소가 컴포넌트화되어 있어 실험용 변형 컴포넌트를 만들기 용이.
🚀 개선 단계
- 1단계: 1) 단기(1~2일): UI 텍스트를 키로 교체하는 리팩토링 계획 수립(자동 변환 스크립트 고려).
- 2단계: 2) 단기(1~2일): shared/ui 컴포넌트들이 children 대신 label/key를 받는 옵션 추가(전역 번역 훅 통합).
- 3단계: 3) 중기(2~4일): 디자인 시스템 변경 시 shared/ui만 교체하도록 리팩토링(디자인 토큰 추출).
10. 📏 코드 일관성
💡 개념 정의
코드 일관성은 네이밍, 파일 구조, import/export 패턴이 팀 내에서 통일되어 있는 정도입니다. 일관성은 온보딩과 코드 합병을 용이하게 합니다.
⚡ 중요성
일관된 컨벤션은 코드 검색 자동화, 리팩토링 안전성, 새로운 개발자 온보딩 시간 단축에 도움이 됩니다.
📊 현재 상황 분석
AS-IS: 전반적 일관성은 양호. 개선 포인트는 Export 방식 통일과 스타일 import 복구입니다.
📝 상세 피드백
대부분 파일명(PascalCase 컴포넌트), 훅 네이밍(use*) 규칙, 타입 파일 분리 등 일관성이 좋습니다. 다만 export 패턴이 혼재되어 있습니다(app/layouts/Header/Footer는 default export, shared/ui는 named export). 또한 일부 유틸(Select index.ts vs Select/ui.tsx 구조)은 일관적이지만 파일 확장자와 export 방식(예: Card/index.tsx naming)에서 미세한 차이가 있습니다. CSS import 삭제(main.tsx에서 index.css 제거)로 인해 스타일 관련 빌드/환경 문제 가능성도 확인 필요합니다.
❌ 현재 구조 (AS-IS)
문제 사례: src/app/layouts/Header.tsx (default export)
export default Header
반면 shared/ui/Button/index.ts exports named: export * from './ui'✅ 권장 구조 (TO-BE)
// 권장: UI 컴포넌트는 named export로 통일
export const Header = () => {}
export { Header }
// import 방식 통일
import { Header } from '@app/layouts'🔄 변경 시나리오별 영향도
- 새 팀 합류/코드 리뷰 자동화: 일관된 export/import 규칙이 없으면 eslint 룰 추가 전에 혼선 발생.
- 자동 번들링/패키징: default vs named export 혼재는 tree-shaking과 패키지화에 영향 줄 수 있음.
🚀 개선 단계
- 1단계: 1) 단기(0.5일): export 컨벤션 문서화(컴포넌트: named export, default export 금지). src/app/layouts/Header/Footer를 named export로 변환(2파일).
- 2단계: 2) 단기(0.5일): ESLint/Prettier 규칙(파일 확장자, import 순서, newline 규칙) 추가.
- 3단계: 3) 중기(1일): CI에서 export/import 규칙 검사하는 lint 단계 추가.
🎯 일관성 체크포인트
파일명 규칙
- 대부분 PascalCase로 통일되어 있음(좋음).
Import/Export 패턴
- Header/Footer: default export 사용 (src/app/layouts/Header.tsx, Footer.tsx)
- shared/ui 컴포넌트: named export 사용(권장 방식) — export 방식 혼재
변수명 규칙
- 대체로 camelCase 사용으로 일관성 있음
코드 스타일
- 파일 내 일부 파일에 newline 누락(main.tsx와 index.css 변경으로 의심되는 스타일 이슈 가능)
11. 🗃️ 상태 관리
💡 개념 정의
데이터 흐름 및 상태 관리는 서버 상태(원격 데이터)와 클라이언트 상태(UI/로컬)를 명확히 구분하고 적절한 도구로 다루는 전략입니다.
⚡ 중요성
서버/클라이언트 상태 분리는 복잡한 기능(실시간 동기화, 오프라인 지원, 낙관적 업데이트) 추가 시 유지보수성과 확장성에 큰 영향을 줍니다.
📊 현재 상황 분석
AS-IS: 책임 분리는 잘 되어 있음. 개선 여지: form 상태(추가/수정 폼)가 아직 각 widget에서 local state로 관리되는데, 복잡해질 경우 form 상태 전용 관리(react-hook-form + zustand)로 분리 권장.
📝 상세 피드백
서버 상태는 TanStack Query로, UI 상태는 zustand 기반 usePostsUI로 분리되어 있어 서버/클라이언트 상태 책임이 명확합니다. usePostsUI가 ID 기반(selectedPostId 등)으로 상태만 보관하는 전략은 데이터 중복을 줄여 아키텍처 변경(오프라인, 실시간)에도 유리합니다. 또한 usePostsUI를 훅으로 래핑해 상태관리 라이브러리 교체 시 수정 범위를 줄인 설계가 돋보입니다.
❌ 현재 구조 (AS-IS)
// 좋은 설계: UI는 ID만 저장
usePostsUIStore = { selectedPostId: 12 }
// server data는 query로 가져옴
usePostQuery(selectedPostId)✅ 권장 구조 (TO-BE)
// TO-BE: 복잡한 form은 전용 store로 분리
// features/post/formStore.ts
export const usePostFormStore = create(...) // 폼 상태 전역 관리🔄 변경 시나리오별 영향도
- 실시간 기능 추가(WebSocket): server-state(react-query)와 실시간 스트림(서버 푸시) 병합 전략 필요 — react-query의 setQueryData로 통합 가능.
- 상태관리 라이브러리 변경(zustand→jotai): usePostsUI 훅만 다시 구현하면 변경 범위는 shared/store/posts-ui/* (4파일)로 제한된다.
🚀 개선 단계
- 1단계: 1) 단기(0.5~1일): form 상태가 커질 경우를 대비해 post/comment form 상태를 전용 store로 분리 계획 수립.
- 2단계: 2) 단기(0.5일): usePostsUI의 public API 문서화(어떤 값/액션을 제공하는지)로 라이브러리 교체 시 롤백 비용 최소화.
- 3단계: 3) 중기(1~2일): 실시간/오프라인 시나리오에 대한 데이터 동기화 전략(react-query + websocket) 설계.
🤔 질문과 답변
PR 본문에 별도의 '리뷰 받고 싶은 내용' 질문은 없으셨습니다. 대신 제가 예상하는 주요 궁금점(보통 검토받고 싶어하는 사항)에 대해 답변드립니다:
-
"내 FSD 구조가 맞나요?": 전반적으로 올바른 방향입니다. entities(데이터/모델/API), features(비즈니스 훅), widgets(UI 재사용 블록), shared(공통 유틸/컴포넌트)로 명확히 분리되어 있어 확장성과 유지보수성 측면에서 우수합니다. 다만 features에서 entities를 우회해 http를 직접 호출하는 케이스를 정리하면 더 완벽합니다.
-
"TanStack Query를 잘 썼나요?": 네. QueryClient 설정과 Devtools, 낙관적 업데이트 패턴은 적절합니다. 개선점은 쿼리 키의 중앙화(queryKeys 파일), 그리고 목록 쿼리에 적절한 staleTime 설정입니다.
-
"Zustand로 전역 UI 상태를 관리한 방식이 괜찮나요?": 좋은 선택입니다. 특히 usePostsUI 훅으로 래핑해두어 다른 라이브러리로의 교체 비용을 낮춘 점이 인상적입니다. 교체 시 파일 수는 shared/store/posts-ui/* 4파일만 변경하면 됩니다.
-
"i18n이나 디자인 시스템 변경은 얼마나 힘들까요?": 현재 구조에서는 디자인 시스템 변경은 shared/ui/* (약 18파일)만 수정하면 되므로 작업 범위가 합리적입니다. 다만 텍스트 하드코딩이 widgets에 분산되어 있어 i18n 도입 시 문자 교체는 약 25~35개 파일을 건드려야 할 수 있습니다. 따라서 early-stage에서 i18n 키로 대체하는 리팩토링을 권합니다.
🎯 셀프 회고 & 제안
과제 셀프회고에서의 통찰이 명확합니다. 특히 'FSD는 비즈니스와의 결합도를 분리하기 위한 구조'라는 관점은 정확하며 실무에서도 매우 중요한 인사이트입니다. 스스로 FSD의 레이어별 책임(entities=데이터, features=사용자 행동/비즈니스, widgets=재사용 UI, shared=범용 유틸/컴포넌트)을 정의한 점도 좋습니다. 이어서 더 생각해볼 질문들:
- entities에 '모델 + api'만 두고 비즈니스 로직을 모두 features에 두는 결정을 내렸는데, 특정 도메인 규칙(예: 게시물의 'published' 계산)은 entities에 두는 편이 나을지 features에 두는 편이 나을지 어떻게 판단하실까요? 어떤 기준으로 책임을 나누시겠습니까?
- shared 레이어로 무엇을 옮길지 결정할 때 '재사용 빈도' 외에 어떤 지표(예: 변경 빈도, 의존성 깊이)를 고려하시겠습니까?
- 지금처럼 usePostsUI가 ID만 저장하는 전략은 실무에서 어떤 장단점을 만들까요(예: 네트워크 요청 최적화 vs 인지 비용)?
챕터 회고 관련 추가 제안:
- '더티 코드'와 '클린 코드' 체험을 기반으로 팀 규칙(코드 스타일, export 컨벤션, 폴더 구조)을 문서화하고 PR 템플릿에 체크리스트로 포함해 보세요. 이렇게 하면 개인의 인사이트를 조직적 실천으로 연결할 수 있습니다.
추가 논의가 필요한 부분이 있다면 언제든 코멘트로 남겨주세요!
코드 리뷰를 통해 더 나은 아키텍처로 발전해 나가는 과정이 즐거웠습니다. 🚀
이 피드백이 도움이 되었다면 👍 를 눌러주세요!
과제 체크포인트
기본과제
목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기
체크포인트
심화과제
목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기
체크포인트
최종과제
배포 링크
https://legitgoons.github.io/front_6th_chapter2-3/
과제 셀프회고
이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.
클린코드와 아키텍처는 도대체 왜 하는가
본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?
스스로 FSD에 대해 정의내리기
FSD는 이전에도 사용해봤지만, 그 때는 제대로 이해하지 못해서 widgets와 features, entities를 합쳐서 components로 사용했었습니다.

또 이번 주차의 팀별모임에서도 서로 FSD에 대한 정의에 대해서 이야기를 나누다가 결국
FSD는 서브웨이다! 구체적인 부분은 알아서하고, 같은 레이어끼리 순환하지 못하게 하고, 하위 레이어에서 상위 레이어를 참조할 수 없도록 하는 철학만 지키면 된다!라는 결론을 냈었습니다.이렇게 사람마다 FSD에 대한 의견이 달랐기에 과제를 시작하기 전에 스스로 FSD에 대해서 정의를 내리고 시작하고 싶었습니다. FSD 공식 홈페이지에서 제공하는 FSD의 레이어 별 단계는 다음과 같습니다.
하지만 공식 홈페이지의 설명들은 명확하게 이해가 되지 않거나 혹은 와닫지 않는 부분들이 있었습니다. 대표적으로 공식 홈페이지에서는 read api는 entities에, 그 외인 create, update, delete api들은 features에 두기를 권장합니다.
다른 분들과도 이를 주제로 많은 이야기를 나누었습니다. 그 과정에서
굳이 api를 나눠서 둘 이유가 있을까?와만약 그렇다면, 다른 기능에서 같은 api를 사용한다면 중복선언을 해야하나?등의 의문점이 생겨났고, 스스로 다음과 같이 정의를 내려보았습니다.아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.
사실 FSD는 현재 나온 프론트엔드 아키텍처 중 결합도를 낮추고, 응집도를 높이기 위한 여러 아이디어가 녹아들어간 방법이라고 생각합니다. 테오의 폴더구조의 변화로 이해하는 프론트엔드 멘탈모델 변천사에서 볼 수 있듯이, 프론트엔드는 여러 방법을 모색해왔습니다.
이런 역사를 통해 FSD라는 방식까지 등장했습니다. FSD는 기능(역할)을 중심으로 도메인별로 분리하는 방식이니 그야말로 결합도는 최대한 낮추고, 그러면서도 응집도는 높힐 수 있습니다.
하지만 그렇다고 FSD를 사용해야한다는 것은 아닙니다. 테오가 말했듯이 사람마다 생각하는 기준이 조금씩 다르다는 문제점이 너무 클 뿐더러, 설령 혼자서 개발하는 극초기 스타트업같은 환경에서도 프론트엔드에서 기능별 + 도메인별로 하나하나 쪼개고 있으면 병목현상이 엄청날테니까요. 폴더구조는 말 그대로
롤 템트리같은거라서 확실한 정답이 없다고 생각합니다.결국
어떤 상황에 어떤 구조를 선택해야할까?에 대해서는 아직 막연하게 느끼는 것 같아서 아쉽습니다. 물론 비즈니스와도 연관이 깊은 부분이라 많은 경험이 필요한 부분이겠지만요.이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.
테오가 FSD에 대한 내용은 개념만 남기고 잊으라고 했지만, 사실 이 구조가 썩 마음에 들어서 실제로 한번쯤 사용해보고 싶은 마음은 있습니다. 물론 적절하게 사용할 수 있는 기회가 온다면요!
챕터 셀프회고
클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기
우선 이번 챕터를 진행하면서 얻은 가장 큰 깨달음은,
결합도를 낮추면서 응집도는 높인다.를 왜 해야하는지를 깨달은 점이라고 생각합니다. 이를 위해서는 여러 방법론들이 있겠지만, 가장 중요한 원칙 하나만 꼽으라고 하면단일 책임의 원칙으로 꼽겠습니다.일단은 함수형 프로그래밍의 순수함수 분리와, FSD를 배우면서 익힌 기능 + 도메인 분리를 해야한다는 사고방식(FSD를 사용하겠다는 뜻은 아닙니다)만 잘 갖추고 있다면 어지간한 문제는 해결할 수 있지 않을까? 라는 자신감도 약간은 생겼습니다.
결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리
응집도 높이기: 서버상태관리, 폴더 구조
"이 코드는 대체 어디에 둬야 하지?"라고 고민했던 시간
저는 1번은 추후 타입 관리에 어려움을 겪을 수 있을 것 같았고, 2는 가능한 타입을 entities에 위치시키고 싶어서 망설여졌습니다. 그래서 선택한 방법이 최소 공통 타입만 shared로 + 확장 타입은 entities에 두는 방식이였어요. 이렇게 하면 FSD의 철학도 지키면서, 정말로 재사용되는 부분만 shared로 보내고, 도메인 세부사항을 따로 분리해서 응집도와 예측 가능성을 높일 수 있을 거라고 생각했습니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문
이번 챕터에서 가장 어려운 것은 결국
어디까지 함수를 쪼갤 것인가와폴더 구조를 무엇을 근거로 선택할 것인가두 가지라고 생각해요. 제가 생각해도 굉장히 막연한 질문이긴 한데, 혹시 테오는 이 두 가지에 어떤 기준을 가지고 하시는지를 알려주시면 감사드리겠습니다!