Skip to content

[9팀 임두현] Chapter 2-3. 관심사 분리와 폴더구조#30

Open
ldhldh07 wants to merge 77 commits intohanghae-plus:mainfrom
ldhldh07:main
Open

[9팀 임두현] Chapter 2-3. 관심사 분리와 폴더구조#30
ldhldh07 wants to merge 77 commits intohanghae-plus:mainfrom
ldhldh07:main

Conversation

@ldhldh07
Copy link

@ldhldh07 ldhldh07 commented Aug 12, 2025

과제 체크포인트

배포링크

https://ldhldh07.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를 도구로써 개발의 편의성을 늘리는 현상을 직접 채험한다

위 내용들을 달성하고자 하는 마음으로 과제를 수행했습니다.

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

FSD 레이어과 프론트엔드의 기능의 선후관계가 명확해졌습니다.
FSD가 임의로 레이어를 나누고 그에 끼워맞춰서 각 레이어들을 명명한게 아니었습니다.

실제로 현대 프론트엔드 개발에서 코드를 분리하고 추상화 단계를 지정할 때 사용되는 개념들을 옮겨놓은 것이 레이어었습니다.

FSD에서 어떤 코드를 어떤 계층에 놓을까 고민하는 과정에서 이를 느꼈습니다.
단순히 엔티티-코드 피쳐스-코드 1대1의 관계속에서 고민하면 답이 안나왔습니다.
반면, 전체 레이어의 큰 그림속에서 상대적인 관계를 생각하면서 코드를 짰을 때 자연스럽게 각 레이어의 역할이 이해됐습니다.

폴더 구조의 요구사항을 수행하기 위해 코드를 작성하다보면 자연스럽게 순수 함수를 만들고 추상화 단계를 맞추고 있었습니다.

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

레이어 구분하기

혼란스러웠던 점은 세그먼트는 다른데 레이어는 동일하게 적용해야한다는 점이었습니다.
기존 스스로의 개념 속에서 apiapi나름의 단계, uiui나름의 단계 modelmodel나름의 단계가 있었습니다.
하지만 FSD에선 해당 개념들은 같은 틀안에서 구분지어야 했습니다.

이를 포괄할만한 관념적인 경계를 만드는 것이 와닿지 않았습니다.

초반 시도

처음에는 FSD에 대해 가장 많이 설명되는 내용인 비즈니스 로직의 포함 여부로 생각했습니다.

  • Entity - 비즈니스 로직이 포함되지 않는 모델, 이 모델을 사용하는 api와 ui 로직
  • Features - Entity에 해당하는 모델에 비즈니스 로직을 적용한 사용자의 행위가 포함되는 모델과 그 사용처

이 개념으로 접근했을 때 혼란스러운 부분은 api였습니다.

// 엔티티
export const getPosts = async ({ limit, skip }: PostsParams): Promise<PostsResponse> => {
  const response = await fetch(`/api/posts?limit=${limit}&skip=${skip}`)
  if (!response.ok) throw new Error()
  return response.json()
};

// 피쳐

export const createPost (...) => {...};
export const deletePost (...) => {...};

고민 과정

자연스럽게 같은 추상화 단계의 모델을 다른 레이어에 두는 것에 대한 문제가 생겼습니다.
만약 피쳐스에 정의한 생성/수정/삭제 모델을 참조 혹은 결합해서 비즈니스 로직을 만든다면 어느 단계에 만들어야 하나 싶었습니다.

결론적으로 저 개념 자체는 크게 변하지 않았지만, 실제 구현에 있어서 추상화 단계에 좀 더 무게를 뒀습니다.

  • 엔티티 API는 도메인 리소스 I/O로 한정
  • 비즈니스 규칙·상호작용은 피처에 배치

또한 useCase의 맥락을 엔티티/피쳐 구분에 사용했습니다.

기존의 개념 접목

다른 많은 패턴들의 개념을 FSD에 적용해서 생소함을 최소화시키고자 했습니다.

  • Presemtational, Container
    UI를 엔티티-피쳐로 구분할 때 해당 패턴과 같은 로직을 적용했습니다.
    엔티티의 UI는 순수 상태만 가지고 단순히 Prop받은 정보를 출력하는 역할만 하는 UI입니다.
    피쳐스의 UI는 엔티티 UI에 핸들러와 상태를 주입해주는 역할만 수행합니다.
import { ChangeEventHandler } from "react";

import { Button } from "@shared/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@shared/ui/dialog";
import { Textarea } from "@shared/ui/textarea";

export interface CommentAddDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  body: string;
  onChange: ChangeEventHandler<HTMLTextAreaElement> | undefined;
  onSubmit: () => Promise<void> | void;
}

export function CommentAddDialog({ open, onOpenChange, body, onChange, onSubmit }: Readonly<CommentAddDialogProps>) {
  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle> 댓글 추가</DialogTitle>
        </DialogHeader>
        <div className="space-y-4">
          <Textarea placeholder="댓글 내용" value={body} onChange={onChange} />
          <Button onClick={onSubmit}>댓글 추가</Button>
        </div>
      </DialogContent>
    </Dialog>
  );
}
import { CommentAddDialog } from "@/entities/comment";

import { useCommentEditor } from "../model/edit-comment.hook";

export function CommentAddDialogContainer() {
  const { newComment, setNewComment, isAddOpen, setIsAddOpen, addComment } = useCommentEditor();

  const handleSubmit = async () => {
    if (newComment.postId == null) return;
    await addComment({ body: newComment.body, postId: newComment.postId, userId: newComment.userId });
  };

  return (
    <CommentAddDialog
      open={isAddOpen}
      onOpenChange={setIsAddOpen}
      body={newComment.body}
      onChange={(e) => setNewComment((prev) => ({ ...prev, body: e.currentTarget.value }))}
      onSubmit={handleSubmit}
    />
  );
}
  • Atomic Design

엔티티/피쳐스가 순수 UI와 비즈니스 로직 컨테이너의 조합으로서 한 덩어리라면 Widget의 UI는 그 덩어리들의 조합이라 설계했습니다.
그리고 그 개념은 아토믹 디자인 패턴의 Organism과 같이 인식했습니다.

이처럼 기존에 익숙한 개념들을 통해 FSD를 일단 유연하게 사용해보았습니다.
사용하는 과정에서 이해도가 높아지고 활용법이 단계별로 발전하는 것을 기대하고 있습니다.

최종 구조

front_6th_chapter2-3/
  - src/
    - app/
      - ui/
    - entities/
      - comment/
        - api/
        - index.ts
        - model/
        - ui/
      - post/
        - api/
        - model/
        - ui/
      - user/
        - api/
        - model/
        - ui/
    - features/
      - comment-edit/
        - model/
        - ui/
      - post-edit/
        - model/
        - ui/
      - post-filter/
        - index.ts
        - model/
        - ui/
      - post-load/
        - api/
        - model/
        - ui/
      - post-pagination/
        - ui/
      - user-load/
        - index.ts
        - model/
        - ui/
    - pages/
      - posts-manager-page.tsx
    - shared/
      - api/
      - lib/
      - ui/
    - widgets/
      - footer/
        - ui/
      - header/
        - ui/
      - posts-manager/
        - ui/

Tanstack Query

기본과제를 통해 tanstack-query를 사용하지 않고 기존의 상태를 전역 관리로 바꾼 뒤 tanstack-query를 도입하는 단계로 진행했습니다.

이 방식을 통해 생각보다 느낀 점이 많았습니다.
기존에 익숙한 툴이었음에도, 정확히 어떤 좋은점이 있고 다른형식의 어떤 기능을 대체하는지 구체적으로 파악했습니다.

기존에는 선언된 전역 상태가 있었습니다. 그리고 이를 명령형으로 조작을 해야합니다.
이를 tanstack-query의 상태와 서버 통신 관리를 결합한 선언적 형태로 바꾸자 많은 보일러 플레이트 코드들이 사라졌습니다.

이는 commit diff로 확인이 가능했습니다.

// 전역 상태 선언
export const commentsAtom = atom<CommentsByPostId>({});
export const isCommentsLoadingAtom = atom<boolean>(false);

// 복잡한 상태 조작 훅
export const useComments = () => {
  const [comments, setComments] = useAtom(commentsAtom);
  const [isLoading, setIsLoading] = useAtom(isCommentsLoadingAtom);

  const setCommentsForPost = (postId: number, comments: Comment[]) => {
    setComments((prev) => ({ ...prev, [postId]: comments }));
  };

  const appendComment = (comment: Comment) => {
    setComments((prev) => ({
      ...prev,
      [comment.postId]: [...(prev[comment.postId] ?? []), comment],
    }));
  };

  const changeComment = (comment: Comment) => {
    setComments((prev) => ({
      ...prev,
      [comment.postId]: (prev[comment.postId] ?? []).map((c) => (c.id === comment.id ? comment : c)),
    }));
  };

  const removeComment = (commentId: number, postId: number) => {
    setComments((prev) => ({
      ...prev,
      [postId]: (prev[postId] ?? []).filter((c) => c.id !== commentId),
    }));
  };

  return {
    comments, isLoading, setComments, setCommentsForPost, 
    appendComment, changeComment, removeComment, // 모든 조작 함수들...
  };
};
// 컴포넌트에서 수동 데이터 패칭 + 상태 동기화
const handleOpenDetail = useCallback(
  async (post: Post) => {
    setSelectedPost(post);
    if (comments[post.id]) {
      setIsDetailOpen(true);
      return;
    }
    // 수동으로 API 호출하고 상태에 저장
    const { comments: selectedPostComments } = await commentApi.get(post.id);
    setComments((prev) => ({ ...prev, [post.id]: selectedPostComments }));
    setIsDetailOpen(true);
  },
  [comments, setComments, setIsDetailOpen, setSelectedPost],
);
// 선언적 쿼리 - 캐싱, 로딩, 에러 처리 자동화
export function useCommentsQuery(postId?: number) {
  return useQuery({
    enabled: !!postId,
    queryKey: commentQueryKeys.byPost(postId),
    queryFn: () => commentApi.get(postId as number).then((res) => res.comments),
  });
}

jotai를 통해 모든 상태를 리팩토링 한 이후에 한번에 tanstack-query를 적용했기에 완연하게 느낄 수 있는 좋은 부분이었습니다.

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

멘토링을 통해 접했던 네임스페이스를 통해 가독성과 명시성을 높이려는 시도를 해봤습니다.

export const postApi = {
  get({ limit, skip, sortBy, order }: PostsParams): Promise<PostsResponse> {
    return http.get<PostsResponse>("/posts", { params: { limit, skip, sortBy, order } });
  },
  create(payload: CreatePostParams): Promise<Post> {
    return http.post<Post>("/posts/add", payload);
  },
  update({ postId, params }: UpdatePostPayload): Promise<Post> {
    return http.put<Post>(`/posts/${postId}`, params);
  },
  remove(id: number): Promise<void> {
    return http.delete<void>(`/posts/${id}`);
  },
} as const;

이 경우 네임스페이스 충돌에 대한 우려도 없을 뿐더러 변수명을 직관적으로 이해할 수 있습니다.
이 활용을 앞으로도 적극적으로 하고자 합니다.

챕터 셀프회고

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

유지보수라는 새로운 접근

앞서 언급된 클린코드의 서술에서 읽기 좋은 것이 클린코드라는 것은 당연하고 익숙한 개념입니다.
가독성이 좋은 것이 왜 좋냐도 직관적인 영역입니다.

하지만 이번 주제를 겪으면서 유지보수하기 좋은 코드에 대한 이해도가 높아졌습니다.
순수 함수로 분리하고 함수를 독립적으로 만들 경우, 수정/삭제가 쉽고 추적이 쉽다는 것을 체득할 수 있었습니다.

개인적으로 코드를 유지보수하는 단계에 있어본 적이 없기 때문에 더 흥미롭게 느껴졌습니다.

왜 읽기 좋고 유지보수하기 좋으면 좋은가

한 단계 더 나아가서 왜 가독성이 좋고 유지 보수가 쉬우면 좋으냐를 개념화하고 싶었습니다.

클린코드를 짜기 위해 소모되는 수고/시간보다 아낄 수 있는 수고/시간이 더 크다.

개발자 경험의 관점에서 클린코드로 인해 얻을 수 있는 수고/시간의 개념을 따져봤습니다.

  • 잃는것 : 작성하는 데 쓰는 시간, 클린코드의 규칙을 결정하는데 드는 커뮤니케이션 비용 등등
  • 얻는것 : 팀원 그리고 스스로의 줄어든 코드 해석 시간

행동은 하면할수록 익숙해지고 편해집니다.

클린코드를 작성함으로서 잃는 것은 시간이 갈수록 줄어듭니다.
이번 과제에서도 같은 양의 코드를 리팩토링하더라도 후반부에 작업한 것은 전반부보다 빨리 작업했습니다.

그 때문에 처음에는 잃는 것이 더 클지 몰라도 잃는 것에 속하는 코드 작성 시간이 우상향함에 따라 얻는 것이 늘어날 수 밖에 없습니다.

그리고 쓰는 사람보다 읽는 사람들이 더 많습니다.
10사람이 2시간씩 아낀다면 20시간이 더 생깁니다.

이 관점에서 클린코드가 개인 그리고 단체에게 큰 효용을 제공합니다.

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

// features/post-load/model/post-load.hook.ts
import { usePosts } from "@/entities/post";
import { getPostsWithAuthors } from "../api/post-load.api";
import { usePostFilter } from "@/features/post-filter"; // 다른 피처에 직접 의존

export const useLoadPost = () => {
  const { setPosts, setTotal, setIsLoading } = usePosts();
  const { skip, limit, selectedTag, sortBy, sortOrder, searchQuery } = usePostFilter();

  const getPosts = async () => {
    setIsLoading(true);
    try {
      const { posts, total } = await getPostsWithAuthors({ limit, skip, selectedTag, sortBy, sortOrder, searchQuery });
      setPosts(posts);
      setTotal(total);
    } finally {
      setIsLoading(false);
    }
  };

  return { getPosts } as const;
};

사고를 많이 하지 않고 훅을 만들었습니다.
Post를 가져오는 훅으로서 url 파라미터와 연동하는 필터 데이터들을 입력해야 했습니다.
하지만 이는 같은 단계인 features를 참조했습니다.

// features/post-load/model/post-load.hook.ts
import { usePosts } from "@/entities/post";
import { getPostsWithAuthors } from "../api/post-load.api";

export const useLoadPost = () => {
  const { setPosts, setTotal, setIsLoading } = usePosts();

  const getPosts = async (params: {
    limit: number;
    skip: number;
    selectedTag?: string;
    sortBy?: string;
    sortOrder?: string;
    searchQuery?: string;
  }) => {
    setIsLoading(true);
    try {
      const { posts, total } = await getPostsWithAuthors(params);
      setPosts(posts);
      setTotal(total);
    } finally {
      setIsLoading(false);
    }
  };

  return { getPosts } as const;
};
// features/post-browse/ui/post-browse-container.tsx
import { useEffect } from "react";
import { usePostFilter } from "@/features/post-filter";
import { useLoadPost } from "@/features/post-load/model/post-load.hook";

export function PostBrowseContainer() {
  const { skip, limit, selectedTag, sortBy, sortOrder, searchQuery } = usePostFilter();
  const { getPosts } = useLoadPost({skip, limit});

  ...
  return null;
}

이를 해결하기 위해 원시타입을 인자로 받는 훅을 만들었습니다.
그리고 그 상단에서 인자를 입력했습니다.

FSD의 룰을 지키기 위한 시도가 자연스럽게 각 영역을 독립적으로 만들고 자동으로 추상화 계층을 통일시키고 있었습니다.
이 경험을 하면서 적절한 분리에 대한 개념이해를 발전시켰습니다.

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

이번 과제는 특히 레퍼런스를 많이 봤습니다.
FSD를 구현한 프로젝트들은 가지각색의 방식들이 있었습니다.

공통점은 응집도를 높이려는 시도가 있었습니다. 세부적인 구현방식은 생각보다도 많이 달랐습니다.

그 방식들을 보면서 개인적으로 맘에 드는 방식들을 쏙쏙 뽑았습니다.
그중 파일 이름을 도메인 혹은 도메인-행위로 통합하고 확장자로 역할을 구분하는 형태의 방식이 FSD와 어울린다 생각해서 적용하기도 했습니다.

image

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

FSD의 경우 리팩토링을 상향식으로 하는 것이 좋을 지 하향식으로 하는 것이 좋을까요
다른 세그먼트들이 하나의 레이어에 묶여있습니다.

개인적으로 상태/모델은 작은것부터 분리해서 조립하여 올라가는 방식이 편하고
UI는 큰 덩어리부터 작은 덩어리로 쪼개나가는 것이 편합니다.

이번 과제를 통해 같은 레이어 단위 속에서 공존하여 이 때 어던식으로 판단하시는지 여쭤보고자 합니다.

Copy link

@susmisc14 susmisc14 left a comment

Choose a reason for hiding this comment

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

안녕하세요! AI 코드 리뷰어입니다. 🤖
FSD + TanStack Query를 적용한 이번 PR에 대한 리뷰를 완료했습니다. 함께 코드를 더 발전시켜 볼까요?

Note

✨ 이번 PR 한 줄 요약
FSD 구조와 TanStack Query를 잘 도입해 응집도를 높이고 낙관적 업데이트를 구현했으나, ESLint/타입/프로바이더 적용 누락, 일부 최적화(쿼리 취소·롤백)와 설정 정리, 폴더/네임스페이스 일관성 보완이 필요합니다.


🎯 핵심 리뷰 요약

가장 중요한 개선 포인트부터 빠르게 확인해 보세요.

  • P1: QueryProvider를 App에 적용하지 않음 — React Query가 전역으로 적용되지 않습니다.
  • P1: ESLint 구성(typescript eslint 관련)과 의존성 불일치로 lint 실행/CI에서 실패할 가능성 있음.
  • P1: onMutate 구현에서 쿼리 취소(cancel)와 롤백 컨텍스트 관리가 일부 누락되어 경쟁 상태(race) 가능성.
  • P2: path alias / shared/ui 컴포넌트들이 실제로 존재하는지 확인 필요(삭제된 components 와 새 shared 참조 불일치).
  • P2: Query Key 관리(Factory)는 잘 되어 있으나 중앙화·타입 안전성 강화를 권장 — 반복 패턴이 보입니다.
  • P3: ESLint import/order 규칙 도입은 좋지만 parserOptions.project 설정이 성능/오류를 유발할 수 있음(설정 위치 검토 권장).
  • P3: FSD 적용 원칙은 잘 적용되어 있음 — 다만 상향/하향 전략에 대한 명확한 방법론 제시 필요.

🔍 상세 리뷰

  1. QueryProvider 적용 누락 (치명)

    • 👍 좋은 점: QueryProvider 컴포넌트를 추가해 React Query를 전역으로 관리하려는 시도는 적절합니다.
    • 💡 개선 제안: App.tsx에서 QueryProvider로 애플리케이션을 감싸야 합니다. 현재 App.tsx는 RootLayout만 감싸고 있어 React Query가 동작하지 않거나 Devtools만 불러오는 상태가 될 수 있습니다.
    • 🤔 이유: useQuery/useMutation는 QueryClientProvider로 감싸진 컴포넌트 트리에서만 정상 동작합니다. Provider 미적용 시 Hook 호출 에러 또는 의도치 않은 동작이 발생합니다.
    • 💻 코드 예시 (AS-IS vs TO-BE)
      - import { BrowserRouter as Router } from "react-router-dom";
      - import { RootLayout } from "@/app/ui/layout.tsx";
      - import PostsManagerPage from "./pages/posts-manager-page.tsx";
      - 
      - const App = () => {
      -   return (
      -     <Router>
      -       <RootLayout>
      -         <PostsManagerPage />
      -       </RootLayout>
      -     </Router>
      -   );
      - };
      + import { BrowserRouter as Router } from "react-router-dom";
      + import { RootLayout } from "@/app/ui/layout.tsx";
      + import { QueryProvider } from "@/app/ui/query-provider";
      + import PostsManagerPage from "./pages/posts-manager-page.tsx";
      + 
      + const App = () => {
      +   return (
      +     <QueryProvider>
      +       <Router>
      +         <RootLayout>
      +           <PostsManagerPage />
      +         </RootLayout>
      +       </Router>
      +     </QueryProvider>
      +   );
      + };
  2. React Query 낙관적 업데이트 구현 — 훌륭하나 보완 필요 (P1)

    • 👍 좋은 점: comment.query.ts와 post.query.ts에서 onMutate/onSuccess/onError 패턴을 사용해 낙관적 업데이트를 구현한 점이 매우 좋습니다. (특히 댓글의 optimistic id 처리, 포스트의 여러 리스트 동기화 시도 등)
    • 💡 개선 제안:
      • onMutate 시작 시 queryClient.cancelQueries(key)를 호출하여 현재 진행 중인 동일 쿼리 요청을 취소하세요.
      • onMutate에서 이전 상태를 스냅샷으로 충분히 캡처하고 반환하여 onError에서 확실히 롤백하도록 하세요.
      • setQueryData를 호출할 때는 기존 데이터 구조가 변경될 수 있음을 고려해 안전한 업데이트(업데이터 함수 사용)를 권장합니다.
    • 🤔 이유: 쿼리가 병렬로 실행되면 optimistic 업데이트가 서버 응답이나 이후 invalidate에 의해 덮어쓰여 엉뚱한 상태가 될 수 있습니다. cancelQueries + snapshot + rollback은 일관된 상태 복원을 보장합니다.
    • 💻 코드 예시 (AS-IS vs TO-BE, comment create)
      - onMutate: async (payload) => {
      -   const key = commentQueryKeys.byPost(payload.postId);
      -   const prev = queryClient.getQueryData<Comment[]>(key);
      -   const optimistic = { id: Date.now(), ... } satisfies Comment;
      -   queryClient.setQueryData<Comment[]>(key, [optimistic, ...(prev ?? [])]);
      -   return { key, prev, optimisticId: optimistic.id } as const;
      - },
      + onMutate: async (payload) => {
  •  const key = commentQueryKeys.byPost(payload.postId);
    
  •  // 1) 현재 진행중인 같은 쿼리 취소
    
  •  await queryClient.cancelQueries({ queryKey: key });
    
  •  // 2) 이전 값 스냅샷
    
  •  const prev = queryClient.getQueryData<Comment[]>(key);
    
  •  // 3) 낙관적 객체 생성
    
  •  const optimistic = { id: Date.now(), ... } as const;
    
  •  // 4) 안전한 업데이트
    
  •  queryClient.setQueryData<Comment[]>(key, (old = []) => [optimistic, ...old]);
    
  •  return { key, prev, optimisticId: optimistic.id } as const;
    
  • },
    
    
  1. Post create/update/delete 쿼리 캐시 동기화 로직 (P2)

    • 👍 좋은 점: postsQueryKeys와 여러 리스트들(posts.list, posts.tag 등)을 고려해 여러 쿼리에 대해 동시 업데이트를 시도한 점은 응집도 높은 캐시 전략입니다.
    • 💡 개선 제안:
      • queryClient.getQueryCache().findAll({ queryKey: postsQueryKeys.all }) 방식은 의도한 모든 리스트를 찾을 수 있지만, 조건을 더 명확히 하거나 (예: findAll({ predicate })) 타입 안전성 확인을 권장합니다.
      • 변경 후 가능하면 invalidateQueries(affectedKey)로 서버 소스 권위(authoritative)를 재검증하는 흐름을 간결하게 유지하세요. (현재 onSuccess에서 invalidateQueries를 사용하고 있지만, 상황에 따라 과도한 invalidate가 아닐지 검토)
    • 🤔 이유: 여러 리스트를 직접 편집하면 일관성 유지를 개발자가 직접 책임져야 합니다. 경우에 따라 단순 invalidate + 서버 재요청이 복잡도를 낮춥니다.
  2. DummyJson(혹은 모킹) 한계 인지 및 대안 (P2)

    • 👍 좋은 점: PR에서 낙관적 업데이트를 구현한 점은 dummy API의 작동하지 않는 CUD 동작에 대한 적절한 클라이언트 대응입니다.
    • 💡 개선 제안:
      • 대안 1 (현재 PR에서 이미 사용 가능한 패턴): TanStack Query의 캐시를 직접 수정(현재 onMutate/onSuccess/onError) — 다만 cancelQueries, snapshot/rollback 보강 필요.
      • 대안 2 (권장 테스트/개발 환경): MSW(Mock Service Worker)를 이용해 클라이언트가 기대하는 API 동작(생성 시 id 반환 등)을 모킹하면 실제 서버 없는 환경에서도 CUD가 "영속성 있는 것처럼" 동작하게 만들 수 있습니다. 테스트와 브라우저 개발 환경에서 매우 유용합니다.
    • 🤔 이유: DummyJson 등 샘플 API는 실제 CUD 반영을 하지 않으므로, 개발 중에는 캐시 조작(낙관적 업데이트)이나 MSW로 서버 동작을 흉내내야 사용성/테스트가 편합니다.
  3. ESLint 설정 및 의존성 불일치 (P1)

    • 👍 좋은 점: import/order 규칙과 pathGroups를 설정해 네임스페이스 순서를 강제한 점은 일관성에 도움이 됩니다.
    • 💡 문제 및 개선 제안:
      • eslint.config.js에서 import tseslint from "typescript-eslint" / tseslint.config(...) 같은 사용은 표준 @typescript-eslint 패키지 사용 방식과 다릅니다. 일반적으로는:
        • parser: "@typescript-eslint/parser"
        • plugins: { "@typescript-eslint": require("@typescript-eslint/eslint-plugin") }
        • extends: ["plugin:@typescript-eslint/recommended"]
      • package.json에 @typescript-eslint/* 패키지가 없습니다. 현재 설정과 패키지가 맞지 않으면 lint 실행이 실패합니다.
      • parserOptions.project를 루트 tsconfig로 지정하면 ESLint 실행 시 타입 검사 기반 규칙이 동작하지만, 속도 저하와 설정 복잡도를 가져옵니다. 타입 기반 규칙을 일부만 활성화하거나 별도 전용 eslint config로 분리하는 것을 권장합니다.
    • 🤔 이유: ESLint 플러그인/파서와 설정의 불일치는 CI 실패의 주된 원인입니다.
  4. shared/ui 컴포넌트 참조 & 이동 (P2)

    • 👍 좋은 점: 기존의 components 를 제거하고 shared/ui 네임스페이스로 정리하려는 의도는 FSD 관점에서 적절합니다.
    • 💡 개선 제안:
      • PR에서 src/components/* 를 삭제했지만, 새로운 shared/ui 컴포넌트 파일들이 PR에 포함되어 있지 않습니다(예: @/shared/ui/button 등). 실제로 해당 파일들이 존재하는지 확인하세요. (만약 별도의 커밋에서 추가되었다면 괜찮음)
      • 경로별 alias(@/shared/...) 사용 시 tsconfig.paths와 bundler(vite) 설정이 올바른지 확인하세요.
    • 🤔 이유: import 오류는 런타임/빌드에서 바로 드러나므로, path alias·존재 여부를 먼저 검증하는 것이 좋습니다.
  5. Query Key Factory 활용 및 개선 아이디어 (P2)

    • 👍 좋은 점: commentQueryKeys, postsQueryKeys 같은 키 팩토리 사용은 쿼리 키 관리에 좋은 출발입니다.
    • 💡 개선 제안:
      • Query Key Factory를 중앙화하여 모든 엔티티의 키를 일관된 방식으로 생성하는 유틸을 만들면 재사용성과 타입 안정성이 높아집니다.
      • 예: createQueryKeyFactory<TParams, TResult>('posts', { list: (p) => ['list', p], detail: (id) => ['detail', id] })
      • 타입 유지를 위해 as const와 ReturnType 활용 권장.
    • 🤔 이유: 쿼리 키가 분산되어 있으면 invalidate/update 시 실수가 생깁니다. 중앙화는 실수 방지와 리팩토링 시 편의성을 줍니다.
  6. FSD 적용/아키텍처 관찰 및 추천 (P2)

    • 👍 좋은 점: Entities / Features / Widgets / Pages / Shared 구조를 명확히 나누고, UI와 컨테이너(Presenter / Container) 역할을 구분한 점이 매우 훌륭합니다. Self-reflection에서도 좋은 이해가 드러납니다.
    • 💡 TO-BE: 더 유연한 구조 제안 (예시)
      • 현재 구조(AS-IS)
        src/
        app/
        entities/
        features/
        pages/
        shared/
        widgets/
      • 제안 구조(TO-BE, 약간 더 명시적)
        src/
        app/
        providers/
        ui/
        base/ (shared)
        api/
        ui/
        lib/
        entities/
        post/
        comment/
        user/
        features/
        post-load/
        post-filter/
        widgets/
        pages/
      • 장점: base(shared)와 app의 프로바이더/설정이 분리되어 재사용성·테스트 용이성 증가.
    • 🤔 이유: 변경 시 영향 범위를 줄이고, 테스트/모킹/도메인 로직의 응집도를 더 높이기 위함입니다.
  7. 코드 스타일 / 일관성 (P3)

    • 좋은 점: 네임스페이스 객체(postApi, commentApi)로 API 모음 관리한 점은 명시적이고 가독성 좋습니다.
    • 개선 제안:
      • import ordering은 eslint import/order로 통일되었지만, 프로젝트 전체가 그 규칙을 만족하는지 확인(자동 정렬 툴 도입 권장).
      • 파일/컴포넌트 네이밍은 kebab-case vs camelCase 혼용을 점검하세요 (예: pages/posts-manager-page.tsx 소문자 파일명 규칙을 일정하게 유지).
    • 이유: 일관성은 협업 시 인지 비용을 낮춥니다.
  8. 라이브러리 버전 및 호환성 체크 (P2)

    • 확인된 버전:
      • @tanstack/react-query 5.85.3 — v5로 보이며, API는 리뷰 내 코드와 호환됩니다.
      • @tanstack/react-query-devtools 5.85.3 — Devtools 사용 OK.
      • react 19.1.1 — 일부 서드파티 패키지(react-router-dom, 기타 UI 라이브러리)가 React 19과 호환되는지 확인 필요합니다.
    • 권장: major upgrade 시 종속성 호환성(특히 react 19 관련 peer dependency)을 CI에서 체크하세요.

🗺️ 전체 개선 로드맵 (우선순위 가이드)

  • P1 (Must-Fix) 🚨

    • QueryProvider를 루트(App)에서 적용 (즉시 수정)
    • ESLint 설정과 @typescript-eslint 관련 의존성 불일치 해결 (lint/CI 오류 방지)
    • onMutate에서 cancelQueries/rollback 패턴 보강 (race 조건 방지)
    • shared/ui 컴포넌트 존재 확인 및 경로(alias) 설정 검증
  • P2 (Recommended) ⚠️

    • Query Key Factory 중앙화 및 타입 안전성 강화
    • MSW 도입으로 개발/테스트 환경에서 CUD 동작 모킹
    • queryClient 기본 옵션(예: defaultOptions, retry 정책) 설정 및 환경별(개발/프로덕션) 조정
  • P3 (Optional) 💡

    • ESLint의 parserOptions.project 사용 최적화(성능과 정확성 균형)
    • 더 엄격한 타입 검사(예: generated types) 적용
    • React Query의 staleTime/cacheTime 정책 프로젝트 전반 재검토

🤔 한 걸음 더: 코드와 의도 돌아보기

PR 본문을 통해 작성자님의 고민과 학습 과정이 잘 드러나 있습니다. 아래 질문을 통해 더 깊게 생각해보세요.

  • 목표와 구현의 연결고리: "FSD를 통한 관심사 분리"라는 목표는 entities/feature/widget 구조에 반영되어 있습니다. 이 목표를 달성하기 위해 다른 접근(예: 도메인-기반 폴더 구조나 레이어 분리)을 고려해보셨나요?
  • 숨겨진 의도 파악: App에서 RootLayout으로 이동하고 components를 shared로 이전하려는 의도로 보입니다. 단, QueryProvider를 붙이지 않은 부분은 의도치 않은 누락인지요?
  • 스스로에게 던지는 질문:
    • "이번 PR에서 내가 가장 자랑하고 싶은 코드 한 줄(또는 한 파일)은 무엇인가요? 왜인가요?"
    • "낙관적 업데이트의 롤백 로직을 더 안전하게 만든다면 어떤 시나리오(동시성, 실패율 높은 네트워크)를 고려할까요?"
    • "FSD의 레이어 분리에서 가장 결정하기 어려웠던 경계는 무엇이었고, 그때의 최종 판단 근거는 무엇이었나요?"
    • "이 구조를 처음 보는 동료는 어디서부터 읽어야 할지 헷갈릴까요? README나 폴더 설명을 추가하면 도움이 될까요?"

📚 참고 자료


마무리 멘트:
이번 PR은 FSD 원칙을 실제 코드에 잘 녹여내셨고, TanStack Query를 통한 선언적 서버 상태 관리 적용도 훌륭합니다. 위에 적은 P1 항목(특히 QueryProvider 적용과 ESLint/타입 설정)은 빠르게 해결하시길 권장합니다. ✨

끝.

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.

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

1. 🏗️ FSD 아키텍처

💡 개념 정의

Feature-Sliced Design(FSD)는 기능 단위로 코드베이스를 계층화( app → pages → widgets → features → entities → shared )하여 관심사(비즈니스 로직, UI, 재사용 가능한 영역)를 분리하는 아키텍처 원칙입니다. 각 레이어는 아래 레이어만 참조하고, 외부 노출은 slice의 Public API(index.ts)를 통해 제한합니다.

⚡ 중요성

FSD는 요구사항 변화 시 코드 변경 범위를 국소화하고 모듈 단위의 독립성을 제공하여 리팩토링·기능추가·아키텍처 전환(모노레포, 마이크로프론트엔드 등) 시 수정 비용을 줄입니다. 또한 온보딩과 코드 이해도를 크게 향상시킵니다.

📊 현재 상황 분석

긍정적 요소: entities에 순수 API(postApi, commentApi 등)와 queryKey 파일(post.keys.ts, comment.keys.ts)을 두어 재사용성과 명시성이 확보되어 있음. 또한 features에서 훅을 통해 비즈니스 로직을 캡슐화함.
위험 요소: 일부 feature 훅(초기 버전)에서 같은 레이어의 다른 feature를 직접 참조하던 흔적이 존재했고, 아직도 feature 간 계약(예: event interface)과 의존성 방향을 자동으로 검증하는 장치(빌드/ESLint 룰)가 없어 무심코 참조가 생길 수 있음.

📝 상세 피드백

전체적으로 FSD 레이어( app → pages → widgets → features → entities → shared )를 의식한 구조가 잘 적용되어 있습니다. entities에 타입/모델/순수 API를 두고, features에서 비즈니스 행위·훅·컨테이너를 둔 설계가 명확합니다. index.ts를 통한 Public API 노출도 대부분 잘 지켜지고 있어 다른 모듈이 내부 구현을 직접 참조하는 일이 줄어듭니다.

다만 몇 군데에서 동일 레이어(예: features/post-load) 내에서 다른 feature를 직간접 참조하는 패턴이 관찰됩니다. PR 본문에도 지적하셨듯 초기에는 usePostFilter를 직접 참조하던 코드가 있었고(변경 전 예시), 이를 매개변수(원시타입)로 분리한 설계로 개선한 점은 매우 긍정적입니다.

요약: FSD 개념이 코드 전반에 적용되어 있어 요구사항 변화(새 기능, 도메인 변경, 모노레포 전환 등)에 대한 유연성이 높음. 다만 feature 간의 경계(특히 feature → feature 의 직접 참조)는 계속 주시해야 합니다.

❌ 현재 구조 (AS-IS)

// AS-IS 예시 (본문 초안에서 문제로 제시되었던 형태)
// features/post-load/model/post-load.hook.ts
import { usePosts } from "@/entities/post";
import { getPostsWithAuthors } from "../api/post-load.api";
import { usePostFilter } from "@/features/post-filter"; // features가 features 참조 (경계 위반)

export const useLoadPost = () => { /* ... */ }

✅ 권장 구조 (TO-BE)

// TO-BE: 경계 유지 (원시 파라미터 전달)
// features/post-load/model/post-load.hook.ts
import { usePosts } from "@/entities/post";
import { getPostsWithAuthors } from "../api/post-load.api";

export const useLoadPost = () => {
  const { setPosts } = usePosts();
  const getPosts = async (params: { skip: number; limit: number; selectedTag?: string; ... }) => {
    const { posts } = await getPostsWithAuthors(params);
    setPosts(posts);
  };
  return { getPosts };
};

// 소비지: features/post-browse/ui/post-browse-container.tsx
const { params } = usePostFilter();
const { getPosts } = useLoadPost();
useEffect(() => { getPosts(params); }, [params]);

🔄 변경 시나리오별 영향도

  1. UI 라이브러리 교체(Material UI → Chakra UI): shared/ui 컴포넌트가 범용적으로 잘 추상화되어 있으면 변경은 shared/ui 내부(예: Button, Dialog, Card, Table) 10~15개 파일 범위에서 끝남. 반면, 도메인 코드가 shared 컴포넌트를 넘어 구체 구현에 의존하면 추가 변경 파일이 늘어남.
  2. 도메인(entities) 변경(예: Post에 author 필드 구조 변경): entities/post/model/post.types.ts, entities/post/api/posts.api.ts, post-load api와 관련 features의 transform 코드(약 610 파일) 수정 필요. 잘 응집되어 있으면 수정 파일 34개로 줄일 수 있음.
  3. 모노레포 전환: 각 slice(index.ts를 통한 public API)가 명확하면 패키지 분리 시 의존성 수정은 build config와 일부 import 경로(예: @/entities/* → @company/entities-post)만으로 가능.

🚀 개선 단계

  • 1단계: 단기(1-2일): ESLint import/order + custom rule로 layers( app/pages/widgets/features/entities/shared ) 간 의존성 위반을 감지하도록 설정. (예: eslint-plugin-boundaries 혹은 프로젝트 규칙)
  • 2단계: 중기(2-4일): 각 slice의 index.ts(공개 API) 표준을 문서화하고 CI에서 export-only 검증(내부 파일 직접 참조 방지)을 적용.
  • 3단계: 장기(1-2주): 모듈화(패키지 분리)를 고려하여 entities/features를 NPM 패키지(또는 workspace)로 분리할 때 수월하도록 public API contract(타입 정의, 범위)를 강화.

2. 🔄 TanStack Query

💡 개념 정의

TanStack Query는 서버 상태를 선언적으로 관리하는 라이브러리로, 쿼리 키를 통한 캐싱, 자동 재요청, 낙관적 업데이트, prefetch 등의 기능을 제공합니다. 핵심은 queryKey 설계와 API 계층/쿼리 훅의 책임 분리입니다.

⚡ 중요성

잘 설계된 query 패턴은 API 변경, 새로운 데이터 소스 추가, 에러/로딩 정책 변경에 대해 코드 수정을 국소화합니다. 일관된 queryKey 패턴은 캐시 invalidation·prefetch·shared cache 접근을 예측 가능하게 해 줍니다.

📊 현재 상황 분석

긍정적: queryKey에 'byPost', 'detail', 'list' 등 의미 있는 분류가 있어 캐시 조작이 용이함. 낙관적 업데이트 구현도 상황별로 잘 되어 있음.
주의점: postsQueryKeys.list에 객체(params)를 queryKey로 포함시키는 것은 좋은 관행이나, 객체 직렬화 또는 참조 동일성 때문에 불필요한 캐시 분리(쿼리 키 폭발)가 발생할 수 있음. 또한 postsQuery의 onSuccess에서 모든 'posts' 관련 캐시를 invalidate하는 전략은 일부 상황에서 과도한 네트워크 호출을 유발할 수 있음.

📝 상세 피드백

TanStack Query 활용이 체계적으로 잘 되어 있습니다. queryKey 정의(comment.keys.ts, post.keys.ts, user.model.user.query.ts 등), API 계층(entities//api)과 query 훅(entities//model/.query.ts, features//model/*.query.ts)의 분리가 잘 이루어졌습니다. 낙관적 업데이트(onMutate)와 캐시 조작, prefetch/ensureQuery 사용 등 고급 패턴도 적절히 사용되어 있어 서버 상태 관리의 유연성이 높습니다.

개선 여지: query key factory(공통 규칙)와 query 옵션(staleTime, cacheTime 등)의 기준(프로젝트 정책)을 문서화하면 새로운 API 추가 시 일관성 유지가 쉬워집니다. 또한 일부 mutation onSuccess에서 불필요한 전체 invalidate(예: postsQuery에서 invalidateQueries({ queryKey: postsQueryKeys.all }))가 사용되어 불필요한 네트워크 호출이 발생할 수 있으므로 상황별 전략 정립이 필요합니다.

❌ 현재 구조 (AS-IS)

// AS-IS: comment query (PR)
export const commentQueryKeys = { all: ['comments'] as const, byPost: (postId) => ['comments', postId] as const };
export function useCommentsQuery(postId) { return useQuery({ enabled: !!postId, queryKey: postId ? commentQueryKeys.byPost(postId) : commentQueryKeys.all, queryFn: () => commentApi.get(postId).then(r => r.comments), staleTime: 30_000 }); }

✅ 권장 구조 (TO-BE)

// TO-BE: 권장 패턴 - 중앙 queryKey factory + 정책 적용
// shared/api/queryKeys.ts
export const queryKeys = { posts: { all: ['posts'] as const, list: (p) => [...this.all, 'list', serializeParams(p)] }, comments: { all: ['comments'] as const, byPost: (id) => [...this.comments.all, id] } }

// entities/*/model/*.query.ts 사용 예시는 동일하지만 queryKey 생성은 shared api로 표준화

🔄 변경 시나리오별 영향도

  1. API 엔드포인트 변경(예: /posts → /v2/posts): entities/*/api 파일 3개(post, comment, user)에서 URL만 변경하면 되고 query 훅은 영향 적음(약 3 파일).
  2. 새 데이터 소스(예: 외부 추천 API) 추가: 새로운 queryKeys 파일 1개와 entities/feature 단위로 query 훅 1~2개 추가. 기존 쿼리를 건드릴 필요는 거의 없음.
  3. 캐쉬 전략 변경(예: staleTime 증가): query 훅(약 10곳)에 존재하는 staleTime을 정책으로 통일하면 개별 변경량은 0. 현재는 hook별로 설정되어 있어 중앙 정책으로 통합하면 유지보수성 향상.

🚀 개선 단계

  • 1단계: 단기(half-day): 프로젝트 레벨의 queryKey 규칙 문서 작성(객체를 키로 넣는 경우 serialize 함수 사용 등)과 staleTime/cacheTime 기본값 설정(예: 30s/5m).
  • 2단계: 중기(1-2일): shared 폴더에 queryKeys 팩토리 파일 생성(ex: src/shared/api/queryKeys.ts)로 모든 queryKey를 중앙화하고 기존 키를 이 파일로 마이그레이션.
  • 3단계: 장기(1주): React Query Devtools와 CI에서 queryKey 패턴(정규화/serialize 사용 여부)을 검사하는 스크립트 또는 테스트를 추가.

3. 🎯 응집도 (Cohesion)

💡 개념 정의

응집도(Cohesion)는 모듈 내부의 요소들이 얼마나 관련성 있게 모여 있는지를 뜻합니다. 높은 응집도는 한 기능을 변경할 때 수정해야 할 파일 수를 줄입니다.

⚡ 중요성

응집도가 높으면 한 기능 변경 시 영향 범위가 좁아지고 디버깅 및 테스트가 쉬워집니다. 모듈을 패키지화하거나 다른 팀과 공유할 때도 유리합니다.

📊 현재 상황 분석

긍정적 요소: 게시물·댓글·사용자에 대해 관련 파일이 인접해 있어 기능 변경 시 찾아야 할 위치가 명확.
미흡한 점: postLoadApi(getWithAuthors)에서 posts와 users를 조합하는 로직은 features/post-load에 위치하지만, 이 변환은 entities/post 내부(도메인 책임)로 더 적합할 수 있음. 현재 위치도 크게 잘못되진 않았으나 응집도를 더 높이려면 도메인 책임 명확화가 필요합니다.

📝 상세 피드백

전반적으로 도메인별 응집도가 높습니다. 예를 들어 entities/post 안에 api, model, ui(PostsTable 등)가 모여 있고, features/post-edit는 post 편집 관련 상태와 UI를 캡슐화합니다. 댓글 관련 로직도 entities/comment와 features/comment-edit로 응집되어 있어 '같이 변경되어야 할 코드'가 인접해 있습니다.

개선 포인트: 일부 util/헬퍼(예: split-by-highlight)는 shared에 잘 두었지만, posts 관련 transform(예: postLoadApi의 author 매핑)은 entities/post 내부(또는 entities/post-load 같은 별도 엔티티)로 옮겨 응집도를 더 높일 수 있습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: postLoadApi in features/post-load/api/post-load.api.ts
export const postLoadApi = { async getWithAuthors(params) { const [{posts, total}, users] = await Promise.all([postApi.get(params), userApi.getProfile()]); return { posts: posts.map(p => ({...p, author: users.find(u => u.id === p.userId)})), total }; } }

✅ 권장 구조 (TO-BE)

// TO-BE: 도메인 응집 강화 (entities/post 내부로 이동)
// entities/post/api/post-with-authors.api.ts
export const postWithAuthorsApi = { async get(params) { const { posts, total } = await postApi.get(params); const users = await userApi.getProfile(); return { posts: posts.map(p => ({...p, author: users.find(u => u.id === p.userId)})), total }; } }

🔄 변경 시나리오별 영향도

  1. 게시물의 author 데이터 구조가 변경될 때: 현재 구조(entities/post types + postLoadApi)에서는 변경 영향 범위가 entities/post 모델 + postLoadApi + PostsTable 관련 UI(약 4-6개 파일)로 국소화됨.
  2. 댓글 포맷 변경(추가 필드 등): entities/comment/model/* 및 features/comment-edit 관련 파일(약 6개)에서 수정이 집중됨.

🚀 개선 단계

  • 1단계: 단기(half-day): postLoadApi의 author 결합 로직을 entities/post로 옮기는 리팩토링 검토(변경 시 영향도 측정: 약 3~5 파일 수정).
  • 2단계: 중기(1-2일): 도메인별 'transform' 책임(예: DTO → 도메인 객체)을 명확히 하는 가이드 작성(entities 안에서 처리).
  • 3단계: 장기(1주): 중요한 도메인(Posts, Comments, Users)을 각각 패키지화(또는 workspace 분리)할 때 응집도가 유지되는지 검증하는 테스트 작성.

4. 🔗 결합도 (Coupling)

💡 개념 정의

결합도(Coupling)는 모듈 간 의존성의 강도를 나타내며, 낮은 결합도는 한 모듈 변경이 다른 모듈에 미치는 영향을 줄입니다. 이상적인 설계는 인터페이스(추상화)에 의존하고 구현체에 직접 의존하지 않는 것입니다.

⚡ 중요성

낮은 결합도는 기술 스택 변경(HTTP 클라이언트, 상태 라이브러리), 아키텍처 전환 시 수정 범위를 줄이고 리스크를 낮춥니다.

📊 현재 상황 분석

긍정: http 클라이언트 중앙화로 axios→fetch와 같은 변경은 createHttpClient 내부만 수정하면 될 가능성이 큼(현재 설계). 문제: QueryClient 직접 접근을 feature 훅에서 광범위하게 사용하면 QueryClient API 변경 또는 swap 시 여러 훅을 수정해야 함.

📝 상세 피드백

현재 구조는 추상화(entities API, query 훅, features 훅)를 통해 결합도를 낮추려는 시도가 명확히 보입니다. entities의 api를 통해 HTTP 호출을 중앙화했고, features는 entities의 public hook/api만 사용하도록 설계되어 있습니다. 그러나 몇몇 지점에서 직접적인 결합 징후가 있습니다:

  • useUserDetailModal에서 QueryClient의 getQueryData/prefetchUserById를 직접 사용해 캐시와 UI를 결합하는 패턴(현재는 의도적이지만, QueryClient API 변경 시 영향 범위가 넓어질 수 있음).
  • features/post-load의 초기 버전은 usePostFilter를 직접 참조했음(같은 레이어 내부 참조로 결합도 상승의 예).

전반적으로 결합도는 적절하지만, 외부 라이브러리(예: fetch→axios, Jotai→Zustand) 변경을 고려하면 의존성 주입 또는 adapter 레이어를 통해 더 안전하게 전환할 수 있습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: QueryClient 직접 접근
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
const cached = queryClient.getQueryData(['users', id]);

// AS-IS: atoms 직접 사용
import { currentUserIdAtom } from '@/entities/user/model/user.atom';
const [id] = useAtom(currentUserIdAtom);

✅ 권장 구조 (TO-BE)

// TO-BE: adapter/port 패턴 적용
// shared/http/index.ts (인터페이스 제공)
export interface IHttp { get<T>(path: string, opts?): Promise<T>; post<T>(path, body?): Promise<T>; }
export const http: IHttp = createHttpClient(BASE_URL);

// QueryClient usage through helper
export const cacheHelpers = { getUserFromCache: (client, id) => client.getQueryData(['users', id]), prefetchUser: (client, id) => client.prefetchQuery(...)};

// feature에서는 cacheHelpers만 사용

🔄 변경 시나리오별 영향도

  1. HTTP 클라이언트 변경(axios → native fetch): good path는 shared/api/client.ts 하나만 수정하면 됨 (1 file). 다만 기존 API 호출 방식을 바꾸어 추가 옵션/에러 포맷이 달라지면 entities/*/api에서 예외 처리 로직(약 3~6 파일)을 검토해야 함.
  2. 상태관리 라이브러리 변경(Jotai → Zustand): atoms를 직접 사용하는 기능(많은 feature/model 파일)에서는 wrapper 훅을 만들어 점진적으로 마이그레이션 필요(예상 영향: atoms를 직접 참조하는 파일 약 20~30개).
  3. QueryClient 교체 또는 옵션 변경: features와 entities에서 queryClient를 직접 사용한 부분(prefetch, getQueryData, ensureQueryData 등)이 있는 파일들(약 8~12개)에서 수정 필요.

🚀 개선 단계

  • 1단계: 단기(half-day): shared layer에 'adapters' 폴더를 만들어 http 클라이언트와 queryClient 조작을 래핑하여 직접참조를 줄임.
  • 2단계: 중기(1-2일): Jotai atoms 접근을 abstract hook(useAuthState 등)으로 옮겨 향후 상태관리 라이브러리 교체 시 교체 지점을 축소.
  • 3단계: 장기(1주): 프로젝트 전반에 의존성 주입(특히 테스트 시 모킹)을 고려하여 핵심 외부 의존(HTTP, QueryClient)을 추상화하는 정책을 수립.

5. 🧹 Shared 레이어 순수성

💡 개념 정의

Shared 레이어는 도메인에 의존하지 않는 범용 컴포넌트와 유틸을 제공하는 레이어입니다. 이 레이어는 순수하고 재사용 가능해야 하며 도메인 정책(비즈니스 규칙)에 영향을 받지 않아야 합니다.

⚡ 중요성

shared가 도메인에 의존하면 재사용성이 떨어지고 다른 프로젝트로의 이식성이 사라집니다. 또한 디자인 시스템 변경 시 영향 범위가 커집니다.

📊 현재 상황 분석

긍정: shared UI는 범용적으로 디자인되어 있어 UI 라이브러리 변경 시 shared 내부만 교체하면 되는 구조입니다. http client와 env 래퍼가 있어 API 엔드포인트 변경의 영향 범위를 줄임.
리스크: shared에 환경 로직(BASE_URL 결정 등)이 있어 infra(빌드/배포) 변화 시 테스트가 필요함. 또한 shared/ui에서 상태나 도메인 타입을 직접 참조하는 흔적은 발견되지 않음(좋음).

📝 상세 피드백

shared 레이어가 UI 구성요소(Button, Dialog, Table, Card 등)와 유틸(splitByHighlight, env, http client)을 제공하고 있어 재사용성과 도메인 독립성이 잘 지켜지고 있습니다. entities나 features에서 shared를 참조할 때 도메인 로직이 shared 내부로 스며들지 않도록 관리한 점 또한 옳습니다.

주의: shared/api/client.ts에서 BASE_URL 처리와 buildUrl 로직을 구현했는데, 환경별 동작(mode에 따른 BASE_URL 결정 등)이 포함되어 있어 이 부분은 infra/환경 설정과 공유되는 경계(도메인과 분리)임을 문서화하는 것이 좋습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: shared API client 사용
// entities/comment/api/comments.api.ts
import { http } from '@/shared/api/client';
export const commentApi = { get(postId){ return http.get(`/comments/post/${postId}`) }, ... }

✅ 권장 구조 (TO-BE)

// TO-BE: shared는 순수하고 의존성 없는 형태 유지
// shared/ui/Button.tsx (범용)
export const Button = ({ children, ...props }) => <button {...props}>{children}</button>;
// entities에서 도메인 로직은 features/entities 내부에 두고 shared는 UI/infra만 담당

🔄 변경 시나리오별 영향도

  1. 디자인 시스템 변경(Material → Chakra): shared/ui 내부(약 10-15개 컴포넌트)만 수정하면 widgets/entities/feature의 사용부는 대부분 변경 없음.
  2. API 기본 경로 변경/멀티 백엔드(테스트서버/프로덕션): shared/api/client.ts only 수정으로 처리 가능(1 file).

🚀 개선 단계

  • 1단계: 단기(half-day): shared/api/client.ts의 동작(환경 결정 로직)을 문서화하고, 환경별 테스트 케이스(DEV/PROD)를 간단히 추가.
  • 2단계: 중기(1일): shared 레이어의 README 작성(사용 규칙: shared는 도메인 불문, 의존성 없음).
  • 3단계: 장기(1주): 디자인 시스템(토큰, 컴포넌트 API) 변경 시 migration guide를 준비하여 다른 프로젝트로의 재사용성을 확보.

6. 📐 추상화 레벨

💡 개념 정의

추상화 수준은 복잡한 구현 세부를 숨기고 명확한 개념(비즈니스 API, 훅 인터페이스)만 노출하는 정도입니다. 좋은 추상화는 구현 변경 시 상위 코드에 영향이 적습니다.

⚡ 중요성

추상화는 기술 스택 변경, 테스트, 재사용성, 유지보수성에 직접적인 영향을 미칩니다. 특히 API 스펙 변경이나 라이브러리 교체 시 유리합니다.

📊 현재 상황 분석

긍정: 훅과 API 레이어가 역할을 잘 분리하고 있어 재사용 및 테스트가 용이.
개선 필요: queryKey 생성과 낙관적 업데이트 로직이 각 entity마다 중복되는 경향이 있어 공통 유틸화 시 이점을 얻을 수 있음.

📝 상세 피드백

비즈니스 로직과 기술적 세부사항의 분리가 상당 부분 이루어져 있습니다. entities/*/api는 HTTP 호출(기술적 세부사항)을 담당하고, features/의 훅은 비즈니스 흐름(예: useCommentEditor, usePostEditor)을 담당합니다. postLoadApi는 도메인 관점에서 'posts with authors'를 조합해 제공하는 좋은 추상화 예시입니다.

추가 권장: 일부 공통 추상(예: query key factory, optimistic update helper, cache helper)을 shared로 더 모아 재사용성·일관성을 향상시키면 좋습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: 개별 낙관 업데이트 (comment.query.ts)
onMutate: async (payload) => { const key = commentQueryKeys.byPost(payload.postId); const prev = queryClient.getQueryData(key); const optimistic = {...}; queryClient.setQueryData(key, [optimistic, ...(prev ?? [])]); return { key, prev, optimisticId: optimistic.id }; }

✅ 권장 구조 (TO-BE)

// TO-BE: 낙관 업데이트 헬퍼 사용
// shared/query/optimistic.ts
export const optimisticInsert = (client, key, item) => { const prev = client.getQueryData(key); client.setQueryData(key, [item, ...(prev ?? [])]); return prev; }

🔄 변경 시나리오별 영향도

  1. 비즈니스 규칙(댓글 낙관적 업데이트 정책) 변경: 각 entity의 mutation onMutate/onSuccess에 같은 로직이 반복되면 다수 파일(약 5~8개) 수정 필요. 공통 helper로 리팩토링하면 1곳의 변경으로 처리 가능.
  2. 폼 검증 로직 공통화(여러 다이얼로그): 현재 다이얼로그가 개별 onChange로 처리되므로 공통 훅으로 추상화하면 재사용률 증가.

🚀 개선 단계

  • 1단계: 단기(half-day): 낙관 업데이트 및 cache helper(공통유틸) 초안 작성 후 comment/post에 적용해 중복 제거.
  • 2단계: 중기(1-2일): 훅 인터페이스 가이드 작성(예: useXxxEditor는 { add, update, delete, isSubmitting } 형태로 통일).
  • 3단계: 장기(1주): 공통 훅 라이브러리(shared/hooks)로 추상화해 신규 feature에서 표준 사용을 권장.

7. 🧪 테스트 용이성

💡 개념 정의

테스트 용이성은 코드가 유닛/통합/엔드투엔드 테스트로 검증되기 쉬운 정도입니다. 순수 함수·의존성 주입·작은 단위의 컴포넌트가 중요합니다.

⚡ 중요성

요구사항 변화(새 API, 에러 정책 변경) 시 회귀를 방지하려면 테스트가 필수입니다. 또한 리팩토링 시 안정성을 확보할 수 있습니다.

📊 현재 상황 분석

구조는 테스트 친화적이지만 실제 테스트 코드 부재. Jotai atom을 직접 사용하는 훅이 테스트 하드닝을 위해 wrapper나 mock hook 제공 여부가 필요함.

📝 상세 피드백

코드 구조(순수한 UI 컴포넌트, 비즈니스 로직 분리된 훅, centralized API clients, query hooks)는 테스트하기 좋은 형태입니다. 예: CommentAddDialog와 CommentAddDialogContainer로 분리되어 UI 단위 테스트와 훅 단위 테스트를 분리할 수 있습니다. entities의 query 훅(useCommentsQuery 등)은 React Query를 모킹하여 단위/통합 테스트 작성이 가능합니다.

하지만 현재 PR에는 테스트 코드가 포함되어 있지 않아 실제 테스트 커버리지와 자동화 수준은 미확인입니다. 또한 Jotai atoms와 QueryClient를 사용하는 훅은 의존성 주입(mock QueryClient, useAtom wrapper 등)을 통해 테스트를 용이하게 만들 필요가 있습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: UI와 로직 분리된 구조
// entities/comment/ui/comment-add-dialog.tsx (순수 UI)
export function CommentAddDialog({ open, onOpenChange, body, onChange, onSubmit }) { /* presentational */ }

// features/comment-edit/ui/comment-add-dialog-container.tsx (비즈니스 로직 훅 사용)
export function CommentAddDialogContainer() { const { newComment, addComment } = useCommentEditor(); /* container */ }

✅ 권장 구조 (TO-BE)

// TO-BE: 테스트 예시 (Jest + React Testing Library)
// 단위: CommentAddDialog 렌더링 검사
render(<CommentAddDialog open onOpenChange={...} body="x" onChange={...} onSubmit={...} />);
// 훅 테스트: useCommentEditor를 react-hooks-testing-library로 테스트, QueryClient를 mock으로 주입

🔄 변경 시나리오별 영향도

  1. 외부 API 스펙 변경: entities/*/api 에 대한 유닛 테스트와 API mocking을 도입하면 리스크 감소(변경 시 테스트 실패로 즉시 감지).
  2. 상태관리 라이브러리 변경: atoms를 추상화한 hook을 통해 mock impl로 대체하면 테스트 유지보수 부담 감소.

🚀 개선 단계

  • 1단계: 단기(half-day): 기본 테스트 설정 추가(Jest/RTL 또는 vitest + testing-library/react)과 QueryClient, Jotai provider를 테스트 유틸로 래핑.
  • 2단계: 중기(1-2일): 주요 훅(useCommentEditor, usePostEditor, useUserDetailModal)에 대한 유닛 테스트 작성(낙관 업데이트, 에러 복구 시나리오 포함).
  • 3단계: 장기(1주): CI에 테스트 실행 추가 및 커버리지 목표(예: 핵심 로직 70% 이상) 설정.

8. ⚛️ 현대적 React 패턴

💡 개념 정의

현대적 React 패턴은 커스텀 훅, Suspense, Error Boundary, 선언적 데이터 처리(React Query 등), 단일 책임 컴포넌트 등을 포함합니다.

⚡ 중요성

이 패턴들을 사용하면 로딩/에러 처리의 일관성 유지, UI-로직 분리, 컴포넌트 재사용성 향상 등 이점을 얻습니다.

📊 현재 상황 분석

긍정: hooks와 Query를 조합해 선언형으로 서버 상태를 다루는 구조 확립.
추가 권장: ErrorBoundary와 Suspense를 적용하여 최상위에서 로딩/에러를 통제하면 개별 컴포넌트에서 반복되는 로딩 UI를 줄일 수 있음.

📝 상세 피드백

커스텀 훅(useCommentEditor, useLoadPost 등)과 QueryProvider(React Query) 사용 등 현대적 React 패턴을 잘 활용하고 있습니다. 컴포넌트와 로직 분리가 잘 되어 있어 Suspense/ErrorBoundary 적용도 가능합니다.

권장: 데이터 로딩/에러 처리를 위해 상위에 ErrorBoundary 및 Suspense(혹은 useQuery의 isLoading/isError에 대한 공통 Fallback)를 도입하면 코드가 더 선언적이고 예측 가능해집니다. 현재 QueryProvider가 존재하므로 RootLayout 혹은 index에 ErrorBoundary와 Suspense를 함께 두는 것이 쉽습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: 현재 QueryProvider 사용
// src/main.tsx
createRoot(...).render(
  <StrictMode>
    <QueryProvider>
      <App />
    </QueryProvider>
  </StrictMode>
);

// 개별 컴포넌트는 isFetching 등을 사용하여 로딩 처리

✅ 권장 구조 (TO-BE)

// TO-BE: 최상위에서 선언적 처리
// src/main.tsx
<QueryProvider>
  <ErrorBoundary fallback={<GlobalError />}>
    <Suspense fallback={<AppSkeleton/>}>
      <App/>
    </Suspense>
  </ErrorBoundary>
</QueryProvider>

// 개별 컴포넌트는 usePostsQuery만 사용하고 로딩/에러는 상위에서 처리

🔄 변경 시나리오별 영향도

  1. 로딩 UX 개선(스켈레톤 컴포넌트 도입): Suspense로 전환하면 PostsTable 등에서 로직 변경 최소화로 스켈레톤 적용 가능.
  2. 에러 처리 정책 변경(글로벌 에러 UI 도입): ErrorBoundary 상단 적용으로 각 컴포넌트 수정 최소화.

🚀 개선 단계

  • 1단계: 단기(half-day): ErrorBoundary 패턴 도입 방안(간단한 GlobalError 컴포넌트) 설계 및 Root에 적용.
  • 2단계: 중기(1일): 일부 큰 뷰(PostsManager 등)에 Suspense 적용(React Query의 useSuspenseQuery와 조합 가능) 및 Skeleton 컴포넌트 제작.
  • 3단계: 장기(1주): 로컬/글로벌 에러 핸들링 정책(로그, 사용자 알림)을 문서화하고, Devtools와 연동한 오류 추적 체계 마련.

9. 🔧 확장성

💡 개념 정의

확장성은 새로운 요구사항을 추가하거나 기존 요구사항을 변경할 때 수정 범위와 복잡도가 낮은 정도를 의미합니다.

⚡ 중요성

비즈니스 요구가 자주 바뀌는 프론트엔드에서 확장성이 낮으면 개발 속도가 급감하고 버그 위험이 커집니다.

📊 현재 상황 분석

긍정: 대부분의 확장(새 API, UI 변경)은 shared 또는 entities 내부에서 처리 가능.
리스크: 상태관리 교체(Jotai→다른 도구)의 경우 atoms를 직접 참조하는 파일이 많아 마이그레이션 비용이 발생할 수 있음(예상 직접 참조 파일 20~30개).

📝 상세 피드백

새 기능(예: 다국어, A/B 테스트, 오프라인 모드)이나 기술 스택 변화(상태관리 라이브러리 전환, UI 라이브러리 변경)에 대해 현재 구조는 비교적 유연합니다. entities/feature 분리, shared 추상화, queryKey 규칙 등은 확장성을 높입니다.

권장: 확장 시 영향을 받는 접점(예: atoms 직접 사용 위치, queryClient 직접 사용 위치)을 문서화하고, 공통 인터페이스(예: auth provider, http adapter)를 통해 확장 지점을 관리하면 더 안전합니다.

❌ 현재 구조 (AS-IS)

// AS-IS: Button 컴포넌트는 props만으로 동작하기 때문에 디자인 시스템 교체 시 shared/ui/button 내부만 수정하면 됨.

✅ 권장 구조 (TO-BE)

// TO-BE: 상태관리 추상화
// shared/state/index.ts
export const useAuth = () => { /* 내부에서 atom 사용 */ }

// 모든 컴포넌트는 useAuth를 통해 상태 접근

🔄 변경 시나리오별 영향도

  1. 다국어 지원 추가: shared/ui 컴포넌트에 i18n context 전달과 text 키 사용으로 대부분의 UI를 점진적 마이그레이션 가능(영향 파일: shared/ui + entities/ui 약 20개).
  2. A/B 테스트 도입: feature 수준에서 토글을 주입하는 전략으로 구현하면 위젯/feature 대부분을 건드리지 않고 확장 가능.

🚀 개선 단계

  • 1단계: 단기(half-day): atoms 직접 접근을 wrapper hook으로 감싸는 작업(예: useAuth, usePostState) 시작.
  • 2단계: 중기(2-3일): 중요한 cross-cutting concern(인증, 로깅, i18n)에 대한 adapter 레이어를 설계하고 적용.
  • 3단계: 장기(1-2주): 기술 스택 전환 시 시나리오(예: Jotai→Zustand)별 migration 가이드와 스몰 스텝 마이그레이션 계획 수립.

10. 📏 코드 일관성

💡 개념 정의

코드 일관성은 네이밍, 파일명, import/export 패턴, 코드 스타일(들여쓰기, 세미콜론 등)이 프로젝트 전반에서 통일되어 있는 정도입니다.

⚡ 중요성

일관성은 신규 개발자 온보딩, 코드 탐색, 자동화 도구(리팩토링, 린팅) 적용에 큰 영향을 미칩니다.

📊 현재 상황 분석

긍정: index.ts 통한 re-export, @ 절대경로 사용, shared/ui 일관 사용 등 좋은 점이 많음.
불일치: 파일명 규칙 문서화 필요(특히 pages와 components naming). atoms와 훅 naming 규칙을 통일하면 가독성·검색성이 향상됩니다.

📝 상세 피드백

대부분 파일 네이밍(components/ui: PascalCase, hooks use*: camelCase)과 import 경로(@/...)이 일관되어 있습니다. export 패턴도 entities/index.ts 등에서 named export를 사용해 일관성을 유지하려는 시도가 보입니다.

관찰된 불일치: 일부 파일명(예: pages/posts-manager-page.tsx 소문자 + hyphen)은 컴포넌트 파일 규칙(PascalCase)과 다릅니다. 또한 atoms/훅 변수명(isAddCommentDialogOpenAtom vs isAddOpen naming)에서 약간의 네이밍 혼재가 존재합니다. 코드 스타일은 대부분 세미콜론 사용 등 일관적이지만 가끔 import/quote 스타일이 혼재되어 있었던 커밋 히스토리가 보입니다(현재 PR은 세미콜론 일관 적용).

🚀 개선 단계

  • 1단계: 단기(half-day): 파일명·네이밍 컨벤션 문서화(컴포넌트: PascalCase, hooks: useCamelCase, atoms: camelCaseAtom, pages: PascalCasePage) 및 예외 규칙 정리.
  • 2단계: 중기(half-day): ESLint/Prettier 규칙 강제 적용 및 CI에서 lint 통과를 필수로 만들기.
  • 3단계: 장기(1-2일): 리포지토리-wide 리네이밍(스크립트 사용)으로 규칙 일괄 적용(변경 범위가 크므로 PR로 나눠 적용).

🎯 일관성 체크포인트

파일명 규칙

  • pages/posts-manager-page.tsx (kebab-case) — 컴포넌트 파일은 PascalCase 권장(PostsManagerPage.tsx).

Import/Export 패턴

  • 대부분 named export 사용: 좋음. 일부 파일에서 default export 사용 여부 확인 필요(현재 PR은 주로 named export).

변수명 규칙

  • isAddCommentDialogOpenAtom vs isAddOpen 변수명 혼재(일관된 prefix/suffix 규칙 필요).
  • newPostAtom / newCommentAtom 사용은 일관적이나 일부 setIsAddOpen 등 축약형 사용과 full name 사용 혼재.

코드 스타일

  • PR에서는 세미콜론 사용으로 통일되어 있으나 이전 커밋 히스토리에서 따옴표·세미콜론 혼재가 있었던 흔적이 있음 — ESLint/Prettier로 고정 권장.

11. 🗃️ 상태 관리

💡 개념 정의

데이터 흐름 및 상태관리는 서버-클라이언트 상태를 명확히 구분하고 전역/로컬 상태를 적절히 분리하는 것을 의미합니다.

⚡ 중요성

명확한 상태 책임 분리는 버그 감소, 예측 가능한 업데이트, 테스트 용이성에 기여합니다.

📊 현재 상황 분석

긍정: Query로 서버 상태를 관리하면서 캐싱/로딩/에러 책임이 명확해짐. UI 상태는 Jotai로 간단히 유지됨.
리스크: 일부 기능에서 Query 결과를 UI atom에 싱크하는 패턴(예: prefetch 후 atom에 넣는 방식)이 있다면 중복 상태로 인한 일관성 문제가 발생할 수 있음.

📝 상세 피드백

서버 상태와 클라이언트 UI 상태가 분리되어 있습니다. TanStack Query를 서버 상태(fetch/caching/mutation)로 사용하고, Jotai를 로컬 UI 상태(다이얼로그 열림, 선택된 항목, draft 등)로 사용한 설계는 권장되는 패턴입니다. 과거엔 전역 상태(atom)로 서버 데이터를 관리하던 부분을 Query로 대체해 보일러플레이트가 줄어든 점도 긍정적입니다.

주의: 일부 케이스에서 동일한 데이터가 atom과 query에 중복으로 존재할 수 있는지 여부를 점검해야 합니다(데이터 동기화 이슈). 예를 들어, 댓글을 atoms로 관리하다가 Query로 전환한 과정에서 레거시 atom 사용 지점이 남아있을 수 있으므로 점검 필요합니다.

❌ 현재 구조 (AS-IS)

// AS-IS: 이전 전역 상태 예시(본문)
export const commentsAtom = atom({}); export const isCommentsLoadingAtom = atom(false); // 수동 fetch 및 상태 동기화

// TO-BE: Query 사용으로 server-state 관리
export function useCommentsQuery(postId) { return useQuery({ queryKey: commentQueryKeys.byPost(postId), queryFn: () => commentApi.get(postId) }) }

✅ 권장 구조 (TO-BE)

// 권장: server-state는 Query로, UI-state는 atoms로 명확히 분리
// UI에서 댓글 로딩
const { data } = useCommentsQuery(postId);
// 다이얼로그 열림 등은 atom으로 관리

🔄 변경 시나리오별 영향도

  1. 실시간 기능 추가(WebSocket): 서버 상태를 Query + subscription(예: setQueryData on socket event) 패턴으로 확장하거나 별도의 실시간 store를 도입. 영향 범위는 댓글/포스트 관련 query 훅과 관련 UI(약 6-10개 파일).
  2. 오프라인 모드 추가: Query의 cache/persist 전략과 client-state(queued mutations) 추상화가 필요.

🚀 개선 단계

  • 1단계: 단기(half-day): 프로젝트에서 server-state vs client-state 책임 매핑 문서화(예: 모든 엔티티 목록 데이터는 Query, 다이얼로그/폼 draft는 atoms).
  • 2단계: 중기(1-2일): 레거시 atom으로 서버 데이터를 보관하던 경우(있다면) Audit을 통해 Query로 완전 마이그레이션하거나 atom을 readonly view로 전환.
  • 3단계: 장기(1주): 실시간/오프라인 시나리오에 대비한 전략(WS 수신시 query cache 갱신 정책, 오프라인 큐)을 설계.

🤔 질문과 답변

질문: "FSD의 경우 리팩토링을 상향식으로 하는 것이 좋을지 하향식으로 하는 것이 좋을까요?"

답변(권장 전략): 혼합(hybrid) 접근을 권장합니다. 이유와 단계는 다음과 같습니다.

  1. 상향식(작은 단위부터): 작은 독립 기능(atomic component, 간단한 feature 훅, 단일 entity api)을 먼저 FSD 규칙에 맞게 정리하면 빠른 승리(WIN)를 얻고 리팩토링 위험을 작게 유지할 수 있습니다. 장점: 낮은 리스크, 빠른 피드백, 테스트가 쉬움.
  2. 하향식(큰 경계 정의): 시스템의 경계(도메인, public API, 레이어 책임)를 먼저 설계하면 일관성 있는 폴더 구조와 export contract를 만들 수 있습니다. 장점: 장기적 일관성, 의존성 규칙을 먼저 정해 팀 합의 도출.

추천 절차(실무 팁):

  • 1단계(설계·합의, 반나절~1일): FSD 핵심 규칙(레이어 의존성, index.ts public API, 네이밍 규칙) 및 변환 우선순위(핵심 도메인 정하기)를 팀 합의로 문서화.
  • 2단계(상향식 적용, 반복): 위험이 적고 변경 빈도가 높은 컴포넌트/훅부터 리팩토링(예: shared 컴포넌트, 작은 features).
  • 3단계(하향식 적용): 전체 구조에서의 경계 검증 및 대규모 변경(예: atoms→Query 전환, 패키지 분리)을 계획.
  • 4단계(검증): CI 규칙(레이어 의존성 검사), 테스트, 코드리뷰 템플릿을 도입해 규칙 준수를 강제.

결론: 하향식으로 큰 경계를 먼저 잡아 가이드라인을 확보하고, 실제 리팩토링은 상향식으로 안전하게 수행하되 정기적으로 하향식 검증(아키텍처 리팩토링 스프린트)을 넣는 혼합 방식이 가장 현실적입니다.

🎯 셀프 회고 & 제안

당신의 셀프회고에서 보이는 핵심 인사이트는 'FSD를 통해 추상화 단계와 책임을 의식적으로 맞추는 과정에서 자연스럽게 순수 함수와 재사용 가능한 구조를 만들었다'는 점입니다. 좋은 관찰입니다. 다음 질문을 스스로 또는 팀과 함께 고민해보면 더 깊은 설계 개선으로 이어질 수 있습니다:

  1. 지금의 'layer boundary' 결정은 팀 차원에서 쉽게 이해되고 재현 가능한가? (예: PR에 문서/예시 스니펫을 추가해 규칙을 명시해보세요)
  2. atoms와 Query를 혼합 사용하면서 중복 상태가 발생할 여지가 있는 곳은 어디인가? 자동 검사(예: lint rule)로 감지할 수 있을까요?
  3. 공통 패턴(낙관적 업데이트, prefetch)을 추상화하면 어떤 부분의 변경량이 줄어들지 수치로 제시할 수 있나요? (예: 낙관 업데이트를 shared helper로 분리하면 post/comment mutation 변경 시 수정 파일 수가 X → Y로 줄어듭니다.)

추가 제안: 현재 얻은 인사이트를 바탕으로 작은 문서(README 형태)로 FSD 규칙과 예제(As-Is/To-Be)를 정리해 두면 향후 리팩토링과 팀원 온보딩에 큰 도움이 됩니다.


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

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

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

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.

3 participants