Skip to content

[1팀 한아름] Chapter 2-3. 관심사 분리와 폴더구조#37

Open
areumH wants to merge 45 commits intohanghae-plus:mainfrom
areumH:main
Open

[1팀 한아름] Chapter 2-3. 관심사 분리와 폴더구조#37
areumH wants to merge 45 commits intohanghae-plus:mainfrom
areumH:main

Conversation

@areumH
Copy link

@areumH areumH commented Aug 13, 2025

과제 체크포인트

배포 링크

https://areumh.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 패턴에 관심이 생겨 공부해보고 싶었는데, 이렇게 과제를 통해 직접 프로젝트에 적용해보며 각 레이어와 도메인 별로 책임을 분리했을 때의 이점을 조금이나마 체감할 수 있었다. 그리고 이번 챕터 과제에 쭉 도메인 상태 업데이트 로직을 순수 함수로 분리하는 방식을 사용하고 있는데 굉장히 깔끔하고 유용하다는 걸 알게 되었다.

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

페이지의 기본 동작을 tanstack query로 구현하기 위해 가장 노력했던 것 같다. 원본 코드처럼 프론트에서 하나의 (전역) 상태로 관리해야 할지, 쿼리 키로 낙관적 업데이트하는 식으로 관리해야 할지 여러 팀원들에게 이리저리 질문하러 다녔던 기억이 난다...

// src/entities/commment/model/store.ts
export const commentModel = {
  /**
   * 댓글 추가
   */
  addComment: (commentData: IComments, newComment: IComment): IComments => {
    return {
      ...commentData,
      comments: [newComment, ...commentData.comments],
    };
  },
}

// src/features/commment/add-comment/model/useAddComment.ts
export const useAddComment = (postId: number, onSuccess?: () => void) => {
  const queryClient = useQueryClient();

  const initialComment: IAddComment = { body: '', postId: postId, userId: 1 };
  const [newComment, setNewComment] = useState<IAddComment>(initialComment);

  const mutation = useMutation({
    mutationFn: (comment: IAddComment) => addCommentApi(comment),

    onSuccess: (createdComment) => {
      const newComment = commentModel.addResponseToComment(createdComment);

      queryClient.setQueryData<IComments>(['comments', postId], (prev) => {
        if (!prev) {
          return {
            comments: [newComment],
            total: '1',
            skip: 0,
            limit: 10,
          };
        }

        return commentModel.addComment(prev, newComment);
      });

      onSuccess?.();
      setNewComment(initialComment);
    },
    onError: (error) => {
      console.error('댓글 추가 오류:', error);
    },
  });

  // 중략 ..
  return { newComment, setBody, addComment };
};

entities 내에 도메인마다 기능에 대해 상태를 업데이트하는 순수 함수를 작성해주었고, 해당 도메인 상태에 관련된 쿼리 훅의 낙관적 업데이트에 사용하도록 했다.

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

dialog 처리 및 게시글 필터 처리 (검색, 태그, 정렬 등) 부분이 개선이 필요하다고 생각한다. 게시글 필터의 경우 지금은 src/widgets/post-filter/ui 내에 로직이 한번에 들어가 있는데 이를 검색/태그/정렬 단위로 잘게 나누는게 더 나은 방식인지 아직도 고민이 끝나지 않는다.. 잘게 쪼개면 기능들이 더 한눈에 보이겠지만 반대로 너무 컴포넌트가 세분화되어 관리가 번거로워질 수도 있다는 생각이 든다.

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

이번 과제를 하면서 팀원들과 각 계층의 역할과 구조를 두고 의견이 많이 갈리는 경험을 했다. 그래서 만약 내가 실제 회사에서 fsd 구조를 도입하고 싶다는 생각이 들어도 , 합의와 이해가 없으면 반영이 쉽지 않겠다는 생각이 들었다.. 대신 혼자 진행하는 개인 프로젝트에서 자유롭게 fsd 구조를 적용해보며 구조 설계 및 유지보수 효율성을 더 깊이 경험해보고 싶다.

챕터 셀프회고

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

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

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

더티 코드를 보면 어디부터 손을 대야 할지 막막하고, 읽을수록 머릿속이 복잡해지는 것 같다.. 클린 코드는 변수와 함수 이름만 봐도 역할이 떠오르고, 어느 한 부분을 수정할 때 다른 부분에 영향을 최소화하도록 하는 코드라고 생각한다.

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

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

이번 과제에서 데이터의 낙관적 업데이트를 위해 각 도메인의 상태를 변경하는 순수 함수를 사용했고, Dialog 모달 처리에 zustand의 전역 상태 관리를 사용했다. 처음 과제를 딱 봤을 때 dialog 처리를 어떻게 해야할 지가 제일 막막했는데, 전역 상태를 도입하며 라이브러리의 편리함을 크게 느꼈다... 전역 상태에 컴포넌트를 직접 주입하는 방식에는 약간 아쉬움이 남지만, 최선을 다해 구현했다고 생각한다.

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

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

각 entities에 관련 api 함수를 모두 정의해주었고, 단순 조회의 쿼리 훅은 entities에, 사용자의 행동에 직결되는 기능의 쿼리 훅은 features 내에 정의했다. (useQuery - entities / useMutation - features 와 같은 구조) 이렇게 구조를 나누니 코드를 어디에 두어야 할지 고민하는 시간이 줄고, 각 레이어의 책임이 보다 명확해진 것 같다. 이전에 프로젝트를 진행할 때 하나의 도메인에 대한 쿼리 훅을 useQuery와 ueMutation의 역할에 관계없이 하나의 폴더에 모아두어특정 기능과 관련된 파일을 찾을 때 이리저리 뒤져야 하는 불편함이 있었는데, 이번에 fsd 폴더 구조를 기반으로 프로젝트를 진행하며 기능 별로 분리하는 것의 편리함을 크게 느꼈다.

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

파일 위치 및 구조를 제일 고민했던 부분은 dialog (대화상자 모달) 관련 로직 및 ui인데요. 코치님이라면 해당 부분을 어떻게, 어느 위치에 두고 리팩토링을 하실 지 궁금합니다.

저는 dialog 처리를 zustand로 진행했고, dialog의 정보를 담는 틀 컴포넌트(src/widgets/dialog/DialogRoot.tsx)는 여러 entities 및 features가 안에 포함될 수 있으니 widgets에, 관련 처리 훅(src/shared/hook/useDialogStore.ts)은 여러 features와 widgets에서 사용될 수 있으니 shared에 두었습니다. 그런데 이 구조가 fsd 패턴에 맞는 구조인지 의문이 들더라구요. 틀 컴포넌트도 shared에 들어가야 하나? 모달 처리 훅도 하나의 기능으로 보고 features에 넣어야 하나? 기능이라기보단 사용자의 액션(게시글 추가, 댓글 수정 등)에 따라오는 부가 기능일 뿐인가? 훅을 DialogRoot 컴포넌트와 같은 폴더에 넣어야 하나? 하는 고민을 수도 없이 했던 것 같아요.

만약 저처럼 zustand로 구현한다면 어떤 구조가 더 나을 지, 아님 아예 zustand로 처리하지 않는다면 어떤 방식으로 구현하실 지 알고 싶습니다 !!

areumH added 30 commits August 10, 2025 20:38
Copy link

Choose a reason for hiding this comment

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

저 궁금한게 있는데
IAddCommentResponse는 Omit으로 해도 될거 같긴 한데
Pick이 필요한 키벨류를 명확하게 보여주기 위해서인가요?

Copy link
Author

Choose a reason for hiding this comment

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

큰 의미를 담고 작성한 건 아니긴 한데, 데이터 타입들을 명확히 하고 낙관적 업데이트를 위한 순수 함수에 데이터 타입 변환 함수를 추가해야겠다고 생각했습니다!

Copy link

@Yangs1s Yangs1s Aug 16, 2025

Choose a reason for hiding this comment

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

순수함수를 객체로 묶어서 정리하니까 보기도 좋고 더 깔끔해진거 같아요.
이렇게 crud가 있을때 저도 적용해봐야겠어요!

Copy link

Choose a reason for hiding this comment

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

editPost,setEditPost 저는 전역상태로 빼는게 더 괜찮지 않나 싶었는데
수정말고는 따로 쓰는게 없고, 포스트만 편집하면 되서 오히려 더 좋아보이네요.

Copy link

Choose a reason for hiding this comment

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

저는 페이지는 뷰라고만 생각해서 그런지
Cotent와 Header는 따로 뺴는 방법도 저는 좋을거 같아요!.

Copy link

Choose a reason for hiding this comment

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

리퀘스트 만드는거도 공통적으로 빼는 방법이 있었네요 좋은거 같아요

Copy link

Choose a reason for hiding this comment

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

파라미터가 워낙 많이 받으니까 파라미터 요소끼리 setter끼리 따로 오히려 묶어놓고 구조분해할당이 길어지는거보단, 프로퍼티로 접근하는게 좀 더 직관적으로 느껴져서 좋은거 같아요

Copy link

@Yangs1s Yangs1s Aug 16, 2025

Choose a reason for hiding this comment

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

19부터는 제가 알기로는 forwardref를 사용하지않는다고 들어서

forwardref를 걷어내고 ref를 프롭스처럼 사용하셔도 될거 같습니다!

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  className?: string;
  ref?: React.Ref<HTMLButtonElement>;
}

export const Button = ({
  className,
  variant,
  size,
  ref,
  ...props
}: ButtonProps) => {
  return (
    <button
      className={buttonVariants({ variant, size, className })}
      ref={ref}
      {...props}
    />
  );
};

Copy link
Author

Choose a reason for hiding this comment

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

오 그렇군용 정보 감사합니다!!! 👍

<App />
</StrictMode>,
)
<ReactQueryProvider>

Choose a reason for hiding this comment

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

Router 는 App 에 포함되어 있는데, ReactQueryProvider 는 main.tsx 에 작성한 이유가 궁금합니다!
<StrictMode> 보다 상위에 설정한 것도 의도된 것일까요?

Copy link
Author

Choose a reason for hiding this comment

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

전혀 의도하지 않았고 크게 신경쓰지 않은 부분이네요,,🥲 지적 감사합니다!!

Comment on lines +3 to +10
export const COMMENT = {
LIST: (postId: number) => `${BASE_URL}/comments/post/${postId}`,

ADD: `${BASE_URL}/comments/add`,
UPDATE: (commentId: number) => `${BASE_URL}/comments/${commentId}`,
DELETE: (id: number) => `${BASE_URL}/comments/${id}`,
LIKE: (id: number) => `${BASE_URL}/comments/${id}`,
};

Choose a reason for hiding this comment

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

ADD 가 동적인 값이 없어도 함수로 통일하면 일관성 측면에서 좋을 것 같아요

Copy link

Choose a reason for hiding this comment

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

좋아요 수정 삭제는 저는 프롭스를 받는거보다 CommentItem에 직접 넣는게 좀 더 명확하게 역할도 나뉘는거 같아서 좋아보여요!

<div className="flex items-center space-x-2 overflow-hidden">
<span className="font-medium truncate">{comment.user.username}:</span>
<span className="truncate">
{HighlightText(comment.body, searchQuery)}

Choose a reason for hiding this comment

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

HighlightText 의 역할이 컴포넌트인지 함수인지 의도가 불분명 한 것 같아요

Choose a reason for hiding this comment

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

앞글자 대문자로 바꾸신김에 컴포넌트 어떠세요

Suggested change
{HighlightText(comment.body, searchQuery)}
<HighlightText text={comment.body} highlight={searchQuery) />

@@ -0,0 +1,65 @@
import { IAddCommentResponse, IComment, IComments } from './type';

export const commentModel = {

Choose a reason for hiding this comment

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

준일 코치님이 이야기 해주신 것 처럼 매서드의 경우엔 중복된 네이밍을 피하는 것도 좋을 것 같습니다

import { Dialog } from '../../shared/ui/components';
import { useDialogStore } from '../../shared/hook/useDialogStore';

export const DialogRoot = () => {

Choose a reason for hiding this comment

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

중복을 줄이는 방법으로 좋은 것 같아요

<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow container mx-auto px-4 py-8">
<DialogRoot />

Choose a reason for hiding this comment

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

Dialog 묶은 거 좋네요!

}: {
children: React.ReactNode;
}) {
const [queryClient] = useState(() => new QueryClient());

Choose a reason for hiding this comment

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

image

공식문서에서도 그냥 전역변수로 관리하는데, 상태로 queryClient 를 관리하면 어떤 이점이 있나요?

Copy link
Author

Choose a reason for hiding this comment

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

컴포넌트 내부에서 1회만 생성하는 건 전역 변수와 똑같지만 굳이 상태로 관리하는 방식을 선택한 특별한 이유는 없는 것 같아요😅 예전에 팀 프로젝트를 하면서 접한 방식인데 뭔가 리액트스럽고 깔끔해보여서 적용했습니다! 공식 문서 한번 읽어봐야겠어요 감사합니다👏

Comment on lines +18 to +23
const { editComment, setBody, updateComment } = useUpdateComment(
comment,
() => {
setCommentModal({ show: false, content: null });
}
);

Choose a reason for hiding this comment

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

좋은 것 같아요, 근데 쓰는 쪽에서 넘기는 함수가 onSuccess 인지 알면 더 좋을 것 같아서 이건 어떠신가요

Suggested change
const { editComment, setBody, updateComment } = useUpdateComment(
comment,
() => {
setCommentModal({ show: false, content: null });
}
);
const { editComment, setBody, updateComment } = useUpdateComment(
comment,
{
onSuccess: () => setCommentModal({ show: false, content: null }),
}
);

Comment on lines +44 to +45
const { deleteComment } = useDeleteComment();
const { likeComment } = useLikeComment();

Choose a reason for hiding this comment

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

하나로 묶는 건 어떠실까요?

Suggested change
const { deleteComment } = useDeleteComment();
const { likeComment } = useLikeComment();
const { deleteComment, likeComment } = useCommentActions();

Copy link
Author

Choose a reason for hiding this comment

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

features 내에서 각 기능별 책임을 명확히 하려고 훅을 각각 작성했는데 이렇게 한 파일 내에서 쓰이는 기능들이면 하나로 묶어서 작성해도 좋을 것 같네요!!! 🤩

Copy link

@CreatiCoding CreatiCoding left a comment

Choose a reason for hiding this comment

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

DialogRoot 로 묶어서 관리한게 좋았습니다, 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.

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

1. 🏗️ FSD 아키텍처

💡 개념 정의

Feature-Sliced Design(FSD)는 기능(Feature) 단위로 코드베이스를 분할하고, 레이어별 책임(app → pages → widgets → features → entities → shared)을 명확히 하여 변경 영향도를 낮추는 구조적 접근 방식입니다.

⚡ 중요성

FSD는 팀 규모가 커지거나 도메인이 확장될 때 변경에 대한 영향도를 줄이고 온보딩을 용이하게 합니다. 명확한 public API 경계는 모듈화, 패키지 분리, 라이브러리 교체(예: UI 라이브러리) 시 최소한의 수정으로 대응할 수 있게 합니다.

📊 현재 상황 분석

AS-IS: 레이어분리는 되어 있으나 public API 캡슐화 부족과 내부 파일 직접 참조로 인해 의존성이 느슨하지 않습니다. 작은 리팩터나 라이브러리 전환 시 여러 파일을 한 번에 건드려야 할 가능성이 큽니다. TO-BE: 각 slice에 index.ts로 Public API만 노출하고 내부 구현은 숨겨 참조를 index를 통해만 하게 하면 모듈 교체/분리 시 수정 파일 수를 대폭 줄일 수 있습니다.

📝 상세 피드백

프로젝트 구조 전반은 Feature-Sliced Design(FSD) 원칙을 따르려는 시도가 명확합니다. entities / features / widgets / shared 계층 분리가 잘 되어 있어 각 레이어의 책임이 비교적 분리되어 있습니다. 다만 Public API(바렐 인덱스)를 통한 외부 노출 규칙이 지켜지지 않아 계층간 결합이 불필요하게 증가할 가능성이 보입니다. 또한 일부 import에서 내부 경로에 .tsx 확장자까지 직접 참조하고 있어(예: import Header from '../shared/ui/layout/Header.tsx') 모듈 교체 또는 패키지 분리시 영향도가 커질 수 있습니다. 개선안은 각 slice(entities, features, widgets, shared)에 index.ts(x)를 만들어 public API만 노출하고, 내부 구현 파일은 internal으로 캡슐화하는 것입니다. 그러면 아키텍처 변경(모노레포, 패키지 독립화 등) 시 수정 범위를 크게 줄일 수 있습니다.

❌ 현재 구조 (AS-IS)

현재: import { addCommentApi } from '../../../../entities/comment/api/comment-api'; // 직접 내부 경로 접근 (index 바렐 미사용)

✅ 권장 구조 (TO-BE)

추천: // entities/comment/index.ts export * from './model'; export * from './api'; // 외부에서는 import { useCommentsQuery } from 'entities/comment' 형태로 사용

🔄 변경 시나리오별 영향도

  1. UI 라이브러리(Material UI → Chakra UI) 교체 시: 현재는 shared/ui/components의 여러 파일을 수정해야 하나, UI 컴포넌트가 public API로 잘 추상화되어 있다면 수정 범위는 shared/ui만으로 제한됩니다.
  2. 모노레포 전환/패키지 분리: entities/post 등 도메인 단위로 패키지 분리 시 public API(바렐)가 잘 되어 있으면 의존성 선언만 바꿔 바로 분리 가능.
  3. entities 내부 타입 변경: entities 모델 변경이 features/widgets에 전파되는 범위를 public API로 줄일 수 있음.

🚀 개선 단계

  • 1단계: 단계 1 (단기, 1~2일): 각 slice(entities/feature/widget/shared)에 index.ts/ index.tsx 바렐 파일 추가. 기존 import를 바렐로 변경하는 Codemod/검색 교체 스크립트 실행.
  • 2단계: 단계 2 (중기, 1~2일): 코드베이스에서 확장자(.tsx 등) 직접 명시 규칙 제거 및 tsconfig/ESLint import 규칙 통일.
  • 3단계: 단계 3 (이상적, 2~4일): 각 slice별 public API 계약 문서화(예: README + 타입 선언) 및 레이어 의존성 검증 스크립트(예: dependency-cruiser) 도입.

2. 🔄 TanStack Query

💡 개념 정의

TanStack Query는 서버 상태 관리 라이브러리로, 쿼리 키를 통해 캐싱/동기화/리패칭을 처리하고 mutation API로 서버 변경을 수행하며 로컬 캐시를 조작할 수 있습니다. 중요 포인트는 안정적인 쿼리 키 설계와 일관된 캐싱/에러/리프레시 정책입니다.

⚡ 중요성

쿼리 키와 캐싱 정책이 불안정하면 동일한 데이터에 대해 중복 호출, 불필요한 리렌더, 캐시 미스가 발생하고 API 변경/추가 시 수정 포인트가 분산됩니다. 낙관적 업데이트는 UX 향상을 제공하지만 잘못 구현하면 데이터 불일치/롤백 처리 복잡도를 초래합니다.

📊 현재 상황 분석

AS-IS: Query를 팀별로 역할(조회: entities, 변경: features)로 분리한 점은 우수하지만, 쿼리 키 설계와 낙관적 업데이트 구현(완전한 onMutate 롤백 없음)이 개선 필요. TO-BE: queryKeys 팩토리 도입, 객체 대신 직렬화된/원시 값 기반 키 사용, onMutate 기반 낙관적 업데이트 및 기본 staleTime 정책 설정.

📝 상세 피드백

TanStack Query를 적절히 도입하여 서버 상태를 분리한 점이 좋습니다. entities 레이어에 조회용 쿼리(hooks)와 api를 위치시키고, features 레이어에서 mutation을 처리하는 구조(entities: useQuery / features: useMutation)를 의도적으로 적용한 점이 FSD 관점에서 타당합니다. 다만 몇 가지 개선 포인트가 있습니다: 1) 쿼리 키로 객체(params)를 그대로 사용하고 있음(usePostListQuery: queryKey: ['posts', params]) — 객체는 참조 불변성이 보장되지 않아 캐싱/리패칭 예측이 어렵습니다. 2) 낙관적 업데이트 패턴을 onSuccess에서 setQueryData로 처리했으나, 진짜 낙관적 업데이트는 onMutate에서 로컬 상태를 즉시 업데이트하고 실패 시 rollback하는 패턴을 권장합니다. 3) 캐싱 전략(staleTime, cacheTime)이 전역적으로 정의되어 있지 않습니다. 4) queryKeys 팩토리(계층화된 키 생성 함수)가 없어 쿼리 키의 일관성이 떨어집니다.

❌ 현재 구조 (AS-IS)

현재: usePostListQuery params 객체를 queryKey로 사용
useQuery({ queryKey: ['posts', params], queryFn: () => getPostListApi(params) })
낙관적 업데이트 : onSuccess에서 setQueryData 로컬 업데이트

✅ 권장 구조 (TO-BE)

권장: queryKeys 팩토리와 onMutate 기반 낙관적 업데이트
// shared/api/queryKeys.ts
export const queryKeys = {
  posts: { list: (p: PostsParams) => ['posts', p.limit, p.skip, p.sortBy, p.sortOrder, p.searchQuery, p.selectedTag] as const }
}
// usePostListQuery
useQuery({ queryKey: queryKeys.posts.list(params), queryFn: () => getPostListApi(params), staleTime: 1000 * 60 * 2 })
// useAddPost (낙관적)
useMutation({
  onMutate: async (newPost) => {
    await queryClient.cancelQueries(queryKeys.posts.list(params));
    const previous = queryClient.getQueryData(queryKeys.posts.list(params));
    queryClient.setQueryData(queryKeys.posts.list(params), (old: IPosts) => postModel.addPost(old, tempPost));
    return { previous };
  },
  onError: (_err, _variables, context) => queryClient.setQueryData(queryKeys.posts.list(params), context.previous),
  onSettled: () => queryClient.invalidateQueries(queryKeys.posts.list(params)),
})

🔄 변경 시나리오별 영향도

  1. API 파라미터 구조 변경(예: PostsParams에 newFlag 추가): 현재는 params 객체가 키에 포함되어 있으므로 키 비교가 느슨하거나 신뢰할 수 없습니다 — 변경 시 모든 곳에서 params가 재생성되어 재요청이 발생할 수 있음.
  2. 새로운 데이터 소스(캐싱 요구가 다른) 추가: 각 쿼리에 일관된 cacheTime/staleTime 설정이 없으면 소스 별로 정책을 분리하기 어려움.
  3. 에러 핸들링 전략 변경(글로벌 에러 바인딩): 현재 각 훅에서 console.error만 사용하므로 전역 에러 전략 도입 시 많은 훅 수정 필요.

🚀 개선 단계

  • 1단계: 단계 1 (단기, 하루 이내): queryKeys 팩토리(shared/api/queryKeys.ts) 추가하고 기존 쿼리의 queryKey를 객체 대신 명시적 원시값 배열로 변경.
  • 2단계: 단계 2 (중기, 1~2일): 주요 mutation 훅들(useAddPost, useAddComment 등)에 onMutate/onError/onSettled 패턴으로 낙관적 업데이트(또는 롤백)를 적용.
  • 3단계: 단계 3 (중기, 1~2일): QueryClient 기본 설정에 staleTime/cacheTime 전략 추가(예: 목록은 2분, 상세는 30초 등) 및 전역 에러/성공 로깅 통합.
  • 4단계: 단계 4 (이상적, 1~2일): TanStack Query Devtools 포함/설정 및 문서화(팀 가이드)로 일관된 사용 유도.

3. 🎯 응집도 (Cohesion)

💡 개념 정의

응집도(Cohesion)는 모듈 내부의 요소들이 얼마나 관련된 기능을 함께 가지고 있는지를 나타냅니다. 높은 응집도는 한 기능 변경 시 수정 파일 수를 최소화합니다.

⚡ 중요성

높은 응집도는 유지보수성, 테스트 용이성, 패키지 분리(도메인 단위 분리) 용이성을 제공합니다. 반대로 낮은 응집도는 변경 시 파편화된 수정과 높은 인지 부하를 유발합니다.

📊 현재 상황 분석

AS-IS: PostFilter가 복수의 책임(입력 상태 관리, 태그 로딩, 태그/정렬 UI)을 가지고 있어 태그 로직을 바꾸면 파일을 건드려야 함. TO-BE: 검색박스, 태그셀렉터, 정렬셀렉터를 각각 작은 위젯(혹은 features의 하위 컴포넌트)으로 분리하면 특정 변경을 해당 컴포넌트 내부에서만 처리할 수 있습니다.

📝 상세 피드백

응집도 측면에서 entities 내부(모델, api, ui)는 비교적 잘 모여 있으며, post/comment 관련 코드가 한 폴더에 응집되어 있어 관련 변경 시 접근성이 높습니다. 반면 widgets/post-filter/ui/PostFilter.tsx는 검색·태그·정렬을 한 파일에서 처리하고 있어 관심사가 약간 섞여 있습니다(검색 입력 UI, 태그 셀렉터, 정렬 셀렉터). 이것은 기능 변경(예: 태그에서 '다중 선택' 지원 또는 검색 debounce 추가) 시 파일 수정 범위를 늘립니다.

❌ 현재 구조 (AS-IS)

현재: PostFilter.tsx 내부에 검색 Input 상태와 tags fetching(usePostTagListQuery), 여러 Select 컴포넌트가 함께 구현되어 있음.

✅ 권장 구조 (TO-BE)

추천: src/widgets/post-filter/ui/SearchBox.tsx, TagSelector.tsx, SortSelector.tsx  분리. PostFilter는 이들을 합성만 .  컴포넌트는 자체 테스트/스타일만 책임짐.

🔄 변경 시나리오별 영향도

  1. 태그를 다중 선택으로 바꾼다 → 현재: PostFilter 전체 수정. 분리된 구조: TagSelector만 변경(1 파일).
  2. 검색 debounce/자동완성 추가 → 현재: PostFilter 전체 수정 및 상태조정 필요. 분리 시 SearchBox만 변경.
  3. 태그 소스가 변경되어 API가 바뀐다 → 현재: PostFilter와 usePostTagListQuery 호출부 점검 필요.

🚀 개선 단계

  • 1단계: 단계 1 (단기, 24시간): PostFilter에서 검색박스, 태그셀렉터, 정렬셀렉터 로직을 각각 함수/컴포넌트로 분리(파일 23개 생성).
  • 2단계: 단계 2 (중기, 1일): TagSelector는 서버 호출(usePostTagListQuery)만 담당하도록 하고, 선택 이벤트는 상위(PostFilter)의 콜백으로 전달.
  • 3단계: 단계 3 (이상적, 1~2일): 각 분리된 컴포넌트를 독립적으로 모킹/유닛 테스트를 추가하여 응집도 향상 검증.

4. 🔗 결합도 (Coupling)

💡 개념 정의

결합도(Coupling)는 모듈 간 의존성의 강도를 나타내며, 낮은 결합은 한 모듈의 변경이 다른 모듈에 미치는 영향을 최소화합니다.

⚡ 중요성

낮은 결합은 기술 스택 변경(HTTP client, 상태관리 라이브러리, 디자인 시스템 전환) 시 수정 범위를 줄이고 리팩터 비용을 낮춥니다.

📊 현재 상황 분석

AS-IS: 구현 세부사항(HTTP, 모달 표현)이 여러 파일에 직접 의존. TO-BE: httpClient abstraction(shared/lib/httpClient.ts) 도입, modal descriptor 패턴 적용(ReactNode 대신 {type, props}), UI 컴포넌트 래퍼(DesignSystemAdapter)로 교체 비용 최소화.

📝 상세 피드백

결합도 측면에서는 전반적으로 인터페이스(entities 모델의 순수 함수)가 잘 정의되어 있어 낮은 결합을 지향합니다(postModel/commentModel). 하지만 몇 군데 높은 결합 포인트가 있습니다: 1) API 호출에서 fetch와 createRequest가 각 엔티티 파일에 직접 사용되어 HTTP클라이언트 교체 시 여러 엔티티 파일을 수정해야 합니다(예: post-api.ts, comment-api.ts). 2) useDialogStore가 React.ReactNode(컴포넌트 인스턴스)를 상태로 저장하고 있어, dialog 내부 구현 변경 시 store를 사용하는 모든 feature가 영향을 받을 수 있습니다. 3) 일부 컴포넌트가 직접 특정 UI 컴포넌트 구현(Button, DialogContent 등)을 가져와 사용하고 있어 디자인 시스템 교체 시 영향을 받습니다.

❌ 현재 구조 (AS-IS)

현재: // useDialogStore 상태에 React.ReactNode 저장
setPostModal({ show: true, content: <UpdatePostForm post={post} /> })
-> 여러 사용처에서 React 노드를 직접 주입

✅ 권장 구조 (TO-BE)

권장: store에는 descriptor만 저장
setPostModal({ show: true, view: 'UpdatePost', props: { postId: post.id } })
DialogRoot는 view->컴포넌트 매핑을 수행하여 해당 컴포넌트를 렌더링

🔄 변경 시나리오별 영향도

  1. axios로 HTTP 클라이언트 교체: 현재 구조에서는 post-api, comment-api 등 여러 파일 직접 수정 필요(약 6개 이상). 추상화하면 shared/httpClient만 변경하면 됨(1개 파일).
  2. zustand → context/state library 변경: useDialogStore를 직접 사용하는 여러 파일(약 7곳)이 영향을 받음. descriptor/dispatch 패턴으로 전환하면 영향 범위를 DialogRoot와 store 어댑터로 축소 가능.
  3. 디자인 시스템 교체(클래스명/컴포넌트 API 변경): shared/ui 에 어댑터 레이어가 있으면 변경 범위가 shared/ui로 집중됨.

🚀 개선 단계

  • 1단계: 단계 1 (단기, 반나절~1일): shared/lib/httpClient 추상 계층을 만들고 현재 fetch 호출을 wrapper로 이동. 기존 api는 wrapper 사용하도록 리팩토링(변경 파일 수: api 파일들).
  • 2단계: 단계 2 (중기, 1~2일): useDialogStore가 ReactNode를 저장하는 현재 방식에서 descriptor pattern으로 전환(모든 set*Modal 호출을 변환). DialogRoot에서 descriptor->컴포넌트 매핑을 담당.
  • 3단계: 단계 3 (이상적, 1~2일): 디자인 시스템 추상화 레이어를 만들고 shared/ui 내부에서만 실제 UI 라이브러리를 사용하게 하여 교체 시 영향도를 최소화.

5. 🧹 Shared 레이어 순수성

💡 개념 정의

Shared 레이어 순수성은 공통 코드가 특정 도메인에 의존하지 않고 다른 프로젝트/모듈에서도 재사용 가능한지를 뜻합니다.

⚡ 중요성

도메인 독립적인 shared 코드는 다른 프로젝트로의 포팅, 라이브러리 분리, 디자인 시스템 변경 시 큰 장점을 제공합니다. 순수성은 테스트/모킹도 쉽게 만듭니다.

📊 현재 상황 분석

AS-IS: shared는 대부분 순수하지만 Modal 상태의 형태 때문에 도메인 의존성이 유입되고 있음. TO-BE: shared는 도메인 독립적인 데이터만 저장하고, UI 컴포넌트는 가능한 prop-driven(variant, children) 방식으로 유지.

📝 상세 피드백

shared 레이어는 UI 컴포넌트, 훅, 유틸 등을 포함하여 재사용 목적으로 잘 모여있습니다. 그러나 shared/hook/useDialogStore.ts에서 React.ReactNode를 상태로 저장하는 것은 shared 레이어의 순수성(도메인 독립성)을 해칩니다. shared는 도메인 로직에 의존하지 않아야 하고, 상태는 가능한 직렬화 가능하고 순수한 데이터(설정/descriptor 등)로 유지하는 것이 재사용성과 테스트에 유리합니다. 또한 shared/ui/HighlightText는 도메인에 종속적이지 않지만, 함수명이 컴포넌트처럼 보이는 점과 반환값(ReactNode)을 명확히 문서화하면 재사용성이 좋습니다.

❌ 현재 구조 (AS-IS)

현재: useDialogStore.postModal.content = <AddPostForm /> (ReactNode  )

✅ 권장 구조 (TO-BE)

권장: useDialogStore.postModal = { view: 'AddPost', props: { postId: 1 } }
DialogRoot: const mapping = { AddPost: AddPostForm, UpdatePost: UpdatePostForm };
const Comp = mapping[view]; return <Comp {...props} />

🔄 변경 시나리오별 영향도

  1. 새 프로젝트에서 shared 재사용: 현재대로 ReactNode를 저장하면 의존성(컴포넌트 타입 등)을 함께 가져가야 함. descriptor 패턴이면 간단한 매핑만 추가하면 됨.
  2. 디자인 시스템 전환: shared/ui 내부에서만 변경되도록 하면 외부 프로젝트 영향 최소화.

🚀 개선 단계

  • 1단계: 단계 1 (단기, 반나절): useDialogStore 상태 형식을 ReactNode -> { view: string, props?: any } 형태로 변경. 기존 set*Modal 호출부(약 7곳)를 변경.
  • 2단계: 단계 2 (중기, 1일): DialogRoot에 view->컴포넌트 매핑 테이블 추가 및 타입 정의(유니온 타입).
  • 3단계: 단계 3 (이상적, 1~2일): shared 훅/컴포넌트에 대한 문서화와 재사용 가이드 작성.

6. 📐 추상화 레벨

💡 개념 정의

추상화는 복잡한 구현 세부사항을 숨기고 재사용 가능한 인터페이스를 제공하는 수준을 말합니다.

⚡ 중요성

적절한 추상화는 기술 스택 교체, 테스트 모킹, 공통 로직 재사용을 쉽게 해 줍니다. 과도한 추상화는 오히려 복잡도를 올리지만, 주요 인프라(HTTP, 쿼리키, 모달)에는 추상화가 필요합니다.

📊 현재 상황 분석

AS-IS: 도메인 변환 추상화는 잘 되어 있으나 HTTP 클라이언트와 쿼리 키는 더 추상화 가능. TO-BE: httpClient abstraction 도입, queryKeys 중앙화, useQueryParameter 타입 정리.

📝 상세 피드백

추상화 수준은 전반적으로 적절합니다. postModel/commentModel 처럼 응답을 도메인 타입으로 변환하는 함수(addResponseToPost 등)를 통해 비즈니스 로직이 API 세부사항과 분리되어 있어 유지보수성이 좋습니다. 개선할 점은 API 호출의 공통 부분(createRequest, fetch)과 query key 생성 로직을 더 추상화하여 교체 비용을 낮추는 것입니다. 또한 useQueryParameter 훅의 내부 인터페이스(QueryParams)를 외부에 노출하는 방식이 약간 헷갈릴 수 있어 공용 타입을 명확히 하면 좋습니다.

❌ 현재 구조 (AS-IS)

현재: createRequest + fetch를  api 파일에서 직접 사용
const response = await fetch(POST.ADD, createRequest('POST', newPost));

✅ 권장 구조 (TO-BE)

권장: shared/lib/httpClient.ts
export const http = { get: (url)=>..., post: (url, body)=>... }
// api
await http.post(POST.ADD, newPost)

🔄 변경 시나리오별 영향도

  1. HTTP 클라이언트 교체(axios): 현재 구조는 api 파일을 직접 수정해야 함. httpClient wrapper 도입 시 wrapper만 수정하면 됨.
  2. 쿼리 키 규칙 변경: queryKeys 중앙화하면 규칙 변경 시 한 곳만 수정하면 됨.

🚀 개선 단계

  • 1단계: 단계 1 (단기, 반나절): shared/lib/httpClient.ts wrapper(예: http.get/post/put/delete) 추가하고 기존 API 파일을 wrapper로 이전.
  • 2단계: 단계 2 (중기, 1일): queryKeys 팩토리 구현 및 기존 쿼리 훅에 적용.
  • 3단계: 단계 3 (중기, 1~2일): useQueryParameter의 반환 타입 정리 및 문서화(명확한 필드명: searchQuery vs search).

7. 🧪 테스트 용이성

💡 개념 정의

테스트 용이성은 코드가 단위 테스트/통합 테스트로 분리되고, 의존성을 주입해 모킹할 수 있는 정도를 의미합니다.

⚡ 중요성

테스트 가능한 코드는 리팩터 시 회귀를 줄이고, CI 환경에서 안정적으로 검증할 수 있게 해 줍니다.

📊 현재 상황 분석

AS-IS: 순수 로직은 테스트가 쉬움. TO-BE: 폼 로직은 UI 컴포넌트(또는 분리된 훅)로 분리하고, mutation 훅은 side-effect만 담당하여 단위 테스트가 쉬워지도록 리팩토링.

📝 상세 피드백

테스트 작성 용이성은 비교적 양호합니다. 비즈니스 로직(모델 변환 함수)은 순수 함수로 분리되어 있고(entities/*/model/store.ts), server-state는 TanStack Query로 분리되어 있어 모킹이 쉬운 편입니다. 그러나 문제점은 일부 훅들이 컴포넌트 상태(useState)와 API 호출을 혼합하고 있다는 점입니다(예: useAddComment 내부에 newComment form state 사용). 이는 훅 단위 테스트에서 폼 상태와 비즈니스 로직을 분리해야 할 때 부담입니다. 또한 useDialogStore가 ReactNode를 저장하면 E2E가 아닌 단위 테스트에서 모달 관련 목업이 복잡해질 수 있습니다.

❌ 현재 구조 (AS-IS)

현재: useAddComment 내부에서 useState로 newComment을 관리하고, mutate 호출까지 포함됨 -> 테스트  mutate를 모킹하면서 UI 상태도 함께 고려해야 .

✅ 권장 구조 (TO-BE)

권장: form 상태는 AddCommentForm 컴포넌트 내부로 유지, useAddComment는 순수 mutation 레이어로 변환(입력은 파라미터로 받아서 처리).

🔄 변경 시나리오별 영향도

  1. 새 외부 API 연동 시: httpClient wrapper가 있으면 네트워크 모킹 지점이 1곳으로 줄어 테스트 변경 범위 감소.
  2. 복잡한 비즈니스 로직 추가 시: 순수 함수로 분리되어 있으면 유닛 테스트로 충분히 검증 가능.

🚀 개선 단계

  • 1단계: 단계 1 (단기, 반나절): useAddComment 등 훅에서 form state를 분리(컴포넌트가 form state를 소유, 훅은 mutation만 담당).
  • 2단계: 단계 2 (중기, 1~2일): httpClient wrapper를 이용해 네트워크를 모킹 가능한 진입점으로 통일.
  • 3단계: 단계 3 (중기, 1~2일): 주요 순수 함수(postModel/commentModel)에 대한 유닛 테스트 추가.

8. ⚛️ 현대적 React 패턴

💡 개념 정의

현대적 React 패턴은 Suspense, ErrorBoundary, 커스텀 훅, 선언적 데이터 페칭을 포함하며 관심사 분리를 통해 컴포넌트를 단일 책임으로 유지합니다.

⚡ 중요성

Suspense/ErrorBoundary를 사용하면 로딩/에러 UI의 일관성과 코드 중복을 줄일 수 있으며, 선언적 패턴은 테스트와 유지보수를 용이하게 합니다.

📊 현재 상황 분석

AS-IS: 선언적 데이터 계층(TanStack Query)은 적용되어 있으나 Suspense/ ErrorBoundary가 없어 개별 컴포넌트에서 로딩/에러 처리를 중복할 가능성 존재. TO-BE: 상위 레벨 ErrorBoundary/Suspense 적용으로 로딩/에러 처리를 통합하고, 일부 쿼리는 suspense: true로 전환을 고려.

📝 상세 피드백

현대적 React 패턴은 부분적으로 적용되어 있습니다(커스텀 훅, TanStack Query 활용, 컴포넌트 분리). 그러나 Suspense와 ErrorBoundary를 활용한 선언적 로딩/에러 처리는 적용되어 있지 않습니다. 또한 ReactQuery의 suspense모드, ErrorBoundary를 활용하면 로딩/에러 UI의 중복을 줄이고 상위 레벨에서 일괄 처리할 수 있습니다. 커스텀 훅들은 적절히 분리되어 있으나 useQueryParameter처럼 URL 파라미터 관련 훅에서 반환 타입/메서드가 섞여 있어 더 명확한 API가 필요합니다.

❌ 현재 구조 (AS-IS)

현재:  컴포넌트가 useQuery 결과의 isLoading/isError를 직접 체크하여 로딩/에러 UI를 렌더링.

✅ 권장 구조 (TO-BE)

권장: <ErrorBoundary fallback={...}><Suspense fallback={<Skeleton/>}><PostTable/></Suspense></ErrorBoundary>
usePostListQuery({ suspense: true })

🔄 변경 시나리오별 영향도

  1. 로딩 UX 변경(스켈레톤 vs 스피너): Suspense를 활용하면 하위 컴포넌트 수정 없이 전역 fallback만 바꾸면 됨.
  2. 에러 처리 정책 통합: ErrorBoundary를 추가하면 각 컴포넌트에서 개별 에러 처리를 줄일 수 있음.

🚀 개선 단계

  • 1단계: 단계 1 (단기, 1일): 앱 최상단(예: App.tsx)에 ErrorBoundary와 Suspense fallback 컴포넌트 추가(단순 도입).
  • 2단계: 단계 2 (중기, 1~2일): 주요 쿼리들에서 suspense 모드 테스트 적용(문서화와 가이드 포함).
  • 3단계: 단계 3 (중기, 1~2일): 공통 에러/로딩 컴포넌트 디자인을 정하고 팀 가이드에 반영.

9. 🔧 확장성

💡 개념 정의

확장성(Extensibility)은 새로운 기능 추가나 비기능 요구사항(다국어, A/B 테스트, 새로운 인증 등) 도입 시 기존 코드의 수정량을 얼마나 줄일 수 있는지를 의미합니다.

⚡ 중요성

확장성은 제품 요구사항 증가나 외부 종속성 변화(인증, 결제, 디자인 시스템) 발생 시 개발 속도와 안정성에 직접 영향을 줍니다.

📊 현재 상황 분석

AS-IS: 일부 확장성 위험 요소 존재(모달, 쿼리 키 설계, UI 라이브러리 직접 의존). TO-BE: descriptor 패턴, queryKeys, httpClient wrapper, UI adapter를 도입하면 기능 추가 시 수정 범위를 크게 줄일 수 있음.

📝 상세 피드백

확장성은 전반적으로 고려된 편입니다. entities의 순수 함수와 features의 mutation 분리는 새로운 기능(예: 다국어, 추가 인증 방식 등)을 도입할 때 도움이 됩니다. 다만 확장성 향상을 위해 다음 개선을 권장합니다: 1) 쿼리 키 팩토리화 및 QueryClient 설정 표준화 2) 모달 시스템의 descriptor 패턴 적용으로 새로운 모달 타입 추가 시 기존 코드 수정 최소화 3) shared/ui 컴포넌트에 디자인 토큰 혹은 어댑터 계층을 추가하여 디자인 시스템 변경 시 영향도 축소.

❌ 현재 구조 (AS-IS)

현재: 모달을 띄우려면 setPostModal({ show: true, content: <Comp/> })   feature  ReactNode  .

✅ 권장 구조 (TO-BE)

추천: setModal({ view: 'PostDetail', props: { postId: 1 } }) 간단히   있고, DialogRoot에서 매핑하여 렌더링.

🔄 변경 시나리오별 영향도

  1. 다국어 지원 추가: UI 텍스트가 컴포넌트 내부에 하드코딩 되어 있으면 글로벌 변경이 필요. i18n wrapper로 추상화하면 영향도 감소.
  2. A/B 테스트 통합: UI 컴포넌트가 어댑터 패턴으로 분리되어 있으면 실험용 컴포넌트 교체가 쉬움.

🚀 개선 단계

  • 1단계: 단계 1 (단기, 1일): modal descriptor 패턴 적용(위의 sharedLayerPurity 단계와 동일).
  • 2단계: 단계 2 (중기, 1~2일): queryKeys와 httpClient wrapper 도입 및 문서화.
  • 3단계: 단계 3 (이상적, 2~3일): i18n/Design token 도입과 shared/ui 어댑터 레이어 완성.

10. 📏 코드 일관성

💡 개념 정의

코드 일관성은 네이밍, 파일명 패턴, import/export 규약, 코드 스타일(세미콜론, 따옴표 등)이 프로젝트 전체에서 통일되어 있는지를 의미합니다.

⚡ 중요성

일관성은 팀 협업 시 학습 곡선을 낮추고 코드 검토/자동화 도구(ESLint, Prettier) 적용을 용이하게 합니다.

📊 현재 상황 분석

AS-IS: 대부분 컨벤션은 잘 지켜지지만 확장자 표기, export 종류, param 네이밍에서 몇몇 일관성 문제가 존재. TO-BE: ESLint/Prettier 규칙을 확립하고 Export 규칙(컴포넌트는 named export 혹은 팀 규칙)을 통일하면 유지보수성이 개선됩니다.

📝 상세 피드백

대체로 네이밍/파일구조는 일관되게 잘 지켜지고 있습니다(components PascalCase, hooks use* 네이밍 등). 다만 아래와 같은 일관성 이슈가 관찰됩니다: - 일부 import 경로에 파일 확장자(.tsx)가 포함되어 있음 (예: import Header from '../shared/ui/layout/Header.tsx') - Export 패턴은 대부분 default export를 사용(컴포넌트 다수)하고 있어 named export 정책이 혼재될 소지 있음 - useQueryParameter의 QueryParams 인터페이스와 실제 반환객체 키(searchQuery vs search) 간 불일치로 혼란가능 - 파일명/폴더명은 비교적 규칙적이나 일부 파일에서 export 방식 통일 필요

🚀 개선 단계

  • 1단계: 단계 1 (단기, 반나절): ESLint/Prettier 설정 적용 및 팀 규칙 문서화(파일명, export 방식, 확장자 표기 규칙 등).
  • 2단계: 단계 2 (단기, 1일): 확장자(.tsx) 표기 제거와 바렐 적용(자동 치환 스크립트 권장).
  • 3단계: 단계 3 (중기, 1~2일): useQueryParameter의 타입과 반환 키 정리(검색 관련 키명 통일).

🎯 일관성 체크포인트

파일명 규칙

  • 경로 import에 .tsx 확장자 사용(권장: 확장자 생략). 예: import Header from '../shared/ui/layout/Header.tsx'

Import/Export 패턴

  • 컴포넌트에 대부분 default export 사용됨 (팀 규칙으로 named vs default 통일 권장)
  • 바렐(index) 파일 미비로 import 경로가 파일 단위로 노출

변수명 규칙

  • useQueryParameter의 내부 QueryParams 인터페이스에 'search' 필드가 정의되어 있으나 실제 반환은 'searchQuery' 사용 — 네이밍 불일치

코드 스타일

  • 대체로 일관적이나 일부 파일에서 세미콜론, 공백 스타일 차이 가능(ESLint/Prettier 적용 권장)

11. 🗃️ 상태 관리

💡 개념 정의

상태 관리는 서버 상태(백엔드 데이터)와 클라이언트 상태(UI/로컬)를 분리하여 각 상태의 생명주기, 캐싱, 동기화 전략을 명확히 하는 것을 의미합니다.

⚡ 중요성

명확한 상태 분리는 버그를 줄이고, 실시간/오프라인 기능 추가 시 의사결정을 단순화합니다.

📊 현재 상황 분석

AS-IS: 전반적 분리는 훌륭하지만 modal/state 디자인에 의해 결합도가 올라감. TO-BE: modal descriptor 패턴 및 form state 컴포넌트 분리로 상태 관리가 더 예측 가능하고 테스트 가능해짐.

📝 상세 피드백

서버 상태(TanStack Query)와 클라이언트 로컬 상태(zustand, component local state)가 분리되어 있어 좋은 설계입니다. 서버 상태는 entities의 useQuery로 캡슐화되어 있고, features 훅은 mutation과 낙관적 업데이트를 담당합니다. 단점으로는 dialog 상태를 zustand에 ReactNode로 저장한 점과 useQueryParameter 훅의 반환값 구조(함수 setters와 params 분리)가 혼합되어 있어 일부 혼란이 있습니다. 또한 일부 훅에서 로컬 폼 상태와 mutation이 결합되어 테스트/유지보수가 번거로울 수 있습니다.

❌ 현재 구조 (AS-IS)

현재: Dialog 상태를 전역(zustand) 저장하고 여러 컴포넌트에서 setPostModal/setCommentModal로 ReactNode를 주입.

✅ 권장 구조 (TO-BE)

권장: Dialog store는 { view: 'PostDetail'|'AddPost'|'UpdatePost', props } 형태의 직렬화 가능한 데이터만 저장. DialogRoot가 이를 해석하여 컴포넌트를 렌더링.

🔄 변경 시나리오별 영향도

  1. 실시간 기능 추가(웹소켓): 서버 상태와 로컬 상태 분리가 명확하면 데이터 동기화/병합 전략을 집중적으로 설계할 수 있음.
  2. 오프라인 모드 요구: 서버 상태는 로컬 캐시(TanStack Query)와 충돌 가능성 있으므로 명확한 정책 필요.

🚀 개선 단계

  • 1단계: 단계 1 (단기, 반나절): modal 상태를 descriptor 형태로 변경(모든 set*Modal 호출 변경 필요, 약 7곳 수정 예상).
  • 2단계: 단계 2 (중기, 1일): form state와 mutation 분리(컴포넌트가 local state 소유, 훅은 mutation만).
  • 3단계: 단계 3 (중기, 1~2일): 서버 상태(쿼리)와 클라이언트 상태(zustand)를 사용하는 개발 가이드 작성.

🤔 질문과 답변

질문: dialog 처리 구조(위치/구조)와 zustand 사용 여부에 대한 권장 방식
답변 요약:

  1. 현재 구조(Widgets에 DialogRoot, Shared에 useDialogStore)는 팀 규모가 작거나 빠르게 구현할 때 적절합니다. 다만 useDialogStore가 React.ReactNode를 저장하는 방식은 결합도를 높이고 테스트/직렬화/확장(예: 서버 렌더링, 로그)을 어렵게 합니다. 2) 권장 패턴: modal descriptor (type/view + props) 패턴을 사용하세요. store에는 직렬화 가능한 descriptor만 저장하고, DialogRoot는 descriptor를 해석해 해당 컴포넌트를 렌더링합니다. 장점: modal 구현 변경 시 store 사용부를 수정할 필요 없음(영향 범위 축소), 모달 타입 추가가 간단, 테스트가 쉬움, 직렬화/로깅 가능. 3) zustand를 유지할 것인지: zustand 자체는 경량 전역상태로 괜찮습니다. 다만 store API를 추상화(예: showModal({view, props}))하고, 내부 구현(로컬 상태/Ctx 전환 등)을 캡슐화하세요. zustand → context 전환 시 영향을 받는 파일 수는 현재 약 7곳(useDialogStore 임포트 위치: AddCommentForm, UpdateCommentForm, AddPostForm, UpdatePostForm, PostManagePage, PostDetail, DialogRoot). descriptor 패턴을 쓰면 실제 수정 파일 수를 DialogRoot와 store 어댑터로 줄일 수 있습니다. 4) 구체적 코드 제안(AS-IS → TO-BE):
    // AS-IS (현재)
    // useDialogStore.postModal.content =

// TO-BE (권장)
// useDialogStore.postModal = { view: 'UpdatePost', props: { postId: post.id } }
// DialogRoot:
// const mapping = { UpdatePost: UpdatePostForm, AddPost: AddPostForm, PostDetail: PostDetail };
// const Comp = mapping[modal.view]; return <Comp {...modal.props} />

  1. 추가 고려사항 및 질문:
  • 만약 모달 애니메이션/포털 구성이나 라이프사이클(열기/닫기 애니메이션) 때문에 ReactNode 직접 주입이 필요하다면, descriptor를 사용하되 필요시 컴포넌트 팩토리를 매핑에서 제공하세요(예: mapping[view] = (props)=> <Comp {...props}/> ).
  • 디자인 시스템을 바꿀 가능성이 있다면 shared/ui의 Dialog wrapper만 변경하도록 컴포넌트를 얇게 유지하세요.
  • 직렬화 가능한 descriptor를 사용하면 (예: URL 기반 모달 공유) 확장성이 좋아집니다.

🎯 셀프 회고 & 제안

셀프회고에 대한 인사이트 및 추가 질문(사고를 확장하기 위한 제안)
요약된 인사이트:

  • FSD를 실제로 적용해보며 레이어별 책임을 체감한 점은 매우 중요합니다. 경험적으로 '어디에 두는가'를 명확히 결정해보는 시행착오가 온보딩과 팀 합의에 큰 도움을 줍니다. 실제로 entities에 조회 훅, features에 mutation 훅을 분리한 접근은 코드 책임 분리에 큰 도움이 됩니다. 이는 재사용성과 테스트성 모두에 긍정적 영향을 줍니다.
  • dialog와 post-filter 같은 UI 인프라를 어디에 둘지 고민한 점은 좋은 신호입니다. 이런 고민은 설계의 경계(Shared vs Widget vs Feature)를 명확히 하는 데 필수적입니다.

추가로 생각해볼 질문들:

  1. dialog의 '경계'를 어떻게 결정하나요? (사용자 액션의 부수효과인가, 재사용 가능한 UI 인프라인가) — 이를 위해 모달을 "도메인 액션(Feature)" vs "UI 인프라(Shared)" 두 관점으로 나눠 각각 장단점을 목록화해 보세요.
  2. post-filter를 '세분화'했을 때 발생하는 테스트/스타일/상태 전달 비용은 어느 정도인가? (예: 컴포넌트가 늘어나도 props 전달 방식으로 복잡도가 낮아지는지 측정해보세요)
  3. 팀 합의 없이 FSD를 도입할 경우 발생할 수 있는 협업 비용(리뷰/병합 충돌)은 어떻게 줄일 수 있을까요? (예: 폴더 템플릿, PR 템플릿, 코드 소유권 규칙 등 실험해보세요)

권장 실험(작은 실험으로 검증):

  • 실험 1: modal descriptor 패턴으로 전환하는 작은 PR을 만들어 영향 범위(파일 수/난이도)를 정량적으로 측정해보세요. 기대: 변경 파일 수를 약 7→2로 줄일 수 있음.
  • 실험 2: PostFilter를 3개의 컴포넌트로 쪼개고 각각에 단위 테스트를 추가해 개발/유지보수에 드는 시간을 비교해보세요.

마지막으로 한 문장: 지금의 구조는 FSD의 원칙을 잘 따르려는 훌륭한 출발점입니다. 작은 추상화(모달, httpClient, queryKeys)와 컨벤션 통일로 변화에 대한 유연성을 크게 향상시킬 수 있습니다.


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

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

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

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.

5 participants