Skip to content

[3팀 김효진] Chapter 2-3. 관심사 🧦분리와 폴더구조🦍#29

Open
hyojin-k wants to merge 44 commits intohanghae-plus:mainfrom
hyojin-k:main
Open

[3팀 김효진] Chapter 2-3. 관심사 🧦분리와 폴더구조🦍#29
hyojin-k wants to merge 44 commits intohanghae-plus:mainfrom
hyojin-k:main

Conversation

@hyojin-k
Copy link

@hyojin-k hyojin-k commented Aug 11, 2025

https://hyojin-k.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를 사용하여 각 계층이 어떤 역할을 가지고 있어야하는지에 대해 학습할 수 있었습니다. 다만, 그 계층 구분의 기준을 명확하게 잡기가 애매하다는 부분이 고민이 많이 되는 부분이었습니다. 사람마다 서로 묘하게 조금씩 다른 기준을 가지고 있다보니 갈팡질팡하는 경우도 많았고, 스스로 정한 기준도 과제를 진행하다보면서 흐트러지는 경우도 있었던 것 같습니다. 어느정도 정답이 정해져있는 듯 하면서 각자 다른 정답을 가지고 있을 수 있는 아키텍처라는 생각이 들었습니다. 그럼에도 불구하고 그 과정에서 고민할 수 있고 학습할 수 있는 경험이 된 것 같습니다.

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

지금까지 클린코드 과제를 진행하면서 컴포넌트를 분리하는 방식은 큰 단위에서 작은 단위로 나누는 방식이었는데, 이 과정에서 느꼈던 불편함을 최소화하고자 이번에는 작은 단위에서 큰 단위로 분리하는 방향으로 진행해보려고했습니다. 이 과정에서 함께 계층 분리에 대한 고민을 많이 했습니다. 특히 entities와 features의 구분에 대한 고민이 많았습니다.

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

FSD 아키텍처를 사용해보긴 했지만 명확한 기준을 정하기도 힘들고, 그걸 유지하는 것도 쉽지않았습니다. 계층 분리에 대해서 정확하게 정립이 되지 않은 채로 과제를 진행하다보니 그런 고민이 더욱 깊었던 것 같습니다. 우선 이번 과제에서는 CRUD 중에서 단순 R 부분을 entities로 분리하고 C,U,D 부분을 features 로 분리하는 방법으로 진행했는데, 이 방식도 여러 방식 중에 제가 이해하기 가장 쉬운 방법을 택한 거라 좀 더 깊은 이해도가 있었다면 좀 더 세밀한 계층분리를 할 수 있지않았을까 싶습니다.

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

tanstack query를 사용하면서 쿼리 키를 관리하는 방법이 실무에서 사용하던 방식과 다른 방식을 사용해보았습니다. 기존에는 쿼리 키들 간의 변별력을 위해, 각 api의 url를 쿼리 키로 사용했었습니다.
이번 과제에서는 키들을 묶어서 관리하는 방법을 사용했는데, 이 방식이 한번에 관리하기에 효율적인 것 같아서 적용해보면 좋을 것 같습니다.

export const commentQueryKeys = {
  all: ["comments"] as const,
  lists: () => [...commentQueryKeys.all, "list"] as const,
  list: (postId: number) => [...commentQueryKeys.lists(), { postId }] as const,
  details: () => [...commentQueryKeys.all, "detail"] as const,
  detail: (id: number) => [...commentQueryKeys.details(), id] as const,
} as const;

챕터 셀프회고

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

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

  • 더티코드를 접했을 때 어떤 기분이었나요? ^^; 클린코드의 중요성, 읽기 좋은 코드란 무엇인지, 유지보수하기 쉬운 코드란 무엇인지에 대한 생각을 공유해주세요

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

  • 거대한 단일 컴포넌트를 봤을때의 느낌! 처음엔 막막했던 상태관리, 디자인 패턴이라는 말이 어렵게만 느껴졌던 시절, 순수함수로 분리하면서 "아하!"했던 순간, 컴포넌트가 독립적이 되어가는 과정에서의 깨달음을 들려주세요

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

  • "이 코드는 대체 어디에 둬야 하지?"라고 고민했던 시간, FSD를 적용해보면서의 느낌, 나만의 구조를 만들어가는 과정, TanStack Query로 서버 상태를 분리하면서 느낀 해방감(?)등을 공유해주세요

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

  • props drilling을 써야하는 상황과 전역상태관리를 써야하는 상황이 구분될 수 있는 걸까요? 아니면 개발자가 선호하는 방향에 따라 정해지는 걸까요?

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 피드백 보고서

현재 남기는 코멘트는 GPT-5 model을 사용하여 AI가 남기는 피드백입니다 🤖

종합 피드백

  • DummyJson 특성 미고려로 인한 mutation 반영 불가 문제와 캐시 전략 부재
  • React Query와 Jotai의 중복 상태 소스(서버 상태 vs 클라이언트 상태) 혼재
  • FSD 레이어 역할 경계 일부 모호(entities/model에 비즈니스 로직/뮤테이션 위치, widgets/feature 분리 기준)
  • Query Key 설계 일관성 부족 및 팩토리 부재
  • 타입 네이밍/일관성(카멜/파스칼 케이스 혼재, skip: string vs number) 및 API 쿼리 파라미터 처리 상 불일치
  • 불필요한 무효화(invalidate) 남용과 낙관적 업데이트 미적용으로 UX 저하
  • PATCH 요청 페이로드 구조 오류(중첩 data 전송)
  • URL 동기화 로직의 page 레이어 과밀/관심사 분리 미흡
  • Devtools 항상 노출(배포 환경 분기 미적용)
  • Toss Frontend Fundamentals 일부 위반(명명 규칙, 불필요한 추상화, 죽은 코드, 에러 UI)

상세 피드백

1) DummyJson 라이브러리 특성 인지 및 대응

긍정: 서버 상태 관리를 위해 React Query를 도입하고 invalidateQueries로 재검증을 시도한 점은 방향성이 좋습니다.

비판:

  • DummyJson은 데이터베이스가 없어 POST/PUT/PATCH/DELETE가 실제 영구 저장되지 않습니다. 현재 구현은 mutate → invalidate → refetch 패턴인데, 리페치 결과가 원본(변경 전) 데이터이므로 사용자는 "추가/수정/삭제/좋아요"가 반영되지 않은 것처럼 보일 수 있습니다.
  • 특히 댓글 추가/삭제/좋아요에서 낙관적 업데이트가 없어 UX가 저하됩니다.

개선 방안 (세 가지 모두 제시):

1) tanstack-query 캐시 직접 수정(낙관적 업데이트)

댓글 추가/삭제/좋아요 시, 서버 응답을 기다리지 않고 queryClient.setQueryData로 즉시 캐시를 수정한 뒤, 실패 시 롤백하세요.

2) MSW(Mock Service Worker)로 목업 백엔드 구성

인메모리 배열을 핸들러 클로저로 캡쳐해 두고, POST/PUT/PATCH/DELETE 시 해당 배열을 갱신하도록 구성하면 "지속되는 것처럼" 동작합니다.

TO-BE (handler 예시):

// src/mocks/handlers/comments.ts
import { Comments } from "@/entities/comments";
import { HttpResponse, http } from "msw";

const db: Comments[] = []; // 인메모리 DB

export const commentHandlers = [
  http.get("/comments/post/:postId", ({ params }) => {
    const postId = Number(params.postId);
    const items = db.filter((c) => c.postId === postId);
    return HttpResponse.json({
      comments: items,
      total: items.length,
      skip: 0,
      limit: items.length,
    });
  }),
  http.post("/comments/add", async ({ request }) => {
    const body = (await request.json()) as { body: string; postId: number; userId: number };
    const newItem: Comments = {
      id: Date.now(),
      likes: 0,
      user: { id: body.userId, username: "me", fullName: "Me" },
      ...body,
    };
    db.unshift(newItem);
    return HttpResponse.json(newItem, { status: 201 });
  }),
  http.patch("/comments/:id", async ({ params, request }) => {
    const id = Number(params.id);
    const patch = (await request.json()) as Partial<Comments>;
    const idx = db.findIndex((c) => c.id === id);
    if (idx >= 0) db[idx] = { ...db[idx], ...patch };
    return HttpResponse.json(db[idx]);
  }),
  http.delete("/comments/:id", ({ params }) => {
    const id = Number(params.id);
    const idx = db.findIndex((c) => c.id === id);
    if (idx >= 0) db.splice(idx, 1);
    return new HttpResponse(null, { status: 204 });
  }),
];

3) Express.js로 인메모리 백엔드 구성

로컬에서 간단한 서버를 띄워 in-memory DB로 CRUD 영속을 시뮬레이션할 수 있습니다.

TO-BE (간단 서버 예시):

// server/index.ts
import cors from "cors";
import express from "express";

const app = express();
app.use(cors());
app.use(express.json());

type Comment = {
  id: number;
  body: string;
  postId: number;
  likes: number;
  user: { id: number; username: string; fullName: string };
};
const comments: Comment[] = [];

app.get("/comments/post/:postId", (req, res) => {
  const postId = Number(req.params.postId);
  const list = comments.filter((c) => c.postId === postId);
  res.json({ comments: list, total: list.length, skip: 0, limit: list.length });
});

app.post("/comments/add", (req, res) => {
  const { body, postId, userId } = req.body;
  const item: Comment = {
    id: Date.now(),
    body,
    postId,
    likes: 0,
    user: { id: userId, username: "me", fullName: "Me" },
  };
  comments.unshift(item);
  res.status(201).json(item);
});

app.patch("/comments/:id", (req, res) => {
  const id = Number(req.params.id);
  const idx = comments.findIndex((c) => c.id === id);
  if (idx < 0) return res.status(404).end();
  comments[idx] = { ...comments[idx], ...req.body };
  res.json(comments[idx]);
});

app.delete("/comments/:id", (req, res) => {
  const id = Number(req.params.id);
  const idx = comments.findIndex((c) => c.id === id);
  if (idx >= 0) comments.splice(idx, 1);
  res.status(204).end();
});

app.listen(3001, () => console.log("Mock API on 3001"));

2) React Query 캐시 수정(낙관적 업데이트) + PATCH 페이로드 버그

긍정: commentsKeys를 사용하고, invalidateQueries로 일관되게 재검증하는 흐름을 도입했습니다.

비판:

  • 좋아요 PATCH 요청이 잘못된 페이로드를 전송합니다. 현재 구현은 { data: { likes } } 형태로 보내고 있습니다.
  • 낙관적 업데이트가 없어 UI가 즉각 반응하지 않습니다.
  • useCommentList.fetchComments는 단순 무효화로 실질적 역할이 약합니다(죽은 코드에 가깝습니다).

AS-IS (문제 코드):

// src/entities/comments/model/index.ts
export const updateCommentsLikes = async (commentId: number, data: UpdateCommentsLikesRequest) => {
  return axiosInstance.patch<UpdateCommentsLikesResponse>(`/comments/${commentId}`, {
    data, // <-- 잘못된 중첩
  });
};

TO-BE (페이로드 수정 + 낙관적 업데이트 예시):

// src/entities/comments/model/index.ts
export const updateCommentsLikes = async (commentId: number, data: UpdateCommentsLikesRequest) => {
  return axiosInstance.patch<UpdateCommentsLikesResponse>(`/comments/${commentId}`, data);
};

// src/features/comments/model/queries.ts (예시)
export const useLikeCommentMutation = (postId: number) => {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: ({ commentId, likes }: { commentId: number; likes: number }) =>
      updateCommentsLikes(commentId, { likes }),
    onMutate: async ({ commentId, likes }) => {
      await queryClient.cancelQueries({ queryKey: commentsKeys.list(postId) });
      const prev = queryClient.getQueryData<Comments[]>(commentsKeys.list(postId));
      // 즉시 캐시 반영
      queryClient.setQueryData<Comments[]>(commentsKeys.list(postId), (old = []) =>
        old.map((c) => (c.id === commentId ? { ...c, likes } : c)),
      );
      return { prev };
    },
    onError: (_err, _vars, ctx) => {
      if (ctx?.prev) queryClient.setQueryData(commentsKeys.list(postId), ctx.prev);
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: commentsKeys.list(postId) });
    },
  });
};

댓글 추가/삭제도 동일한 패턴으로 onMutate/onError/onSettled를 구성하세요. 현재 useAddComment/useCommentList에서 mutateAsync 후 invalidate만 하는 부분을 위 패턴으로 개선 권장합니다.


3) Toss Frontend Fundamentals 관점

긍정:

  • 함수/훅 네이밍이 비교적 의도가 드러나며, early return(예: 선택된 포스트 없을 때)도 잘 활용하고 있습니다.
  • 관심사 분리와 재사용성(entities/functions, features/hooks)이 전반적으로 의식되어 있습니다.

비판 및 개선:

  • 타입 네이밍: Type/Interface는 PascalCase가 권장됩니다. getPostsParams, getUsersData 등은 GetPostsParams, GetUsersData로 개선하세요.
  • 일관성: skip이 string으로 정의되어 있으나 사용부에서 숫자-문자열이 섞여 있습니다. 한 곳(number)으로 통일하고 URLSearchParams로 변환 시점에만 문자열화하세요.
  • 중복과 죽은 코드: useCommentList.fetchComments는 invalidate 래퍼에 불과합니다. 삭제하거나 실제 책임을 부여하세요. 또한 Jotai의 commentsAtom를 안 쓰고 있다면 정리 필요.
  • 오류 처리: UI에서 에러를 console.error로만 처리합니다. 사용자 피드백(Toast/Alert)을 추가하고, 뮤테이션 중 버튼 비활성화 등 접근성/피드백 제공이 필요합니다.
  • 매직 넘버: staleTime(5분), limit 기본값(10) 등은 상수로 분리하세요.
  • Devtools: 프로덕션에서 열려 있지 않도록 분기 처리하세요.

AS-IS:

export interface getPostsParams {
  limit?: number;
  skip?: string;
}

TO-BE:

export interface GetPostsParams {
  limit?: number;
  skip?: number;
}

// 사용 시
const searchParams = new URLSearchParams({
  limit: String(params.limit ?? 0),
  skip: String(params.skip ?? 0),
});

Devtools 분기:

{
  import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />;
}

4) FSD 아키텍처(공식 가이드) 준수도

긍정:

  • entities/posts|comments|users로 핵심 데이터 소스를 분리했고, pages → features로 도메인 기능을 내렸습니다.
  • shared/ui, shared/utils(axios) 등 공용 레이어 분리가 보입니다.

비판:

  • "entities/데이터/api = GET, entities/데이터/model = 나머지"로 나눈 의사결정은 일관되게 "요청 함수의 물리적 위치"를 강제할 위험이 있습니다. FSD의 핵심은 "사용 지점에서 의미 있는 형태로 노출"입니다.
  • 원시 요청 함수(raw request)는 entities나 shared/api에 둬도 되지만, "useMutation/useQuery 훅"과 같이 UI에 붙는 모델은 보통 features(or entities)에서 "케이스별 훅"으로 감쌉니다.
  • 현재는 features에서 use*Mutation 훅을 만들면서, raw 요청은 entities에 있는 구조인데, 이는 합리적입니다. 다만 "GET만 api"라는 규칙은 굳이 강제하지 않아도 됩니다. 기준은 "다른 레이어에서 재사용되어 가치가 있는가(entities)"와 "유스케이스 기반 묶음(feature)"입니다.

개선 제안:

  • entities에는 "재사용 가능한 원시 모델(타입, 원시 요청 함수, 변환기)"까지만 두고,
  • features에는 "유스케이스 단위 훅(useAddComment, useLikeComment 등)"과 UI를 둬서 조립하세요.
  • widgets(아래 참조)는 페이지-레벨 조립에서 공통/복합 UI 블록을 모읍니다.

5) 변형된 FSD(테오 코치: base(shared)/entities/features/modules(widgets)/page)

긍정:

  • Header/Footer 같은 레이아웃/공용 컴포넌트를 widgets로 이동한 점은 변형 구조 의도와 부합합니다.
  • "페이지는 UI 집합, 비즈니스 로직은 features로"라는 기준을 명시적으로 잡은 점이 좋습니다.

비판/정교화:

  • 한때 widgets에 "데이터를 받아 사용하는 컴포넌트"를 모두 넣으려 했던 판단은 과하였습니다. widgets는 "복합 UI 블록"으로서 재사용 가능한 수준에서 데이터/이벤트 바인딩의 정도가 낮은 편이 자연스럽습니다. 도메인 유스케이스(예: AddPostDialog, PostDetailView 등)는 features가 적합합니다.
  • 현재 App.tsx에서 { Footer, Header }를 widgets에서 import하는데, widgets barrel과 실제 파일 경로/이동 이력이 PR에 모두 포함되어 있지 않아 참조 경로 일관성(패스 별칭 포함)을 재점검하세요.

추천 기준:

  • shared: 순수한 UI 원자/함수, 공통 유틸
  • entities: 도메인 모델/타입/원시 API/파서(도메인 불변식을 아는 레벨)
  • features: 유스케이스 단위 훅 + UI(사용자의 행동 단위)
  • widgets(modules): 페이지 안에서 조립되는 복합 UI 블록(여러 features를 조합)
  • pages: 라우트 단위, 조립만 담당

6) Query Key Factory 도입 제안

설명: @lukemorales/query-key-factory는 Query Key를 "도메인별 이름있는 팩토리"로 생성해 재사용성과 일관성을 확보하게 해 줍니다. 중첩 키, 파라미터화, 스코프 분리를 체계적으로 할 수 있고 오타/중복을 예방합니다.

TO-BE (도입 예시):

// src/shared/queryKeys.ts
import { createQueryKeys } from "@lukemorales/query-key-factory";

export const commentsKeys = createQueryKeys("comments", {
  list: (postId: number) => ["list", postId],
  detail: (id: number) => ["detail", id],
});

export const postsKeys = createQueryKeys("posts", {
  list: (params: { skip: number; limit: number; tag?: string; q?: string }) => ["list", params],
  detail: (id: number) => ["detail", id],
});

사용:

// v5
useQuery({
  queryKey: commentsKeys.list(postId).queryKey,
  queryFn: () => getCommentsPost(postId),
});
queryClient.invalidateQueries({ queryKey: commentsKeys.list(postId).queryKey });

효과:

  • 키 생성 규칙이 코드 한 곳에 모이고, 파라미터 모양이 타입 안전해집니다.
  • invalidate, setQueryData 등 모든 캐시 조작 포인트에서 키 일관성 유지가 쉬워집니다.

7) React Query vs Jotai: 상태 소스 단일화

긍정: Jotai로 "대화상자 열림/닫힘" 같은 UI 상태와 "선택된 댓글/새 댓글" 같은 폼 상태를 관리하는 것은 적합합니다.

비판: commentsAtom(서버 데이터)와 useCommentsQuery(서버 상태)의 중복 소스가 보입니다. 서버에서 오는 리스트는 React Query로 단일화하고, 폼/선택 상태만 Jotai로 관리하세요. 현재 사용하지 않는 atom/훅은 제거하여 복잡도를 낮추길 권합니다.

AS-IS:

// src/features/comments/model/hooks.ts
export const useComments = () => {
  const [comments, setComments] = useAtom(commentsAtom)
  ...
}

TO-BE:

// 서버 리스트는 전부 React Query로
// Jotai는 selectedComment, newComment, dialog open 등 UI 전용으로 축소

8) URL 동기화 useEffect의 위치와 구조

페이지 레이어에서 라우팅/URL 파라미터 동기화는 맞는 선택에 가깝습니다. 다만 이 로직을 "재사용 가능한 훅"으로 추출해 pages에서 호출하면 관심사가 명확해집니다. React Router의 useSearchParams를 적극 활용해 양방향 동기화를 선언적으로 만드는 것을 추천합니다.

AS-IS:

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

useEffect(() => {
  const params = new URLSearchParams(location.search)
  setSkip(parseInt(params.get("skip") || "0"))
  ...
}, [location.search, setSkip, ...])

TO-BE:

// src/shared/lib/router/usePostsSearchParams.ts
import { useSearchParams } from "react-router-dom";

export function usePostsSearchParams() {
  const [sp, setSp] = useSearchParams();
  const state = {
    skip: Number(sp.get("skip") ?? 0),
    limit: Number(sp.get("limit") ?? 10),
    sortBy: sp.get("sortBy") ?? "",
    sortOrder: sp.get("sortOrder") ?? "asc",
    tag: sp.get("tag") ?? "",
    q: sp.get("q") ?? "",
  };
  const setState = (patch: Partial<typeof state>) => {
    const next = new URLSearchParams(sp);
    Object.entries(patch).forEach(([k, v]) => next.set(k, String(v)));
    setSp(next, { replace: true });
  };
  return [state, setState] as const;
}

// pages
const [params, setParams] = usePostsSearchParams();
// 내부 상태 없이 params를 직접 소스로 삼거나,
// 필요 시 Zustand/Jotai와 동기화 훅을 얇게 작성

이렇게 하면 "URL이 곧 소스 오브 트루스"가 되고, 이펙트 난립을 줄일 수 있습니다.


9) PostDetailDialog children 설계

현재처럼 children으로 CommentList를 받는 것은 "컴포지션" 관점에서는 좋습니다. 다만 레이어 규칙 상 "features/ui 내부에서 features/ui를 children으로" 넣는 것이 어색하게 느껴졌다면, 조립 책임을 widgets로 끌어올리는 방법이 더 자연스럽습니다. 즉, PostDetailDialog는 widgets에 두고, features의 CommentList를 children으로 주입하는 식입니다.

또는 "slot props" 패턴으로 명시적 슬롯을 제공하면 읽기성이 좋아집니다.

AS-IS:

<PostDetailDialog children={<CommentList />} />

TO-BE (컴포지션):

<PostDetailDialog>
  <CommentList />
</PostDetailDialog>

TO-BE (슬롯 패턴):

<PostDetailDialog footer={<AddCommentButton />} body={<CommentList />} />

10) 세부 코드/타입 개선 포인트

  • API 파라미터 생성 중복 제거: URLSearchParams 조립이 posts/users에서 반복됩니다. 공통 유틸(예: buildSearchParams)로 추출하세요.
  • axiosInstance: 에러 인터셉터/응답 인터셉터로 .data 언래핑, 공통 헤더, 에러 메시지 통일을 적용하면 소비 측 코드가 단순해집니다.
  • highlightText의 위치: UI 로직이 아니라 문자열 유틸에 가깝다면 shared/lib로 이동을 고려하세요.
  • 버튼 상태: isPending 동안 버튼 비활성화 및 스피너 표시를 권장합니다.

11) 셀프 회고 분석과 인사이트 제안

매우 좋은 고민을 촘촘히 기록하신 점이 인상적이었습니다. 다음 인사이트를 확장 제안드립니다.

  • Props Drilling vs 전역 상태: "전역으로 뺐더니 어떤 컴포넌트가 어떤 상태/액션을 쓰는지 추적이 어렵다"는 통찰이 핵심입니다. 해결책은 "소유권을 가장 가까운 공통 조상으로 올리고(co-location), 진입점에서 훅을 주입"하는 방식입니다. FSD에서는 보통 features 단위에서 훅/상태를 캡슐화하여 외부에는 명확한 props/API만 노출합니다. 전역 상태는 "전역이어야만 하는 것(라우트/세션/테마/토스트)"으로 좁히세요.

  • 응집도: "높은 응집도를 위해 시간과 노력이 많이 든다"는 반성 또한 정확합니다. 이를 가속하려면 테스트(컴포지션/계약 테스트)와 스토리북을 병행하여 "모듈 경계가 바뀌면 어디가 깨질지"를 가시화해보세요.

  • 타입 재가공 위치: page에서만 쓰는 타입을 page에 두었다가 위반이 생긴 경험은 좋습니다. "소비자 레이어가 둘 이상"이면 shared(types)/entities(adapters)로 올려 공통 포맷(뷰 모델)을 제공하세요. 서버 응답 모델과 뷰 모델을 분리하면 레이어 간 결합도도 낮아집니다.

  • FSD가 좋은가?: 패턴 자체보다 "팀/도메인/스케일에 맞춘 합리적 변형"이 더 중요합니다. 이번 변형(FEATURE 중심 + widgets 조립)은 올바른 방향입니다. 다만 규칙을 과도하게 경직시키지 말고 "사용 지점에서 의미 있는 경계"를 우선시하면 더 유연해집니다.


AS-IS → TO-BE 요약 스니펫 모음

1) 좋아요 PATCH 페이로드

// AS-IS
axiosInstance.patch(`/comments/${commentId}`, { data });

// TO-BE
axiosInstance.patch(`/comments/${commentId}`, data);

2) 댓글 추가 낙관적 업데이트

// AS-IS (useAddComment)
await addCommentMutation.mutateAsync(newComment);
queryClient.invalidateQueries({ queryKey: commentsKeys.list(newComment.postId) });

// TO-BE (useAddCommentMutation 내부)
useMutation({
  mutationFn: createComments,
  onMutate: async (newComment) => {
    const postId = newComment.postId!;
    await queryClient.cancelQueries({ queryKey: commentsKeys.list(postId) });
    const prev = queryClient.getQueryData<Comments[]>(commentsKeys.list(postId));
    const optimistic: Comments = {
      id: Date.now(),
      body: newComment.body,
      postId,
      likes: 0,
      user: { id: newComment.userId, username: "me", fullName: "Me" },
    };
    queryClient.setQueryData<Comments[]>(commentsKeys.list(postId), (old = []) => [
      optimistic,
      ...old,
    ]);
    return { prev, postId };
  },
  onError: (_e, _v, ctx) => {
    if (ctx?.prev) queryClient.setQueryData(commentsKeys.list(ctx.postId), ctx.prev);
  },
  onSettled: (_d, _e, _v, ctx) => {
    queryClient.invalidateQueries({ queryKey: commentsKeys.list(ctx!.postId) });
  },
});

3) 삭제 낙관적 업데이트

useMutation({
  mutationFn: deleteComments,
  onMutate: async (commentId: number) => {
    await queryClient.cancelQueries({ queryKey: commentsKeys.list(postId) });
    const prev = queryClient.getQueryData<Comments[]>(commentsKeys.list(postId));
    queryClient.setQueryData<Comments[]>(commentsKeys.list(postId), (old = []) =>
      old.filter((c) => c.id !== commentId),
    );
    return { prev };
  },
  onError: (_e, _v, ctx) => {
    if (ctx?.prev) queryClient.setQueryData(commentsKeys.list(postId), ctx.prev);
  },
  onSettled: () => queryClient.invalidateQueries({ queryKey: commentsKeys.list(postId) }),
});

4) 타입 네이밍/일관성

// AS-IS
export interface getUsersData { ... }

// TO-BE
export interface GetUsersData { ... }

5) Devtools 분기

{
  import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />;
}

이번 PR로 확인된 장점

  • React Query, Jotai, FSD 도입을 통해 구조화를 적극적으로 시도했습니다.
  • 셀프회고의 깊이가 충분하여 다음 단계로의 점프업이 기대됩니다.
  • shared/ui, entities, features의 물리적 분리는 잘 출발했습니다.

이번 PR로 꼭 개선했으면 하는 점 (우선순위 순)

  1. DummyJson 특성에 맞춘 캐시 수정(낙관적 업데이트) 또는 MSW/Express 목 서버 도입
  2. PATCH 페이로드 수정과 타입/파라미터 일관성 정리
  3. React Query vs Jotai 상태 소스 중복 제거
  4. URL 동기화 훅 추출 및 페이지 단순화
  5. Query Key Factory 도입으로 키/무효화 일관성 확보

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)은 기능별(Feature)/도메인(Entities)/공유(Shared)/위젯(Widget)/페이지(Page)/애플리케이션(App) 계층으로 관심사를 분리하고, 하위 계층만 상위 계층에게 제공함으로써 책임을 명확히 하는 폴더 구조 원칙입니다. 핵심은 의존성 방향과 각 계층의 ‘공개 API(index)’를 통한 접근 제어입니다.

⚡ 중요성

아키텍처 일관성은 변화에 대한 유연성(기술 스택 전환, 아키텍처 분리, 기능 확장)을 좌우합니다. 잘 정리된 FSD는 새로운 feature 추가 시 수정 파일 수를 줄이고, 모노레포 전환·마이크로프론트엔드 분리 시 경계를 명확히 해줍니다.

📊 현재 상황 분석

좋은 점: entities/post, entities/comment, entities/user로 도메인 구분이 되어 있고 각 도메인에 model/types, api, ui, store가 있음. features/post와 features/comment는 다이얼로그·테이블·훅을 통해 비즈니스 로직을 분리. 문제점:

  • 일부 modules가 계층 규칙을 흐릴 소지가 있음(예: features가 entities를 참조하는 것은 OK이나 entities가 features를 참조하면 위반). 현재 PR에서는 entities → features 직접 참조는 보이지 않지만, shared/ui와 features/components의 경계가 애매해질 수 있음.
  • public API 규칙(각 slice는 index.ts로 외부 노출)이 일부 잘 지켜짐(comment/api/index.ts 존재)이나 모든 slice에 일관되게 적용됐는지는 불명확.
    구체적 파일 근거: src/pages/PostsManagerPage.tsx가 대규모 리팩터로 축소되어 features와 entities 훅을 사용하는 쪽으로 바뀌었음(이전 로직이 전부 제거되고 usePosts, PostTable 등으로 분리됨).

📝 상세 피드백

전체 폴더 구조가 Feature-Sliced Design(FSD) 철학을 따르려는 의도가 명확합니다. entities(도메인 모델, api, ui), features(비즈니스 행동/다이얼로그), widgets(Header/SearchBar/Footer), shared(디자인 토큰/컴포넌트/라이브러리), app(provider와 App 진입점)으로 분리되어 있어 책임 경계가 비교적 잘 드러납니다. 다만 몇몇 파일에서 계층 간 의존성 방향(상위→하위)이 혼재되거나, public API(index) 노출 규칙이 일관되지 않아 FSD의 장점을 온전히 살리지 못할 수 있습니다.

❌ 현재 구조 (AS-IS)

AS-IS: entities/comment/ui, entities/comment/api, features/comment/ui가 존재하지만 일부 shared/ui 함수(: highlightText) shared/lib로 분리되어 있어 재사용 가능한 구조. 그러나 PostsManagerPage 초기 커다란 파일(기존 버전이 주석 형태로 존재)에서  파일에 많은 책임이 모여 있었음(리팩터 ).

✅ 권장 구조 (TO-BE)

TO-BE:  slice의 외부 노출은 해당 폴더의 index.ts만 사용하도록 강제. : import { useGetPostsQuery } from 'entities/post' 대신 import { postApi } from 'entities/post/api' 대신 entities/post/index.ts에서 노출하여 상위 계층은 'entities/post'로만 접근.

🔄 변경 시나리오별 영향도

  1. UI 라이브러리 교체(Material UI → Chakra): shared/ui 하위 컴포넌트(예: Button, Dialog, Table 등)만 교체하면 되고, features/widgets는 동일한 API로 동작하면 대부분 변경 불필요. 현재 구현은 shared/ui가 추상화되어 있어 영향도가 낮음(약 8-12 파일 수정 예상).
  2. 도메인 변경(예: post의 author 필드 구조 변경): entities/post/model/types.ts와 postApi, post hooks(entities/post/api/hooks.ts), features에서 author를 사용하는 PostTable/PostDetailDialog 등 총 약 6-12개 파일 영향.
  3. 모노레포 전환 시 패키지 독립화: entities, features, shared를 패키지 단위로 분리할 때 현재 구조는 비교적 준비되어 있으나 각 slice의 public index와 의존성 명확화가 필요.

🚀 개선 단계

  • 1단계: 단기(1-2일): 각 slice에 index.ts(공개 API)를 점검하여 의도치 않은 내부 경로 노출 제거. (예상 4~6 파일 작업)
  • 2단계: 중기(2-4일): 의존성 방향 검사 스크립트(ESLint 규칙 또는 dependency-cruise) 추가: FSD 계층을 위반하는 import를 자동으로 검사하도록 설정.
  • 3단계: 장기(1~2주): 모듈별 boundary 테스트(패키지화 준비): entities, features, shared를 독립 패키지로 분리했을 때 최소한의 변경으로 동작하는지 검증.

2. 🔄 TanStack Query

💡 개념 정의

TanStack Query는 서버 상태 관리 라이브러리로 쿼리 키 기반 캐싱, 비동기 상태의 선언적 관리, 자동 재시도 및 캐싱 전략을 제공합니다. 핵심은 queryKey의 일관성, queryFn의 순수성, 그리고 클라이언트/서버 상태 분리입니다.

⚡ 중요성

API 엔드포인트 변경, 새로운 데이터 소스 추가, 에러 정책 변경 시 수정 범위를 최소화합니다. 또한 낙관 업데이트와 캐시 전략은 UX와 네트워크 비용에 직접적인 영향을 미칩니다.

📊 현재 상황 분석

좋은 점:

  • queryKeys 팩토리 패턴 적용: postQueryKeys.list(params), commentQueryKeys.list(postId) 등.
  • API 호출 추상화: shared/api/createApi를 통해 HTTP 구현을 캡슐화.
  • 낙관적 업데이트(onMutate)와 후속 보정(onSuccess) 구현(예: useCreatePostMutation).
    문제점/개선점:
  • queryKey에 객체(params)를 직접 넣음(예: postQueryKeys.list({limit, skip, sortBy,...})). 객체 키는 직렬화/동등성 이슈를 유발할 수 있어, 안정적 비교를 위해 primitive 조합 또는 stable serializer 사용 권장.
  • 에러 처리 방식이 일부에선 console.error/alert 수준으로 흩어져 있음(일관된 사용자 친화적 에러 전략과 추적 로깅 필요).
  • useGetUserQuery의 signature가 useGetUserQuery(user: UserType) 형태로 user 전체를 받는데, 보통 id만 받아서 키를 단순화하는 것이 안전.

📝 상세 피드백

TanStack Query 사용 패턴이 잘 적용되어 있습니다. 쿼리 키 관리(queryKeys 모듈)가 체계적으로 되어 있고(entities/post/api/queryKeys.ts, entities/comment/api/queryKeys.ts 등), api 계층(createApi)와 hooks(useQuery/useMutation) 분리가 명확합니다. 캐싱/낙관 업데이트(onMutate/onSuccess 로직)도 구현되어 있어 변화에 대한 회복력과 성능 측면에서 긍정적입니다. 다만 몇 가지 개선 포인트(쿼리 키의 파라미터 직렬화 일관성, invalidate vs setQueryData 사용의 기준, 에러 처리 일관성)가 있습니다.

❌ 현재 구조 (AS-IS)

AS-IS: postQueryKeys.list(params) params 객체를 그대로 포함함:
postQueryKeys.list: (params) => [...postQueryKeys.lists(), params] as const
useGetPostsQuery uses that key directly.

✅ 권장 구조 (TO-BE)

TO-BE: queryKey를 primitive 조합(또는 안정적 문자열 변환)으로 변경:
postQueryKeys.list: (limit, skip, sortBy, sortOrder) => [...postQueryKeys.lists(), limit, skip, sortBy || 'none', sortOrder || 'asc'] as const
그리고 useGetPostsQuery에서 인자 분해로 호출하여 불필요한 재랜더링/재요청 방지.

🔄 변경 시나리오별 영향도

  1. API 엔드포인트 변경(경로/응답 스키마 변경): 현재 구조는 api 레이어(createApi, postApi 등)에서만 수정하면 hooks와 UI는 대부분 영향 적음. 예상 변경 파일: entities//api/ (3~6 파일).
  2. 새 데이터 소스 추가(예: 외부 캐시 서버): queryKey 규칙과 queryFn 분리가 잘 되어 있으므로 새로운 source를 추가해도 기존 훅의 변경은 최소화 가능.
  3. 에러 처리 정책 변경(예: 사용자 알림 통합): 현재 분산된 onError/console.error 패턴을 중앙 에러 핸들러로 통합하면 전체 코드 수정량은 moderate.

🚀 개선 단계

  • 1단계: 단기(반나절): queryKeys 객체 중 params로 객체를 넣는 부분을 primitive 조합으로 변경하여 키 안정성 확보(예상 1-2시간).
  • 2단계: 단기(1일): 공통 에러 핸들러(예: shared/api/errorHandler) 도입 및 useMutation/onError에서 일관적으로 사용하도록 리팩터(2-4파일).
  • 3단계: 중기(2-3일): 쿼리 staleTime/cacheTime 정책을 각 리소스 특성에 맞춰 튜닝(예: comments는 짧게, posts 목록은 중간값).
  • 4단계: 장기(1주): TanStack Query Devtools와 모니터링 연동 및 SSR/SSG(필요 시) 전략 검토.

3. 🎯 응집도 (Cohesion)

💡 개념 정의

응집도(Cohesion)는 한 모듈(또는 폴더)이 얼마나 단일 책임에 집중되어 있는지를 나타내는 지표입니다. 높은 응집도는 관련 변경이 한곳에서 발생하도록 하여 유지보수를 용이하게 합니다.

⚡ 중요성

기능 추가(예: 댓글 기능 확장) 시 수정해야 할 파일 수를 줄여 개발 속도와 안정성을 높입니다. 또한 패키지화 시 모듈을 독립적으로 떼어내기 쉬워집니다.

📊 현재 상황 분석

좋음: post/comment/user 관련 파일이 도메인 단위로 모여 있어 이해하기 쉬움. 개선 여지: PostState(store)가 pagination, selectedUser, searchQuery, newPost 등을 함께 관리. 일부 행동(예: 태그 선택, 페이징, 검색)은 별도의 작은 store 또는 훅으로 분리 가능(응집도 향상).

📝 상세 피드백

응집도는 전반적으로 높은 편입니다. entities 내부에 타입/모델/api/ui가 모여 있고, features는 UI 행동/다이얼로그를 담당해 변경 시 수정 범위가 좁습니다. 그러나 일부 상태(예: PostStore의 pagination/search/sort 등)가 거대해져 한곳에 많은 책임이 모여있어 세분화가 가능해 보입니다.

❌ 현재 구조 (AS-IS)

AS-IS: usePostStore가 pagination, newPost, selectedUser까지 모두 담당( src/entities/post/model/store.ts ).

✅ 권장 구조 (TO-BE)

TO-BE: 기능 관점으로 store를  세분화:
- usePostPaginationStore({ skip, limit, setSkip, setLimit })
- usePostEditorStore({ newPost, setNewPost, resetNewPost })
이렇게 하면 게시물 편집 로직과 페이지네이션 로직의 응집도가 높아집니다.

🔄 변경 시나리오별 영향도

  1. 다국어 지원(i18n) 추가: UI 문자열은 widgets/shared으로 분리되어 있으면 영향 적음. 현재 문구가 컴포넌트에 하드코딩돼 있어 shared/i18n로 추출 필요(관련 파일 약 20여곳).
  2. A/B 테스트 추가: presentation 컴포넌트가 분리되어 있으면 실험용 컴포넌트 주입이 쉬움. 현재 구조는 분리되어 있어 비교적 수월.

🚀 개선 단계

  • 1단계: 단기(반나절): usePostStore에서 '새 게시물 편집(newPost)/편집 관련 액션'을 별도 모듈로 분리(예상 3-4파일 변경).
  • 2단계: 중기(1-2일): 검색/필터/페이지네이션을 담당하는 작은 store(또는 훅)로 분리하여 책임을 명확히 함.
  • 3단계: 장기(3-5일): 각 도메인별로 단위테스트를 작성하여 변경 시 응집도 보장을 자동화.

4. 🔗 결합도 (Coupling)

💡 개념 정의

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

⚡ 중요성

HTTP 클라이언트(axios→fetch) 변경, 상태관리 라이브러리 변경(zustand→jotai) 등 환경 변화에 대해 수용력을 결정합니다.

📊 현재 상황 분석

좋은 점: HTTP 레이어 추상화. 나쁜 점: Zustand에 강하게 결합된 store 사용으로 상태관리 교체 비용이 높음. 예: src/entities/post/model/store.ts와 src/entities/comment/model/store.ts는 create from 'zustand'에 종속.

📝 상세 피드백

전체적으로 인터페이스 기반 결합을 잘 유지하려는 시도가 보입니다(shared/api/createApi, entities/*/api/*와 hooks 분리). 다만 컴포넌트들이 shared/ui에 구체적 클래스명/스타일 규약에 의존하고 있고, 일부 훅은 store와 직접 강하게 결합되어 있어 상태관리 라이브러리 변경 시 영향이 큽니다.

❌ 현재 구조 (AS-IS)

AS-IS: useCommentStore = create<CommentStore>((set) => ({ ... })) 형태로 Zustand에 직접 의존.

✅ 권장 구조 (TO-BE)

TO-BE: store 접근을 인터페이스(wrapper) 추상화하거나 provider를 통해 의존성 주입. : createCommentStoreAdapter에서 get/set 메서드만 노출하여 내부 구현을 교체 가능하게 .

🔄 변경 시나리오별 영향도

  1. 상태관리 변경(zustand→jotai): 현재 store API가 직접적으로 사용되는 컴포넌트/훅(약 2030곳)을 리팩터해야 함. 비용: moderate(13일) depending on exports abstraction.
  2. HTTP 클라이언트 변경(내장 fetch로 통합): createApi에서만 변경하면 대부분 훅/컴포넌트 영향 없음(1파일 변경으로 가능).

🚀 개선 단계

  • 1단계: 단기(반나절): zustand store를 래핑하는 adapter(간단한 인터페이스)를 만들어 컴포넌트/훅은 adapter를 통해 접근하도록 변경(예상 2-4파일).
  • 2단계: 중기(1-2일): shared/api와 shared/ui의 public API 문서화(간단한 README)로 교체 시 가이드 제공.
  • 3단계: 장기(3-5일): 의존성 변경 시나리오(예: zustand→jotai) 테스트 및 마이그레이션 스크립트 마련.

5. 🧹 Shared 레이어 순수성

💡 개념 정의

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

⚡ 중요성

새로운 프로젝트에 shared 레이어를 재사용하거나 디자인 시스템을 교체할 때 영향을 최소화합니다.

📊 현재 상황 분석

좋음: shared/ui는 범용성 확보. 체크포인트: shared/ui 내부에서 도메인 타입(import '@/entities/...')을 참조하는지 확인할 것(현재 PR에서 발견되지는 않음).

📝 상세 피드백

shared 레이어가 UI 컴포넌트(Button, Dialog, Table 등), lib(highlightText, urlManager), api(createApi)로 잘 분리되어 있어 재사용성이 높습니다. 다만 shared/ui 컴포넌트에 app/도메인 특화 로직이 들어가 있지 않은지 점검 필요(현재는 도메인 의존성 없이 범용적 구현).

❌ 현재 구조 (AS-IS)

AS-IS: src/shared/ui/Button.tsx, Dialog.tsx, Table.tsx  범용 컴포넌트 존재.

✅ 권장 구조 (TO-BE)

TO-BE: shared/ui의 모든 컴포넌트가 문서화된 public props(README) Storybook 스토리를 가짐. 디자인 교체  story 기반으로 컴포넌트만 교체.

🔄 변경 시나리오별 영향도

  1. 디자인 시스템 교체(MUI→Chakra): shared/ui 내부 컴포넌트 812개 수정으로 대부분 해결 가능하며, features/widgets는 거의 변경 불필요(대략 2030개 사용 지점만 스타일 prop 적응 필요).
  2. 새 프로젝트 재사용: shared/ui와 shared/lib로 잘 추상화되어 있어 재사용 용이.

🚀 개선 단계

  • 1단계: 단기(반나절): shared/ui 컴포넌트별 간단한 사용 예시와 props 문서(README) 추가.
  • 2단계: 중기(1-2일): Storybook 도입 또는 기존 컴포넌트 스냅샷을 작성해 디자인 변경 시 회귀 확인.
  • 3단계: 장기: 디자인 토큰 추상화(테마 제공)로 스타일 시스템 교체 비용 최소화.

6. 📐 추상화 레벨

💡 개념 정의

추상화 수준은 비즈니스 로직과 기술적 세부사항(HTTP, 상태관리)을 분리해 재사용 가능한 인터페이스만을 노출하는 정도를 의미합니다.

⚡ 중요성

추상화가 잘 되어 있으면 기술 스택 변경(예: axios→fetch, zustand→jotai) 시 영향 범위를 낮출 수 있습니다. 또한 테스트 용이성과 재사용성이 향상됩니다.

📊 현재 상황 분석

좋은 점: API와 훅, UI의 관심사 분리가 잘 되어 있음. 개선점: 컴포넌트 레벨에서 다이얼로그 로컬 상태(예: PostTable의 showUserDialog 등)가 복수의 곳에서 관리되어 추상화로 줄일 수 있음.

📝 상세 피드백

비즈니스 로직(포스트 결합, 정렬, 페이지네이션)은 훅(useGetPostsQuery 등)과 api 레이어(postApi)로 적절히 추상화되어 있습니다. 그러나 일부 컴포넌트(예: PostTable)에서 로컬 다이얼로그 상태와 도메인 상태가 섞여 있어 추가 추상화 여지가 있습니다.

❌ 현재 구조 (AS-IS)

AS-IS: combinePostsWithUsers(posts, users)   내부에서 합쳐져 사용됨(entities/post/api/hooks.ts).

✅ 권장 구조 (TO-BE)

TO-BE: combinePostsWithUsers를 entities/post/service.ts 같은 서비스 계층으로 옮겨 여러 /작업에서 재사용 가능하게 .

🔄 변경 시나리오별 영향도

  1. 비즈니스 규칙 변경(예: 정렬 기준 추가): sortPosts 내부 수정으로 대부분 훅에서 일괄 반영 가능(1 파일 수정).
  2. HTTP 세부사항 변경: createApi만 조정하면 대부분의 훅/컴포넌트는 무수정.

🚀 개선 단계

  • 1단계: 단기(반나절): PostTable의 로컬 다이얼로그 상태(3개)를 PostDialogs 훅으로 이동시켜 UI와 상태 관리를 분리(예상 2-3시간).
  • 2단계: 중기(1일): 비즈니스 유틸(combinePostsWithUsers, sortPosts)을 service 모듈로 분리하여 테스트 가능하게 만듦.

7. 🧪 테스트 용이성

💡 개념 정의

테스트 용이성은 단위/통합/엔드투엔드 테스트를 얼마나 쉽게 작성하고 유지할 수 있는지를 뜻합니다. 의존성 주입, 순수 함수, 사이드 이펙트 분리는 핵심입니다.

⚡ 중요성

새로운 외부 API 연동, 복잡한 비즈니스 로직 변경 시 자동화된 테스트는 리그레션을 방지합니다.

📊 현재 상황 분석

현재 테스트 파일 없음. 권장: entities의 pure 유틸 함수부터 단위 테스트 추가, hooks는 React Testing Library와 msw(Mock Service Worker)를 사용해 통합 테스트 작성, Zustand store 접근을 adapter로 감싸면 스토어 mocking이 쉬워집니다.

📝 상세 피드백

테스트 코드는 아직 포함되어 있지 않습니다. 구조 자체는 테스트하기 좋게 설계된 부분(useQuery/useMutation 훅, pure util 함수들)이 많으나 컴포넌트들에서 side-effect(예: direct fetch 이전 코드)나 글로벌 상태 사용(zustand)을 많이 사용하면 단위 테스트와 mocking이 더 어려워질 수 있습니다.

❌ 현재 구조 (AS-IS)

AS-IS: combinePostsWithUsers(posts, users)  내부에서 사용되지만 별도의 테스트 코드 없음.

✅ 권장 구조 (TO-BE)

TO-BE: src/entities/post/service.test.ts에서 combinePostsWithUsers와 sortPosts의 다양한 케이스 단위 테스트 추가.

🔄 변경 시나리오별 영향도

  1. 새 외부 API 도입: msw로 네트워크 레이어를 모킹하면 useQuery 훅 테스트가 안정적.
  2. 복잡한 비즈니스 로직 변경: 로직을 순수 함수로 분리하면 단위 테스트만으로 검증 가능.

🚀 개선 단계

  • 1단계: 단기(반나절): 순수 함수(utils/service)부터 단위 테스트 추가(예: combinePostsWithUsers, sortPosts, highlightText).
  • 2단계: 중기(1-2일): hooks(useGetPostsQuery, useGetCommentsQuery)에 대한 통합 테스트(msw 사용) 추가.
  • 3단계: 장기: CI에 테스트 파이프라인 추가 및 코드커버리지 기준 설정.

8. ⚛️ 현대적 React 패턴

💡 개념 정의

현대적 패턴은 Suspense, Error Boundary, 커스텀 훅, 선언적 데이터 패칭과 같은 React 최신 권장 방식으로 관심사를 분리하고 선언적으로 표현하는 것입니다.

⚡ 중요성

로딩/에러 UX를 중앙화하고 컴포넌트 단순화를 통해 변경 시 영향 범위를 줄입니다.

📊 현재 상황 분석

권장할 사항: ErrorBoundary로 전역 에러 핸들링, Suspense(React Query의 suspense 옵션 활용)를 통해 로딩 UI를 상위 컴포넌트에서 선언적으로 처리하면 개별 컴포넌트가 단순해집니다.

📝 상세 피드백

현대적 React 패턴(커스텀 훅 분리, React Query 활용)을 잘 활용하고 있습니다. 다만 Suspense, Error Boundary 사용은 보이지 않으며, 일부 컴포넌트에서 로컬 상태와 전역 상태가 섞여 있어 선언적 에러/로딩 처리를 더 도입할 수 있습니다.

❌ 현재 구조 (AS-IS)

AS-IS:  /컴포넌트에서 isLoading/isError를 직접 다루는 패턴(: PostsManagerPage가 로딩 분기 처리).

✅ 권장 구조 (TO-BE)

TO-BE: 상위에서 <ErrorBoundary><Suspense fallback={...}><Posts /></Suspense></ErrorBoundary> 방식으로 선언적 처리.

🔄 변경 시나리오별 영향도

  1. 로딩 전략 변경(스켈레톤→스피너→지연 로딩): Suspense 도입 시 각 컴포넌트의 로딩 코드 대부분 제거 가능.
  2. 에러 처리 일원화: ErrorBoundary 추가로 onError에 분산된 alert/console 처리를 제거.

🚀 개선 단계

  • 1단계: 단기(반나절): ErrorBoundary 컴포넌트 추가 및 최상단(App)에 적용.
  • 2단계: 중기(1일): 주요 데이터 페칭 훅(suspense 옵션 사용)에 대해 Suspense 적용 테스트.
  • 3단계: 장기: Lazy 로딩(React.lazy)으로 페이지/위젯 분리해 초기 번들 크기 최적화.

9. 🔧 확장성

💡 개념 정의

확장성은 새로운 기능을 추가하거나 기존 기능을 수정할 때 필요한 변경량과 난이도를 의미합니다.

⚡ 중요성

제품 요구사항이 계속 변화하는 현실에서 빠른 기능 추가와 안전한 배포를 가능하게 합니다.

📊 현재 상황 분석

좋은 점: query-hooks와 api 분리로 새로운 엔드포인트 추가 시 영향 범위 적음. 개선점: 상태관리 교체 시 영향(상단에서 언급)과 queryKey 객체 사용은 확장시 디버깅 포인트가 될 수 있음.

📝 상세 피드백

새로운 기능(다국어, A/B 테스트, 오프라인 지원 등)을 추가하기 용이한 구조입니다. shared/ui의 범용성, TanStack Query의 사용, entities 기반 타입·api 분리는 확장성을 높입니다. 그러나 상태관리 추상화(현재는 Zustand 직접 사용)가 더 잘 되어 있으면 확장 비용이 더 낮아집니다.

❌ 현재 구조 (AS-IS)

AS-IS: 댓글 낙관 업데이트(useLikeCommentMutation) 구현으로 기능 확장  패턴 재사용 가능.

✅ 권장 구조 (TO-BE)

TO-BE: shared/feature-toggles 또는 experiment 레이어를 추가하여 A/B 테스트와 기능 플래그를 관리.

🔄 변경 시나리오별 영향도

  1. 다국어 지원: UI 문자열 추출 후 shared/i18n 또는 react-i18next 적용(영향: widgets, features의 텍스트 약 30여곳).
  2. A/B 테스트 추가: 컴포넌트 주입 패턴을 통해 실험 대체 컴포넌트 등록으로 영향 최소화.

🚀 개선 단계

  • 1단계: 단기(1일): 텍스트 하드코딩 위치 검색 및 i18n 라이브러리 도입 계획 수립.
  • 2단계: 중기(2-3일): 기능 플래그(간단한 toggle 서비스) 도입으로 실험/롤아웃 제어.
  • 3단계: 장기: 오프라인 모드 요구 시 서비스 워커와 로컬 큐잉 전략 설계.

10. 📏 코드 일관성

💡 개념 정의

코드 일관성은 네이밍, 파일명, import/export 방식 및 스타일 규칙이 팀 전반에 걸쳐 일관된 정도입니다.

⚡ 중요성

새로운 팀원이 프로젝트에 합류하거나 다른 코드베이스와 병합할 때 학습 곡선을 낮추고 자동화 도구 활용을 가능하게 합니다.

📊 현재 상황 분석

발견된 세부 이슈 목록은 아래와 같습니다.

📝 상세 피드백

전반적으로 파일명·네이밍·import 패턴이 꽤 일관되어 있습니다(components/훅 패턴, types 파일 배치 등). 다만 약간의 혼재 가능성(예: main.tsx에서 기본 export -> named export로 변경)과 일부 경로 표기 스타일(절대 경로 '@/...' 반복)이 관찰됩니다. 일관된 export 스타일(컴포넌트는 named export 권장) 및 코드 스타일(Prettier/ESLint) 적용이 있으면 유지보수가 더 쉬워집니다.

❌ 현재 구조 (AS-IS)

AS-IS : components와 shared/ui 컴포넌트들은 대부분 named export로 구성되어 있으나 이전 버전 PostsManagerPage에서는 default export 사용 예가 남아 있는  과거 스타일이 일부 보임.

✅ 권장 구조 (TO-BE)

TO-BE: 컴포넌트는 PascalCase + named export, hooks는 usePrefix camelCase, 타입 파일은 PascalCase로 통일. 프로젝트 루트에 CODEOWNERS/CONTRIBUTING/STYLEGUIDE 문서 추가.

🔄 변경 시나리오별 영향도

  1. 새 개발자 합류: 일관된 컨벤션이 없다면 온보딩 시간이 늘어나므로 ESLint/Prettier + 코드베이스 룰 문서화가 중요.
  2. 자동화 스크립트(코드 분석) 도입: 네이밍 규칙과 경로 규칙이 일관되어야 분석 스크립트가 안정적으로 동작.

🚀 개선 단계

  • 1단계: 단기(반나절): ESLint와 Prettier 구성(또는 규칙 강화) 및 commit hook에서 자동 적용하도록 설정.
  • 2단계: 단기(반나절): 파일명/네이밍 규칙 문서화(README에 핵심 규칙 1페이지 작성).
  • 3단계: 중기(1-2일): 기존 코드베이스의 불일치 점(몇몇 파일의 default export 등)을 자동/수동으로 정리.

🎯 일관성 체크포인트

파일명 규칙

  • 대부분 PascalCase 사용(예: PostTable.tsx)으로 일관되어 있음 — 특별한 위반 파일은 PR에서 드러나지 않음

Import/Export 패턴

  • main.tsx 변경: 이전 default export 패턴(App)에서 named export(App, Providers) 사용으로 기존 문서/예제와 불일치 가능
  • 일부 index.ts가 잘 사용되고 있으나 모든 slice에 일관되게 적용되었는지 검증 필요

변수명 규칙

  • 대체로 camelCase 사용. 큰 이슈 없음

코드 스타일

  • 문자열 인용부호, 세미콜론 등 스타일은 프로젝트 전반적으로 통일된 것으로 보이나 설정 파일(.eslintrc/.prettierrc)은 PR에 없음 — 팀 규칙 문서화 권장

11. 🗃️ 상태 관리

💡 개념 정의

상태 관리는 전역 상태(클라이언트 UI 상태)와 서버 상태(원격 데이터)를 명확히 분리해 각 책임에 맞는 도구로 관리하는 것입니다. 전역상태엔 Zustand 등, 서버 상태엔 TanStack Query를 사용합니다.

⚡ 중요성

실시간 기능/오프라인 지원/상태관리 라이브러리 교체와 같은 요구사항 변화에 얼마나 유연하게 대응할 수 있는지를 결정합니다.

📊 현재 상황 분석

좋음: props drilling 최소화, 서버/클라이언트 상태 분리. 주의: 상태의 범위가 큰 store(usePostStore)에 집중되어 있어, 오프라인 큐잉 또는 실시간 동기화 추가 시 store를 세분화하면 유리함.

📝 상세 피드백

전역 상태 관리는 Zustand를 사용해 잘 분리되어 있습니다(usePostStore, useCommentStore). 서버 상태는 TanStack Query로 분리되어 클라이언트/서버 상태 구분이 명확합니다. props drilling은 SearchBar/PostTable 등에서 거의 사라졌습니다. 단, Zustand에 강하게 결합된 접근 패턴은 다른 상태관리로 변경할 때 비용을 증대시킬 수 있습니다.

❌ 현재 구조 (AS-IS)

AS-IS: usePostStore는 pagination, newPost, selectedUser  다양한 책임을 갖고 있음( src/entities/post/model/store.ts ).

✅ 권장 구조 (TO-BE)

TO-BE: 책임 기반 분리 : usePostPaginationStore, usePostEditorStore, usePostSelectionStore로 쪼개면 특정 기능만 교체/확장 가능.

🔄 변경 시나리오별 영향도

  1. 실시간 데이터(WebSocket) 추가: 서버 상태는 TanStack Query로는 폴링/리패치로 대응 가능하나 실시간 양방향 동기화는 별도의 실시간 store 또는 queryClient.setQueryData를 구독하는 레이어 필요.
  2. 상태관리 라이브러리 변경: Zustand adapter를 도입하면 마이그레이션 비용 감소.

🚀 개선 단계

  • 1단계: 단기(반나절): usePostStore에서 페이징/검색/필터 관련 부분을 분리하는 작업(1-2시간).
  • 2단계: 중기(1-2일): Zustand adapter 패턴 도입으로 DI(의존성 주입) 방식 지원 — 테스트/마이그레이션 용이.
  • 3단계: 장기: 실시간/오프라인 요구사항을 고려한 상태 전략(로컬 큐, sync 룰) 설계.

🤔 질문과 답변

질문: "props drilling을 써야하는 상황과 전역상태관리를 써야하는 상황이 구분될 수 있는 걸까요?"
답변(근거 기반 권장):

  1. 기본 원칙(Decision heuristic):
  • 로컬 UI 상태(토글, 입력 폼의 임시 값, 모달 열림 여부 등)는 컴포넌트 자체 혹은 작은 하위 트리에서 useState/useReducer로 관리(또는 해당 컴포넌트의 커스텀 훅). 이 경우 props drilling이 1~2단계 정도라면 굳이 전역 상태로 올리지 않는 것이 단순성 측면에서 유리합니다.
  • 여러, 서로 먼 컴포넌트에서 동일 상태를 읽거나 수정해야 하거나 상태의 생명주기가 페이지 전체에 걸쳐 있으면 전역 상태로 올리는 편이 낫습니다(예: 현재의 pagination, global searchQuery, selectedPost 등).
  1. 실무 체크리스트(언제 전역 상태로 할지):
  • 공유 범위: 상태를 3개 이상의 독립 컴포넌트가 공유하면 전역 고려.
  • 수명 주기: 상태가 페이지 전환을 넘어 유지되어야 하면 전역.
  • 테스트/디버깅 용이성: 전역 상태로 두면 상태 재현과 디버깅이 쉬워질 수 있음(도구 지원 여부 고려).
  • 성능: 빈번한 업데이트(매 키 입력)인데 전역으로 올리면 불필요한 렌더링을 유발할 수 있으므로 로컬로 유지하거나 debouncing 필요.
  1. 구체적 예시(본 PR 기준):
  • searchQuery, pagination: 여러 컴포넌트(PostTable, SearchBar, usePosts)가 참조하므로 전역(store)로 적절히 분리되어 있음. 좋은 선택입니다.
  • modal open/close(예: 각 PostDetailDialog의 show 상태): 로컬로 관리하고 parent에서 토글하는 것이 더 단순함. 전역으로 올리면 관리 복잡도가 증가할 수 있음.
  1. 권장 아키텍처 접근법:
  • 작은 공통 상태(shared UI state)는 'feature-local store'로 분리(예: usePostPaginationStore). 전역(앱 전역) 상태는 정말로 앱 전역에서 필요한 것만 유지.
  • 상태관리 라이브러리 선택 기준: 단순 데이터 공유면 Zustand/Jotai, 복잡한 미들웨어/이력관리/디버깅 필요시 Redux (또는 Redux Toolkit)를 고려.
    결론: '절대 규칙'은 없으나 위 체크리스트를 기준으로 판단하면 변화에 따른 유지보수 비용을 최소화할 수 있습니다.

🎯 셀프 회고 & 제안

작성하신 셀프회고는 매우 건설적이며 학습 포인트가 명확합니다. 특히 FSD의 모호성(계층을 어디까지 구분할지)과 작은 단위에서 큰 단위로 분리한 시도는 좋은 접근입니다. 몇 가지 추가 생각거리와 질문을 드립니다:

  1. 계층 기준의 명문화: 'R은 entities, CUD는 features'로 규칙을 세우신 점이 실용적입니다. 팀 규칙으로 발전시키려면 예외 케이스(예: read지만 비즈니스 로직이 많은 경우)를 문서화하면 좋습니다. 어떤 경계 케이스가 있었나요? 예: read지만 joined 데이터를 가공해야 할 때는 어디로 두셨나요?
  2. 작은 단위에서 큰 단위로 분리한 경험을 더 체계화: 이 접근은 테스트와 재사용성에 유리합니다. 다음번에 시도해볼 질문: '이 컴포넌트를 독립 패키지로 떼어낼 수 있는가?'를 기준으로 응집도/결합도를 평가해보세요.
  3. TanStack Query의 queryKey 패턴에 대한 통찰 좋습니다. 이어서 고려해볼 점: queryKey에 객체를 그대로 넣는 방식은 '동등성' 이슈를 만들 수 있습니다. 키를 primitive로 분해하거나 stable serializer를 적용하면 더 안전합니다.
    추가 질문들(생각을 확장시키기 위한 제안):
  • entities와 features 사이에 'service' 또는 'use-cases' 계층을 도입해 CUD 비즈니스 로직을 더 명확히 분리하면 어떨까요?
  • 현재 Zustand로 잘 운영되고 있는데, 만약 상태관리 라이브러리를 교체해야 한다면 어떤 추상화(어댑터)를 도입하면 가장 적은 수정으로 가능한가요?
  • UI 라이브러리 전환 시 shared/ui의 어떤 컴포넌트를 먼저 바꾸면 리스크가 가장 낮을까요?(우선순위 제안: Button/Input/Dialog -> Table/Pagination 순)

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

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

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

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