[5팀 양성진] Chapter 2-3. 관심사 🦍분리와 폴더구조#39
Conversation
There was a problem hiding this comment.
이 친구를 app 폴더로 옮겼어도 괜찮지 않았을까 싶어요!
개인적으로 src 하위에 레이어 폴더들만 있는 것이 가독성과 직관성이 좋게 느껴져서요
|
|
||
| import userApi from '../api'; | ||
|
|
||
| export const useGetUser = (userId: number) => { |
There was a problem hiding this comment.
마이너) 개인적으로 네이밍을 중간의 Get 없이 간단히 useUser, usePost 라고 해봐도 나쁘지 않았을 것 같아요
There was a problem hiding this comment.
동사를 넣어야지 하는 생각때문에 그랬던거같은데 없어도 될거같네요 ㅋㅋ
There was a problem hiding this comment.
이렇게 객체처럼 묶으니까 더 깔끔한 것 같네요... 저는 왜 이생각을 자꾸 못할까요 ㅠㅠ 예쁜 코드! 👍
There was a problem hiding this comment.
이렇게 객체처럼 묶으니까 더 깔끔한 것 같네요... 저는 왜 이생각을 자꾸 못할까요 ㅠㅠ 예쁜 코드! 👍
앗 저도 다른분들거 보고 배워서 감사합니다.!
| tag?: string; | ||
| searchQuery?: string; | ||
| } | ||
| export const useGetPosts = ({ |
There was a problem hiding this comment.
useGetPosts 랑 usePosts 가 분리된 이유가 있을까요?
의도가 없었다면 약간 이런 인터페이스는 어떤지도 제안해보고 싶었어요
const posts = usePosts();
posts.create(...);
posts.delete(...);
posts.update(...);
posts.data;There was a problem hiding this comment.
처음엔 객체로 썼다가 리액트hooks규칙때문에 사용이 안되서 따로 뺐었습니다. 지금 보니까 네이밍이 너무 헷갈리네요..ㅠ
There was a problem hiding this comment.
오 먼가 다른 도메인들은 entities/domain/api/api.ts 이런 경로의 api.ts 내부에 관련 api들이 한번에 선언된 느낌이었는데
얘는 혼자 파일명이 fetch네요? 혹시 이렇게 해두신 이유를 알 수 있을까요?
There was a problem hiding this comment.
오 먼가 다른 도메인들은 entities/domain/api/api.ts 이런 경로의 api.ts 내부에 관련 api들이 한번에 선언된 느낌이었는데 얘는 혼자 파일명이 fetch네요? 혹시 이렇게 해두신 이유를 알 수 있을까요?
앗 처음에 패치를 썻다가 get으로 통일하려고 했다가 까먹고 바꾸질 못했던거 같아요.
| import { usePostFilterStore } from '../model/store'; | ||
|
|
||
| const Pagination = ({ total }: { total: number }) => { | ||
| const { skip, limit, setFilter } = usePostFilterStore(); |
There was a problem hiding this comment.
컴포넌트 이름만 놓고 보면 shared 에 있을 법한 도메인 맥락이 없는 컴포넌트 같은데 폴더구조와 내부 구현을 보니까 Post 관련 컴포넌트인 걸 알았어요. 차라리 PostPagination 이라고 명명해보면 어떨까 싶숩니다
| @@ -0,0 +1,20 @@ | |||
| // 하이라이트 함수 추가 | |||
| export const highlightText = (text: string, highlight: string) => { | |||
There was a problem hiding this comment.
요거 그냥 리액트 엘리먼트를 반환하는 함수인데 컴포넌트로 바꾸면 어땠을까 싶어요!
export const HighlightText = ({ text, highlight }: {text: string, highlight: string}) => {
There was a problem hiding this comment.
useCommentDialog.ts 파일과 useCommentStore 파일의 내용이 똑같은 것 같은데 혹시 이렇게 하신 이유가 있나요?
There was a problem hiding this comment.
이건 좀 개인적인 취향과 관련이 되어 있는데 이 컴포넌트가 Dialog 즉 modal의 성격을 띄잖아요! 그러면 저는 이 컴포넌트의 제목에 "아 얘는 modal 이구나" 하고 정보를 얻을 수 있는 힌트가 있었으면 좋겠어요!
ex> AddCommentModal, AddCommentDialog
| <div | ||
| key={comment.id} | ||
| className="flex items-center justify-between text-sm border-b pb-1" | ||
| > | ||
| <div className="flex items-center space-x-2 overflow-hidden"> | ||
| <span className="font-medium truncate"> | ||
| {comment.user.username}: | ||
| </span> | ||
| <span className="truncate"> | ||
| {highlightText(comment.body, searchValue)} | ||
| </span> | ||
| </div> | ||
| <div className="flex items-center space-x-1"> | ||
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| // onClick={() => likeComment(comment.id, postId)} | ||
| > | ||
| <ThumbsUp className="w-3 h-3" /> | ||
| <span className="ml-1 text-xs">{comment.likes}</span> | ||
| </Button> | ||
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| onClick={() => { | ||
| setSelectedComment(comment); | ||
| setShowEditCommentDialog(true); | ||
| }} | ||
| > | ||
| <Edit2 className="w-3 h-3" /> | ||
| </Button> | ||
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| onClick={() => deleteComment.mutate({ id: comment.id, postId })} | ||
| > | ||
| <Trash2 className="w-3 h-3" /> | ||
| </Button> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
여기는 Comment 컴포넌트로 분리해도 괜찮았을 것 같다는 생각이 들어요.
지금은 이 코드를 보면 이게 무슨 코드지? 하고 이 ui 코드들을 다 읽어보고 나서야 아~ 단일 Comment 컴포넌트 코드구나? 하고 와닿게 되는데 Comment 컴포넌트로 미리 분리해 둔다면 코드를 읽는 다른 개발자들이 더 직관적으로 받아들일 수 있지 않을까 싶어욧
There was a problem hiding this comment.
헉스 이 hook을 lib에 넣으셨네요! 근데 저도 이걸 보고 아 이건 models보다는 lib이 더 어울리겠구나 하는 생각이 들었어요.. 배워갑니다!
There was a problem hiding this comment.
저는 페이지네이션 컴포넌트는 shared라고 생각했는데 features/posts 안에 넣으셨네요!
posts를 컨트롤 하는 컴포넌트라 생각하셔서 여기 배치하신건가요?
posts를 직접적으로 컨트롤 하는 함수를 전부 밖에서 참조하도록 하고 이 컴포넌트를 shared로 빼는건 어떻게 생각하세요?
| <Select | ||
| value={sortBy} | ||
| onValueChange={(value) => setFilter('sortBy', value)} | ||
| > | ||
| <Select.Trigger className="w-[180px]"> | ||
| <Select.Value placeholder="정렬 기준" /> | ||
| </Select.Trigger> | ||
| <Select.Content> | ||
| <Select.Item value="none">없음</Select.Item> | ||
| <Select.Item value="id">ID</Select.Item> | ||
| <Select.Item value="title">제목</Select.Item> | ||
| <Select.Item value="reactions">반응</Select.Item> | ||
| </Select.Content> | ||
| </Select> |
There was a problem hiding this comment.
이 Select 쪽 UI들이 구조가 반복되길래 저는 아예 그 부분을 공통 컴포넌트로 만들어서 빼버렸어요!
그랬더니 제가 보기에는 반복되는 코드가 좀 더 줄어든 것 같은데 그런건 어떠신가요?
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./index";
export const DropdownSelect = ({
value,
onChange,
options,
placeholder,
}: {
value: string;
onChange: (value: string) => void;
options: { label: string | number; value: string; key?: string }[];
placeholder: string;
}) => {
return (
<Select value={value} onValueChange={onChange}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.key || option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
};<DropdownSelect
value={sortBy}
onChange={(value) => {
setSortBy(value);
}}
options={[
{ key: "none", label: "없음", value: "none" },
{ key: "id", label: "ID", value: "id" },
{ key: "title", label: "제목", value: "title" },
{ key: "reactions", label: "반응", value: "reactions" },
]}
placeholder="정렬 기준"
/>There was a problem hiding this comment.
이 Select 쪽 UI들이 구조가 반복되길래 저는 아예 그 부분을 공통 컴포넌트로 만들어서 빼버렸어요! 그랬더니 제가 보기에는 반복되는 코드가 좀 더 줄어든 것 같은데 그런건 어떠신가요?
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./index"; export const DropdownSelect = ({ value, onChange, options, placeholder, }: { value: string; onChange: (value: string) => void; options: { label: string | number; value: string; key?: string }[]; placeholder: string; }) => { return ( <Select value={value} onValueChange={onChange}> <SelectTrigger className="w-[180px]"> <SelectValue placeholder={placeholder} /> </SelectTrigger> <SelectContent> {options.map((option) => ( <SelectItem key={option.key || option.value} value={option.value}> {option.label} </SelectItem> ))} </SelectContent> </Select> ); };<DropdownSelect value={sortBy} onChange={(value) => { setSortBy(value); }} options={[ { key: "none", label: "없음", value: "none" }, { key: "id", label: "ID", value: "id" }, { key: "title", label: "제목", value: "title" }, { key: "reactions", label: "반응", value: "reactions" }, ]} placeholder="정렬 기준" />
음 그런거같아요 공통으로 빼면 좀더 코드가 깔끔해질거 같네요, 알려주신대로 수정한번 해봐야겠어요!
There was a problem hiding this comment.
버튼 컴포넌트들도 분리하셨네요!
근데 분리하시고 shared에 두셨을 줄 알았는데 features에 있네욧
onClick 함수를 외부에서 받는 형태로 변경하고 shared로 이동시켜도 괜찮지 않을까 싶은데 어떠신가요?
There was a problem hiding this comment.
이녀석 뭔가 제가 저번에 스크럼때 이야기한 가짜녀석 같습니다... 지워도 되는 파일 같아요
커밋 참고: 0c69880
|
성진님 이번주 고생 많으셨습니다!! 코드 잘 보고 갑니다 😃 |
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요, 양성진 님! 5주차 과제 잘 진행해주셨네요. 고생하셨습니다 👍🏻👍🏻
현재 남기는 코멘트는 GPT-5-mini model을 사용하여 AI가 남기는 피드백입니다 🤖
과제 피드백과는 별개이므로 참고해주세요!
1. 🏗️ FSD 아키텍처
💡 개념 정의
Feature-Sliced Design(FSD)은 기능 단위로 코드베이스를 나누고 레이어 의존성을 명확히 하여 관심사 분리를 실현하는 아키텍처 규칙입니다. 기본 의존성 방향은 app → pages → widgets → features → entities → shared 이며, 각 slice는 public API(index.ts)를 통해서만 외부에 노출되어야 합니다.
⚡ 중요성
의존성 방향이 올바르면 도메인 변경(entities)에서 상위 레이어(features/pages 등)의 수정이 최소화됩니다. 반대로 위반 시 작은 변경이 계층 전반에 전파되어 유지보수 비용과 버그 발생 가능성이 증가합니다. 모노레포/마이크로프론트엔드로 확장할 때 경계가 명확해야 패키지 분리와 배포가 쉬워집니다.
📊 현재 상황 분석
AS-IS: entities 계층이 features 훅을 직접 참조/재노출하고 있어 의존성 역전이 발생했습니다. 결과적으로 entities의 public API가 features 내부 구현에 결합되어 있으며, features 리팩토링 시 entities 파일들도 수정 대상이 됩니다. 또한 소스 트리에 src_final/ 와 src/ 의 중복이 있어 구조 파악과 유지보수가 복잡해졌습니다.
📝 상세 피드백
전반적으로 FSD 레이어 구조(src/features, src/entities, src/shared, src/widgets, src/pages)가 적용되어 있으며 barrel exports와 eslint fsd 플러그인 설정도 도입되어 있는 점이 긍정적입니다. 다만 일부 파일들이 FSD 의존성 규칙(하위 계층만 참조, Public API 통해서만 노출)을 위반하는 사례가 발견됩니다. 특히 entities 계층이 features 내부 훅을 re-export 하고 있어 의존성 방향이 뒤집혀 있습니다. 이로 인해 아키텍처 변화(예: features를 리팩터링하거나 모듈을 분리) 시 entities 쪽이 불필요하게 영향을 받습니다. 개선하면 FSD의 장점(변경영향 축소, 레이어간 책임 명확화)을 더 확실히 누릴 수 있습니다.
❌ 현재 구조 (AS-IS)
src/entities/comments/model/index.ts
export * from '../../../features/comments/model/hooks.ts';
// → entities가 features를 직접 재노출(의존성 역전)✅ 권장 구조 (TO-BE)
// TO-BE: entities는 자체 모델/훅만 노출
// src/entities/comments/model/index.ts
export * from './types';
export * from './hooks';
// features는 entities의 public API를 사용
import { commentsApi } from '@/entities/comments';
import { useQuery } from '@tanstack/react-query';
export const useGetComment = (id: number) => useQuery(['comments', id], () => commentsApi.getComments(id));🔄 변경 시나리오별 영향도
- 만약 features/comments 의 훅 시그니처가 바뀐다면 현재 구조에서는 src/entities/comments/model/index.ts 와 이를 import 하는 모든 features 파일까지 (약 5-12개 파일) 변경이 필요할 가능성이 큽니다.
- 만약 모노레포로 entities 패키지를 분리한다면 역참조 때문에 entities 패키지로의 의존성 이동(또는 빼내기)을 수행하기 어려워집니다.
- 만약 shared 레이어에 공통 훅(예: useAuth)이 추가되어 entities가 그 훅을 직접 사용한다면, shared 추상화 규칙이 흐트러집니다.
🚀 개선 단계
- 1단계: 단기(1-2일): entities/model/index.ts 파일들을 스캔해 features로의 역참조가 있는지 식별(현재 1건 발견). 해당 파일들을 임시로 주석처리하고 빌드/테스트 실행.
- 2단계: 단기: entities가 노출해야 할 Public API(예: ./api, ./model/types) 목록을 명세하고 index.ts로 한 곳에서만 export하도록 강제.
- 3단계: 중기(2-4일): entities 가 참조하던 features 훅을 features로 복귀시키고, entities에서는 순수 타입/CRUD(HTTP)만 제공하도록 리팩토링. (예상 변경 파일수: 현재 구조에서는 역참조 제거 시 3
8개 파일 수정 → 개선 후 12개 파일만 변경) - 4단계: 중기: fsd ESLint 룰 활성화 및 CI에 적용(fsd/no-public-api-sidestep 등). 규칙 위반은 PR에서 차단.
- 5단계: 장기(1~2주): 팀 내 FSD 가이드 문서화(예: 어떤 코드는 entities, 어떤 코드는 features에 둬야 하는지 기준표) 및 onboarding 템플릿 제공.
2. 🔄 TanStack Query
💡 개념 정의
TanStack Query는 서버 상태를 선언적으로 관리하는 라이브러리로, 핵심은 일관된 queryKey 설계, queryFn의 순수성, 캐싱·staleTime 전략, 그리고 mutation의 낙관적 업데이트/롤백 전략입니다.
⚡ 중요성
쿼리 키가 체계적이면 API 변경(예: endpoint path 또는 pagination 파라미터 추가) 시 수정 범위를 좁힐 수 있고, 캐시 일관성이 확보되면 UI 반응성 및 성능을 크게 개선할 수 있습니다. 반대로 키 설계가 불분명하면 같은 데이터에 대해 중복 호출과 상태 충돌이 발생합니다.
📊 현재 상황 분석
AS-IS: queryKey 네이밍이 통일되어 있지 않음 → 같은 리소스(댓글)에 대해 서로 다른 key로 캐시가 분리될 수 있음. 또한 mutation의 캐시 업데이트 방식(setQueryData vs invalidateQueries) 혼용으로 예측 가능한 캐시 상태 유지가 어려움.
📝 상세 피드백
TanStack Query가 프로젝트 전역에 적용되어 있고 api 계층(api 객체)와 훅(useQuery/useMutation)을 분리하려는 시도가 보입니다. 좋은 방향이지만 쿼리 키 관리와 일관성, 그리고 cache 업데이트 전략의 일관성에 개선 여지가 있습니다. 일부 훅에서 queryKey 사용이 일관되지 않거나(예: ['comments'] vs ['comments', id]) 캐시 업데이트 방법(setQueryData vs invalidateQueries)의 사용이 혼재되어 있어 변경 시 디버깅 비용이 증가합니다.
❌ 현재 구조 (AS-IS)
❌ AS-IS: 불일치한 쿼리 키
// src/features/comments/model/hooks.ts
return useQuery({ queryKey: ['comments', id], queryFn: () => commentsApi.getComments(id) })
// 다른 파일
return useQuery({ queryKey: ['comments'], queryFn: () => commentsApi.getComments(id) })✅ 권장 구조 (TO-BE)
✅ TO-BE: 체계화된 queryKeys 및 계층 분리
// shared/api/queryKeys.ts
export const queryKeys = {
comments: (postId?: number) => (postId ? ['comments', postId] : ['comments']) as const,
}
// entities/comments/api/index.ts (순수 CRUD)
export const commentsApi = { getComments: (postId) => api.get(`/comments/post/${postId}`) }
// features/comments/hooks.ts
export const useGetComments = (postId) => useQuery({ queryKey: queryKeys.comments(postId), queryFn: () => commentsApi.getComments(postId) })🔄 변경 시나리오별 영향도
- API 엔드포인트가 /comments/post/:postId → /posts/:postId/comments 로 변경된다면: 현재 구조에서는 commentsApi와 관련 훅들(약 4-8개)이 모두 수정되어야 함. 체계적 queryKeys 사용 시 수정 범위를 api 계층으로 한정 가능.
- 새로운 데이터 소스(예: WebSocket 실시간 댓글 추가)가 추가된다면: 현재 queryKey가 일관적이면 useQuery + subscription 패턴으로 기존 캐시를 안전하게 업데이트 가능.
- 에러 핸들링 정책이 변경되어 모든 에러를 toast로 노출하려면: 각 useMutation의 onError로 분산되어 있는 로직을 공통 에러 훅으로 추상화하면 수정 파일 수 감소.
🚀 개선 단계
- 1단계: 단기(half day): 프로젝트 전체에서 사용 중인 queryKey 패턴을 검색해 표준화(예: resource + identifier: ['comments', postId]).
- 2단계: 단기: shared 폴더에 queryKeys 팩토리 파일 생성(queryKeys.comments(postId) 등)하고 모든 훅에서 이를 사용하도록 리팩토링(예상 소요 1-2일). 수정 파일수 추정: 6~12개.
- 3단계: 중기: mutation 후 캐시 갱신 전략을 정책화(낙관적 업데이트가 필요할 경우 setQueryData 사용, 단순 변경은 invalidateQueries)하고 useAutoMutation 같은 공통 훅으로 추상화하여 재사용(예상 1~3일).
- 4단계: 장기: 중요한 데이터에 대해 staleTime, cacheTime를 도메인 별로 정의하고 성능 테스트(네트워크 시뮬레이션)를 통해 최적값 적용.
3. 🎯 응집도 (Cohesion)
💡 개념 정의
응집도(Cohesion)는 한 모듈 내부의 요소들이 얼마나 관련되어 있는지를 나타냅니다. 높은 응집도는 관련 변경이 모듈 내부에 국한되어 유지보수성을 높입니다.
⚡ 중요성
높은 응집도는 변경 시 영향 범위를 축소하고 버그 추적을 용이하게 하며, 모듈을 패키지화/재사용할 때 경계를 분명히 해줍니다.
📊 현재 상황 분석
AS-IS: 대부분의 기능은 도메인 단위로 모여 있으나, 일부 파일이 잘못된 계층에 위치하거나 중복 디렉터리로 인해 관련 코드가 여러 위치에 존재합니다(결과: 기능 변경 시 검색 시간 증가, PR 리뷰 난이도 상승).
📝 상세 피드백
대체로 기능 단위(예: features/posts, features/comments)로 컴포넌트/훅/스토어가 모여 있어 응집도가 높은 편입니다. Post 관련 UI/로직이 features/posts 아래에 잘 모여 있고, entities는 타입과 API를 책임지는 의도가 보입니다. 다만 일부 책임(특히 features↔entities 경계)이 흐려진 곳이 있어 응집도가 떨어지는 사례가 있습니다(entities가 features 훅을 re-export).
❌ 현재 구조 (AS-IS)
// AS-IS: entities가 features 훅을 re-export 하여 응집도 저하
src/entities/comments/model/index.ts
export * from '../../../features/comments/model/hooks.ts';✅ 권장 구조 (TO-BE)
// TO-BE: 댓글 관련 모든 로직은 features/comments에 응집, entities는 타입과 CRUD만 제공
// entities/comments/model/index.ts
export * from './types';
// features/comments/model/hooks.ts (모든 비즈니스 로직 포함)
export const useComments = () => { /* 댓글 CRUD + 캐시 전략 */ }🔄 변경 시나리오별 영향도
- 새로운 포스트 필터(예: 날짜 범위)를 추가하면: 현재 구조에서는 features/posts 내부에서만 작업하면 되지만, entities/posts/types/ api를 수정해야 하는 경우라면 여러 파일(entities + features + widgets)을 건드려야 할 수 있음.
- entities 타입(예: Post.reactions 구조 변경)이 변경되면: AS-IS에서는 해당 타입을 사용하는 features 및 widgets(약 8~15 파일)이 영향받을 가능성 있음; TO-BE에서는 entities만 변경 후 상위 레이어는 최소한의 변경으로 적응.
🚀 개선 단계
- 1단계: 단기: 중복된 src_final/ 디렉터리 제거(또는 통합) — 중복 파일로 인한 혼란 해소(예상 0.5~1일).
- 2단계: 단기: entities 폴더 내에 features로의 의존이 있는 파일 식별 후 이동 또는 Public API로 리팩토링.
- 3단계: 중기: 도메인별 체크리스트 작성(예: comment 도메인에서는 어떤 파일이 entities에, 어떤 것이 features에 위치해야 하는지) 및 PR 템플릿에 반영.
- 4단계: 장기: 모듈 경계 테스트(간단한 단위 테스트/정적 분석)로 응집도 저하를 자동 감지하도록 설정.
4. 🔗 결합도 (Coupling)
💡 개념 정의
결합도(Coupling)는 모듈 간 의존성의 강도를 나타내는 지표로, 낮은 결합도는 모듈 변경이 다른 모듈에 미치는 영향을 최소화합니다.
⚡ 중요성
낮은 결합도는 기술 스택 교체(예: axios→fetch), 아키텍처 변화(예: 모노레포 분리) 시 수정 범위를 크게 줄여줍니다.
📊 현재 상황 분석
AS-IS: api wrapper(api.get/post 등)를 도입한 것은 추상화의 좋은 시작이지만 코드베이스 전반에 일관되게 적용되지 않아 일부 모듈이 직접 fetch를 호출하고, 일부는 api wrapper를 사용합니다. 이로 인해 HTTP 클라이언트 교체 시 수정 파일수가 증가합니다.
📝 상세 피드백
의존성 주입/추상화 수준이 부분적으로 잘 되어있지만 구체 구현에 결합된 코드가 일부 존재합니다. 예를 들어 features 훅에서 userApi.getUserPost() 결과를 바로 사용해 posts에 author를 결합하는 로직은 중앙에서 추상화하면 변화에 더 유연해집니다. 또한 entities가 features를 재노출하는 현상은 높은 결합도를 초래합니다. HTTP 클라이언트 변경(예: fetch → axios)이나 상태관리 라이브러리 변경 시 리팩토링 필요 범위를 줄이려면 더 명확한 추상화가 필요합니다.
❌ 현재 구조 (AS-IS)
// AS-IS: 일부 파일은 wrapper를 사용, 일부는 직접 fetch 사용
// shared/lib/api.ts -> api.get('/posts') 사용
// entities/tags/api/fetch.ts -> 직접 fetch('/api/posts/tags') 호출✅ 권장 구조 (TO-BE)
// TO-BE: 모든 HTTP 호출은 api wrapper를 사용
// entities/tags/api/fetch.ts
import { api } from '@/shared/lib/api';
export const fetchTags = () => api.get('/posts/tags');🔄 변경 시나리오별 영향도
- HTTP 클라이언트 교체(예: fetch → axios): api wrapper만 수정하면 되도록 되어 있으면 1파일만 수정하면 되지만, 현재 구조에서는 direct fetch 호출(src/entities/tags/api/fetch.ts 등)을 찾아 수정해야 하므로 약 3~8개 파일을 고쳐야 할 수 있음.
- 상태관리 라이브러리 교체(예: zustand→redux): 상태 관련 코드가 features 내에 적절히 캡슐화되어 있으면 영향이 최소화되지만, 전역에 흩어진 상태 사용(예: direct import of stores)이 있을 경우 영향 범위가 확장됩니다.
🚀 개선 단계
- 1단계: 단기: repository 전체에서 direct fetch 호출을 검색해 api wrapper로 통일(예상 1일, 2~6개 파일).
- 2단계: 단기: shared/lib/api.ts 에 인터셉터(토큰/로깅) 확장 포인트를 제공하여 교체 비용을 더욱 낮춤.
- 3단계: 중기: 상태관리 사용 규칙 문서화(특정 slice만 전역 상태 사용, 로컬 상태는 컴포넌트/훅에 유지).
- 4단계: 장기: 의존성 주입 패턴(예: hooks에 client 주입)을 도입하여 런타임/테스트 시 구현을 교체하기 쉽게 설계.
5. 🧹 Shared 레이어 순수성
💡 개념 정의
Shared 레이어는 도메인 독립적인 유틸과 UI 컴포넌트를 제공하여 여러 도메인에서 재사용 가능한 코드 집합입니다. 핵심은 도메인 종속성을 제거하는 것입니다.
⚡ 중요성
Shared의 순수성이 보장되면 새로운 프로젝트나 패키지로 재사용할 때 적은 수정으로 적용 가능하며, 디자인 시스템 변경 시 비용을 낮춥니다.
📊 현재 상황 분석
AS-IS: 대부분 shared 컴포넌트는 도메인 독립적이지만 환경 설정(BASE_URL) 처럼 프로젝트별로 달라질 수 있는 값이 직접 하드코딩(또는 서로 다른 방식으로)되어 있어 재사용 시 환경별 설정이 번거로움.
📝 상세 피드백
shared 레이어에 UI 컴포넌트(Button, Dialog, Table 등)와 유틸(api wrapper, highlightText)이 잘 들어가 있습니다. 전반적으로 재사용 가능한 컴포넌트로 보이나 일부 shared 파일이 프로젝트 특정 타입(shared/model/types.ts는 간단함)이나 클래스명이 도메인에 결합된 스타일 변수(예: bg-card, text-card-foreground)를 사용하고 있어 디자인 시스템 변경 시 조정이 필요합니다. 다행히 shared/ui 컴포넌트는 props와 컴파운드 패턴(예: Card, Dialog, Select)을 잘 활용해 재사용성은 우수합니다.
❌ 현재 구조 (AS-IS)
// AS-IS: shared/lib/api.ts (환경 기반 BASE_URL)
const BASE_URL = import.meta.env.VITE_API_BASE_URL;
// src_final/shared/lib/api.ts
const BASE_URL = '/api';
// → 환경별 사용법이 혼재✅ 권장 구조 (TO-BE)
// TO-BE: 환경을 주입 가능한 설정으로 추상화
// shared/lib/config.ts
export const apiConfig = { baseUrl: () => import.meta.env.VITE_API_BASE_URL || '/api' };
// shared/lib/api.ts
const BASE_URL = apiConfig.baseUrl();🔄 변경 시나리오별 영향도
- 디자인 시스템이 Material-UI → Chakra UI로 변경되면: shared/ui 컴포넌트가 잘 추상화되어 있으면 교체는 shared 내부(약 6~12파일)에서 끝나지만, 도메인 코드가 직접 Chakra 컴포넌트에 의존하고 있다면 많은 파일 수정 필요.
- 다른 프로젝트에서 shared 라이브러리를 재사용할 때: 환경 변수 처리와 CSS 변수 사용 방식이 일관되어 있지 않으면 추가 작업 필요.
🚀 개선 단계
- 1단계: 단기: shared/lib/api.ts의 BASE_URL 추상화 및 하나의 방식으로 통일 (환경변수 제공자/설정 파일 사용).
- 2단계: 단기: 디자인 토큰(CSS 변수)과 컴포넌트 클래스 네이밍 가이드 정리(예: bg-card 같은 유틸 클래스의 의미 문서화).
- 3단계: 중기: shared 컴포넌트의 Props 명세(TypeScript 인터페이스) 강화 및 Storybook 같은 문서화 도구로 사용성 정리.
- 4단계: 장기: shared 패키지를 별도 npm 패키지/monorepo 패키지로 분리해 재사용성을 극대화.
6. 📐 추상화 레벨
💡 개념 정의
추상화 수준은 구현 세부사항을 숨기고 재사용 가능한 인터페이스(서비스, 훅)를 제공하는 정도를 뜻합니다. 충분한 추상화는 기술 변화(HTTP 클라이언트, 인증 방식) 시 영향 범위를 줄여줍니다.
⚡ 중요성
추상화가 적절하면 새로운 기능 추가(다국어, 실시간 등)와 기술 스택 교체 시 수정이 한정된 모듈에서만 발생합니다.
📊 현재 상황 분석
AS-IS: 기본적인 추상화는 있으나 서비스 레이어(예: postsService)나 queryKeys 같은 중앙 추상화가 완전히 자리잡지 않아 비슷한 데이터 조합 로직이 여러 훅에 중복될 위험이 있음.
📝 상세 피드백
API 호출을 감싸는 api wrapper와 useAutoMutation 같은 추상화 시도는 좋은 방향입니다. 하지만 일부 비즈니스 로직은 features 훅 내부에 분산되어 있고 공통 추상화 레이어(예: queryKeys, api clients)의 활용이 완전하지 않아 중복과 분기 처리가 보입니다. 더 높은 수준의 추상화(예: resource service 레이어, 공통 mutation 훅)를 적용하면 요구사항 변화에 대한 적응력이 향상됩니다.
❌ 현재 구조 (AS-IS)
// AS-IS: useGetPosts 내부에서 posts + users 조합
const postData = await postsApi.getPosts({ limit, skip });
const userData = await userApi.getUserPost();
const postsWithUsers = postData.posts.map(post => ({ ...post, author: userData.users.find(u => u.id === post.userId) }));✅ 권장 구조 (TO-BE)
// TO-BE: postsService에서 조합 책임을 가짐
// services/postsService.ts
export const postsService = {
fetchPostsWithAuthors: async ({limit, skip}) => {
const posts = await postsApi.getPosts({limit, skip});
const users = await userApi.getUserPost();
return posts.posts.map(p => ({ ...p, author: users.users.find(u => u.id === p.userId) }));
}
}
// hooks
useQuery(['posts', skip, limit], () => postsService.fetchPostsWithAuthors({skip, limit}))🔄 변경 시나리오별 영향도
- 만약 유저 조회 API가 pagination을 지원하도록 변경되면: 현재 useGetPosts 내에서 직접 호출하는 로직을 일괄 추상화해두면 posts와 users 결합 전략만 한 곳에서 수정하면 됨.
- 만약 인증 방식 변경(토큰 전달 방식 변경) 시: api wrapper만 수정하면 된다고 예상되어야 하나, 일부 direct fetch 호출이 존재하면 다른 파일들까지 수정 필요.
🚀 개선 단계
- 1단계: 단기: shared 레이어에 queryKeys와 services 폴더를 만들어 posts/users 결합 같은 로직을 서비스로 이동(예상 1~2일).
- 2단계: 단기: useAutoMutation의 사용을 확대하여 mutation 패턴 통일.
- 3단계: 중기: 서비스 레이어에 대한 타입 문서화와 예제 추가로 팀 합의된 추상화 규약 수립.
7. 🧪 테스트 용이성
💡 개념 정의
테스트 용이성은 코드가 단위 테스트/통합 테스트/동안 테스트를 얼마나 쉽게 작성할 수 있는지를 뜻합니다. 순수 함수 분리, side-effect 분리, 의존성 주입은 테스트 용이성을 높이는 주요 기법입니다.
⚡ 중요성
새로운 외부 API 연동이나 복잡한 비즈니스 로직 변경이 잦을 때 테스트가 있으면 리그레션을 방지하고 리팩토링 자신감을 높일 수 있습니다.
📊 현재 상황 분석
AS-IS: 테스트를 위한 구조적 분리는 존재하나 테스트 코드가 없고(또는 포함되어 있지 않음) 일부 전역 임포트 방식은 모킹을 약간 번거롭게 합니다.
📝 상세 피드백
UI와 비즈니스 로직을 훅과 컴포넌트로 분리한 구조는 테스트하기 좋은 편입니다(예: useGetPosts, useComments 같은 훅 존재). 그러나 현재 테스트 파일(.spec/.test)이 포함되어있지 않아 실제 테스트 커버리지는 낮습니다. 또한 일부 훅/컴포넌트가 전역 상태(store)를 직접 import 하여 사용하는 패턴은 모킹이 가능하지만, 의존성 주입을 더 활용하면 테스트 작성이 더 쉬워집니다.
❌ 현재 구조 (AS-IS)
✅ TO-BE 테스트하기 좋은 컴포넌트 분리 예
// UI 컴포넌트는 props로 data, isLoading를 받음
const UserProfile = ({ user, isLoading }) => { /* pure render */ }
// useUserProfile 훅은 useQuery를 래핑하여 테스트 가능한 단위로 존재✅ 권장 구조 (TO-BE)
// 테스트용 모킹이 쉬운 구조
// features/posts/hooks.ts
export const postsService = { fetchPostsWithAuthors: async () => {/*...*/} }
// useGetPosts -> postsService를 import(혹은 주입)하여 사용
// 테스트: postsService.fetchPostsWithAuthors를 jest.mock으로 모킹 가능🔄 변경 시나리오별 영향도
- 새 API 오류 응답 처리 정책 추가 시: 테스트가 있으면 오류 케이스를 자동화해 회귀를 방지할 수 있음.
- 상태관리 라이브러리 교체 시: create로 직접 만든 zustand 스토어는 모킹 가능하지만, DI(의존성 주입)로 바꾸면 테스트가 더 단순해짐.
🚀 개선 단계
- 1단계: 단기: 각 훅에 대해 최소 하나의 단위 테스트(React Query는 msw + testing-library 조합 권장)를 작성해 테스트 골격 마련(예상 1~2일).
- 2단계: 중기: zustand 스토어 사용 시 store 초기화/모킹 방법을 문서화하여 테스트 작성 표준화.
- 3단계: 장기: CI에 테스트 실행 추가 및 커버리지 기준 설정.
8. ⚛️ 현대적 React 패턴
💡 개념 정의
현대적 React 패턴은 Suspense, Error Boundary, 커스텀 훅, 컴파운드 패턴 등을 통해 관심사 분리와 선언적 UI 표현을 강화하는 방식을 의미합니다.
⚡ 중요성
선언적 에러/로딩 처리를 사용하면 UI 컴포넌트가 더 단순해지고 로딩 전략 변경 시 영향 범위가 줄어듭니다.
📊 현재 상황 분석
AS-IS: 커스텀 훅과 컴포넌트 패턴은 잘 되어있으나 선언적 로딩/에러 패턴(Suspense/ErrorBoundary) 미적용으로 인해 각 컴포넌트에서 로딩/에러 처리를 직접 하는 코드가 남아있을 가능성이 있습니다.
📝 상세 피드백
커스텀 훅과 React Query를 활용해 비즈니스 로직과 UI를 분리한 점은 훌륭합니다. Dialog 컴포넌트, Card 컴파운드 패턴 등 현대적 패턴이 잘 적용되어 있습니다. 다만 Suspense와 Error Boundary를 통한 선언적 로딩/에러 처리는 아직 적용되지 않았습니다. React 18의 Suspense + useQuery의 suspense 옵션 또는 ErrorBoundary 적용을 검토하면 로딩/에러 처리 변경 시 수정 범위를 더욱 줄일 수 있습니다.
❌ 현재 구조 (AS-IS)
// AS-IS: 각각의 컴포넌트에서 로딩 처리
if (isLoading) return <div>로딩 중...</div>
// TO-BE: Suspense + ErrorBoundary로 상위에서 처리
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Skeleton />}>
<PostTable />
</Suspense>
</ErrorBoundary>✅ 권장 구조 (TO-BE)
// useGetPosts에서 suspense 옵션 사용
useQuery({ queryKey: ['posts', skip, limit], queryFn: fetch, suspense: true })🔄 변경 시나리오별 영향도
- 만약 로딩 UX를 skeleton 기반으로 통일하고 싶다면 Suspense를 도입하면 PostTable 등에서 로직 변경이 간소화됩니다.
- 전역 에러 정책(예: 글로벌 에러 배너)을 도입하면 ErrorBoundary를 통해 효과적으로 적용 가능합니다.
🚀 개선 단계
- 1단계: 단기: 주요 리스트(PostsTable 등)에 Suspense 도입을 실험적으로 적용(하위 컴포넌트와 함께).
- 2단계: 단기: 전역 ErrorBoundary 컴포넌트 추가 및 몇몇 비핵심 경로에 적용해 동작 확인.
- 3단계: 중기: 팀 합의 후 점진적으로 suspense/error boundary 패턴을 확장.
9. 🔧 확장성
💡 개념 정의
확장성은 새로운 기능이나 요구사항 변화(다국어, 인증 방식 변경, 마이크로프론트)를 얼마나 적은 비용으로 수용할 수 있는지를 나타냅니다.
⚡ 중요성
프로덕트가 성장하거나 팀 규모가 커질 때 확장성이 낮으면 리팩토링 비용이 급증합니다. 아키텍처 초기부터 확장성을 염두에 두면 장기 유지보수 비용을 절감할 수 있습니다.
📊 현재 상황 분석
AS-IS: 초기 확장에는 무리가 없으나, 패키지 분리(entities를 별도 패키지로 추출)나 UI 라이브러리 교체 시 파일 수정 범위가 늘어날 위험이 있음.
📝 상세 피드백
폴더 구조 자체(FSD)와 API/service 계층 분리가 확장성에 유리합니다. shared 컴포넌트와 entities/api 추상화는 새로운 기능(다국어, A/B 테스트, 오프라인)을 수용하기에 좋은 기초입니다. 다만 현재 일부 규칙 회피(ESLint disable)와 불일치한 추상화 수준이 있어 확장 시 반복 작업이 발생할 수 있습니다. 특히 entities 내부의 역참조와 src/ vs src_final 중복은 패키지화/모듈 분리 시 큰 방해가 됩니다.
❌ 현재 구조 (AS-IS)
// AS-IS: 일부 UI 텍스트 하드코딩
<Card.Title>게시물 관리자</Card.Title>
// TO-BE: i18n key 사용
<Card.Title>{t('posts.managerTitle')}</Card.Title>✅ 권장 구조 (TO-BE)
// TO-BE: entities를 별도 패키지로 분리 가능하도록 public API 정리
// entities/comments/index.ts
export * from './api';
export * from './model/types';🔄 변경 시나리오별 영향도
- 다국어(i18n) 도입: shared/ui 컴포넌트가 텍스트 상수를 내부에 갖지 않고 props/locale key로 분리되어 있으면 미리 준비된 상태. 현재 일부 컴포넌트에서 텍스트가 하드코딩되어 있어 10~30개의 컴포넌트 수정 필요할 수 있음.
- 마이크로프론트 전환: entities와 shared를 잘 분리하여 패키지로 분리하면, 작업은 빌드/배포 설정 중심으로 수월하지만, entities→features 역참조가 있으면 모듈 분리가 복잡해짐.
🚀 개선 단계
- 1단계: 단기: src_final/ 중복 제거 및 코드베이스 단일화(우선순위 높음).
- 2단계: 단기: entities public API 정리(타입/CRUD만 노출) 및 역참조 제거.
- 3단계: 중기: i18n(react-i18next 등) 도입 시 텍스트 하드코딩을 찾아 키로 대체.
- 4단계: 장기: entities/shared를 분리 패키지로 이동하는 시뮬레이션(브랜치에서 모듈 분리 테스트) 진행.
10. 📏 코드 일관성
💡 개념 정의
코드 일관성은 네이밍, 파일 구조, import/exports 패턴, 스타일 규칙 등이 프로젝트 전반에서 얼마나 일관적인지를 뜻합니다.
⚡ 중요성
일관성은 신규 개발자 온보딩 속도, 코드 리뷰 효율, 자동화 도구(정적분석) 적용 용이성에 직접적인 영향을 줍니다.
📊 현재 상황 분석
AS-IS: 대부분 코드 스타일은 일정하지만 프로젝트에 중복/혼재된 파일 경로 및 import 패턴(상대경로/alias 혼용)이 있어 일관성 준수를 방해합니다.
📝 상세 피드백
파일 네이밍(PascalCase for components)과 훅 네이밍(useX)은 대부분 일관되게 잘 지켜지고 있습니다. Import alias(@) 사용으로 가독성도 좋아졌습니다. 하지만 불일치 항목도 관찰됩니다: src_final과 src의 중복 파일들, 일부 파일에서는 확장자(.tsx) 명시/미명시가 섞여 있고, queryKey 네이밍/쿼리 훅 명명 규칙 불일치가 존재합니다. 또한 eslint 설정에 많은 룰이 주석 처리되어 있어 팀 전체 일관성 적용이 일부 미완료 상태입니다.
❌ 현재 구조 (AS-IS)
consistencyIssues:
fileNaming: ["src/features/posts/ui/PostTable.tsx (PascalCase) vs some snake/kebab elsewhere? (mostly consistent but duplicates exist)"],
importExport: ["Mix of alias imports '@/...' and relative '../../../' imports; some default exports vs named exports mix"],
variableNaming: ["Generally camelCase used, but some types or constants may not follow UPPER_SNAKE_CASE"],
codeStyle: ["Project contains both single and double quotes in patches, trailing semicolons presence mixed due to formatting changes"]✅ 권장 구조 (TO-BE)
// TO-BE: 일관된 import 패턴과 barrel 사용
// 파일 상단 import 순서 규칙 적용
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@/shared/ui';
import { usePosts } from '@/features/posts';
import './PostsManager.css';🔄 변경 시나리오별 영향도
- 새로운 개발자가 합류하면: 현재 중복 구조와 혼재된 네이밍 때문에 초기 1~2일 더 소요될 가능성(파일 위치/의존성 파악 시간 증가).
- 정적 분석 도구(예: 코드베이스 자동 리팩토링)를 적용할 때: 규칙이 혼재되어 있어 false positive/negative 발생 가능.
🚀 개선 단계
- 1단계: 단기: src_final/ 중복 제거 및 프로젝트 루트에 실제 소스 위치 통일(0.5~1일).
- 2단계: 단기: eslint/prettier 설정 정리(금지된/비활성화된 규칙 재검토) 및 CI에서 lint 통과하지 않으면 PR 차단하도록 설정.
- 3단계: 중기: import 정렬/alias 사용 가이드(외부 라이브러리 → 내부 alias → 상대경로) 문서화 및 팀 합의.
- 4단계: 중기: Export 패턴(컴포넌트는 named export 권장 등)과 파일명 규칙(컴포넌트 PascalCase, 훅 useCamelCase 등) 문서화.
11. 🗃️ 상태 관리
💡 개념 정의
데이터 흐름 및 상태 관리는 전역/로컬 상태, 서버/클라이언트 상태를 어떻게 분리하고 관리하는지에 관한 설계입니다. TanStack Query는 서버 상태, zustand 등은 클라이언트 상태에 적합합니다.
⚡ 중요성
명확한 상태 분리는 리얼타임, 오프라인, 낙관적 업데이트 같은 비기능 요구사항을 확장하기 쉽도록 합니다.
📊 현재 상황 분석
AS-IS: 서버/클라이언트 상태 분리는 잘 되어 있으나, store의 소비 위치와 import 방식이 분산되어 있어 스토어 API 변경 시 광범위 영향 발생 가능.
📝 상세 피드백
클라이언트 상태는 zustand store (usePostFilterStore, useSearchStore, useSelectedPostStore, useCommentStore 등)를 통해 잘 분리되어 있고, 서버 상태는 TanStack Query로 관리하는 명확한 분리가 존재합니다. 이 분리는 Props Drilling 최소화와 testability 향상에 기여합니다. 다만 일부 컴포넌트가 서로의 store를 상대경로로 직접 참조하거나 eslint 규칙을 우회하는 등 접근 방식이 분산되어 있어 스토어 위치 변경 시 영향이 커질 수 있습니다.
❌ 현재 구조 (AS-IS)
// AS-IS: AddComment가 다른 feature store를 상대경로로 참조
// eslint-disable-next-line fsd/no-relative-imports
import { useSelectedPostStore } from '../../../features/posts/model/store';
// TO-BE: 공용 public API 통해 import
import { useSelectedPostStore } from '@/features/posts';✅ 권장 구조 (TO-BE)
// TO-BE: 모든 features의 public API를 통해 store 접근
// features/posts/index.ts
export * from './model';
// 사용처
import { useSelectedPostStore } from '@/features/posts';🔄 변경 시나리오별 영향도
- 오프라인 모드 도입: server-state와 queue된 mutation의 분리는 TanStack Query와 zustand의 조합으로 수월하지만, 스토어가 잘 캡슐화되어 있어야 함.
- 실시간 기능(웹소켓) 추가: 기존 queryKeys와 zustand를 이용한 캐시 업데이트 전략을 잘 설계하면 적은 변경으로 대응 가능.
🚀 개선 단계
- 1단계: 단기: 모든 store import를 feature의 public API(index.ts)로 통일하고 상대경로 import 제거(ESLint 규칙으로 강제).
- 2단계: 단기: store API 안정성(메서드 명세, 초기값 문서화) 확보하여 변경 비용 감소.
- 3단계: 중기: 서버-클라이언트 경계(무엇이 query vs zustand인지)에 대한 팀 합의 문서화.
🤔 질문과 답변
질문: “FSD 아키텍처가 진입장벽이 높고, 레이어 경계에서 어떤 요소를 둘지 헷갈립니다. FSD가 좋은 구조인지 코치의 관점을 듣고 싶습니다.”
답변(실무 관점): FSD는 중/대형 프로젝트에서 유지보수성과 협업을 개선하기 위한 유용한 가이드입니다. 장점은 책임 경계가 명확해져 변경 영향이 국한된다는 것(특히 엔티티/도메인 중심 설계), 단점은 초기 러닝커브와 파일/폴더 수 증가로 인한 초기 생산성 저하입니다. 실무 팁:
- 점진적 도입: 모든 규칙을 한 번에 적용하지 말고, 핵심 규칙(의존성 방향, public API)를 먼저 강제하세요. 나머지는 팀 합의로 점진 확장합니다.
- 명확한 기준표: ‘데이터 타입과 순수 CRUD는 entities, UI 행위·유저 상호작용은 features’ 같은 간단한 의사결정 표를 만들면 토론을 줄일 수 있습니다.
- 자동화와 가이드: fsd-eslint 설정을 CI에 적용하고, PR 템플릿에 ‘어떤 레이어에 파일을 둔 이유’를 간단히 적도록 하면 리뷰에서 일관성 유지가 쉬워집니다.
- 실무적 트레이드오프: 작은 프로젝트라면 FSD 규칙을 느슨하게 적용(폴더 단순화)하고, 프로젝트가 성장할 때 점차 엄격히 적용하세요.
추가로 고려할 질문들: - 팀 규모가 커진다면 어느 시점에 entities를 패키지로 분리할지 기준은 무엇인가요? (예: 팀 수, 라우팅 변경 횟수)
- 어떤 케이스에서 단순한 util이나 훅을 shared에 두고, 어떤 케이스에서 domain에 두는 것이 적절할까요? (재사용 예상도, 도메인 의존성 기준)
🎯 셀프 회고 & 제안
작성하신 회고에서 얻을 수 있는 인사이트가 명확하고 성찰적입니다. 특히 “FSD는 단순한 폴더 구조가 아니라 의존성과 책임을 다루는 것”이라는 인식과 Features/Entities 경계 고민은 매우 건강한 접근입니다. 몇 가지 더 생각해볼 질문을 남깁니다:
- Features와 Entities 경계 기준을 문서로 정리해 보셨나요? 만약 문서가 있다면 그 규칙을 실제 파일에 적용했을 때 모호한 사례 3가지를 골라보고 각각에 대해 팀 합의를 기록해보세요. 실제 사례를 기록하면 다음 프로젝트에서 토론 시간을 줄일 수 있습니다.
- fsd 규칙을 프로젝트에 엄격히 적용할 때 초기 생산성 저하를 어떻게 측정/완화할 수 있을까요? (예: 온보딩 체크리스트, 템플릿 PR, 예제 코드 제공)
- barrel export(공개 Public API)가 번거롭게 느껴졌다면, 어떤 부분이 불편했는지(자동 완성, import 경로, 파일 생성 시 반복작업 등)를 정리해보세요. 자동 생성 스크립트(예: plop) 도입으로 반복 작업을 줄일 수 있습니다.
계속 진행하면서 작은 실험(예: 한 도메인에 대해 완전한 FSD 적용 후 성능/개발 속도 측정)을 통해 팀의 최적 규칙 세트를 구성해보시길 권합니다.
추가 논의가 필요한 부분이 있다면 언제든 코멘트로 남겨주세요!
코드 리뷰를 통해 더 나은 아키텍처로 발전해 나가는 과정이 즐거웠습니다. 🚀
이 피드백이 도움이 되었다면 👍 를 눌러주세요!
과제 체크포인트
https://yangs1s.github.io/front_6th_chapter2-3/?limit=10&sortOrder=asc
기본과제
목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기
체크포인트
심화과제
목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기
체크포인트
최종과제
과제 셀프회고
이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.
아키텍처 ≠ 단순한 폴더 구조
처음에는 FSD를 단순히 "정해진 규칙에 따라 폴더를 나누는 방법"으로 생각했는데, 실제로 적용해보니 관심사의 분리와 의존성 관리라는 더 깊은 개념이었습니다.
의존성 방향의 중요성
상위 레이어가 하위 레이어에만 의존할 수 있다는 규칙과 Public API를 통해서만 외부 노출한다는 원칙이 코드 결합도를 낮추고 유지보수성을 높이는 핵심이라는 걸 깨달았다.
또한 UI 분리보다 비즈니스 로직 분리가 훨씬 어렵다는 점도 새로운 발견이었습니다. 컴포넌트를 나누는 것은 비교적 직관적이여서 나누는데 어려움은 없었는데, 어떤 로직이 entities에 들어가야 하고 어떤 것이 features에 들어가야 하는지 판단하는 것은 생각보다 복잡했습니다.
본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?
Features와 Entities의 경계를 명확히 하려고 가장 많이 고민했습니다.
특히 카트 기능을 구현할 때, 카트 데이터 자체는 entities에 두어야 하는지, 아니면 카트에 상품을 추가/삭제하는 비즈니스 로직이 features에 들어가야 하는지 계속 고민했습니다. 이 과정에서 비즈니스 로직이 뭔지 기준을 세우려고 노력했고, 엔티티는 데이터와 기본 조작, 피처는 사용자가 실제 수행하는 작업으로 구분하려고 했습니다.
또한 post-filter 같은 복합적인 기능에서 언제 Widget으로 분리하고 언제 Page에 둘지도 많이 고민했습니다. 여러 features를 조합하되 재사용성이 없는 경우의 판단 기준을 세우려고 애썼습니다.
페어 코딩으로 같이 의견을 나누고 기본적인 프로젝트 세팅을 같이 가져가봤습니다.
이 내용은 같이 의견을 나누면서 서로 본인들이 생각하기엔 좋은 폴더구조를 어떻게 가져갈까가 어떻게 나눌까 이런 생각들을 계속 공유하는거에 있어서 가장 핵심이라고 느껴졌습니다.
페어보단 쉐어에 가까웠지만, 그래도 fsd-lint을 세팅하고 프리티어를 같이 세팅하고, 어떤 폴더 구조식으로 가져갈지 한번 트라이 해봤습니다.
우선 둘다 처음이다 보니까 위 언급 내용인 언제 features인지 엔티티인지 계속해서 얘기를 나눴고, 지금의 프로젝트 구조를 약간 가지게 된 초석이 되었던거 같습니다. 후에는 서로 같이 할 타이밍이 안되서 조금씩 바꿔나갔지만, 이런 의견을 나누고 , 다른사람들과 소통을 하면서 제가 생각한 프로젝트의 구조를 가져가보려고 노력했습니다.
같이 작성한 eslint,
아쉬운점은 네이밍 컨벤션같은걸 좀 미리 정해놓고 해보면 더 좋은 시너지와 시간 절약이 가능할거라고 생각했습니다.
아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.
이런 점은 솔직히 너무 막연하게 느껴집니다.
2.작은 프로젝트여서 그런걸수도 있겠지만, 임포트 해오는 과정에서 같은 계층은 임포트를 못하다 보니 엄격함에서 오는 장벽이 있습니다.
3.barrel export을 통해서 public api를 정의하는데 이걸 이용하다 보니까 파일 수가 많아지고 번거로움이 많아졌던거 같아요
챕터 셀프회고
클린코드, 리팩토링, 아키텍처, 디자인패턴 이런 건 확실히 코드를 단순하게만 잘 분리하고, 나누는 건만이 아니라는 걸 좀 알게 되는 챕터였어요.
🔍 핵심 깨달음들
1. 클린코드와 리팩토링의 진짜 의미
단순히 코드만 깔끔하게 쓰는 게 아니라, 설계 원칙을 지키면서 관심사를 분리하는 게 진짜 클린코드라는 걸 깨달았다. 이전에는 "함수 이름 잘 짓고, 주석 잘 달면 클린코드"라고 생각했는데 완전히 다른 차원의 얘기였다.
2. 응집도, 결합도 고려의 중요성
단순히 분리한다고 되는 게 아니라 높은 응집도와 낮은 결합도를 유지할 수 있도록, 단일 설계 원칙을 따르면서 해야 한다는 것. 이전 과제에서 엔티티 기준 계층 분리를 해봤던 경험이 도움이 됐다.
3. 관심사별 분류의 효과
props drilling에 대한 과제에서 전역상태가 아니어도, 관심사별로 분류하면 오히려 더 직관적으로 눈에 잘 들어온다는 게 가장 재밌고 흥미로웠던 경험이었던 것 같습니다. 특히 준일 코치님 멘토링 시간에 같은 페어팀인 휘린의 질문에서도 말해주시고 실시간으로 코딩을 해서 보여주셨던 게 가장 크게 와닿았던 인사이트였습니다.
4. useContext는 전역상태관리가 아니다
이건 발제 때 처음 듣고 가장 놀랐습니다.
5. Props Drilling 제거의 효과
코드가 깔끔해지는 걸 체감하는 건 이전부터 알았지만, 관심사별로 그룹화하며 상태를 이용하면 코드 자체가 굉장히 깔끔해진다는 걸 알 수 있었습니다.
6. FSD 아키텍처
이번 과제를 통해서 FSD의 단점을 가장 크게 느꼈지만, 좋았던 건 도메인 로직과 공유 로직의 계층분리가 정확하다는 점. 배치 자체도 명확하니까 찾아가서 보기가 쉽다는 점, 맥락파악이 용이하다는 게 가장 큰 장점이라고 생각했고, 단점들은 위에 언급했듯이 진입장벽이 높고, import가 ### FSD 레이어 규칙에 의해 어렵다는 점.
전체적으로 총평은 기대보다는 정말 많은 걸 알게 되었다. 하지만 알게 된 거에 비해서는 깊이감 있게 다가오지는 못했던 것 같아요. 시간도 저는 좀 짧다고 느껴지기도 했어서 아쉬웠고, 그리고 다른 사람들은 어떤 리액트 프로젝트 구조를 가지고 일을 하고 경험해봤는지 서로의 생각과 내용을 공유할 수 있는 게 가장 좋았고, 가장 큰 인사이트라고 생각합니다.
💼 실무 경험과의 연결
내가 일했던 경험에서 이번 챕터는 많은 깨달음을 줬습니다.
저는 항상 클린코드나 리팩토링은 잘 분리하고 이름 잘 짓고, 단순한 업무의 반복이라고 생각을 했습니다.
이번 챕터를 겪어보면서 거대한 단일함수를 분리하고 나눌 때마다 항상 이상하게 느껴지고 분리하니까 더 불편해지고, 나눠도 나눈 게 또 너무 지저분하고 가독성도 떨어지고, 점점 유지보수하기 어렵게 되어버렸다는 점입니다.
실제 경험
코드가 없지만 예전에 엑셀처럼 셀이 수정이 되고 키보드로 왔다 갔다 하고 이런 복잡한 기능을 가진 데이터 테이블을 만든 적이 있는데, 이 당시 기능들을 전체적으로 한곳에 다 넣다 보니 테이블만 템플릿 코드까지 합쳐서 1000줄 가까이 가진 적이 있었습니다.
그때, 잠시 짬이 나기에 이 코드를 정리하자 해서, 단순하게 어떤 기능의 이름만 붙이고 나누는 식으로 하다 보니까 파일은 많아지고, 이름도 헷갈리고 분리도 관심사별로... 테이블 안에는 각각의 도메인들이 다른 데이터들이 있는데 이런 거 싹 무시하고 단순하게만 나누다 보니 완전히 엉키고 기능이 이상해지는 경우도 많아서 다시 되돌리고 다른 작업 진행하고 항상 이런 악순환의 반복이었습니다.
깨달음
이번 챕터를 통해서 "아, 내가 너무 단순하게 생각했던 것이 생각보다는 더 복잡하고 체계적인 내용과 패턴이 존재했었구나"라고 생각하게 되는 계기가 되었고, 그러면서 이런 부분에서 깨달음을 알게 되는 3주였던 것 같습니다.
생각을 하고 머리를 짜내는 건 정말 고통스럽고 어렵고 힘들었지만, 그 고통만큼 값졌던 순간도 많았다고 생각이 듭니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문