Skip to content

[1팀 주산들] Chapter 2-3. 관심사 분리와 폴더구조#26

Open
DEV4N4 wants to merge 33 commits intohanghae-plus:mainfrom
DEV4N4:main
Open

[1팀 주산들] Chapter 2-3. 관심사 분리와 폴더구조#26
DEV4N4 wants to merge 33 commits intohanghae-plus:mainfrom
DEV4N4:main

Conversation

@DEV4N4
Copy link

@DEV4N4 DEV4N4 commented Aug 11, 2025

배포 링크

https://dev4n4.github.io/front_6th_chapter2-3

과제 체크포인트

기본과제

목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기

  • 전역상태관리를 사용해서 상태를 분리하고 관리하는 방법에 대한 이해
  • Context API, Jotai, Zustand 등 상태관리 라이브러리 사용하기
  • FSD(Feature-Sliced Design)에 대한 이해
  • FSD를 통한 관심사의 분리에 대한 이해
  • 단일책임과 역할이란 무엇인가?
  • 관심사를 하나만 가지고 있는가?
  • 어디에 무엇을 넣어야 하는가?

체크포인트

  • 전역상태관리를 사용해서 상태를 분리하고 관리했나요?
  • Props Drilling을 최소화했나요?
  • shared 공통 컴포넌트를 분리했나요?
  • shared 공통 로직을 분리했나요?
  • entities를 중심으로 type을 정의하고 model을 분리했나요?
  • entities를 중심으로 ui를 분리했나요?
  • entities를 중심으로 api를 분리했나요?
  • feature를 중심으로 사용자행동(이벤트 처리)를 분리했나요?
  • feature를 중심으로 ui를 분리했나요?
  • feature를 중심으로 api를 분리했나요?
  • widget을 중심으로 데이터를 재사용가능한 형태로 분리했나요?

심화과제

목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기

  • TanstackQuery의 사용법에 대한 이해
  • TanstackQuery를 이용한 비동기 코드 작성에 대한 이해
  • 비동기 코드를 선언적인 함수형 프로그래밍으로 작성하는 방법에 대한 이해

체크포인트

  • 모든 API 호출이 TanStack Query의 useQuery와 useMutation으로 대체되었는가?
  • 쿼리 키가 적절히 설정되었는가?
  • fetch와 useState가 아닌 선언적인 함수형 프로그래밍이 적절히 적용되었는가?
  • 캐싱과 리프레시 전략이 올바르게 구현되었는가?
  • 낙관적인 업데이트가 적용되었는가?
  • 에러 핸들링이 적절히 구현되었는가?
  • 서버 상태와 클라이언트 상태가 명확히 분리되었는가?
  • 코드가 간결하고 유지보수가 용이한 구조로 작성되었는가?
  • TanStack Query의 Devtools가 정상적으로 작동하는가?

최종과제

  • 폴더구조와 나의 멘탈모데일이 일치하나요?
  • 다른 사람이 봐도 이해하기 쉬운 구조인가요?

과제 셀프회고

이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.

우선 FSD는 말만 많이 들어봤고 제대로 써본 경험은 없었다. 과거에 FSD 공부를 해보려고 FSD 공식 문서를 읽으면서 익숙해지려고 노력했었는데 문서의 내용이 와닿지 않아서 결국 아 이런게 있구나~ 정도로만 이해하고 지나갔던 기억이 난다...반성..
그때도 여전히 widgets, features, entities 가 무슨 차이인가 하고 헷갈려했던 것 같다.
이번에 이렇게 직접 해보니까 확실히 해보기 전에 비해 FSD 감각이 생겼다고 생각한닷.

이번 과제를 하면서 이전에 비해 새롭게 배웠다고 느낀 점은...(개념적인거 말구)

머릿속으로 생각만 하지말고 일단 코드로 표현해보기! 그러고 나서도 안되면 AI를 갈구던가 주위에 물어보던가 하기...

나는 행동하기 전에 생각을 많이 하는 편이라 실행이 느리다는 단점이 있다.
그래서 개발할때 턱괴고 스크롤만 내리면서 생각만 10분 20분 하고 그런다.
이번 과제는 더욱이 정답이 있다기 보다는 본인이 판단한 FSD를 설계하는 느낌이었어서 생각할 거리가 너무 많았다.
그래서 혼자 삽질도 엄청 하고 뭐 별거 다 찾아서 읽어보고 고민도 많이 하고 그러면서 시간을 많이 흘려보낸 것 같다...
힘든 밸런스게임이었다...ㅜㅜ
근데 하면서 깨달은건데 그렇게 고민이 될 때가 AI를 활용해야 하는 시점이 아닌가 하는 생각이 들었다.
AI한테 시뮬레이션을 돌려보라고 하거나.. 내 가설이 적절할지를 판단해 달라 하면 나도 생각이 더 빨리 정리될 것 같았다.
뭔가 나만의 AI 쓰는 타이밍을 알게된 것 같은 느낌인데 안쓰는 것 보다는 이렇게라도 활용하는 게 좋지 않을까..
어쨋든 손으로 적어봐야 생각이 구체화가 되고 그걸 기반으로 판단을 빨리 할 수 있게 되니까 그런 걸 잘 해보려고 노력해야겠다는 생각이 든다.

본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?

  1. 과잉생각 멈추가
image

멘토링 이후에 "과잉 생각 그만하고 일단 뭐라도 코드를 짜보자!" 라는 생각이 들어서 대체로 그렇게 하려 했던 것 같다.
나는 행동하기 전에 생각을 많이 하려는 편이고, 욕심도 많다.
그래서 자꾸 한 번도 안해본 것을 한 번에 잘 하고 싶어한다.
그래서 개발 속도가 느려지고 질문도 안(못)하게 되는 것 같다.
당장 다른 사람들 코드를 보고 자괴감이 들고 막 내가 엄청 못한거같고... 이러면 안될거같고 그런 생각이 들어도 그냥 할 수 있는 데 까지(내가 생각이 닿은 데 까지) 일단 만들어 보고 리뷰를 많이 받거나 다른 사람들의 코드를 많이 보는 편이 더 도움이 될 것 같다.

  1. FSD 밸런스게임
image image image image image

각 레이어의 성격을 이해하고 최대한 적확하게 배치를 하고 싶었다.
그래서 정말 애매하다 싶은거는 주변에 자주 물어보며 다녔다.
근데 물어봐서 더 헷갈리게 된 것 같기도 한데 오히려 내 호불호가 확실히 생긴 것 같아서 좋다는 생각도 들었다...
물어보고 다같이 토론하면서 밸런스게임 하는것도 재밌었고 FSD에 대한 이해도가 오르는 기분이었다.
다른 사람에게 내 의견을 설명해주는 것도 나의 성장에 도움이 되는구나 하고 느끼기도 했다.

image

FSD 논란을 종결해보고 싶었다... 그래서 코드를 쪼개서 넣으면서 이건 왜 여기 들어가야 하는가에 대한 이유를 머릿속에 꼭 정리하고 배치하려고 노력했다. 지금 누가 코드 아무데나 짚으면서 이건 왜 저기가 아니라 여기 넣었냐! 하면 이유 다 설명할 수 있다.

아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.

코드 구조를 변경하면서 받은 피드백중에 내가 FSD를 너무 Presentation vs Container 패턴 처럼 사용하고 있다는 피드백을 받았다.
FSD는 Presentation vs Container 패턴만을 위한 폴더 구조가 아니기 때문에 상태와 뷰를 그렇게까지 분리할 필요가 있느냐? 합치는게 낫지 않겠느냐 라고 하셨는데 그 부분이 아직 감이 안온다.
말로는 이해가 됐는데 뭔가 코드로 어떻게 저게 가능하지 하는 생각이 든다..
그래서 아직 내가 잘 모르는 것 같다.

이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.

전에 비해 FSD에 대한 이해도가 높아졌기 때문에 나중에 FSD 구조를 보게 된다면 뜯어보면서 생각해 볼 것 같다.
이전에는 그냥 FSD로 된 폴더 구조를 보면 뭐야 몰라 무서워 상태였는데 이제는 다행이도 질문이나 토론 정도는 가능하지 않을까 하는 자신감이 생겼다.

챕터 셀프회고

클린코드와 아키텍처 챕터 함께 하느라 고생 많으셨습니다!
지난 3주간의 여정을 돌이켜 볼 수 있도록 준비해보았습니다.
아래에 적힌 질문들은 추억(?)을 회상할 수 있도록 도와주려고 만든 질문이며, 꼭 질문에 대한 대답이 아니어도 좋으니 내가 느꼈던 인사이트들을 자유롭게 적어주세요.

클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기

  • 더티코드를 접했을 때 어떤 기분이었나요? ^^; 클린코드의 중요성, 읽기 좋은 코드란 무엇인지, 유지보수하기 쉬운 코드란 무엇인지에 대한 생각을 공유해주세요
  • 어떤 기분이었나요 : 솔직히 전 회사의 코드(급하게 개발하느라 정리되지 않은 코드들)가 생각이 났고 그래서 오 나는 이런걸 잘 고칠 수 있겠다 하는 자신감이 뿜뿜했었습니다.. 막상 해보니 생각보다 고단했지만 뭔가를 뚝딱뚝딱 만드는 ASMR 영상을 보는 기분이 들었고 깔끔해지는 것을 보니 기분이 좋았습니다... 저는 더티코드를 깔끔하게 바꾸는 것을 좋아하나봅니다..
  • 클린코드의 중요성 : 클린코드는 동료 개발자의 이해를 돕고 잠재적으로 버그를 줄일 수 있으므로 개발자는 개발을 할 때 이를 신경써야 할 의무가 있다고 생각합니다.
  • 읽기 좋은 코드 : 흐름이 명확하고 변수명이 깔끔히 잘 정리되어 있고 적당한 크기로 분리되어 있으며 서로 얽혀있지 않은 각자의 역할이 확실한 코드..
  • 유지보수하기 쉬운 코드 : 한번의 하나의 일만 하며 되도록 순수함수를 추구하는 코드. 역할이 명확히 분리된 코드?

결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리

  • 거대한 단일 컴포넌트를 봤을때의 느낌! 처음엔 막막했던 상태관리, 디자인 패턴이라는 말이 어렵게만 느껴졌던 시절, 순수함수로 분리하면서 "아하!"했던 순간, 컴포넌트가 독립적이 되어가는 과정에서의 깨달음을 들려주세요
  • 커스텀 훅을 제가 잘 안써봤어서 이때 제대로 공부를 하고 사용해 봤습니다. 사실 이미 쓰고 있었는데 개념으로써 정리가 안되었던 것이었습니다..
  • 비슷한 역할을 하는 코드끼리 모아서 사용하는 것의 편리성을 알았고, 전역 상태관리를 하면서 props drilling이 없어지고 컴포넌트가 독립적이 되어가는 것을 보며 즐거웠던 기억이 납니다.
  • 코드들을 보면서 처음에 이걸 어떻게 순수함수로 바꾸나... 하고 막막해서 고민을 좀 했었는데, 결국 대체로 받은 인자를 조건에 따라 일정한 객체 형태로 return 하고 그걸 hooks에서 가져다 쓰는 형태로 만들면서 이렇게만 해도 코드가 어느정도 패턴화 되면서 뭔가 깔끔해진 것 같다는 느낌이 들어서 좋았습니다.

응집도 높이기: 서버상태관리, 폴더 구조

  • "이 코드는 대체 어디에 둬야 하지?"라고 고민했던 시간, FSD를 적용해보면서의 느낌, 나만의 구조를 만들어가는 과정, TanStack Query로 서버 상태를 분리하면서 느낀 해방감(?)등을 공유해주세요
  • FSD는 역시 진입장벽이 좀 있는게 맞구나 하는 생각이 들었고 나만의 FSD... 에 대한 정리는 된 것 같은데 다른 사람들도 이에 공감해줄지는 사실 잘 모르겠습니다.
  • 멘토링때 테오님이 "그래서 FSD가 마이너한거다" 라고 하셨는데 무슨 말씀이신지 알 것 같습니다..
  • 그래도 FSD 구조를 사용하면서 코드를 배치할 때 본인만의 확고한 이유와 기준이 있으면 괜찮지 않을까 생각합니다.

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문

  • 실무를 하는 팀에서 FSD를 사용할 때, 뭔가 그 팀 만의 FSD 룰이 생기나요? 지금 과제를 하면서 팀원들과 이야기를 나눴을 때 다 각자가 생각하는 기준이 달랐거든요. 실무에서도 각자가 생각하는 기준이 다를 것 같은데 이런건 어떻게 통합되고 합의되나요?

DEV4N4 added 30 commits August 12, 2025 01:01
@@ -0,0 +1,7 @@
const API_BASE = import.meta.env.VITE_API_BASE_URL;

export async function api<T>(path: string, init?: RequestInit): Promise<T> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

공통으로 사용되는 쿼리스트링을 다루는 부분과, JSON.stringify 을 여기서 처리해도 좋을 것 같아요

Comment on lines +1 to +13
export const HighlightText = ({ text, highlight }: { text: string; highlight: string }) => {
if (!text) return null;
if (!highlight.trim()) {
return <span>{text}</span>;
}
const regex = new RegExp(`(${highlight})`, "gi");
const parts = text.split(regex);
return (
<span>
{parts.map((part, i) => (regex.test(part) ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span>))}
</span>
);
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컴포넌트로 만든게 더 직관적인 것 같네요

@JiHoon-0330
Copy link

낙관적 업데이트를 state 를 사용해 구현하게된 배경이 궁금합니다!

Comment on lines +15 to +102
export function usePosts({
limit,
skip,
selectedTag,
searchQuery,
sortBy,
sortOrder,
newPost,
selectedPost,
updateURL,
setNewPost,
setShowAddDialog,
setShowEditDialog,
}: {
limit: number;
skip: number;
selectedTag: string;
searchQuery: string;
newPost: Partial<Post>;
selectedPost: Post | null;
sortBy: string;
sortOrder: string;
updateURL: () => void;
setNewPost: (post: Partial<Post>) => void;
setShowAddDialog: (show: boolean) => void;
setShowEditDialog: (show: boolean) => void;
}) {
const pageQ = usePostsPageQuery({ skip, limit });
const searchQ = usePostsSearchQuery(searchQuery, !!searchQuery);
const tagQ = usePostsByTagQuery(selectedTag, !!selectedTag && selectedTag !== "all");
const usersQ = useUsersQuery();

const active = searchQuery ? searchQ : selectedTag && selectedTag !== "all" ? tagQ : pageQ;

const posts = useMemo(() => {
const raw = active.data?.posts ?? [];
const users = usersQ.data?.users ?? [];
if (!users.length) return raw;
return raw.map((p) => ({
...p,
author: users.find((u) => u.id === p.userId),
}));
}, [active.data, usersQ.data]);

const total = active.data?.total ?? 0;
const loading = active.isPending || usersQ.isPending;

const addMutation = useAddPostMutation();
const updateMutation = useUpdatePostMutation();
const deleteMutation = useDeletePostMutation();

const handleAddPost = async () => {
await addMutation.mutateAsync({ post: newPost });
setShowAddDialog(false);
setNewPost({ title: "", body: "", userId: 1 });
updateURL();
};

const handleUpdatePost = async () => {
if (!selectedPost) return;
await updateMutation.mutateAsync({ post: selectedPost });
setShowEditDialog(false);
updateURL();
};

const handleDeletePost = async (id: number) => {
await deleteMutation.mutateAsync({ id });
updateURL();
};

useEffect(() => {
updateURL();
}, [skip, limit, sortBy, sortOrder, selectedTag]);

const searchPosts = () => updateURL();
const fetchPostsByTag = () => updateURL();

return {
posts,
total,
loading,
searchPosts,
fetchPostsByTag,
handleAddPost,
handleUpdatePost,
handleDeletePost,
} as const;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개인적으로 해당 훅이 God 으로 느껴집니다.

Comment on lines +7 to +8
staleTime: 30_000,
gcTime: 5 * 60_000,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단위로 초(s)를 사용하는 것도 좋을 것 같아요

staleTime: 1_000 * 30,
gcTime: 1_000 * 60 * 5

Comment on lines +3 to +28
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>
);
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Select 를 별도로 추상화 한 것이 좋아 보입니다. 지금은 value 에 해당하는 타입들이 string 으로 되어있는데, 제네릭을 사용하면 더 안정적인 타입을 사용할 수 있을 것 같아요

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fsd 에서는 배럴 파일을 사용하다 보니 index 라는 파일은 export 만 하는 것이 일관성 측면에서 좋다고 생각됩니다

Comment on lines +7 to +9
export const getComments = async ({ postId }: GetCommentsParams): Promise<CommentsResponse> => {
return api<CommentsResponse>(`/comments/post/${postId}`);
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api 에 이미 generic 으로 응답 타입을 정의해주고 있어서 이렇게 해볼 수도 있을 것 같습니다.

Suggested change
export const getComments = async ({ postId }: GetCommentsParams): Promise<CommentsResponse> => {
return api<CommentsResponse>(`/comments/post/${postId}`);
};
export const getComments = async ({ postId }: GetCommentsParams) => {
return api<CommentsResponse>(`/comments/post/${postId}`);
};

Comment on lines +3 to +7
export async function api<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, init);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지훈님 말씀처럼 약간 이런식으로 api를 개선하면 api.get / api.post / api.put 같은 함수를 써볼 수 있을 것 같습니다. (그대로 쓰면 타입 오류날 수도 있어요)

Suggested change
export async function api<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, init);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
}
export const api = {
get: async <T>(path: string) => {
const res = await fetch(`${API_BASE}${path}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
},
post: async <T, Body extends object>(path: string, body: Body) => {
const res = await fetch(`${API_BASE}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
},
put: async <T, Body extends object>(path: string, body: Body) => {
const res = await fetch(`${API_BASE}${path}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
},
...
}

Comment on lines +7 to +17
export function useCommentsByPostQuery(postId: number, enabled = true) {
return useQuery<CommentsResponse>({
queryKey: commentsKeys.byPost(postId),
queryFn: () => getComments({ postId }),
enabled,
staleTime: 30_000,
gcTime: 5 * 60_000,
refetchOnWindowFocus: false,
retry: 1,
});
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보니까 쓰이는 곳이 없어요..(?!)


export function useEnsureCommentsByPost() {
const qc = useQueryClient();
return useCallback(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qc를 기준으로 useCallback 을 사용하신 이유가 궁금해요~!

export function useDeleteCommentMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id }: { id: number; postId: number }) => deleteComment({ id }),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어차피 id 만 쓰고 있는 것 같아요

Suggested change
mutationFn: ({ id }: { id: number; postId: number }) => deleteComment({ id }),
mutationFn: ({ id }: { id: number }) => deleteComment({ id }),

export function useLikeCommentMutation() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, likes }: { id: number; likes: number; postId: number }) => likeComment({ id, likes }),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 어차피 id 랑 likes 만 쓰이는 것 같아요

Suggested change
mutationFn: ({ id, likes }: { id: number; likes: number; postId: number }) => likeComment({ id, likes }),
mutationFn: ({ id, likes }: { id: number; likes: number }) => likeComment({ id, likes }),

const qc = useQueryClient();
return useMutation({
mutationFn: ({ post }: { post: Partial<Post> }) => addPost({ post }),
onSuccess: (created) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뭔가 이런 식으로 타이핑이 안되어서 밑에서 as 를 쓰셨나요?
addPost 가 Promise 를 잘 반환하고 있어서 왠지 Post 를 잘 내보내고 있어야할 것 같았어요.

Suggested change
onSuccess: (created) => {
onSuccess: (created: Post) => {

Comment on lines +40 to +42
qc.invalidateQueries({ queryKey: commentsKeys.byPost(postId) });
} else {
qc.invalidateQueries({ queryKey: commentsKeys.all });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.byPost 나 .all 나뉜거 나쁘지 않은 것 같아요 👍

Comment on lines +33 to +37
const ensureCommentsByPost = useEnsureCommentsByPost();
const addMutation = useAddCommentMutation();
const updateMutation = useUpdateCommentMutation();
const deleteMutation = useDeleteCommentMutation();
const likeMutation = useLikeCommentMutation();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 한번에 묶어버리면 더 가독성도 좋고 사용성도 좋을 것 같은데 어떠신지 👀

  const comment = useComment();
  const data = await comment.fetch();
  comment.add(...);
  comment.update(...);
  comment.delete(...);
  comment.like(...);

@CreatiCoding
Copy link

6주차 정말 수고 많으셨습니다~!

Copy link
Contributor

@JunilHwang JunilHwang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요, 1팀 주산들 님! 5주차 과제 잘 진행해주셨네요. 고생하셨습니다 👍🏻👍🏻
현재 남기는 코멘트는 GPT-5-mini model을 사용하여 AI가 남기는 피드백입니다 🤖
과제 피드백과는 별개이므로 참고해주세요!

1. 🏗️ FSD 아키텍처

💡 개념 정의

Feature-Sliced Design(FSD)은 기능 단위로 코드를 분리하여 app → pages → widgets → features → entities → shared 의 의존성 방향을 지키고, 각 slice는 index 파일을 통해 외부에 Public API만 노출하는 구조적 접근입니다.

⚡ 중요성

FSD를 올바르게 지키면 기능 추가/이동/분리 시 수정 범위를 줄이고 모듈을 별도 패키지로 떼내기 쉬워집니다. 실무에서 아키텍처 변경(모노레포, 마이크로프론트 등)이나 기술 교체 시 안전한 리팩토링을 도와줍니다.

📊 현재 상황 분석

AS-IS: 각 slice가 물리적으로 분리되어 있으나 slice 경계의 '공식 진입점'이 부족. 결과적으로 이동/독립화를 할 때 다수의 파일(대부분의 imports) 수정이 필요. 예: entities/comment 내부 함수를 외부에서 직접 참조하면 이 entities를 패키지로 분리할 때 import 경로를 일괄 수정해야 함.

📝 상세 피드백

FSD 레이어를 전반적으로 적용한 점이 명확합니다 (src/app, src/pages, src/widgets, src/features, src/entities, src/shared). 하지만 몇 가지 FSD 관점에서 개선을 권합니다: (1) slice의 Public API(인덱스)를 통한 노출 부재로 인해 깊은 상대경로(import '../../../entities/comment/models/queries')가 빈번히 나타나며, 이는 의존성 방향 제약과 모듈 추출시 수정 비용을 증가시킵니다. (2) shared와 entities/feature 경계는 잘 나뉘어 있으나 일부 UI/로직 책임이 features 내부와 widgets 내부에서 섞여 있습니다(예: useComments가 로컬 상태와 서버 상태를 병행 관리).

❌ 현재 구조 (AS-IS)

import { useCommentsByPostQuery } from '../../../entities/comment/models/queries'; // 다수 파일에서 내부 경로로 직접 참조

✅ 권장 구조 (TO-BE)

// entities/comment/index.ts (public API)
export * from './models/queries';

// 다른 모듈
import { useCommentsByPostQuery } from 'entities/comment'; // 슬라이스의 인덱스를 통해 접근

🔄 변경 시나리오별 영향도

  1. UI 라이브러리 교체(MUI → Chakra 등) 시: shared/ui의 중심화가 잘 되어 있으면 변경 범위는 shared/ui 안의 컴포넌트 파일 수 개(예: src/shared/ui/*)로 제한됩니다. 하지만 현재처럼 엔티티/feature에서 직접 Radix/스타일 세부 구현을 직접 사용하면 해당 파일들(약 20+ 파일)도 수정 필요.
  2. 모노레포 전환 후 entities를 별도 패키지로 분리하려면, deep imports 를 public index로 바꾸는 작업이 필요. 현 구조에서는 약 40~60개의 import 경로 수정이 예상됩니다.
  3. 새로운 feature 추가 시 기존 feature에 직접 의존하는 deep import가 있으면 변경 전파가 커짐.

🚀 개선 단계

  • 1단계: 단기(1-2일): 각 slice(src/entities/, src/features/, src/widgets/*)의 public index 파일(index.ts)를 만들고 외부에서 사용하는 API만 re-export 하게 정리. 코드베이스 검색으로 모든 deep-import를 public import로 대체 (자동 치환 스크립트 권장).
  • 2단계: 중기(1주): CI에 import 규칙(예: ESLint 규칙)을 추가해 slice 경계 위반 감지(예: 'no-restricted-imports'로 상하 레이어 위반 차단).
  • 3단계: 장기(2-4주): 각 slice의 책임을 문서화(README)하고 새로운 PR 템플릿에 '왜 이 파일이 이 slice에 속하는가' 체크리스트 추가.

2. 🔄 TanStack Query

💡 개념 정의

TanStack Query는 서버 상태를 선언적으로 관리하는 라이브러리로, queryKey 패턴, cacheTime/staleTime, 쿼리 무효화/optimistic updates 등을 통해 서버 데이터의 일관성 및 성능을 관리합니다.

⚡ 중요성

쿼리 키 체계와 옵션이 잘 설계되어 있으면 API endpoint 변경이나 신규 데이터 소스 통합 시 수정 범위를 국한시키고 캐시 전략으로 UX 성능을 개선할 수 있습니다. 반대로 옵션/키 오류는 캐시 정책을 무력화하고 디버깅 비용을 늘립니다.

📊 현재 상황 분석

AS-IS: 잘 구성된 query key 패턴과 일부 최적화(placeholderData, onMutate optimistic update)가 있으나 설정 실수(cacheTime → gcTime)로 캐시 동작이 의도대로 작동하지 않을 가능성이 큽니다. 또한 local state + react-query 혼합으로 인해, 서버가 변경되었을 때 로컬 상태 동기화가 누락될 위험이 있습니다.

📝 상세 피드백

TanStack Query 패턴을 전반적으로 잘 활용하고 있습니다: queryKeys 설계(postsKeys, commentsKeys 등), QueryClient 중앙화(src/shared/lib/react-query.ts), useQuery/useMutation 활용 등. 다만 설정 키 이름 오류와 서버/클라이언트 상태 중복 관리가 발견됩니다. 특히 'gcTime' 옵션 사용은 TanStack Query에서 지원하는 설정명이 아닙니다(올바른 옵션은 cacheTime). 이로 인해 의도한 캐싱 동작이 무시되어 성능/네트워크 호출에 영향이 있을 수 있습니다. 또한 features/useComments는 react-query 데이터와 로컬 useState를 병행하여 단일 출처(single source of truth)가 훼손되어 있습니다.

❌ 현재 구조 (AS-IS)

useQuery({ queryKey: postsKeys.page(params), queryFn: () => getPosts(params), staleTime: 30_000, gcTime: 5 * 60_000 }) // 'gcTime'은 잘못된 옵션명

✅ 권장 구조 (TO-BE)

useQuery({ queryKey: postsKeys.page(params), queryFn: () => getPosts(params), staleTime: 30_000, cacheTime: 5 * 60_000 })
// useComments 훅은 로컬 상태 대신 queryClient/ensureQueryData만 사용해 단일 출처로 통합

🔄 변경 시나리오별 영향도

  1. API 엔드포인트 변경: api client (src/shared/api/client.ts)에 추상화가 있으므로 엔드포인트가 바뀌어도 대부분 entities/*의 api 함수만 수정하면 됩니다. 그러나 queryKey가 잘못 설계되어 있으면 변화가 여러 feature로 전파됩니다.
  2. 새로운 데이터 소스(예: WebSocket 실시간) 추가: 현재는 react-query를 기본으로 사용하므로 subscription 데이터를 queryClient.setQueryData로 주입하면 적응이 비교적 쉬움.
  3. 캐싱 정책 수정(예: staleTime 증가): gcTime 오류가 있다면 의도한 cacheTime 적용이 안되므로 모든 쿼리 파일을 점검해야 합니다.

🚀 개선 단계

  • 1단계: 긴급 수정(1일): 모든 쿼리 파일에서 'gcTime' → 'cacheTime'로 교체하고 타입(혹은 linter)을 통해 잘못된 옵션을 잡을 수 있도록 설정.
  • 2단계: 단기(2-3일): useComments 같은 훅을 refactor하여 로컬 useState를 제거하고 react-query의 데이터를 직접 사용하거나, local UI 상태는 전적으로 atoms로 분리(대화상자/폼 등).
  • 3단계: 중기(1주): 쿼리 키 정책 문서화(파일: src/shared/query-keys/README.md) 및 query key factory 사용 강제(예: 게시물 페이징 키 규칙 정의).
  • 4단계: 테스트(1주): 주요 쿼리의 cache/stale 동작을 단위 테스트(react-query testing utilities)로 검증.

3. 🎯 응집도 (Cohesion)

💡 개념 정의

응집도(Cohesion)는 관련 책임들이 한 모듈(폴더/파일)에 얼마나 함께 모여 있는지를 나타냅니다. 높은 응집도는 변경이 발생했을 때 수정 범위가 집중되는 장점이 있습니다.

⚡ 중요성

높은 응집도는 유지보수 비용 감소, 온보딩 시간 단축, 모듈 단위로 분리/배포(패키지화)하기 쉬운 구조를 제공합니다.

📊 현재 상황 분석

AS-IS: 도메인별 응집은 우수하나, '서버 상태 vs 로컬 UI 상태' 경계가 일부 모호합니다. usePosts/useComments 훅이 이 둘을 동시에 관리하면 변경 시 여러 레이어를 건드려야 합니다.

📝 상세 피드백

응집도 측면에서는 entities 단위(entities/post, entities/comment 등)가 비교적 높은 응집도를 보입니다: 타입, API, 쿼리, UI가 같은 도메인 내부에 위치합니다. 또한 widgets로 공통 UI(예: PostFiltersBar, UserModal)가 분리되어 재사용성이 높아 보입니다. 다만 일부 기능은 응집도의 이점을 덜 활용하고 있습니다: 예를 들어 features/comment/models/useComment.tsx 가 UI 관련 로컬 상태(newComment, comments)와 서버 상태 상호작용을 함께 관리해 변경 시 수정 파일이 분산되는 경향이 있습니다.

❌ 현재 구조 (AS-IS)

features/comment/models/useComment.tsx: useState<CommentsState>  로컬 캐시 관리 + react-query mutate 호출(중복 책임)

✅ 권장 구조 (TO-BE)

댓글은 react-query의 query를 1 소스로 삼고, UI 관련 편집 상태( 입력) features의 local state 또는 atom으로 관리. ex)
const { data } = useCommentsByPostQuery(postId);
// UI: selectedComment, draftBody 등은 atom/useState로만 관리

🔄 변경 시나리오별 영향도

  1. 댓글 기능 확장(예: 답글(threaded) 추가): 현재 local comments 구조(Record<PostId, Comment[]>)는 확장 시 복잡한 변환/마이그레이션이 필요. 통합된 react-query 기반 구조로 바꾸면 영향 파일 수를 줄일 수 있음.
  2. 다국어화 추가: UI 텍스트가 컴포넌트 내부에 산재하면 각 컴포넌트 수정 필요. 이를 위해 shared i18n 레이어로 추상화 필요.

🚀 개선 단계

  • 1단계: 단기(1-2일): useComments 훅에서 로컬 comments 상태를 제거하고 ensureQueryData/getQueryData로 react-query 데이터만 사용하도록 리팩토링.
  • 2단계: 단기(1-2일): 댓글 관련 UI 상태(newComment, selectedComment 등)는 feature-level atoms로 옮겨 책임 분리.
  • 3단계: 중기(1주): 도메인별 응집도 체크리스트 작성(예: '서로 함께 변경되는 파일은 동일 폴더에 위치') 및 PR 템플릿에 응집도 평가 항목 추가.

4. 🔗 결합도 (Coupling)

💡 개념 정의

결합도(Coupling)는 모듈 간 의존성의 강도입니다. 낮은 결합도는 한 모듈의 변경이 다른 모듈에 미치는 영향을 줄여 유연성을 높입니다.

⚡ 중요성

낮은 결합도는 기술 스택 변경(예: axios→fetch), 아키텍처 변화(모노레포 분리)에서 코드 수정 범위를 최소화합니다.

📊 현재 상황 분석

AS-IS: entities ← features 의 의존성 방향은 지켜지고 있으나 '구체적 구현'에 대한 의존(예: fetch)과 deep import로 인한 경로 의존이 남아 있음.

📝 상세 피드백

결합도는 전반적으로 낮추려는 노력이 보입니다(entities ← features 방향 유지). 그러나 구체적인 구현에 의존하는 부분(예: 직접 api client 사용, deep imports, mutation 내부에서 UI 콜백 호출)이 남아 있습니다. 특히 API 클라이언트가 직접 fetch 기반으로 구현되어 있어( src/shared/api/client.ts ) HTTP 클라이언트를 교체할 때 전역 수정이 필요합니다. 또한 Jotai atoms가 프로젝트 전역에서 직접 사용되는데, atom 이름/위치 관리를 잘못하면 전역 결합도가 높아질 수 있습니다.

❌ 현재 구조 (AS-IS)

src/shared/api/client.ts
export async function api<T>(path:string, init?:RequestInit){ const res = await fetch(`${API_BASE}${path}`, init); ... }
// 엔티티에서 직접 사용: import { api } from '../../../shared/api/client';

✅ 권장 구조 (TO-BE)

src/shared/api/client.ts exposes a thin adapter + DI pattern
export type HttpClient = { request<T>(path:string, init?:RequestInit): Promise<T> }
export const httpClient: HttpClient = createFetchClient(API_BASE)
// entities receive httpClient via import 'shared/api' but could be injected in higher-level initialization for swap

🔄 변경 시나리오별 영향도

  1. HTTP 클라이언트 변경(axios → fetch): 현재 api wrapper를 교체하면 대부분 엔티티의 api 호출은 중앙 api 함수만 수정하면 되지만, 만약 개별 엔티티에서 axios를 직접 사용했다면 수십 파일을 수정해야 함.
  2. 상태관리 라이브러리 변경(jotai → zustand): atoms가 전역적으로 사용 중이라면 atom→store 변환 비용이 커짐. atoms 노출을 feature index 뒤로 숨기면 변경 범위를 줄일 수 있음.

🚀 개선 단계

  • 1단계: 단기(1-2일): api(client) 레이어를 인터페이스(HttpClient)로 추상화하여 fetch/axios 교체가 쉬운 구조로 변경.
  • 2단계: 단기(1-2일): deep import를 public index로 리팩터링(앞서 FSD 개선과 병행).
  • 3단계: 중기(1주): atoms 네이밍/소유권 규칙 정의(예: feature 내부 atoms는 feature에서만 export), 전역 노출은 최소화.

5. 🧹 Shared 레이어 순수성

💡 개념 정의

shared 레이어 순수성은 shared 코드가 특정 도메인(entities, features)에 의존하지 않고 범용적으로 재사용 가능한지를 의미합니다.

⚡ 중요성

도메인 의존성이 없는 shared는 다른 프로젝트나 마이크로프론트로 옮길 때 재사용성이 높아지며, 디자인 시스템 변경 시 영향 범위를 줄입니다.

📊 현재 상황 분석

AS-IS: 대부분의 shared 컴포넌트는 범용적이지만, UI 텍스트/스타일에 대한 추상화(i18n, theme token)가 부족해 디자인 시스템 변경 시 다수 컴포넌트 수정 필요 가능.

📝 상세 피드백

shared 레이어는 UI 컴포넌트(src/shared/ui/*), query-keys, api client 등으로 잘 구성되어 있어 재사용 가능성이 높습니다. 공통 UI 컴포넌트(HighlightText, Pagination, DropdownSelect, Dialog primitives 등)는 범용적으로 설계되어 있습니다. 다만 shared/ui/index.tsx에는 앱 전반에서 사용하는 유틸 및 구현 세부(한국어 버튼 라벨 등)가 일부 포함되어 있어 완전한 도메인 독립성 관점에서 소폭 개선 여지가 있습니다.

❌ 현재 구조 (AS-IS)

src/shared/ui/DialogContent.tsx 내부 닫기 버튼: <span className="sr-only">닫기</span> // 텍스트가 한국어로 하드코딩됨

✅ 권장 구조 (TO-BE)

Dialog 사용  i18n hook 또는 prop으로 레이블 주입:
<DialogPrimitive.Close aria-label={t('close')}>
  <X />
</DialogPrimitive.Close>
// 스타일 토큰은 theme 클래스/Context로 주입

🔄 변경 시나리오별 영향도

  1. 디자인 시스템 변경(Material → Tailwind + 커스텀 테마): shared/ui 내부 스타일을 토큰화하면 변경 파일 수를 shared/ui 내부로 국한 가능(현재는 일부 컴포넌트에서 직접 Tailwind 클래스 사용).
  2. 새 프로젝트에서 재사용: 현재 구조로는 재사용 가능하지만 i18n/테마 의존성 제거시 더 범용적으로 활용 가능.

🚀 개선 단계

  • 1단계: 단기(반나절): shared/ui 컴포넌트에서 텍스트 레이블과 theme 값을 prop(i18n 및 theme token)으로 받도록 변경.
  • 2단계: 단기(1일): shared의 핵심 컴포넌트(버튼, 입력, dialog 등)에 Storybook/샘플을 만들어 재사용성 검증.
  • 3단계: 중기(1주): 디자인 토큰(색상, spacing)을 중앙화하여 shared 컴포넌트가 토큰만 참조하도록 리팩터링.

6. 📐 추상화 레벨

💡 개념 정의

추상화 수준은 기술적 세부사항(HTTP, 라우터, 스토리지 등)을 숨기고 비즈니스 개념(게시물 가져오기, 댓글 추가 등)만 노출하는 정도를 말합니다.

⚡ 중요성

적절한 추상화는 기술 교체와 아키텍처 변경 시 영향을 국한시키며, 재사용 가능한 인터페이스를 제공합니다.

📊 현재 상황 분석

AS-IS: 엔티티 수준의 API 추상화는 적절하지만 http client 추상화 부족 및 일부 훅에서 UI 로직과 비즈니스 로직 혼재로 인해 추상화 수준이 완전하진 않습니다.

📝 상세 피드백

비즈니스 로직과 기술적 세부사항을 분리하려는 시도가 보입니다: entities/*에 순수한 API 함수, features/*에 UI/사용자 행동 로직을 배치한 점은 적절합니다. 하지만 몇몇 훅(useComments, usePosts)에서 UI 제어(모달 표시, URL 업데이트)와 비즈니스 로직(API 호출/캐시 조작)가 섞여 있어 추상화 수준을 높일 여지가 있습니다. 또한 api wrapper는 fetch 기반으로 단일 구현이므로 HttpClient 추상화가 있으면 기술 스택 변경에 더 유연해집니다.

❌ 현재 구조 (AS-IS)

src/shared/api/client.ts: 직접 fetch 사용  import 하는 엔티티들이 fetch 구현에 간접적으로 의존

✅ 권장 구조 (TO-BE)

interface HttpClient { request<T>(path:string, init?:RequestInit): Promise<T> }
// createFetchClient, createAxiosClient 구현체 제공
// 앱 초기화에서 적절한 인스턴스를 주입

🔄 변경 시나리오별 영향도

  1. HTTP 클라이언트 변경(axios 도입 등): 현재는 api wrapper만 교체하면 되지만, 더 안전하게 HttpClient 인터페이스로 추상화하면 테스트 및 교체가 쉬워짐.
  2. 라우터 전략 변경(예: hash → history): usePostQuery 같은 훅이 라우터 구현에 종속적이면 훅을 수정해야 함. 라우터 의존을 한 곳(indexed adapter)으로 모으면 영향 줄일 수 있음.

🚀 개선 단계

  • 1단계: 단기(1-2일): api wrapper를 HttpClient 인터페이스로 리팩토링하고, createFetchClient를 구현해 기존 동작 유지.
  • 2단계: 단기(1-2일): usePosts/useComments 훅에서 URL/모달 제어 같은 UI 관리를 hook의 반환값(handlers)으로 분리하여 추상화 수준을 높임.
  • 3단계: 중기(1주): 주요 도메인(게시물/댓글) 인터페이스 문서화 및 예제 코드 추가.

7. 🧪 테스트 용이성

💡 개념 정의

테스트 용이성은 단위/통합 테스트를 작성하고 유지하기 쉬운 구조인지의 여부로, 순수 함수 분리와 의존성 주입(DI)이 핵심입니다.

⚡ 중요성

외부 API 변경, 회귀 방지, 리팩토링 안전성을 확보하려면 테스트 가능성이 높아야 합니다.

📊 현재 상황 분석

AS-IS: 컴포넌트들은 대체로 분리되어 있고, react-query를 사용함으로써 서버 상태 테스트가 수월함. 그러나 전역 환경 참조 및 로컬 상태/쿼리 혼합 훅은 테스트 설정을 복잡하게 만듭니다.

📝 상세 피드백

테스트 용이성을 고려한 분리가 일부 적용되어 있습니다: Pure UI 컴포넌트(entities/*/ui, shared/ui)와 API/쿼리 레이어의 분리는 좋은 신호입니다. 그러나 useComments 훅은 내부 상태와 side-effect(fetch, mutate)를 섞어 단위 테스트 작성이 번거로울 수 있습니다. 또한 shared/api/client.ts가 전역 환경(import.meta.env)을 직접 참조하므로 테스트 시 환경 주입이 필요합니다.

❌ 현재 구조 (AS-IS)

features/comment/models/useComment.tsx는 내부 useState  mutateAsync 호출을 병행하여 단위 테스트  모킹/상태 초기화가 복잡

✅ 권장 구조 (TO-BE)

useComments 훅은 react-query의 query 결과만 사용하고, 로컬 UI 상태는  외부(컴포넌트 혹은 atom) 분리. API 의존성은 HttpClient 인터페이스로 주입. 이렇게 하면 훅은  얇고 테스트하기 쉬움.

🔄 변경 시나리오별 영향도

  1. 새 외부 API 연동: entities api 함수가 명확하면 mock이 쉬움. 반대로 fetch가 직접 호출되면 fetch mocking 설정이 반복적으로 필요.
  2. E2E 테스트: Dialog/Modal 등의 UI는 atoms를 통해 토글되므로 E2E에서 작동 검증이 쉬움.

🚀 개선 단계

  • 1단계: 단기(1-2일): shared/api/client.ts 에서 환경 의존을 제거하고 테스트용 mock client를 주입할 수 있게 변경.
  • 2단계: 단기(1-3일): useComments를 쪼개어 서버 상호작용(react-query)과 UI 상태(선택된 댓글, draft) 분리.
  • 3단계: 중기(1주): react-query + MSW(Mock Service Worker) 기반 통합 테스트 케이스 추가.

8. ⚛️ 현대적 React 패턴

💡 개념 정의

현대적 React 패턴은 Suspense, ErrorBoundary, custom hooks, hook-based 상태 추상화 등을 포함하며, 관심사 분리를 통해 컴포넌트를 단순하게 유지하는 것을 목표로 합니다.

⚡ 중요성

패턴을 사용하면 로딩/에러 처리를 일관적으로 하고, 컴포넌트 재사용성을 높이며 변경 시 영향 범위를 줄일 수 있습니다.

📊 현재 상황 분석

AS-IS: hook 기반 관심사 분리는 잘 되어 있으나, Suspense 전환 시 최소한의 작업(쿼리에서 suspense:true)으로 로딩 처리 패턴을 전환 가능. ErrorBoundary 도입으로 전체 앱의 에러 복구 전략을 향상시킬 수 있음.

📝 상세 피드백

현대적 패턴(커스텀 훅, QueryProvider, ReactQueryDevtools, forwardRef 등)을 적절히 사용하고 있습니다. 하지만 Suspense나 ErrorBoundary를 활용한 선언적 로딩/에러 처리는 아직 도입되지 않았습니다. 또한 일부 컴포넌트는 로직과 UI가 분리되어 있지만 더 깔끔하게 custom hooks로 뽑아낼 수 있습니다.

❌ 현재 구조 (AS-IS)

현재 개별 컴포넌트(PostsManagerPage)에서 loading 상태를 직접 렌더링함.

✅ 권장 구조 (TO-BE)

<ErrorBoundary fallback={<ErrorFallback/>}><Suspense fallback={<Skeleton/>}><PostsManager/></Suspense></ErrorBoundary>
// useQuery({ suspense: true })로 전환

🔄 변경 시나리오별 영향도

  1. 로딩 UX 전략 변경(Suspense 기반): 대부분의 useQuery에서 suspense 옵션을 활성화하면, 상위 레벨에서 Suspense로 일괄 처리 가능 — 컴포넌트 레벨의 로딩 분기 코드가 줄어듦.
  2. 에러 처리 통합: ErrorBoundary 도입 시 개별 컴포넌트의 에러 처리 코드가 간소화됨.

🚀 개선 단계

  • 1단계: 단기(1-2일): 에러 바운더리와 Suspense 데모를 한 페이지에 시범 적용하여 마이그레이션 비용 파악.
  • 2단계: 단기(1-3일): useQuery에서 suspense 옵션을 적용할 수 있는 기준(네트워크 중요도 등) 문서화.
  • 3단계: 중기(1주): 팀 컨벤션으로 Suspense/ErrorBoundary 사용 가이드 마련.

9. 🔧 확장성

💡 개념 정의

확장성은 새로운 요구사항(기능/비기능)이 추가될 때 기존 코드 변경을 최소화하면서 새로운 기능을 통합할 수 있는 능력입니다.

⚡ 중요성

실무에서는 종종 새로운 외부 API, 실시간 기능, 다국어, A/B 실험 등이 추가되므로 확장성이 낮으면 작업량과 위험도가 급증합니다.

📊 현재 상황 분석

AS-IS: 새로운 서버 리소스(예: 댓글 페이징 확장, 실시간 동기화)를 도입할 때 엔티티 레이어의 API 추가로 대부분 해결 가능. 그러나 UI 라이브러리 교체 시 shared/ui 내부 수정량에 따라 영향을 받을 수 있음.

📝 상세 피드백

구조적으로 feature/entities/shared로 관심사가 잘 분리되어 있어 기능 추가(예: 다국어, A/B 테스트, 새로운 결제 수단 등) 대응성이 좋습니다. 특히 TanStack Query와 query-keys 체계는 새로운 데이터 엔드포인트를 추가할 때 확장성을 높입니다. 다만 shared/api/client와 훅 내부의 로컬 상태 중복은 확장 시 유지보수 비용을 증가시킬 수 있습니다. Jotai atoms를 UI 토글에 적절히 사용한 점은 기능 확장(모달 추가 등)에 유리합니다만, atoms의 네임스페이싱/소유권이 불분명하면 전역 상태 스파게티가 될 수 있습니다.

❌ 현재 구조 (AS-IS)

새로운 API 'posts/featured' 추가하려면 src/entities/post/models/api.ts에 함수 추가 + queries + UI 연결. 영향은 주로 posts 관련 파일에 국한됨.

✅ 권장 구조 (TO-BE)

추가 엔드포인트는 entities/post/models/api.ts에 함수 추가하고 postsKeys에  추가, features에서 해당 query를 사용해 바로 확장 가능. shared/api는 동일하게 유지.

🔄 변경 시나리오별 영향도

  1. 다국어 지원 추가: UI 텍스트를 모두 i18n으로 추출하면 PostFiltersBar, DialogTitle 등 다수 파일 수정 필요. 미리 i18n hook을 연결해두면 변경 범위가 shared layer로 국한됨.
  2. A/B 테스트 도입: feature 레벨에서 분기하도록 훅을 작성하면 확장 시 리스크를 낮출 수 있음.

🚀 개선 단계

  • 1단계: 단기(1-2일): atoms 및 전역 상태의 ownership 규칙 정의(어떤 atoms는 feature 내부에서만 사용되는지 명시).
  • 2단계: 단기(1-3일): HttpClient 추상화 및 query-keys 문서화로 새로운 엔드포인트 추가 절차 표준화.
  • 3단계: 중기(2주): 실시간/오프라인 시나리오를 고려한 상태 전략(react-query + indexedDB 또는 socket integration 가이드) 마련.

10. 📏 코드 일관성

💡 개념 정의

코드 일관성은 네이밍 규칙, 파일명 규칙, import/export 스타일, 코드 스타일(들여쓰기, 따옴표 등)의 통일을 의미합니다.

⚡ 중요성

일관성은 팀 규모가 커질수록 중요하며, 자동화 도구(ESLint, Prettier, Codemods)를 통해 온보딩과 병합 충돌을 줄여줍니다.

📊 현재 상황 분석

AS-IS: 전반적 일관성은 양호. 주요 수정 포인트는 import 경로 표준화(절대/alias 도입), export 패턴 통일(컴포넌트는 named export 권장), 잘못된 옵션명('gcTime') 교정.

📝 상세 피드백

전반적으로 네이밍(컴포넌트 PascalCase, 훅 use* 패턴 등)과 폴더 구조 일관성이 잘 지켜지고 있습니다. shared/ui의 컴포넌트 네이밍도 일관적입니다. 다만 몇 가지 일관성 이슈가 발견됩니다: (1) 파일명/경로 표기(예: shared/query-keys/post.ts 파일 내부 주석은 posts.ts로 표기), (2) import/export 패턴은 대부분 named export이나 일부 파일은 default export 혼용 가능성 존재(검색 필요), (3) 'gcTime' 오타는 옵션명 컨벤션 위반으로 기능적 문제를 일으킵니다.

❌ 현재 구조 (AS-IS)

혼재된 import 스타일: import { useTagsQuery } from "./queries" vs import { commentsKeys } from "../../../shared/query-keys/comments" // 상대경로가 깊음

✅ 권장 구조 (TO-BE)

tsconfig paths 설정  절대경로 사용: import { commentsKeys } from 'shared/query-keys/comments'
// 모든 컴포넌트는 named export로 통일: export const PostsManager = () => {}

🔄 변경 시나리오별 영향도

  1. 새로운 개발자 합류: 일관된 네이밍과 규칙이 없으면 학습 비용 증가. ESLint/Prettier + 규칙 문서화가 필요.
  2. 코드 자동화 도구 적용: import 정렬, no-deep-imports 룰 적용으로 일관성 유지 가능.

🚀 개선 단계

  • 1단계: 단기(반나절): ESLint + Prettier 설정 적용 및 no-restricted-imports 규칙으로 slice 경계 위반 감지.
  • 2단계: 단기(1-2일): tsconfig paths로 절대 import(예: 'shared/', 'entities/') 설정 후 코드베이스 일괄 치환.
  • 3단계: 중기(1주): 코드 컨벤션 문서(파일명/훅/컴포넌트/타입 규칙) 작성 및 PR 체크리스트에 포함.

🎯 일관성 체크포인트

파일명 규칙

  • 일관적으로 PascalCase를 사용하고 있으나 일부 주석/파일 내부 표기가 혼재됨 (예: shared/query-keys/post.ts 내부 주석이 posts.ts라고 표기).

Import/Export 패턴

  • deep relative import('../../../entities/...') 가 많은데, public index를 통한 import로 표준화 필요.
  • 대부분 named export 사용중이지만 일부 파일에서 default export 사용 여부가 혼재되어 있을 가능성 있음(검사 필요).

변수명 규칙

  • 대체로 camelCase 규칙 준수. 다만 일부 초기 코드(수정 전)에서는 snake_case 등이 보였으므로 코드베이스 내 잔존 여부 점검 필요.

코드 스타일

  • 코드 스타일은 대부분 일관되지만 React Query 옵션에서 'gcTime' 오타는 기능적 문제를 야기하므로 컨벤션을 넘어 규칙적 타입 검사가 필요.

11. 🗃️ 상태 관리

💡 개념 정의

상태관리는 전역/로컬/서버 상태를 적절히 구분하고 각 상태의 소유권을 명확히 하는 것입니다. 서버 상태는 react-query, 클라이언트 상태는 feature-level atoms 또는 local state를 사용합니다.

⚡ 중요성

상태의 위치가 명확하면 디버깅이 쉬워지고 기능 추가 시 의도치 않은 상태 불일치 버그를 줄일 수 있습니다.

📊 현재 상황 분석

AS-IS: 서버 상태는 양호하게 구성. 클라이언트 상태는 atoms와 local state가 섞여 있어 소유권 규칙을 정의하면 더 예측 가능하고 테스트 가능한 구조가 됨.

📝 상세 피드백

서버 상태는 TanStack Query로 잘 분리되어 있으며 query-keys와 중앙 QueryClient가 있어서 일관적입니다. 클라이언트 UI 상태는 Jotai atoms(모달 토글 등)과 컴포넌트 로컬 state가 병행되어 사용되고 있습니다. 전반적으로 server vs client 상태 분리 의도는 옳지만, 일부 훅(useComments)에서 서버 상태(react-query)와 로컬 캐시(useState)를 중복 관리하고 있어 단일 출처 원칙이 위배될 수 있습니다.

❌ 현재 구조 (AS-IS)

features/comment/models/useComment.tsx: const [comments, setComments] = useState<CommentsState>({}); // react-query의 데이터와 중첩

✅ 권장 구조 (TO-BE)

서버 데이터는 react-query가 단일 소스
const { data } = useCommentsByPostQuery(postId);
// UI 상태(폼 입력 등)는 atom/useState로 관리. mutate 후 react-query invalidate 혹은 setQueryData로 반영

🔄 변경 시나리오별 영향도

  1. 오프라인 모드 요구: react-query + indexedDB(예: react-query-persist-client)로 서버 캐시를 보존하면 비교적 적은 변경으로 오프라인 대응 가능. 현재 cacheTime 오류가 해결되어야 함.
  2. 실시간 동기화: socket을 통해 변경 데이터가 들어올 때 queryClient.setQueryData로 업데이트 하면 기존 구조에서 확장 가능.

🚀 개선 단계

  • 1단계: 단기(1-2일): useComments 내부 로컬 comments 제거, react-query 데이터 사용으로 전환.
  • 2단계: 단기(1-2일): atoms 사용 규칙 정의(예: atoms는 'UI state'만 보유, 서버 데이터는 react-query만 사용).
  • 3단계: 중기(1주): react-query의 cache/persist 전략 문서화 및 cacheTime 옵션을 재설계.

🤔 질문과 답변

Q: 실무에서 팀만의 FSD 룰이 생기나요? 그리고 팀 간 기준 차이는 어떻게 통합하나요?
A: 네, 실무에서는 팀별 FSD 규칙(폴더 배치, public API 노출 방식, import 규칙 등)이 거의 항상 생깁니다. 이유는 프로젝트의 도메인, 팀 규모, 배포 전략(모노레포 vs 단일 레포), 기존 레거시와의 통합 등 각 프로젝트의 맥락이 다르기 때문입니다. 통합/합의 방법(실무 권장절차):

  • 초안 문서화: 팀 내부에서 FSD 적용에 대한 초안을 만들고 예시(파일/폴더/import 패턴)를 포함합니다.
  • 작은 실험: 하나의 feature(예: posts)를 새 규칙으로 리팩토링해 비용/이득을 평가합니다.
  • 린(Lean) 규칙으로 시작: 핵심 규칙(의존성 방향, public index, 파일 네이밍)만 우선 강제하고 점차 확장합니다.
  • 도구화: ESLint(no-restricted-imports), tsconfig paths, Codemod 스크립트로 규칙을 자동화합니다.
  • 리뷰 가이드: PR 템플릿에 'FSD 체크리스트'를 넣고 코드 리뷰 시 일관성 유지.
  • 운영 거버넌스: 1~2주 단위로 규칙을 업데이트하고 변경 이력을 기록합니다. 이렇게 하면 개인의 의견 차이를 실무적 트레이드오프로 수렴할 수 있습니다.
    실무 팁: 모든 사람이 '완벽한 FSD'를 따르려 하기보다는, 팀이 공통으로 지키는 최소 규칙(base-line)를 만들고, 그 위에서 개인/기능별 예외를 문서화하는 것이 합의 도출에 현실적입니다.

🎯 셀프 회고 & 제안

작성하신 셀프회고에서의 인사이트가 매우 인상적입니다. 특히 '손으로 해보고, 안되면 AI나 주변에 물어보라'는 접근과 '과잉생각을 멈추고 구현해보는 실험 정신'은 실무에서 빠르게 성장하는 데 핵심입니다. FSD 밸런스 게임(어디에 무엇을 둘지 고민)도 아주 건강한 과정입니다. 추가로 생각해볼 질문 몇 가지를 제안합니다:

  1. 당신이 내린 '이 파일은 왜 이 레이어에 속하는가'라는 근거들을 문서(README 또는 CODEOWNERS)로 정리한다면, 팀 합의 시 어떤 항목이 가장 설득력이 있을까요? (예: 변경 빈도, 재사용 가능성, 외부 의존성)
  2. 'Presentation vs Container' 패턴과 FSD의 교집합을 실제 코드 예시로 만들어보면 감이 빠르게 옵니다. 지금 구조에서 한 컴포넌트를 합치거나 분리했을 때 수정되는 파일 수를 계수해보시겠어요? (정량적 실험을 권합니다)
  3. AI를 사용할 때 어떤 가설을 검증하고 싶은지(예: '이 컴포넌트는 widgets에 있어야 한다'라는 가설)에 대해 구체적 질문을 만들면 AI가 더 효과적으로 도와줄 수 있습니다.

추가 논의가 필요한 부분이 있다면 언제든 코멘트로 남겨주세요!

코드 리뷰를 통해 더 나은 아키텍처로 발전해 나가는 과정이 즐거웠습니다. 🚀

이 피드백이 도움이 되었다면 👍 를 눌러주세요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants