Skip to content

Conversation

@jy0813
Copy link

@jy0813 jy0813 commented Dec 9, 2025

과제 체크포인트

기본과제

목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 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가 정상적으로 작동하는가?

최종과제

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

과제 셀프회고

전체 디렉토리 구조

src/
├── App.tsx
├── main.tsx
│
├── app/
│   ├── providers/
│   │   ├── ModalProvider.tsx
│   │   └── QueryProvider.tsx
│   ├── stores/
│   │   └── modalStore.ts
│   └── ui/
│       └── ModalContainer.tsx
│
├── pages/
│   └── PostsManagerPage.tsx
│
├── widgets/
│   ├── comment/
│   │   └── ui/
│   │       ├── CommentListWidget.tsx
│   │       └── index.ts
│   ├── footer/
│   │   └── ui/
│   │       ├── Footer.tsx
│   │       └── index.ts
│   ├── header/
│   │   └── ui/
│   │       ├── Header.tsx
│   │       └── index.ts
│   └── post/
│       └── ui/
│           ├── PostDetailWidget.tsx
│           ├── PostTableWidget.tsx
│           └── index.ts
│
├── features/
│   ├── comment/
│   │   ├── model/
│   │   │   └── useCommentMutation.ts
│   │   └── ui/
│   │       ├── AddCommentButton.tsx
│   │       ├── CommentEditContent.tsx
│   │       ├── CommentForm.tsx
│   │       ├── DeleteCommentButton.tsx
│   │       ├── EditCommentButton.tsx
│   │       ├── LikeCommentButton.tsx
│   │       └── index.ts
│   ├── post/
│   │   ├── model/
│   │   │   └── usePostMutation.ts
│   │   └── ui/
│   │       ├── AddPostButton.tsx
│   │       ├── DeletePostButton.tsx
│   │       ├── EditPostButton.tsx
│   │       ├── PostDetailButton.tsx
│   │       ├── PostForm.tsx
│   │       ├── PostSearchFilter.tsx
│   │       └── index.ts
│   └── user/
│       └── ui/
│           ├── UserDetailContent.tsx
│           └── index.ts
│
├── entities/
│   ├── comment/
│   │   ├── api/
│   │   │   └── commentApi.ts
│   │   ├── lib/
│   │   │   ├── commentCacheUtils.ts
│   │   │   ├── commentFormUtils.ts
│   │   │   ├── commentLikeUtils.ts
│   │   │   └── index.ts
│   │   └── model/
│   │       ├── likedCommentStore.ts
│   │       ├── queryKeys.ts
│   │       ├── types.ts
│   │       └── useCommentQuery.ts
│   ├── post/
│   │   ├── api/
│   │   │   └── postApi.ts
│   │   ├── lib/
│   │   │   ├── index.ts
│   │   │   ├── postCacheUtils.ts
│   │   │   ├── postFilterUtils.ts
│   │   │   ├── postFormUtils.ts
│   │   │   ├── postMapperUtils.ts
│   │   │   └── postSortUtils.ts
│   │   └── model/
│   │       ├── modifiedPostStore.ts
│   │       ├── postIdStore.ts
│   │       ├── queryKeys.ts
│   │       ├── types.ts
│   │       ├── usePostQuery.ts
│   │       └── useProcessedPosts.ts
│   ├── tag/
│   │   ├── api/
│   │   │   └── tagApi.ts
│   │   └── model/
│   │       ├── queryKeys.ts
│   │       ├── types.ts
│   │       └── useTagQuery.ts
│   └── user/
│       ├── api/
│       │   └── userApi.ts
│       └── model/
│           ├── queryKeys.ts
│           ├── types.ts
│           └── useUserQuery.ts
│
└── shared/
    ├── config/
    │   ├── constants.ts
    │   └── index.ts
    ├── hooks/
    │   ├── index.ts
    │   └── useURLState.ts
    ├── lib/
    │   ├── highlightText.tsx
    │   ├── modal-context.ts
    │   └── modalUtils.ts
    └── ui/
        ├── Button.tsx
        ├── Card.tsx
        ├── Dialog.tsx
        ├── Input.tsx
        ├── Select.tsx
        ├── Table.tsx
        ├── Textarea.tsx
        └── index.ts

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

  1. 이전 과제에서 **Entity = 냉장고(데이터), Features = 요리사(행동)**라는 비유로 FSD를 이해했습니다. 이번 과제에서는 이 비유가 TanStack Query와 결합되면서 더 구체적인 공식이 되었습니다.
entities = useQuery (Read) → "이게 뭐야?"
features = useMutation (CUD) → "이걸로 뭘 해?"
┌──────────┬──────────┬─────────────┐
│  연산    │  변화?   │   레이어    │
├──────────┼──────────┼─────────────┤
│  Read    │   ❌     │  entities   │
│  Create  │   ✅     │  features   │
│  Update  │   ✅     │  features   │
│  Delete  │   ✅     │  features   │
└──────────┴──────────┴─────────────┘
entities/post/model/usePostQuery.ts    → usePostList, usePostDetail, usePostSearch
features/post/model/usePostMutation.ts → useCreatePost, useUpdatePost, useDeletePost

첫째, 관심사 분리. "이 데이터가 뭐지?"와 "이 데이터를 어떻게 바꾸지?"는 다른 질문이다.

둘째, FSD 원칙 준수. FSD 공식 문서에서 features를 "비즈니스 가치를 주는 액션"이라고 정의한다. 데이터를 바꾸는 건 확실히 액션이다.

셋째, 의존성 방향. features가 entities를 import하는 건 자연스러운데, 반대는 이상하다.

게시물 삭제 버튼이 게시물 데이터를 알아야 하는 건 당연한데, 게시물 데이터가 삭제 버튼을 알 필요 없다.

API는 왜 entities에만 설계했나? Create, Update, Delete API 도 있는데, 위에 Query와 Mutation을 생각하면 features에 들어가야하는것이 아닌가?

API 함수 자체는 그냥 "서버랑 통신하는 인프라"다. 진짜 "변화"는 useMutation Hook에서 일어난다.

그래서 API 함수(postApi.ts)는 모두 entities에 두면서, useMutation만 features로 분리하는 게 핵심이었습니다. API 함수 자체는 "도구"일 뿐이고, 그 도구를 "어떻게 사용하는가"(Mutation + 캐시 무효화)가 진짜 "행동"이기 때문입니다.

  1. "의존성 역전"의 활용법

SOLID 원칙을 배울 때 "의존성 역전"이라는 말을 들었습니다. 하지만 솔직히 뭔 소린지 이해를 못했고, "인터페이스에 의존해라", "추상화에 의존해라"... 말은 알겠는데 실제로 어떻게 쓰는 건지 감이 안 왔습니다.

이번 과제에서 FSD 아키텍처를 적용하면서 진짜 의존성 역전이 필요한 상황을 마주쳤습니다.

// 문제 상황: features 레이어에서 app 레이어의 store를 쓰고 싶다.
// 하지만 FSD 규칙상 features → app import는 금지됨

// ❌ 이렇게 하면 ESLint 에러
// features/post/ui/AddPostButton.tsx
import { useModalStore } from "@/app/stores/modalStore"  // Error!

앞서, FSD 아키텍처의 핵심은 "계층 간 단방향 의존성" 입니다. 그래서 ESLintno-restricted-imports 규칙으로 컴파일 타임에 강제하기로 했습니다.

// eslint.config.js
// 코드 일부분 첨부
{
  files: ["src/features/**/*"],
  rules: {
    "no-restricted-imports": ["error", {
      patterns: [
        {
          group: ["@/app/*"],
          message: "features는 app을 import할 수 없습니다 (FSD 규칙 위반)"
        },
        {
          group: ["@/pages/*"],
          message: "features는 pages를 import할 수 없습니다"
        },
        {
          group: ["@/widgets/*"],
          message: "features는 widgets를 import할 수 없습니다"
        },
      ]
    }]
  }
}

이를 지키기 위해 적용한 게 의존성 역전 패턴 입니다.

// 1단계: shared에 "인터페이스"만 정의
// shared/lib/modal-context.ts
export interface ModalContextType {
  openModal: (type: ModalType, data?: unknown) => void
  closeModal: (id: string) => void
  // ...
}

export const ModalContext = createContext<ModalContextType | null>(null)
export const useModalContext = () => {
  const context = useContext(ModalContext)
  if (!context) throw new Error("ModalProvider 필요")
  return context
}

// 2단계: app에서 "구현" 제공
// app/providers/ModalProvider.tsx
const store = useModalStore()  // Zustand store
return <ModalContext.Provider value={store}>{children}</ModalContext.Provider>

// 3단계: features에서 "인터페이스"만 사용
// features/post/ui/AddPostButton.tsx
import { useModalContext } from "@/shared/lib/modal-context"  // OK!

const { openModal } = useModalContext()

이 패턴을 적용하고 나서야 "아, 의존성 역전이 이거구나!"를 체감했습니다.

  • shared는 app을 모른다: Context 타입만 정의하고, 실제 구현이 뭔지는 모른다.
  • features도 app을 모른다: Context 훅만 쓰고, 그게 Zustand인지 Redux인지는 모른다.
  • app만 전체를 알고 조립한다: Store를 만들고, Context에 주입한다.

이게 바로 "상위 레이어가 하위 레이어의 인터페이스에 의존한다"는 의미이고, 추상적으로 느껴졌는데, 실제로 적용해보니 왜 이런 패턴이 필요한지 이해가 됐습니다.


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

  1. 단계별 진행과 검증에 집중했습니다.

708줄짜리 PostsManagerPage.tsx를 한 번에 리팩토링하는 대신, Phase 1부터 7까지 단계별로 진행하며 각 단계마다 ESLint 검증을 수행했습니다. 덕분에 중간에 문제가 생겨도 어느 단계에서 발생했는지 쉽게 파악할 수 있었습니다.

FSD 의존성 방향을 철저히 지키려고 노력했습니다.

app → pages → widgets → features → entities → shared

이 방향을 거스르는 import가 발생하면 즉시 수정하고, ESLint 규칙으로 자동 검증되도록 설정했습니다. 특히 highlightText 함수의 위치를 entities/post/lib에서 shared/lib로 옮긴 것처럼, 여러 도메인에서 사용되는 코드는 적절한 레이어로 이동시키려고 노력했습니다.

또한, AI 대본 작성을 통해 개념을 내재화하려고 했습니다. 단순히 코드를 작성하는 것에서 그치지 않고, "사수가 부사수에게 설명하는 대본", "팀 토론 대본", "YAGNI에 대한 솔직한 대화" 등을 작성하면서 왜 이렇게 하는지를 스스로 설명할 수 있는 수준까지 이해하려고 노력했습니다.

image

대본 링크

  1. 기존 코드의 버그들 수정 및 낙관적인 업데이트로 추가 구현하기.
  • 기존코드의 sortOrder=asc Params는 잘못된 코드 => order 로 변경하여 정렬 버그 수정 dummyjson

  • 기존코드는 게시글 수정 시 작성자가 깨지는 이슈 수정

image image
  • 기존 코드 게시글 추가 및 수정에서 넘버 인풋으로는 User ID 를 모름, 태그 생성도 불가능. => UI 개선 및 낙관적 업데이트를 통한 UX 개선
    • 게시글 수정 동일 이슈 수정
    • user 를 선택하여 작성자를 테이블에서 확인 가능해짐.
    • 태그를 등록하여 태그 필터링 가능해짐.

image image

image image
  • 기존 코드는 추가 게시글은 댓글을 작성 못함 (추가된 게시글이 실제로 db에 저장되는 데이터가 아님)
    • 캐시가 없으면 새로 생성 (낙관적 게시글의 경우), 수정의 경우 실제 DummyJSON 데이터(ID 1~340)만 롤백 하고 ID 341+는 낙관적으로 생성된 가짜 데이터이므로 롤백 안 함. 삭제 등 도 동일 처리.
    • 댓글에도 댓글 작성자 등록가능하게 추가
image image
  • 그외 등등 전체적인 기존코드에서의 버그 또는 api 요청만으로 진행하여 불가능 했던 요소들 캐시로 관리하여 구현
  • 좋아요 버튼 계속 누를 수 있는 것이 의도된 기능인지는 모르겠지만, 토글 형식으로 낙관적 업데이트 적용
  • Select height 이슈 수정 및 모달에 컨텐츠가 넘치는 등 의 요소들 수정
  • 필터 및 검색 등 초기화 버튼 추가
  • playwright mcp e2e 테스트 진행

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

  1. YAGNI vs 아키텍처 투자 사이의 균형점을 찾는 것이 어렵습니다.

"지금 필요한 것만 만들자"와 "나중을 위해 구조를 잡아두자" 사이에서 어디까지가 적절한지 판단하기 어려웠습니다.

YAGNI 극단 → 기술 부채 누적
오버엔지니어링 극단 → 불필요한 복잡성

"비용이 낮으면 해도 괜찮다"는 기준이 있지만, 실제 프로젝트에서 이 기준을 적용하려면 더 많은 경험이 필요할 것 같습니다.

  1. 테스트 전략에 대한 고민

이번에는 기존의 과제들과 다르게 테스트 코드가 없어서, 직접 작성해볼까도 고민해봤는데, 구현 내용만 우선 제대로 동작하는지
테스트 하기 위해서 playwright mcp 를 사용하여 E2E 테스트 밖에 진행하지 못했습니다.

FSD는 레이어가 잘 분리되어 있어서 각 레이어를 독립적으로 테스트할 수 있을텐데, 모든 레이어를 모두 테스트해야 하는지, 아니면 우선순위가 있는지 이런게 가장 큰 고민입니다.

  • 순수함수 단위 테스트 (Vitest)
  • Custom Hook 테스트 (React Testing Library)
  • TanStack Query의 mutation 테스트
  • E2E 직접 작성 하여 테스트 (Playwright)

순수함수로 분리만 해놓고 테스트를 하나도 못 했다는 점에서 많이 아쉽고, 앞으로 이 부분을 확실히 보완하고 싶다는 생각이 들었습니다.

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

이번에 배운 내용을 통해 앞으로 개발을 할 때, 기능을 ‘막연하게 구현’하는 것이 아니라 각 단계를 더 명확하게 구조화해서 접근하는 습관을 적용해보고 싶습니다.

기능을 만들 때 가장 먼저 “이 기능의 데이터는 무엇인가?”부터 정의하고, 그 데이터를 entities 레이어의 model/types.ts에 정리하는 방식으로
도메인 중심의 개발 흐름을 의식적으로 가져가려고 합니다.

그 다음으로 “이 데이터를 서버로부터 어떻게 가져올까?”라는 관점에서 필요한 API들을 entities/api에 분리하고, 조회(Read) 기능은 entities/model/useXxxQuery.ts에서 관리해 데이터 접근 로직을 UI와 확실히 분리하는 구조를 적용할 계획입니다.

반대로 생성/수정/삭제(CUD) 액션은 feature 단위에서 발생하는 사용자 의도이기 때문에 features/model/useXxxMutation.ts에서 관리하며, 사용자 인터랙션 UI는 features/ui 쪽으로 모아 “사용자 액션 단위”로 슬라이스를 구성하는 방식도 실전에서 사용해보고 싶습니다.

여러 feature와 entity를 조합해 화면을 구성해야 하는 경우에는 이를 widgets/ui로 올려 좀 더 재사용 가능한 조합 단위로 구성함으로써,
pages 레이어에서는 가능한 한 화면 배치와 라우팅에만 집중하는 구조를 유지하려고 합니다.

정리하자면, 앞으로는 기능을 구현할 때마다 ‘데이터 정의 → API → 조회 → 변경 → 액션 UI → 조합’이라는 일련의 흐름을
FSD 구조에 맞게 체계적으로 나눠서 개발하며, 기능을 더 명확하게 분리하고 유지보수하기 쉬운 형태로 작성하는 것을 목표로 하고 있습니다.

챕터 셀프회고

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

이번 챕터를 진행하면서 가장 크게 느낀 점은 “좋은 구조를 만들기 위해서는 좋은 분석이 먼저 필요하다”는 것입니다.

클린코드를 지향하거나, 결합도를 낮추고 응집도를 높이며, 순수함수/컴포넌트 분리/상태 관리/폴더 구조를 잘 가져가는 것이 결국 목표였지만,
이 모든 것은 as-is를 정확하게 파악하지 못하면 적용할 수 없다는 걸 다시 깨달았습니다.

기능을 구현하기 전에는 항상 이렇게 스스로에게 질문해야 합니다.

  • 이 기능의 핵심 데이터는 무엇인가?
  • 이 데이터는 어디에서 오고, 어디에서 사용되는가?
  • 변하는 값은 무엇이고, 변하지 않는 값은 무엇인가?
  • 어떤 로직이 비즈니스 로직이고, 어떤 로직이 단순한 UI 표현인가?
  • 무엇이 순수해야 하고, 무엇이 외부 환경(API/브라우저)에 의존하는가?
  • 컴포넌트가 어떤 역할을 해야 하며, 이 역할은 분리될 수 있는가?
  • 전역 상태가 정말 필요한가? 아니면 props/state로 충분한가?
  • 여러 기능이 섞여 있지는 않은가? (기능 간 결합도)
  • 동일한 관심사가 한 곳에 모여 있는가? (응집도)

이 질문들에 명확하게 답할 수 있어야 그 다음에야 비로소 FSD 구조든, 디자인 패턴이든, 클린한 컴포넌트 구조든 “적용할 때 고민 없이” 자연스럽게 결정할 수 있다는 걸 느꼈습니다.

결론은 좋은 코드 구조는 분석의 결과라고 생각합니다. 테오의 말대로 그 모습이 상당히 유니크하고 독창적이지 않았고, 다른 사람들도 이해하기 쉬운 코드 였습니다.

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

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

어디서부터 건드려야 하지?

처음 Before 패키지를 분석했을 때, 가장 먼저 든 감정은 혼란감이었습니다.
같은 컴포넌트 내부에서 인라인 스타일 + CSS 클래스 + 하드코딩 색상이 뒤섞여 있었고 UI 컴포넌트가 도메인 로직을 내부에서 판단하고 있었으며, 하나의 페이지가 648줄짜리 God Component로 모든 상태, 모든 로직을 다 들고 있었기 때문입니다.

특히 스타일링 같은 경우에는 인라인 스타일이 컴포넌트 곳곳에 뿌려져 있어, 눈으로 일일이 찾아가며 수정해야 했습니다.
“이 변경이 다른 곳에 영향을 줄까?”라는 불안함도 따라다녔습니다. 더티코드의 가장 큰 문제는 **“코드를 수정하는 순간 확신이 없어진다”**는 점입니다. 읽히지 않는 코드 안에서는 작은 수정조차도 에러가 되곤 했습니다.

그렇다면 읽기 좋은 코드란 무엇일까?

이번 챕터를 통해 저는 “읽기 좋은 코드”의 기준을 다시 생각하게 되었습니다.

  1. 한 눈에 역할이 보이는 코드

Before에는 button.tsx 안에서 UI Props와 도메인 로직이 섞여 있었지만, After에서는 UI는 UI만 비즈니스 로직은 훅으로,
디자인 설정은 토큰으로 명확히 분리되었습니다.

이렇게 관심사가 분리되면 파일을 열자마자 **"아, 얘는 이런 역할을 하는구나"**를 바로 이해할 수 있습니다.

  1. 재사용 가능한 구조

Before 코드에서는 특정 색상이나 spacing 값이 여러 파일에 하드코딩 되어 있어 변경 시 모든 컴포넌트를 직접 찾아 수정해야 했지만,
After에서는 디자인 토큰을 만들고 Tailwind + CVA 기반으로 일관된 스타일 시스템을 도입했습니다. 유지보수가 쉬워지고 의도도 명확해지고
컴포넌트 사용법도 직관적입니다. “UI는 적어도 스타일 때문에 고생하면 안 된다”는 것을 깨달았습니다.

  1. 작고 명확한 단위로 나뉜 코드

리팩토링을 통해 648줄짜리 ManagementPage가 137줄로 분리되었고, 각각의 훅이 단일 책임만 담당하게 되었습니다.

  • 탭 관리 → useManagementTab
  • 데이터 로딩 → useManagementData
  • 유효성 검증 → useManagementForm
  • CRUD 조합 → useManagementActions

이렇게 나누고 나니 코드는 읽기 쉬워졌을 뿐 아니라, 변경도 훨씬 안전해졌습니다.

유지보수하기 좋은 코드란 무엇일까?

3-1 작업을 통해 저는 유지보수성이란 단순히 “버그가 없고 튼튼한 코드”가 아니라, 변경 비용을 최소화할 수 있는 구조라고 생각하게 되었습니다.

Before 에는 로직과 UI가 섞여 있어 수정하면 사이드 이펙트가 불명확하고, 도메인 규칙이 곳곳에 산재해 있어 검색이 힘들고,
컴포넌트 구조가 일관성이 없어 새 기능 추가가 항상 모험이었습니다.

After 에는 폴더 구조가 역할 기준으로 재정리되고 컴포넌트가 UI-only 역할을 하며 토큰 기반 디자인 시스템으로 변경 영향도가 줄어들었고
상태와 액션이 훅으로 분리되며 테스트 가능성도 올라갔습니다. 결국 “예측 가능한 코드”가 유지보수하기 좋은 코드라는 것을 명확히 느꼈습니다.

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

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

상태를 어디에 둬야 하는가?

예를 들어 searchTerm(검색어)을 다룰 때, HeaderWidget에서 입력하고 ProductListWidget에서 사용해야 하는데
두 컴포넌트가 서로 연결되어 있지 않았습니다.

처음엔 “전역으로 올리면 끝 아닌가?”라고 생각했지만 곰곰이 따져보니 검색어는 쇼핑 페이지 안에서만 의미가 있는 값이었고, 앱 전체에서 필요한 전역 상태라고 보기 어려웠습니다.

그래서 검색어는 App에서만 관리하고 props로 내려보내는 방식을 선택했고, 이 과정에서 처음으로 전역 상태의 편의성 vs props의 명시성
이라는 설계적 균형을 처음 체감했습니다.

그 결과, 나름의 기준도 생겼습니다. "3단계 이내의 전달이면 props, 그 이상이면 전역"
단순한 기술적 선택이 아니라, “왜 그렇게 해야 하는지”를 스스로 따져본 결정이었습니다.

설계적 사고가 자리 잡기 시작하다.

이 경험을 거치면서 저는 점점 상태를 이렇게 구분하게 됐습니다.

  1. 도메인 데이터와 관련 있다 → Zustand 같은 전역 상태 라이브러리 사용
    예: Product, Cart, User 등 여러 페이지에서 사용되고 서버 상태와 동기화되며 앱의 핵심 데이터(domain)에 해당한다.
    따라서 전역/비즈니스 상태는 Zustand로 관리하는 게 자연스럽다.

  2. UI 상태이며 스코프가 넓다 → Context로 명확한 범위를 지정
    예: Toast, Modal, Theme, 도메인과 직접적 연관은 없지만 여러 컴포넌트에서 사용하고 UI적으로 “광역 범위”가 필요한 값들 이런 값은 Context/Provider로 해결하는 게 더 적절하다.

searchTerm은? 과제가 끝나고 현재 3번째 챕터를 겪어보니, 처음엔 Zustand에 올릴까 고민했지만, 보통 검색어는 **queryParams(주소)**와 동기화하는 게 일반적이라는 걸 알게 됐습니다. 그래야 새로고침해도 유지되고, URL 공유도 가능하기 때문입니다.
즉 searchTerm은 실제로는 상태 관리보다 라우팅 컨텍스트에 더 가깝다라는 정리가 되면서 "상태를 어디에 둬야 하는가?"에 대한 기준이 생겼고, 상태가 담고 있는 의미(도메인 vs UI vs 라우팅)에 기반한 설계적 사고의 틀이 잡히기 시작했습니다.

순수함수 분리 여정

상태 관리 기준이 잡힌 뒤, 다음 혼란은 “로직을 어디에 둘 것인가?”였습니다. 처음에는 컴포넌트 안에 모든 계산 로직과 조건문이 들어가 있었는데 이걸 분리하기 시작하면서 또 하나의 깨달음(?)을 느꼈습니다. 예를 들어 getCartSummary() 같은 순수함수는 입력만 같으면 항상 같은 결과를 주기 때문에 테스트하기 쉽고 어디서 호출해도 안정적이고 UI 로직과 비즈니스 로직을 명확히 분리할 수 있었습니다.

이걸 경험하면서 “UI는 UI만, 계산은 계산만, 서버 통신은 서버만” 이라는 설계 원리를 몸으로 이해하게 되었습니다. 함수형 프로그래밍, 아토믹 디자인, 데이터 흐름 분리 같은 개념들이 처음으로 “그래서 이런 걸 하는구나”로 바뀌었습니다.

설계적 사고가 생기면서 결합도가 자연스럽게 낮아진다.

처음엔 전역 상태냐 props냐, 이런 문제들로만 고민했는데 이제는 상태가 가진 “의미"를 기준으로 판단하게 되었습니다.
이 값은 도메인인가? 이 값은 UI인가? 이 값은 어느 범위에서 의미가 생기는가? 이 로직은 계산인가, 액션인가?
이 함수는 어디에 있어야 가장 결합도가 낮아지는가? 이런 질문을 하면서 설계적 사고 → 자연스러운 구조 분리 → 낮은 결합도 라는 흐름이 몸에 잡히기 시작했습니다. 결과적으로 단순히 "파일을 나누는" 게 아니라 “코드를 왜 그렇게 설계해야 하는지”를 이해한 게 이번 챕터에서 가장 큰 성장 포인트였습니다.

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

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

"이 함수는 어디에 둬야 하지?"라는 질문을 던지는 것 자체가 응집도를 높이는 과정 입니다.

질문을 던지면 자연스럽게 답이 나옵니다.

  • "이 함수가 하는 일이 뭐지?" → 게시물 필터링
  • "이건 어떤 도메인에 속하지?" → Post 도메인
  • "UI 관련이야, 비즈니스 로직이야?" → 순수한 계산 로직
  • 결론 도달

이 사고 과정을 반복하다 보니, 코드를 작성하기 전에 "이건 어디에 속하는 코드지?"를 먼저 생각하는 습관이 생겼습니다.

TanStack Query로 서버 상태를 분리하면서 느낀 해방감

  • Before: "내가 다 관리해야 해"
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)

const fetchPosts = async () => {
  setLoading(true)
  setError(null)
  try {
    const response = await fetch('/api/posts')
    const data = await response.json()
    setPosts(data.posts)
  } catch (e) {
    setError(e.message)
  } finally {
    setLoading(false)
  }
}

useEffect(() => {
  fetchPosts()
}, [skip, limit, selectedTag])  // deps 관리 지옥

이 패턴의 문제점:

  1. useState 3개: posts, loading, error를 직접 관리
  2. useEffect: deps 배열 관리가 고통
  3. 캐싱 없음: 같은 데이터 매번 fetch
  4. 중복 요청: 빠르게 연속 클릭하면 race condition
  5. 재시도 없음: 실패하면 그냥 에러
  • After: "그냥 선언만 하면 돼"
const { data, isLoading, error } = useQuery({
  queryKey: ['posts', { skip, limit, tag: selectedTag }],
  queryFn: () => postApi.getList({ skip, limit, tag: selectedTag })
})
  • isLoading, error를 useState로 관리 안 해도 됨
  • 캐싱 자동 (같은 queryKey면 캐시에서 반환)
  • 중복 요청 자동 제거
  • 실패 시 자동 재시도 (기본 3회)
  • 백그라운드 리페칭 (탭 전환 시 자동 갱신)

결국.. TanstackQuery도 관심사의 분리만 잘하면, 서버 상태와 UI 상태의 명확한 분리가 됩니다.

  • 서버에서 오는 데이터 → TanStack Query
  • UI 상태 (모달, 토글 등) → Zustand / Context
  • URL 상태 (페이지네이션, 필터) → useState + URL 동기화
Before: 모든 상태 = useState
├── posts (서버에서 온 데이터) → useState
├── searchQuery (사용자 입력) → useState
├── selectedTag (필터 선택) → useState
├── isModalOpen (UI 상태) → useState
└── loading (비동기 상태) → useState

After: 상태별로 적절한 도구
├── posts → useQuery (서버 상태)
├── searchQuery → useSearchParams (URL 상태)
├── selectedTag → useSearchParams (URL 상태)
├── isModalOpen → Zustand (전역 UI 상태)
└── loading → useQuery.isLoading (파생 상태)

나만의 폴더 및 레이어 계층 기준

레이어 계층

┌─────────────────────────────────────────────────────────────┐
│ app          │ 앱 초기화, Provider, 전역 Store, 모달 렌더링  │
├─────────────────────────────────────────────────────────────┤
│ pages        │ URL 라우트별 페이지, Widget 배치               │
├─────────────────────────────────────────────────────────────┤
│ widgets      │ 독립적 UI 블록, Features + Entities 조합       │
├─────────────────────────────────────────────────────────────┤
│ features     │ 도메인 변화 액션 (Mutation + UI)               │
├─────────────────────────────────────────────────────────────┤
│ entities     │ 도메인 데이터 + 조회 (Query + 표시 UI)         │
├─────────────────────────────────────────────────────────────┤
│ shared       │ 도메인 무관 코드 (UI 키트, 유틸, Context)      │
└─────────────────────────────────────────────────────────────┘

간단한 폴더 구조 (맨 위 전체 구조 참고)

질문 답변 위치
서버에서 데이터 가져오는 함수? API 호출 entities/*/api/
가져온 데이터의 타입? 타입 정의 entities/*/model/types.ts
데이터 읽기 (useQuery)? 서버 상태 구독 entities/*/model/use*Query.ts
데이터 변경 (useMutation)? 사용자 액션 features/*/model/use*Mutation.ts
순수한 계산/변환 로직? 비즈니스 로직 entities/*/lib/*.ts
버튼, 폼 같은 액션 UI? 사용자 인터랙션 features/*/ui/*.tsx
여러 Feature 조합한 UI 블록? 독립적 위젯 widgets/*/ui/*.tsx
도메인 무관한 공통 코드? 범용 유틸 shared/lib/, shared/ui/

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

  • PR 을 작성하면서 저 또한 개념과 학습한것들에 대하여 정리가 많이 되는거 같지만, 그래도 한번 더 확인하고 싶어서 질문드립니다. 회고에 작성한 내용들이 제가 이번 챕터에서 배워가야할 것들을 제대로 짚고 넘어가고있는걸까요?

  • 레이어 계층은 나름의 기준과 다른 사람이 봐도 이해하기 쉬운 구조로 작성했다고 생각하고 있습니다. 코치님이 제가 올려놓은 전체 디렉토리 구조 를 보고 내부의 파일들이 각각 알맞게 잘 적용된건지 피드백을 주실수 있을까요?

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.

1 participant