Skip to content

[3팀 장루빈] Chapter 2-3. 관심사 분리와 폴더구조#31

Open
JangRuBin2 wants to merge 12 commits into
hanghae-plus:mainfrom
JangRuBin2:main
Open

[3팀 장루빈] Chapter 2-3. 관심사 분리와 폴더구조#31
JangRuBin2 wants to merge 12 commits into
hanghae-plus:mainfrom
JangRuBin2:main

Conversation

@JangRuBin2
Copy link
Copy Markdown

@JangRuBin2 JangRuBin2 commented Aug 12, 2025


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

과제 체크포인트

기본과제

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

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

체크포인트

  • 전역상태관리를 사용해서 상태를 분리하고 관리했나요?
  • Props Drilling을 최소화했나요?
    → zustand, jotai, tanstack-query로 props 넘김 거의 없음!
  • 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를 쓰면 서버 상태랑 클라이언트 상태가 확실히 분리돼서, 코드가 훨씬 깔끔해진다는 걸 체감함.
  • zustand, jotai, tanstack-query 각각의 장단점이 확실히 다르다는 것도 실전에서 느꼈음.

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

  • 상태를 어디에 두는 게 맞는지, 그리고 그걸 어떤 라이브러리로 관리할지 고민을 진짜 많이 했음.
  • FSD 구조에 맞게 폴더를 나누고, 각 계층에 맞는 책임만 갖도록 신경 썼습니다.
  • 기존 fetch 로직을 tanstack-query로 바꾸면서, 기존의 동작을 해치지 않으려고 시행착오도 많았습니다.

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

  • entities/feature/widget 레이어를 실제로 언제, 어떻게 나누는 게 좋은지 아직은 좀 헷갈립니다.
  • “이건 shared냐, feature냐, entity냐” 경계가 애매한 경우가 종종 있어서, 더 많은 실전 경험이 필요할 듯 합니다.
  • 폴더 구조가 커질 때, 실제로 협업에서 어떻게 유지보수할지 더 고민해보고 싶음.

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

  • 앞으로는 무조건 “관심사 분리”를 먼저 생각하고 폴더 구조를 짤 것 같음.
  • 서버 상태는 tanstack-query, 클라이언트 상태는 jotai/ zustand로 명확히 분리해서 관리할 예정.
  • 코드가 커질수록 FSD 구조의 힘이 더 커진다는 걸 느꼈으니, 실무에도 적극적으로 적용해볼 생각임.

챕터 셀프회고

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

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

  • 더티코드를 보면 “이걸 내가 고쳐야 하나…” 싶어서 한숨부터 나오는...?
  • 클린코드는 결국 “내가 나중에 다시 봐도 이해할 수 있는 코드”라고 생각하게 됨.
  • 유지보수하기 쉬운 코드는 결국 “관심사 분리”랑 “명확한 네이밍”에서 시작된다는 걸 다시 한 번 느끼게 됨

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

  • 거대한 단일 컴포넌트는 진짜… 보기만 해도 머리 아픔.
  • 상태관리 처음엔 어렵고, 디자인 패턴도 추상적으로만 느껴졌는데, 직접 분리해보니까 “아 이래서 하는구나!” 싶었습니다.

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

  • FSD 적용하면서, 내 나름의 기준이 조금씩 생겼다.
  • TanStack Query로 서버 상태를 분리하니까, 코드가 훨씬 명확해지고, 협업할 때도 훨씬 편할 것 같다는 생각이 듭니다.

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

  • 실제 실무에서 FSD 구조를 적용할 때, 팀원들과의 합의나 기준을 어떻게 맞추는 게 좋을지 궁금합니다.
  • entities/feature/widget 레이어의 경계가 애매할 때, 어떤 기준으로 결정하면 좋을까요?
  • tanstack-query와 jotai/zustand를 같이 쓸 때, 상태의 소유권(서버/클라)을 더 명확하게 구분하는 팁이 있을까요?

- Add Zustand store for PostsManager page state
- Move interfaces to appropriate type files (FSD structure)
- Replace local state with Zustand store actions
- Organize shared stores folder structure
- Add Jotai atoms for posts, comments, tags, loading, total
- Replace useState with useAtom for data states
- Add derived atoms for computed values
- Maintain Zustand for UI state management
- Fix API response structure to match original logic
- Add QueryClientProvider to main.tsx for TanStack Query
- Restore user data enrichment logic in posts queries
- Fix mutation functions to use direct fetch calls
- Maintain original error handling and data flow
@tooth-is-silver
Copy link
Copy Markdown

tanstack-query와 jotai/zustand를 같이 쓸 때, 상태의 소유권(서버/클라)을 더 명확하게 구분하는 팁이 있을까요?

두 개의 경계가 명확하지 않으면 서버 상태가 뭔지 먼저 정하면 좋을 것 같단 생각이 듭니다!
제 PR에 진석님이 코멘트 달아주신 부분 있는데 참고하면 좋을 것 같아요!

모든 상태를 useQuery로 분리되는 것은 아닙니다! 서버상태와 로컬상태를 분리해서 생각해야합니다!
searchQuery, setSearchQuery, sortOrder, setSortOrder, selectedTag, setSelectedTag이쪽 상태들은 local 상태니까 분리될 수 없는 상태입니다.
#19 (comment)

@tooth-is-silver
Copy link
Copy Markdown

entities/feature/widget 레이어의 경계가 애매할 때, 어떤 기준으로 결정하면 좋을까요?

저는 이렇게 정했습니다!
entities는 데이터와 관련이 있는 로직들
feature는 외부에 영향을 받는 기능 단위
widget은 외부에 영향을 받지않는 순수한 것들 (순수 함수나 순수한 컴포넌트)

@BBAK-jun
Copy link
Copy Markdown

주스탄드를 사용하지않는게 제일 좋을거같은데요. 전역상태를 사용하다보면 대부분의 전역상태에서 리액트 렌더트리 밖에서 상태를 제어하다보니, 렌더링 생명주기와 다르게 상태가 변이될거에요. 그리고 로컬상태로 만든다보면 생명주기가 끝났을떄 자연스럽게 상태도없어질거거든요. 그런데 전역상태는 죽지 않을거고, 클린업함수로 비워줘야하는 제가 비선호하는 코드의 형태가 나올거에요

@BBAK-jun
Copy link
Copy Markdown

entities/feature/widget 레이어의 경계가 애매할 때, 어떤 기준으로 결정하면 좋을까요?

우선 나누지않습니다. 페이지에서 통으로 쓰다가, 필요할떄 하나씩 조금씩 잘라내는 형태로만 될거같고요. 만약 이 레이어에서 모든걸 평가하기 어려울거같은데? 라고하면 다시한번 나누는게 좋을거같아요

성급한 추상화가 제일 좋지않다고 생각해요

Copy link
Copy Markdown
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)별로 나누어, 하위 계층만 상위 계층에서 참조하도록 하는 아키텍처 규칙입니다. 각 slice는 index로 Public API를 노출하고 내부 세부 구현을 숨깁니다.

⚡ 중요성

FSD를 제대로 지키면 새로운 도메인(entities) 추가, UI 라이브러리 교체, 모노레포 전환 등 변화 시 상위 코드 변경량을 크게 줄여 유지보수성과 확장성을 보장합니다. 특히 여러 상태관리 도구(jotai/zustand/React Query)를 혼용할 때 책임 경계가 명확해야 충돌을 줄일 수 있습니다.

📊 현재 상황 분석

AS-IS: FSD 의도를 따르고 있으나 entities 레이어 부재와 API/서비스/훅 간 역할이 완전히 분리되지 않아 변화를 적용할 때 수정 범위가 넓어질 수 있습니다. 예: 게시물 관련 로직이 shared/api/postsApi.ts, shared/hooks/usePosts.ts, shared/services/postsService.ts, pages/PostsManagerPage.tsx에 분산되어 있음 → 같은 개념(게시물 조회/가공)을 변경할 때 3~4 파일을 건드려야 할 가능성 있음.

📝 상세 피드백

전체적으로 FSD 철학(관심사 분리)을 의도적으로 적용한 흔적이 분명합니다. pages(PostsManagerPage), shared/{api,hooks,stores,services,config}, components, types 로 역할을 나누었고, UI 컴포넌트들은 components에 모아두는 등 계층 분리가 이루어졌습니다. 다만 entities 레이어(도메인 중심의 모델·UI·API)가 명시적으로 분리되지 않았고, 일부 도메인(게시물, 사용자, 댓글) 관련 로직이 shared/hooks, shared/api, shared/services, pages에 중복되거나 분산되어 있어 FSD의 의존성 방향과 Public API 원칙(각 slice는 index로만 노출) 측면에서 개선 여지가 있습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: 중복된 게시물 조회 로직
// src/shared/api/postsApi.ts
export const fetchPosts = async (limit, skip) { return fetch(`/api/posts?limit=${limit}&skip=${skip}`).then(r=>r.json()) }

// src/shared/hooks/usePosts.ts (쿼리 내부에서 fetch 직접 사용)
return useQuery({ queryKey: ['posts', limit, skip], queryFn: async () => { const response = await fetch(`${URL_PATH.POSTS.LIST}?limit=${limit}&skip=${skip}`); const postsData = await response.json(); /* ... */ } })

// pages/PostsManagerPage.tsx에서도 직접 fetch 사용한 흔적 존재 (openUserModal 등)

✅ 권장 구조 (TO-BE)

// TO-BE: 책임을 명확히 분리
// src/entities/posts/api/index.ts  <-- entities 레이어
export const postsApi = { fetchPosts: (limit,skip)=>fetch(...).then(r=>r.json()), searchPosts: (...) => ... }

// src/entities/posts/services/enrich.ts
export const enrichPostsWithUsers = async (posts, usersApi) => { /* 도메인 로직 */ }

// src/features/posts/hooks/usePosts.ts  <-- features는 entities의 public api 사용
import { postsApi } from 'entities/posts/api'
return useQuery({ queryKey:['posts',limit,skip], queryFn: () => postsApi.fetchPosts(limit,skip) })

// pages/PostsManagerPage.tsx
import { usePosts } from 'features/posts/hooks'
// UI 로직만 담당

🔄 변경 시나리오별 영향도

  1. UI 라이브러리 교체 (Radix UI / lucide → MUI) 시: components/index.tsx 에 집중된 스타일·컴포넌트 래퍼가 대부분의 수정지점이 되지만, pages에서 직접 Radix 컴포넌트를 사용했다면 해당 페이지들도 수정해야 함(현재 대부분 components를 통해 사용되어 영향도는 낮음).
  2. 도메인 변화(게시물 API 응답 스펙 변경) 시: postsApi + usePosts + services/enrichPostsWithUsers + pages에서 관련 필드를 사용하는 모든 곳을 수정해야 함(현재 약 4개 파일).
  3. 모노레포 전환 시: entities 레이어(API 모델·타입·도메인 로직)가 없으면 다른 패키로 추출하기 어려움 — 추출 시 의존 정리가 필요함.

🚀 개선 단계

  • 1단계: 단기(1~2일): entities/posts 폴더 생성 → postsApi 함수(fetchPosts, searchPosts, create/update/delete)와 타입을 이동하고 shared/api/postsApi.ts는 entities로 리팩토링 (영향: ~3 파일 변경).
  • 2단계: 단기(1일): pages에서 직접 fetch 사용되는 부분(openUserModal 등)을 entities.api 호출로 교체하여 일관화 (영향: ~2 파일).
  • 3단계: 중기(2~3일): features 레이어(예: features/posts/hooks/usePosts)에서 entities의 public api(index.ts)를 통해 모든 쿼리를 호출하도록 리팩토링. queryKeys 팩토리(예: shared/api/queryKeys.ts) 도입.
  • 4단계: 장기(3~5일): 폴더 루트에 각 slice의 index.js(혹은 index.ts)를 만들어 외부에 노출되는 Public API를 통제하고, 내부 세부 구현은 캡슐화.

2. 🔄 TanStack Query

💡 개념 정의

TanStack Query는 서버 상태(fetch, cache, sync)를 선언적으로 관리하는 도구로, queryKey의 일관된 관리, 쿼리 함수의 분리(entities/api), mutation의 낙관적 업데이트/롤백, 캐싱 전략(staleTime, cacheTime)을 통해 복잡한 비동기 로직을 단순화합니다.

⚡ 중요성

일관된 쿼리 키 및 API 계층 분리는 API 변화(엔드포인트 변경)나 새로운 데이터 소스 추가 시 수정 범위를 최소화합니다. 잘못된 쿼리 키 설계는 캐시 무효화 오류와 성능 문제(불필요한 재요청)를 유발합니다.

📊 현재 상황 분석

AS-IS: hooks 계층은 존재하지만 API/쿼리 함수가 한 곳으로 모여있지 않아 API 변경 시 수정범위가 불명확. 예: API 경로 변경하면 shared/api/postsApi.ts와 usePosts의 queryFn(직접 fetch 사용) 둘 다 수정해야 함.

📝 상세 피드백

TanStack Query 도입이 잘 이루어졌습니다: QueryClientProvider 등록, useQuery/useMutation을 래핑한 custom hooks(usePosts, useComments, useCreatePost 등)를 만든 점은 긍정적입니다. 그러나 쿼리 키 전략과 API 계층 분리가 일부 개선될 수 있습니다. queryKey에 계층화된 팩토리 패턴(queryKeys.users(), queryKeys.user(id))이 없고, 일부 훅에서 fetch를 직접 호출하는 반면 다른 곳은 shared/api의 함수를 사용해 일관성이 떨어집니다. 또한 enabled/refetch 전략은 사용하고 있으나 staleTime/cacheTime 같은 캐싱 옵션이 전역 defaultOptions에 반영되지 않았습니다(queries.retry/refetchOnWindowFocus 만 설정).

❌ 현재 구조 (AS-IS)

// AS-IS: usePosts가 직접 fetch 사용
// src/shared/hooks/usePosts.ts
return useQuery({ queryKey: ['posts', limit, skip], queryFn: async () => { const response = await fetch(`${URL_PATH.POSTS.LIST}?limit=${limit}&skip=${skip}`); const postsData = await response.json(); /* ... */ } })

✅ 권장 구조 (TO-BE)

// TO-BE: API 계층과 queryKey 팩토리를 사용
// src/shared/api/queryKeys.ts
export const queryKeys = { posts: () => ['posts'] as const, postsList: (limit,skip)=>[...queryKeys.posts(), 'list', limit, skip] as const }

// src/entities/posts/api/index.ts
export const postsApi = { fetchPosts: (limit,skip)=>fetch(...).then(r=>r.json()) }

// src/features/posts/hooks/usePosts.ts
return useQuery({ queryKey: queryKeys.postsList(limit,skip), queryFn: () => postsApi.fetchPosts(limit,skip), staleTime: 1000*60 })

🔄 변경 시나리오별 영향도

  1. API 엔드포인트 변경(예: /api/posts → /v2/posts) 시: 현재 구조에서는 shared/api/postsApi.ts, shared/hooks/usePosts.ts, shared/hooks/usePostsByTag 관련 파일 등을 모두 수정해야 할 가능성이 있음.
  2. 새 데이터 소스(예: 캐시 레이어 또는 서버사이드 렌더링) 추가 시: queryKeys 팩토리와 API 추상화가 되어 있으면 hooks에 미치는 변화는 적음.
  3. 에러 처리 방식(전역 에러 포맷統一) 변경 시: 각 useMutation/useQuery의 onError가 분산되어 있으면 대규모 수정 필요.

🚀 개선 단계

  • 1단계: 단기(half day): queryKeys 팩토리 파일 생성 (shared/api/queryKeys.ts) 및 기존 훅의 queryKey를 이 팩토리로 대체.
  • 2단계: 단기(1일): shared/api/postsApi.ts의 fetch* 함수들을 모든 훅에서 사용하도록 리팩토링하여 중복 fetch 제거 (영향: usePosts의 queryFn을 postsApi.fetchPosts로 변경).
  • 3단계: 중기(1~2일): QueryClient defaultOptions에 staleTime/cacheTime와 mutation의 onError/onSettled 패턴을 추가하여 일관된 캐싱/에러 전략 적용.
  • 4단계: 중기(1~2일): 낙관적 업데이트가 필요한 mutation에 대해 useMutation의 onMutate/onError/onSettled 패턴을 명확히 문서화 및 적용.

3. 🎯 응집도 (Cohesion)

💡 개념 정의

응집도(Cohesion)는 같은 모듈 내부 요소들이 얼마나 밀접한 관련을 갖는지를 나타냅니다. 높은 응집도는 관련 변경이 하나의 모듈 내에 국소화되어 유지보수성을 높입니다.

⚡ 중요성

높은 응집도는 기능 변경 시 수정 파일 수를 줄이고, 패키지화(패키지 독립화)나 기능 이식(마이크로프론트엔드)에 유리합니다.

📊 현재 상황 분석

AS-IS: 게시물 도메인을 따로 떼어내긴 했으나 실제 조회·가공·상태 반영 로직이 여러 계층에 산재. 예: 사용자 결합(enrichPostsWithUsers)은 services에 있지만 usePosts도 내부에서 getUserById 호출로 유사한 일을 하고 있음 → 중복 책임.

📝 상세 피드백

응집도는 과제 의도처럼 도메인별로 파일을 모으려는 시도가 보입니다(타입, API, 훅, 서비스가 게시물·댓글·사용자별로 존재). 그러나 일부 기능들이 shared 레이어와 pages에 혼합되어 있어 완전한 응집도 향상을 위해 추가 정리가 필요합니다. 특히 '게시물 조회 + 사용자 결합' 로직이 hooks, services, api에 혼재되어 있어 한 변경(예: author 필드 추가)에 대해 여러 파일을 건드려야 할 가능성이 있습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: 사용자 결합이 여러 장소에 산재
// src/shared/hooks/usePosts.ts -> postsWithUsers: await getUserById(post.userId)
// src/shared/services/postsService.ts -> enrichPostsWithUsers(posts, getUserById)
// pages/PostsManagerPage.tsx -> post.author?.username 사용

✅ 권장 구조 (TO-BE)

// TO-BE: 단일 책임으로 응집
// src/entities/posts/
// - api/index.ts (fetchPosts, searchPosts...)
// - services/index.ts (enrichPostsWithUsers 등 도메인 로직)
// - hooks/usePosts.ts (entities/api와 services만 호출, 상태 반영 담당)
// pages/PostsManagerPage -> hooks만 사용하여 UI 렌더링

🔄 변경 시나리오별 영향도

  1. 새 필드(author.profileUrl) 추가 시: postsApi에서 응답 스펙 변경, services/enrichPostsWithUsers에서 매핑, pages에서 렌더링 변경 등 3~4곳을 수정해야 할 가능성.
  2. 도메인 분리(entities 패키지로 추출) 시: 응집도가 높다면 entities 폴더만 추출하면 되나, 현재처럼 책임이 흩어져 있으면 추가 의존 정리가 필요.

🚀 개선 단계

  • 1단계: 단기(1일): services/enrichPostsWithUsers의 역할을 명확히 하고 usePosts에서 중복 로직을 제거(모든 사용자 결합은 서비스에 위임).
  • 2단계: 단기(1~2일): posts 관련 모든 로직을 entities/posts로 모으고 features/hooks는 entities의 public API만 사용하도록 리팩토링.
  • 3단계: 중기(2~3일): 응집도 측정을 위해 변경이 필요한 파일 수(예: author 필드 추가 시)를 계산하여 현재 구조와 개선 구조의 정량적 비교 문서화.

4. 🔗 결합도 (Coupling)

💡 개념 정의

결합도(Coupling)는 모듈 간 의존성의 강도를 나타냅니다. 낮은 결합도는 모듈을 독립적으로 바꿀 수 있게 하며 인터페이스(추상화)를 통해 상호작용하는 것이 바람직합니다.

⚡ 중요성

낮은 결합도는 기술 스택 변경(예: axios→fetch), 상태관리 교체(예: redux→zustand), 알림 시스템 변경 등 환경 변화에 대한 유연성을 제공합니다.

📊 현재 상황 분석

AS-IS: 상태 소유권이 'postsAtom'과 'usePostsManagerStore.postDraft'로 분할되어 있어 같은 데이터(예: posts list, drafts)에 대해 서로 다른 책임자가 존재. 이런 분산은 한쪽 라이브러리 제거 시 추가 작업을 초래합니다.

📝 상세 피드백

결합도는 전반적으로 낮추려는 노력이 보입니다(entities/api 분리 시도, QueryClient, atoms, zustand 사용). 그러나 현재는 다음과 같은 결합 포인트가 남아 있습니다: pages가 shared/hooks와 shared/stores(jotai + zustand) 둘 다 직접 참조 — 상태 소유권과 흐름이 부분적으로 중복되어 있어 한 라이브러리 교체(예: jotai→zustand) 시 수정범위가 커질 수 있습니다. 또한 API 호출 경로(URL_PATH)를 전역 상수로 관리한 점은 좋지만, 일부 fetch 호출이 직접 URL_PATH를 사용하고 있어 HTTP 클라이언트 변경 시 범위가 넓어질 가능성이 있습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: Pages가 다양한 상태 소스로부터 직접 읽음
const [posts] = useAtom(postsAtom)
const { selectedPost, setSelectedPost } = usePostsManagerStore()
// 두 시스템이 동일 관심사를 공유

✅ 권장 구조 (TO-BE)

// TO-BE: 단일 소유권 원칙 적용
// features/posts exposes usePostsState hook (internally uses query + atom or local state)
const { posts, setSelectedPost } = usePostsFeature()
// page는 단일 훅만 사용하여 결합도를 낮춤

🔄 변경 시나리오별 영향도

  1. HTTP 클라이언트 변경(예: fetch→axios) 시: 현재 구조에서는 shared/api/*.ts 파일만 추상화되어 있으면 수정 범위는 작아지지만, pages/PostsManagerPage에서 직접 fetch를 사용한 부분도 수정해야 함.
  2. 상태관리 라이브러리 축소(예: jotai 제거) 시: postsAtom을 사용하는 모든 훅과 컴포넌트를 찾아 대체해야 함(현재 postsAtom 사용 파일 수: usePosts, useComments, Pages 등으로 약 4~6곳).

🚀 개선 단계

  • 1단계: 단기(1일): 상태 소유권을 명시적으로 문서화(예: 서버 상태는 React Query, UI 폼/모달 상태는 zustand, 캐시성 데이터는 jotai 사용 금지 또는 명확 조건).
  • 2단계: 단기중기(12일): pages가 직접 atoms를 import하지 않고, features 레이어의 'public hooks'만 사용하도록 리팩토링.
  • 3단계: 중기(2~3일): HTTP 클라이언트 추상화 레이어(httpClient) 추가하여 fetch→axios 전환 시 한 파일만 교체하면 되도록 구성.

5. 🧹 Shared 레이어 순수성

💡 개념 정의

Shared 레이어는 도메인에 의존하지 않는 범용 유틸, UI 컴포넌트, 인프라(HTTP clients, queryKeys) 등을 담아 여러 도메인에서 재사용할 수 있어야 합니다. 도메인 로직이 포함되면 재사용성이 저하됩니다.

⚡ 중요성

shared가 순수하면 새로운 프로젝트나 패키로 추출하기 쉬워집니다. 도메인 의존성이 있으면 다른 프로젝트로의 이전이 어려워집니다.

📊 현재 상황 분석

AS-IS: shared/components와 shared/config(routes) 등은 범용적으로 잘 설계되어 있어 재사용성이 높음. 그러나 postsService 같은 도메인 로직은 shared에서 분리되어 entities/posts로 이동해야 합니다.

📝 상세 피드백

shared 레이어는 UI 컴포넌트, API 추상화, hooks, stores, services 등으로 확장되어 재사용 중심으로 구성되어 있습니다. 공통 URL_PATH와 API 모듈, components/index.tsx의 UI 컴포넌트 정리는 재사용성과 적응성을 확보하는 좋은 출발입니다. 다만 shared에 도메인 로직(예: postsService.enrichPostsWithUsers)이 포함되어 있는데, 이는 도메인별 entities로 옮기는 것이 재사용성과 독립성 측면에서 더 바람직합니다.

❌ 현재 구조 (AS-IS)

// AS-IS: shared/services/postsService.ts (도메인 로직)
export const enrichPostsWithUsers = async (posts, getUserById) => { /* 도메인 로직 */ }

// shared는 보통 UI/유틸/infra만 포함하는 게 권장됩니다.

✅ 권장 구조 (TO-BE)

// TO-BE: 도메인 로직을 entities로 이동
// src/entities/posts/services/enrich.ts (도메인 전용)
export const enrichPostsWithUsers = async (posts, getUserById) => { /* same impl */ }

// src/shared/services는 범용 서비스만 유지

🔄 변경 시나리오별 영향도

  1. 다른 프로젝트에서 shared 재사용 필요 시: shared/components, shared/config는 바로 가져다 쓸 수 있으나 postsService와 같은 도메인 로직은 불필요하게 포함되어 의존성을 증가시킴.
  2. 도메인 변경 시: postsService가 shared에 있으면 shared 계층을 수정해야 하므로 전파 범위가 넓어짐.

🚀 개선 단계

  • 1단계: 단기(half day): shared/services/postsService.ts를 entities/posts/services로 이동하는 작업 계획 수립.
  • 2단계: 단기(1~2일): shared는 UI, config, http 클라이언트, queryKeys 등으로 정리하고 도메인 코드를 모두 entities로 옮기기.
  • 3단계: 중기(1~2일): 다른 도메인(예: users, comments)에 대해서도 동일한 분리 규칙을 적용하여 일관성 확보.

6. 📐 추상화 레벨

💡 개념 정의

추상화는 기술적 세부사항(HTTP, 캐싱)과 비즈니스 로직(도메인 규칙)을 분리하여 상위 레벨의 API만 제공하는 것을 의미합니다. 이를 통해 내부 구현을 바꿔도 외부 호출자는 영향을 덜 받습니다.

⚡ 중요성

추상화 수준이 적절하면 기술 스택 변경(HTTP client, 인증 방식), 백엔드 API 변화 등에 대해 최소한의 수정만으로 대응할 수 있습니다.

📊 현재 상황 분석

AS-IS: 부분적으로 추상화됨(URL_PATH, postsApi), 그러나 fetch가 여러 곳에 흩어져 있어 HTTP 클라이언트 교체 시 영향 범위가 커짐. 비즈니스 규칙(search/sort/enrich)은 services로 일부 뽑혀 있지만 위치가 shared여서 경계가 불명확.

📝 상세 피드백

추상화는 부분적으로 잘 되어 있습니다(예: URL_PATH로 엔드포인트 추상화, postsApi/createPost 등 API 함수 분리). 그러나 추상화 일관성이 부족합니다: 일부 훅과 페이지는 postsApi의 함수를 사용하고, 다른 부분은 직접 fetch로 동일한 일을 수행하고 있어 추상화 레벨이 일관되지 않습니다. 또한 http client 추상화(예: httpClient wrapper)가 없어서 fetch→axios 같은 변경에 대비하기 어렵습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: 직접 fetch 사용
const response = await fetch(URL_PATH.USERS.DETAIL(user.id))
// -> 이 호출을 httpClient.get(URL_PATH.USERS.DETAIL(user.id)) 로 바꿀 수 있도록 추상화

✅ 권장 구조 (TO-BE)

// TO-BE: httpClient 래퍼 + entities/api 사용
// shared/httpClient.ts
export const httpClient = { get: (url)=>fetch(url).then(r=>r.json()), post: (...) => ... }

// entities/posts/api/index.ts
export const postsApi = { fetchPosts: (l,s)=>httpClient.get(`${URL_PATH.POSTS.LIST}?limit=${l}&skip=${s}`) }

🔄 변경 시나리오별 영향도

  1. HTTP 클라이언트 교체(fetch→axios) 시: postsApi 등 central api만 수정하면 되지만, pages나 훅에 직접 fetch가 남아있으면 해당 위치도 수정해야 함.
  2. API 응답 포맷(versioning) 시: 중앙 API 계층만 변경하면 되도록 더 강한 추상화 필요.

🚀 개선 단계

  • 1단계: 단기(half day): 프로젝트 루트에 shared/httpClient.ts 생성(간단한 get/post wrapper).
  • 2단계: 단기(1일): 모든 직접 fetch 호출을 httpClient로 교체(검색 및 자동 리팩토링 사용 가능).
  • 3단계: 중기(1~2일): API 레이어(entities/*/api)를 httpClient 기반으로 재정비하여 기술적 세부사항을 캡슐화.

7. 🧪 테스트 용이성

💡 개념 정의

테스트 용이성이란 코드가 단위 테스트/통합 테스트/E2E 테스트로 검증하기 쉬운지를 의미합니다. 순수 함수, 의존성 주입, 사이드 이펙트 분리 등이 핵심입니다.

⚡ 중요성

테스트 용이성은 리팩토링 및 기능 추가 시 회귀를 방지하고 배포 안정성을 높입니다. 특히 외부 API가 자주 변경되는 프로젝트에서는 필수입니다.

📊 현재 상황 분석

AS-IS: 유닛 테스트 대상으로 좋은 후보(services, components)가 존재. 하지만 e2e 혹은 통합 테스트를 작성할 때는 pages의 URL sync(useEffect)와 store 연동을 준비해야 함.

📝 상세 피드백

테스트 용이성은 개선되어 있습니다. 서버 상태를 TanStack Query 훅으로 분리했고, UI 컴포넌트는 components/index.tsx에서 순수한 props 기반 컴포넌트(예: Input, Card)를 제공하도록 제네릭 타입을 추가해 테스트 가능한 구조가 되었습니다. 또한 비즈니스 로직(search, sort, enrich)은 services로 분리되어 단위 테스트 대상이 됩니다. 다만 hooks가 atoms과 직접 결합되어 있어 단위 테스트 시 모킹이 다소 어렵고, pages에서 많은 로직(이펙트, URL sync)을 가지고 있어 통합 테스트가 복잡할 수 있습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: 테스트하기 쉬운 부분
// src/shared/services/postsService.ts -> searchPostsLogic(posts, query) 는 순수 함수(단위 테스트 가능)

// 테스트하기 어려운 부분
// src/shared/hooks/usePosts.ts -> 내부에서 setPosts(atom) 와 fetch를 동시에 수행 -> 훅 유닛 테스트 시 atom provider 모킹 필요

✅ 권장 구조 (TO-BE)

// TO-BE: 의존성 주입으로 테스트 용이성 향상
// usePosts({ postsApi }) 형태로 api를 주입하거나, 내부에서 httpClient를 주입 받아 mock 가능.
return useQuery({ queryFn: () => postsApi.fetchPosts(limit,skip) })

🔄 변경 시나리오별 영향도

  1. 외부 API 변경 시: services와 api 계층만 모킹하면 대부분 테스트로 커버 가능(현재는 중복된 fetch 호출을 모두 모킹해야 하는 곳이 존재).
  2. 상태관리 라이브러리 변경 시: hooks 테스트에서 사용하는 mobk/Provider 설정을 바꿔야 함.

🚀 개선 단계

  • 1단계: 단기(half day): services 함수들에 대한 유닛 테스트 작성 (searchPostsLogic, sortPosts, enrichPostsWithUsers).
  • 2단계: 단기(1일): 훅 테스트용 헬퍼(예: 테스트용 Jotai Provider, zustand 테스트 util)를 프로젝트에 추가하여 훅 단위 테스트를 쉽게 만듦.
  • 3단계: 중기(1~2일): usePosts/useComments에서 API 의존성을 주입(injection) 가능하도록 리팩토링하여 테스트용 mock을 주입할 수 있게 함.

8. ⚛️ 현대적 React 패턴

💡 개념 정의

현대적 React 패턴은 Suspense·ErrorBoundary를 통한 선언적 로딩/에러 처리, custom hooks로 관심사 분리, 컴포넌트의 단일 책임 준수를 의미합니다.

⚡ 중요성

이런 패턴을 적용하면 로딩/에러 처리 방식 변경 시 코드 수정을 최소화하고, 컴포넌트 테스트·재사용성을 향상시킵니다.

📊 현재 상황 분석

AS-IS: modern hook 기반 설계로 전반적 패턴은 좋음. TO-BE: Suspense를 활용한 로딩 분리(React Query의 useSuspenseQuery 또는 Suspense integration)와 ErrorBoundary 적용이 권장됩니다.

📝 상세 피드백

현대적 React 패턴은 일부 채택되어 있습니다: TanStack Query 사용, custom hooks 추출, forwardRef와 타입 제네릭으로 컴포넌트 강화, zustand로 UI 상태 분리 등. 하지만 Suspense, ErrorBoundary, React.lazy/Suspense 기반 비동기 로딩 패턴은 사용되지 않았고, pages에는 useEffect로 URL sync 등 명령형 로직이 많아 선언적 패턴으로 더 개선할 수 있습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: 명령형 URL sync와 refetch 호출
useEffect(() => { tagsQuery.refetch() }, [])

// TO-BE: Suspense로 로딩을 위임
<Suspense fallback={<PostsSkeleton/>}><PostsManagerContent /></Suspense>

✅ 권장 구조 (TO-BE)

// TO-BE: usePosts를 Suspense-friendly하게 구성
return useQuery({ queryKey: qk, queryFn: fn, suspense: true })

// 최상위에서
<ErrorBoundary fallback={<ErrorFallback />}>
  <Suspense fallback={<Loading/>}>
    <PostsManager />
  </Suspense>
</ErrorBoundary>

🔄 변경 시나리오별 영향도

  1. 로딩 UI 전략을 Skeleton → Suspense로 전환하면 PostsManagerPage에서 로직 변경이 적음(단, useQuery의 suspense:true 설정이 필요).
  2. 에러 표시를 전역 ErrorBoundary로 통일하면 개별 컴포넌트의 onError 로직을 단순화할 수 있음.

🚀 개선 단계

  • 1단계: 단기(half day): ErrorBoundary/Loading 컴포넌트 템플릿 추가 및 App 수준에서 적용 계획 수립.
  • 2단계: 단기(1일): 일부 핵심 리스트(usePosts)에서 suspense:true 시도 및 UI에서 Suspense 래핑 시범 적용.
  • 3단계: 중기(1~2일): pages의 useEffect 기반 로딩/에러 핸들링을 hooks와 Suspense로 점진적 이동.

9. 🔧 확장성

💡 개념 정의

확장성은 새로운 기능(예: 다국어, 오프라인, 실시간)이나 비기능 요구사항(성능, 접근성)을 추가할 때 기존 코드를 최소한으로 변경하여 확장할 수 있는 능력입니다.

⚡ 중요성

실무에서 결제 수단 추가, 인증 방식 변경, A/B 테스트 도입 등 변화에 빠르게 대응하려면 낮은 변경 비용이 필수입니다.

📊 현재 상황 분석

AS-IS: 새로운 기능(예: 실시간 소켓 동기화)을 도입하려면 서버 상태를 담당하는 hooks에 기능을 추가하면 되지만, 클라이언트 상태(모달, draft 등)는 zustand로 잘 분리되어 있어 통합이 용이. 문제는 동일 데이터(예: posts list)의 소유권이 혼재되어 캐시 동기화 로직을 신중히 해야 함.

📝 상세 피드백

확장성 측면에서 좋은 선택지들이 있습니다: URL_PATH 상수화, API 훅화(usePosts/useComments), 상태관리의 분리(jotai vs zustand) 등은 새로운 기능(다국어, A/B 테스트, 실시간 기능) 추가 시 유리합니다. 다만 일부 중복 로직과 상태 소유권 혼재는 확장 시 복잡도를 증가시킬 수 있으므로 정리 필요합니다.

❌ 현재 구조 (AS-IS)

// AS-IS: 서버 상태와 클라이언트 상태 분리(좋음)
// 서버: usePosts -> Query
// 클라이언트: usePostsManagerStore -> drafts/modals

// 그러나 postsAtom도 있어 소유권이 약간 분산됨

✅ 권장 구조 (TO-BE)

// TO-BE: 소유권 정리
// 서버 상태: usePosts (Query) - canonical source
// 클라이언트: usePostsManagerStore (local drafts) - UI 전용
// postsAtom 제거 or read-only derived atom로 변경

🔄 변경 시나리오별 영향도

  1. 다국어 지원 추가 시: UI 문자열이 components에 흩어져 있으면 작업이 크지만, components에 일괄적인 i18n 래핑을 적용하면 영향은 제한적.
  2. 실시간 기능(웹소켓) 추가 시: 서버 상태는 TanStack Query로 관리하면서, 실시간 업데이트는 queryClient.setQueryData로 연동하면 큰 변경 없이 통합 가능.
  3. A/B 테스트 도입 시: feature toggle을 zustand 혹은 context로 관리하고, features/hooks에서 실험 결과를 분기 처리하도록 설계하면 확장성이 높음.

🚀 개선 단계

  • 1단계: 단기(half day): 소유권(서버 vs 클라이언트) 문서화 및 postsAtom 역할 정의(필요시 read-only로 전환).
  • 2단계: 단기(1~2일): 실시간 기능 도입 시 queryClient.setQueryData 패턴 시범 적용 문서화.
  • 3단계: 중기(2~3일): 다국어 및 A/B 테스트를 고려한 공통 인프라(i18n wrapper, feature flag service) 추가.

10. 📏 코드 일관성

💡 개념 정의

코드 일관성은 네이밍, 파일 구조, import/ export 패턴, 코드 스타일(들여쓰기, 따옴표 등)을 팀이 일관되게 지키는 것을 말합니다.

⚡ 중요성

일관성은 신규 개발자 온보딩 속도를 높이고 자동화 도구(ESLint, Prettier, CodeMods) 적용을 수월하게 합니다.

📊 현재 상황 분석

중복 export 패턴이나 파일 분리 정책만 정리하면 대형 코드베이스로 확장할 때 통일성을 유지하기 쉬워집니다.

📝 상세 피드백

코드 스타일 및 네이밍은 전반적으로 일관성이 높습니다. components에 제네릭 타입을 추가해 통일한 점, types 폴더로 타입을 정리한 점이 좋습니다. 다만 import/export 패턴에 일부 혼재(예: shared/api/index.ts는 export * 사용, components는 named exports) 및 파일 네이밍 규칙(components/index.tsx 사용)은 허용 범위이나 규칙화(예: 컴포넌트는 PascalCase 파일별 export)하면 더 좋습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: components/index.tsx 한 파일 내 여러 컴포넌트 export
export const Button = ...
export const Input = ...

// 권장: 각 컴포넌트를 개별 파일로 분리하고 index.ts로 집계

✅ 권장 구조 (TO-BE)

// TO-BE 파일 구조
src/components/Button.tsx
src/components/Input.tsx
src/components/index.ts // export { Button } from './Button'

// Export 정책: named exports 만 허용 (팀 규칙)

🔄 변경 시나리오별 영향도

  1. 팀 확장/다수 PR 병합 시: 명확한 export/import 규칙(absolute imports 사용 기준)과 파일 네이밍 컨벤션이 있으면 충돌/리뷰 비용을 줄일 수 있음.

🚀 개선 단계

  • 1단계: 단기(half day): 팀 컨벤션 문서(파일명, export 규칙, import 순서)를 README에 정리.
  • 2단계: 단기(1~2일): components/index.tsx에 있는 컴포넌트를 개별 파일로 분리(코드모드로 자동 분리 가능).
  • 3단계: 중기(1~2일): ESLint/Prettier와 함께 import 정렬 규칙(import/order) 적용.

🎯 일관성 체크포인트

파일명 규칙

  • components/index.tsx 사용으로 컴포넌트가 한 파일에 몰려 있음 — 나중에 파일 분리 시 PascalCase 파일(Button.tsx, Input.tsx 등)로 분리 권장

Import/Export 패턴

  • shared/api/index.ts uses export * (괜찮음) — 팀 컨벤션으로 default export 금지 / named export 선호를 문서화 권장

변수명 규칙

  • 전반적으로 camelCase 사용으로 일관적임

코드 스타일

  • 코드 스타일은 대체로 일관됨(Prettier/ESLint 규칙 적용되어 있는 듯함)

11. 🗃️ 상태 관리

💡 개념 정의

데이터 흐름 및 상태 관리는 서버 상태(원격 데이터), 전역 UI 상태(모달, 드래프트), 로컬 상태(컴포넌트 레벨)의 적절한 분리와 소유권 할당을 의미합니다.

⚡ 중요성

명확한 상태 소유권은 Props Drilling 제거, 테스트 용이성, 특정 상태관리 라이브러리 교체 시 영향 범위 축소에 결정적입니다.

📊 현재 상황 분석

AS-IS: 서버 상태는 Query 훅으로 중앙화하는 게 이상적인데 postsAtom에도 서버 데이터를 복제(setPosts)하는 패턴이 있어 캐시/동기화 문제 가능. 예: useCreatePost의 onSuccess에서는 setPosts([data, ...posts])로 atom을 갱신하고 queryClient.invalidateQueries(['posts'])도 호출 — 두 상태 소스가 일관성을 유지해야 함.

📝 상세 피드백

서버 상태(게시물/댓글/태그)는 TanStack Query 훅으로, UI 상태(모달 열기/선택된 항목, draft)는 zustand로, 글로벌 데이터(게시물 목록, comments cache, loading, total)는 jotai atoms로 분리한 시도는 흥미롭습니다. 결과적으로 '어디에 상태를 둬야 할지'에 대한 명확한 고민이 반영되어 있습니다. 다만 상태 소유권(특히 posts list와 관련된 postsAtom vs usePosts Query 사이)이 혼재되어 있어 향후 혼란 요인이 될 수 있습니다.

❌ 현재 구조 (AS-IS)

// AS-IS: 서버 데이터가 postsAtom에도 저장됨
usePosts -> setPosts(postsWithUsers)
useCreatePost.onSuccess -> setPosts([data, ...posts])

// 결과: 서버 Canonical Query cache vs atom 두 곳에 데이터가 존재

✅ 권장 구조 (TO-BE)

// TO-BE: 서버 상태는 Query가 canonical source
// pages는 usePosts 훅으로 읽고, 필요한 경우 derived atom(읽기 전용)으로 제공
// createPost는 mutation 후 queryClient.invalidateQueries(['posts']) 만 호출

🔄 변경 시나리오별 영향도

  1. 오프라인/동기화 요구사항 추가 시: server canonical source를 하나로 정해두면 오프라인 동기화 전략(IndexedDB, background sync)을 그 계층에만 적용하면 됨. 현재는 분산 상태라 복잡도가 증가.
  2. 상태관리 라이브러리 교체 시: jotai 나 zustand 중 하나를 제거하려면 posts 소유권을 명확히 재정의해야 함.

🚀 개선 단계

  • 1단계: 단기(half day): 서버 상태의 canonical owner(React Query)와 UI 상태(zustand/jotai)를 문서화하여 소유권 규칙을 정함.
  • 2단계: 단기중기(12일): postsAtom에 대한 쓰기 경로를 줄이고 Query cache를 canonical로 사용하도록 useCreatePost/useUpdatePost의 onSuccess 로직을 조정(예: setPosts 대신 invalidateQueries).
  • 3단계: 중기(1~2일): state ownership 규칙을 팀 내 컨벤션으로 정리하고 PR 템플릿에 체크리스트 추가.

🤔 질문과 답변

Q1: 실제 실무에서 FSD 구조를 적용할 때, 팀원들과의 합의나 기준을 어떻게 맞추는 게 좋을까요?
A1: 소규모이라도 '공식 문서 한 페이지'를 만들어 기준을 정하세요. 핵심은(1) 각 레이어의 책임(예: entities: 타입+도메인 API+비즈니스 로직, features: 화면별 훅+비즈니스 플로우, shared: 범용 UI/유틸), (2) Public API 노출 규칙(각 폴더 index만 사용), (3) 상태 소유권 규칙(server vs client). 문서 + 간단한 코드 샘플(AS-IS→TO-BE) + 1~2회 워크숍으로 합의하면 실무 적용이 빨라집니다. CI에 Lint/Import 규칙이나 폴더 구조 체크 스크립트(간단한 검증)를 추가하면 일관성 유지에 도움이 됩니다.

Q2: entities/feature/widget 레이어의 경계가 애매할 때, 어떤 기준으로 결정하면 좋을까요?
A2: 실무 기준으로 권장되는 규칙은 다음과 같습니다: (1) entities는 도메인 진입점(타입, 순수 CRUD api, 핵심 비즈니스 로직)을 가짐 — 재사용성과 독립성이 높아야 함. (2) features는 특정 화면/유저 시나리오(탐색, 필터링, 페이징) 책임을 가짐 — entities의 public API를 사용. (3) widgets는 재사용 가능한 UI 조각(예: TagList, PostPreview)로, 여러 페이지에서 재사용될 가능성이 있는 컴포넌트. 실무 팁: 변화 시 '어떤 변경이 발생하면 이 파일을 수정해야 하는가'라는 질문으로 경계를 테스트해보세요. 만약 도메인 변경에 따라 많이 수정된다면 entities로 옮기세요.

Q3: tanstack-query와 jotai/zustand를 같이 쓸 때, 상태의 소유권(서버/클라)을 더 명확하게 구분하는 팁이 있을까요?
A3: 권장 원칙: (1) 서버 상태는 TanStack Query가 canonical source. (2) 영구적 전역 데이터(여러 페이지에서 재사용되는 서버 기반 데이터)를 Query cache에 두고, 필요 시 read-only atom/selector로 파생. (3) UI 상태(모달 열림, 폼 드래프트, 선택된 항목)는 zustand 같은 전역 클라이언트 상태로 관리. (4) Jotai는 파생 상태(간단한 derived state) 용도로 제한. 실무적으로는 각 종류의 상태에 대해 'owner'를 문서화하고 PR 체크리스트에 '이 상태는 어디 소유인지'를 추가하면 혼선이 줄어듭니다.

🎯 셀프 회고 & 제안

작성하신 셀프회고에서 보이는 강점은 '왜' 관심사 분리를 했는지에 대한 명확한 인식과 실제로 여러 상태관리 도구를 실험해보며 장단점을 체감하신 점입니다. 이는 단순한 이론 이해를 넘어 실제 설계 의사결정 능력으로 이어질 수 있습니다. 몇 가지 이어서 생각해볼 질문을 남깁니다:

  • 현재 프로젝트에서 'posts' 데이터의 canonical owner를 Query로 확정한다면 postsAtom/ zustand draft 중 어느 부분을 변환하시겠습니까? 변환 후 예상 작업량은 얼마일까요? (예: postsAtom 제거로 3개 파일 변경)
  • entities 레이어를 도입할 때 우선적으로 옮길 파일(또는 기능)은 무엇이 될까요? (예: postsApi, types, enrich service 순으로 우선순위 부여 가능)
  • 프로젝트가 커졌을 때 폴더 네이밍/인덱스 노출 규칙을 위반하는 PR을 자동으로 막기 위한 린트 규칙은 어떤 것이 좋을까요? (예: import from 'entities/*' 만 허용 등)

이 질문들을 중심으로 작은 실험(예: postsAtom을 read-only로 바꿔보기, usePosts의 queryFn을 postsApi로 완전 이전)부터 해보시면 설계 확신이 더 빨라질 것입니다.


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

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

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants