angielxx 님의 상세페이지[1팀 이은지] Chapter 2-3. 관심사 분리와 폴더구조

배포 링크

https://angielxx.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을 중심으로 데이터를 재사용가능한 형태로 분리했나요?

심화과제

목표: 서버상태관리 도구인 TanStack Query를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기

  • TanStack Query의 사용법에 대한 이해
  • TanStack Query를 이용한 비동기 코드 작성에 대한 이해
  • 비동기 코드를 선언적인 함수형 프로그래밍으로 작성하는 방법에 대한 이해

체크포인트

  • 모든 API 호출이 TanStack Query의 useQuery와 useMutation으로 대체되었는가?
  • 쿼리 키가 적절히 설정되었는가?
  • fetch와 useState가 아닌 선언적인 함수형 프로그래밍이 적절히 적용되었는가?
  • 캐싱과 리프레시 전략이 올바르게 구현되었는가?
  • 낙관적인 업데이트가 적용되었는가?
  • 에러 핸들링이 적절히 구현되었는가?
  • 서버 상태와 클라이언트 상태가 명확히 분리되었는가?
  • 코드가 간결하고 유지보수가 용이한 구조로 작성되었는가?
  • TanStack Query의 Devtools가 정상적으로 작동하는가?

최종과제

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

과제 셀프회고

최종 과제는 final_src 폴더에 있습니다.

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

  1. FSD 아키텍처의 진짜 가치 체감해보기

레이어별 책임 분리의 강력함

FSD를 처음 적용할 때 가장 인상적이었던 건 "어디에 무엇을 넣어야 할지"가 명확해진다는 점이었습니다. 이전에는 새로운 기능을 추가할 때마다 "이 코드는 어느 폴더에 넣지?"라고 고민했는데, FSD를 적용하고 나니 자연스럽게 적절한 위치가 보이더라구요.

이렇게 각 레이어가 명확한 역할을 가지니까 코드를 찾기도 쉽고, 새로운 팀원이 와도 구조를 이해하기 쉬울 것 같았습니다.

하지만! 이런 장점을 체감하려면 FSD에 대해 정확히 이해하고 있고, 어느 정도 FSD에 익숙한 상태여야 가능합니다.

FSD 구조하에 모듈을 어디에 위치시킬지에 대한 zep 토론 흔적..

  • 공통 UI 컴포넌트는 어떤식으로 분류할 것인가? Screenshot 2025-08-12 at 1 30 55 AM
Screenshot 2025-08-12 at 1 30 43 AM

primitive 컴포넌트를 기능별로 분류하여 더 쉽게 찾아서 쓸 수 있도록 했습니다.

Screenshot 2025-08-15 at 9 36 41 PM
  • 헤더를 어느 레이어에 넣을 것인가? Screenshot 2025-08-12 at 10 29 44 PM

헤더,푸터의 경우 shared에 포함되는 primitive 컴포넌트보다 더 넓은 범위의 컴포넌트라고 생각해서 widget에 두었습니다. shared 내의 코드들은 어느 상황의 프로젝트에서도 사용할 수 있으만한 공통 모듈을 넣으려고 했습니다. 또한 의존성 부분에서도 pages에서만 사용되기 때문에 widget에 두는 것이 더 적절하다고 생각했습니다!

FSD 구조를 기반으로 모듈을 분리하고 위치시키면서 각 모듈에 대한 기능, 역할에 대해 더 얇은 레이어로(?) 디테일하게 생각해볼 수 있었던 것 같습니다. 기존 모듈 역할 단위로만 나누던 폴더 구조에서는 모듈의 분류에 대해 깊이 생각하지 않기 때문에 프로젝트가 커질 경우 모듈이 서로 뒤섞이게 되는 것 같습니다.

  1. 낙관적 업데이트를 제대로 구현하고 사용자 경험 개선 효과 직접 느껴보기

즉시 반응하는 UI의 쾌감

낙관적 업데이트를 적용하기 전에는 댓글을 추가하거나 좋아요를 누를 때마다 서버 응답을 기다려야 했습니다. 사용자 입장에서는 "내가 뭔가 했는데 반응이 없네?"라는 답답함이 있었어요.

하지만 낙관적 업데이트를 적용하고 나니 버튼을 누르는 순간 즉시 UI가 업데이트되면서 훨씬 반응성 있는 앱이 되었습니다. "아, 이래서 요즘 앱들이 이렇게 빠르게 느껴지는구나"라고 깨달았어요.

하지만 낙관적 업데이트를 구현하면서 예상보다 복잡한 부분이 많았습니다. 성공했을 때는 문제없지만, 실패했을 때 이전 상태로 롤백하는 로직이 생각보다 까다로웠어요.

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

  1. FSD 아키텍처의 완벽한 이해와 적용
  2. 타입 안전성을 보장하는 낙관적 업데이트 시스템
  3. 개발자 경험을 향상시키는 도구와 설정

1. FSD 아키텍처의 완벽한 이해와 적용

각 레이어의 역할 명확히 정의하기 FSD를 단순히 폴더 구조로만 이해하지 않고, 각 레이어가 왜 존재하는지, 어떤 책임을 가져야 하는지 깊이 고민했습니다.

엔티티에 서버 관련 코드 모아놓기

post
- index.ts
- post.api.ts
- post.constant.ts
- post.model.ts
- post.queries.ts
- post-type.ts

이런 식으로 같은 엔티티 기반으로 파일명을 가독성이 높게 파일명을 작성하려고 했습니다. 가독성 뿐만 아니라 엔티티 하위에 다중 폴더 구조가 되지 않게 관련 코드끼리 모아뒀습니다.

index.ts로 공개 API를 슬라이스 단위에서만 명시적으로 사용

import 할 때 어느 슬라이스에 포함되는 모듈인지 명시적으로 확인할 수 있도록 layer 최상단에서는 index.ts로 노출시키지 않고 각 slice에서만 index.ts로 export 했습니다.

// 지양
from '@/shared'

// 지향
from '@/shared/ui'

2. 제네릭을 활용한 타입 안전성

타입 정의 시 타입을 통해 데이터의 구조나 기능이 상상이 가능할 수 있도록 실제 기능과 구조를 잘 표현하기 위해 노력했습니다.

예를들어, 사용자 목록 조회 시 'select' 파라미터를 통해 사용자 모델의 속성을 선택할 수 있게 되어있습니다. 이 기능을 온전히 타입에 표현되도록 제네릭 타입을 사용하여 표현했습니다.

// 동적으로 User 속성을 선택할 수 있는 제네릭 타입
export interface GetUsersParams<K extends keyof User> {
  limit: number;
  select: K[];
}

// 제네릭 함수로 타입 안전성 확보
export const getUsers = async <K extends keyof User>({
  limit,
  select,
}: GetUsersParams<K>): Promise<GetUsersResponse<UserPick<K>>> => {
  // ...
};

// UserPick 타입으로 선택된 속성만 포함하는 타입
export type UserPick<T extends keyof User> = Pick<User, T>;

API 응답에서 필요한 속성만 선택적으로 가져올 수 있도록 제네릭을 활용하고 타입 안정성을 보장하여 런타임 에러를 방지할 수 있었습니다.

유틸리티 타입을 사용하여 단순 타입(?) 형태로 표현하지 못하는 경우들을 표현할 수 있도록 했습니다.

// src/6_shared/types/utility.type.ts

export type Nullable<T> = T | null;
export type EmptyStringable<T> = T | '';

// 사용
export interface BaseFilterParams {
  limit: number;
  skip: number;
  sortBy?: EmptyStringable<SORT_BY>;
  sortOrder?: SortOrder;
}

export interface UIState {
  showAddDialog: boolean;
  showEditDialog: boolean;
  showAddCommentDialog: boolean;
  showEditCommentDialog: boolean;
  showPostDetailDialog: boolean;
  showUserModifyDialog: boolean;
  selectedPost: Nullable<Post>;
  selectedComment: Nullable<Comment>;
  selectedUser: Nullable<Partial<User>>;
}

명시적이고 의도가 명확하게 타입을 정의하려고 했고, 재사용 가능한 유틸리티 타입으로 일관성 있는 타입 시스템 구축하고자 했습니다.

3. 쿼리키 관리

export const QUERY_DOMAINS = {
  POSTS: 'posts',
  USERS: 'users',
  COMMENTS: 'comments',
  TAGS: 'tags',
} as const;

export const QUERY_OPERATIONS = {
  LIST: 'list',
  DETAIL: 'detail',
  INFINITE: 'infinite',
  SEARCH: 'search',
  COUNT: 'count',
} as const;

const postQueryKeys = {
  all: [QUERY_DOMAINS.POSTS] as const,

  lists: () => [QUERY_DOMAINS.POSTS, QUERY_OPERATIONS.LIST] as const,
  list: (params: Partial<AllFilterParams>) =>
    [...postQueryKeys.lists(), params] as const,
} as const;

Work in progress... 더 작성하겠습니다. 죄송합니다..

4. FSD 폴더 구조 사용을 위한 개발자 경험을 향상시키는 도구와 설정

FSD 폴더 구조에서는 Layer 계층이 분명하고 단방향 의존성을 지켜야하기 때문에 그 계층이 시각적으로 눈에 보여서 어느 폴더가 상위이고 하위인지 생각하는 시간을 줄이고자 했습니다.

FSD의 최상위 폴더, 즉 Layer의 상위 ~ 하위 계층은

app
pages
widgets
features
entities
shared

이런 순서로 구성이 되는데 만약 이 폴더를 그대로 만들게 되면 알파벳 순서로 정렬되기 때문에

app
entities  
features
pages
shared
widgets

이렇게 뒤섞여보이기 때문에 의존성 방향을 시각적으로 바로 확인하기가 어려웠습니다.

그래서 폴더 순서가 의존성 계층 순서와 일치하여 폴더 위치만 봐도 의존성 관게를 파악할 수 있도록 폴더명 앞에 순서를 붙였습니다.

1_app/
2_pages/
3_widgets/
4_features/
5_entities/
6_shared/

하지만 이 폴더명 그대로 import문에 지저분하게 노출하여 사용하고 싶진 않았습니다. 그래서 절대 경로 설정을 통해 import 문에서 숫자없이 Layer 명만 깔끔하게 노출하여 사용할 수 있게 했습니다. 그리고 절대 경로 입력시 각 레이어가 몇번 폴더인지 생각할 필요없이 바로 Layer명으로 자동 완성할 수 있기 때문에 개발 생산성을 높일 수 있었습니다

from '@/features/add-to-cart'
from '@/entities/post'
from '@/shared/ui'

이렇게 절대경로를 실제 숫자가 붙은 폴더명을 Layer명으로 alias하고 싶었습니다

그래서 절대 경로 설정은 아래와 같이 설정했습니다.

// vite.config.js

'@/app': path.resolve(__dirname, './src/1_app'),
'@/app/*': path.resolve(__dirname, './src/1_app/*'),
'@/pages': path.resolve(__dirname, './src/2_pages'),
'@/pages/*': path.resolve(__dirname, './src/2_pages/*'),
'@/widgets': path.resolve(__dirname, './src/3_widgets'),
'@/widgets/*': path.resolve(__dirname, './src/3_widgets/*'),
'@/features': path.resolve(__dirname, './src/4_features'),
'@/features/*': path.resolve(__dirname, './src/4_features/*'),
'@/entities': path.resolve(__dirname, './src/5_entities'),
'@/entities/*': path.resolve(__dirname, './src/5_entities/*'),
'@/shared': path.resolve(__dirname, './src/6_shared'),
'@/shared/*': path.resolve(__dirname, './src/6_shared/*'),
'@final_src': path.resolve(__dirname, './src/final_src'),
'@': path.resolve(__dirname, './src'),

처음에 가장 최상단 폴더에 대한 절대 경로 설정을 맨 위에 두었더니, 다른 모든 절대경로를 인식하지 못하는 문제가 있었습니다

문제가 발생한 설정

'@': path.resolve(__dirname, './src'),
'@/app': path.resolve(__dirname, './src/1_app'),
'@/app/*': path.resolve(__dirname, './src/1_app/*'),
'@/pages': path.resolve(__dirname, './src/2_pages'),
'@/pages/*': path.resolve(__dirname, './src/2_pages/*'),
'@/widgets': path.resolve(__dirname, './src/3_widgets'),
'@/widgets/*': path.resolve(__dirname, './src/3_widgets/*'),
// ...생략

이렇게 설정했을 때는

from '@/shared/ui'

이런 경로가 에러가 발생하게 됩니다!

'@': path.resolve(__dirname, './src'), 여기에서 먼저 걸러지기 때문에


from '@/6_shared/ui'

‘@’ 하위의 정확한 폴더명을 적어줘야 정상적으로 작동하게 됩니다.

그래서 alias되는 절대 경로를 우선 처리하고 나머지 절대 경로를 인식할 수 있도록 ‘@’를 최하단에 두었더니 모든 절대 경로가 정상적으로 동작했습니다!!

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

  • FSD 레이어 간 경계가 모호한 경우의 판단 기준
  • 대규모 프로젝트에서의 FSD 확장성
  • 복잡한 비즈니스 로직에서의 낙관적 업데이트 전략

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

  • 실무 프로젝트에 FSD 아키텍처 도입 검토
  • TanStack Query를 활용한 서버 상태 관리 개선
  • 타입 안전성을 고려한 API 설계 및 구현

챕터 셀프회고

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

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

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

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

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

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

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

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

질문 1: FSD 아키텍처에서 도메인 중심 슬라이스 설계의 적절성

FSD 아키텍처에서 도메인 중심의 슬라이스 사용이 적절한지, 그리고 현재 구조를 개선할 수 있는 더 나은 방법이 있는지 궁금합니다.

현재 src/final_src/4_features/post-management 경로에서 게시물 관련 기능들을 하나의 도메인 중심 슬라이스로 구성하고 있습니다.

현재 접근 방식의 배경:

게시물 기능을 CRUD 단위로 세분화하면 post-create, post-read, post-update, post-delete와 같은 개별 슬라이스들이 생성되는데, 각각이 너무 작은 단위가 되어 오히려 관리 복잡도가 증가할 수 있다고 판단했습니다. 그래서 현재는 post-management라는 도메인 중심의 상위 개념으로 묶어서 관리하고 있습니다.

검토하고 싶은 부분:

  • 도메인 중심의 슬라이스 설계가 FSD 철학에 적합한지
  • 기능별 세분화 vs 도메인별 통합 중 어떤 접근이 더 적절한지
  • 현재 구조에서 발생할 수 있는 잠재적 문제점과 개선 방안

코드위치: src/final_src/4_features/post-management

질문 2: 낙관적 업데이트의 선언적 모듈화 방안

낙관적 업데이트 로직을 선언적이고 재사용 가능한 형태로 추상화하여 코드 중복을 줄이고 유지보수성을 향상시키고 싶습니다.

문제 상황: 현재 React Query의 useMutation에서 낙관적 업데이트를 onMutate, onError, onSuccess 콜백들을 통해 명령형으로 구현하고 있습니다. 이로 인해 다음과 같은 문제들이 발생하고 있습니다:

구체적인 문제점:

  • 각 뮤테이션(생성, 수정, 삭제)마다 유사한 낙관적 업데이트 패턴이 반복되고 있습니다.
  • queryClient.setQueryData 호출이 여러 파일에 분산되어 있어 일관성 있는 관리가 어렵습니다.
  • 각 뮤테이션에서 에러 발생 시 롤백 로직을 독립적으로 구현하고 있어 에러 처리 전략이 파편화되어 있습니다.

코드위치: src/final_src/5_entities/post/post.queries.ts 의 useCreatePostMutation (46번째 줄)

질문 3: 다중 쿼리 타입 환경에서의 캐시 동기화 전략

게시물에 변경사항(생성, 수정, 삭제)이 발생할 때, 현재 활성화된 쿼리 타입을 감지하여 해당하는 쿼리키로 캐시된 데이터에만 낙관적 업데이트를 수행하도록 구현했습니다. 이러한 방식이 적절한지, 더 나은 방법은 없는지 궁금합니다.

실무에서 이러한 상황이 발생했을 때 확장성, 유지보수성을 고려하여 세련되게 처리할 수 있는 방법이 없을까요?

현재 상황: 게시물 목록을 조회하는 방식이 세 가지 타입(기본 목록, 태그별 필터링, 검색어 기반)으로 나뉘어 있으며, 각각 다른 API 엔드포인트에서 데이터를 가져옵니다.

코드위치:

  • 쿼리키 선택 : src/final_src/6_shared/lib/hooks/useCachedPostsQueryKey.ts
  • 해당 훅을 사용하여 캐시 데이터 선택 : src/final_src/5_entities/post/post.queries.ts 의 useCreatePostMutation (51번째 줄에서 useCachedPostsQueryKey사용)

과제 피드백

수고했습니다. 지난 3주간 클린코드를 비롯한 소프트웨어 공학적으로 결합도 낮추기 응집도 높이기를 위한 이론과 프론트엔드에서의 적용등을 통해서 좋은 코드와 구조에 대한 다각도의 시야가 생겼기를 기대합니다.

"이전에는 새로운 기능을 추가할 때마다 "이 코드는 어느 폴더에 넣지?"라고 고민했는데, FSD를 적용하고 나니 자연스럽게 적절한 위치가 보이더라구요." "FSD 구조를 기반으로 모듈을 분리하고 위치시키면서 각 모듈에 대한 기능, 역할에 대해 더 얇은 레이어로(?) 디테일하게 생각해볼 수 있었던 것 같습니다. "

수고했습니다. 코드를 보니 다양한 시도들을 해본 것 같아서 너무 좋네요. 특히 이번 과제는 정답이 있는게 아니라 여러가지의 선택지가 있는 문제에서 다양한 관점 그자체들을 고민하고 배우는데 있었는데 잘 해준 것 같아 좋습니다.

FSD라는 틀을 통해서 바라보면 그전에 희미했던 코드의 계층과 구조에 대해서 조금 더 잘게 선명하게 보일 수 있을 거에요! 나머지는 실무를 통해서 폴더구조가 아니라 코드의 계층과 역할 그리고 자리를 떠올려볼 수 있게 되기를 바래요 :)

Q) 질문 1: FSD 아키텍처에서 도메인 중심 슬라이스 설계의 적절성 => 폴더구조는 일종의 멘탈 모델입니다. 사실 폴더구조가 어떻든지 간에 대부분 코드는 검색해서 찾아가고 결합도만 낮다면 스파게티 코드가 되지는 않습니다. 그렇지만 내가 시각적으로 드러나는 구조가 내 머리속의 지도가 되어 주기에 폴더구조만큼의 생각 구조를 머리에 담을 수 있죠.

=> 세분화를 하는게 좋으냐 아니냐는 실제 기획단위의 대화에서 어떤 관점으로 보느냐가 중요합니다. 이번 과제의 경우 포스트 삭제 편집 생성 기능을 분리하지 않아도 될거라고 생각해요. 일부 기능만 on/off를 하거나 변경하거나 하지는 않을 것 같아요. 그렇지만 기획하고 만드는 과정에서 하나씩 분리해서 다루고 말하고 기획서가 되어 있다면 나눠주는게 더 낫습니다. 저는 가급적 기획서에 가깝게 혹은 말하는 그대로의 폴더구조를 만들고 싶거든요.

Q) 질문 2: 낙관적 업데이트의 선언적 모듈화 방안

=> 패턴이 반복되는 것은 함수형 프로그래밍을 풀 수 있습니다. 우선 동일한 함수에서 변경이 되어야 하는 부분을 인자로 받는 함수를 만들고 이를 통해서 원 함수를 다시 작성을 하면 뼈대만 사용하고 나머지는 교체하는 템플릿 메소드 패턴을 함수로 만들 수 있습니다. (코드 예시는 이 내용으로 AI에게 알려달라고 해보세요)

그렇게해서 유사한 코드 패턴을 템플릿으로 교체하면서 작성하게 되면 자연적으로 선언적 모듈화가 됩니다. 무조건 이 방식이 더 나은 코드를 만들지는 않습니다. 어떻게 구성을 하느냐에 따라서 유연성없고 불편한 코드가 되거나 변하지 않은 부분이 잘 감춰진 멋진 라이브러리가 될 수 있어요.

Q) 질문 3: 다중 쿼리 타입 환경에서의 캐시 동기화 전략

잘했습니다. TanstackQuery가 제안하는 표준적인 방식입니다. 이렇게 서버데이터를 클라이언트 상태관리를 대신해서 관리할 수 있도록 해주는 거죠. 실무에서도 이런 방식으로 활용합니다.

BP 선정이유 : 과제를 하면서 많은 고민들을 담아내어보고 시도를 많이 해본 것들이 잘 느껴졌습니다!