- 전역상태관리를 사용해서 상태를 분리하고 관리하는 방법에 대한 이해
- 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를 이용한 비동기 코드 작성에 대한 이해
- 비동기 코드를 선언적인 함수형 프로그래밍으로 작성하는 방법에 대한 이해
- 모든 API 호출이 TanStack Query의 useQuery와 useMutation으로 대체되었는가?
- 쿼리 키가 적절히 설정되었는가?
- fetch와 useState가 아닌 선언적인 함수형 프로그래밍이 적절히 적용되었는가?
- 캐싱과 리프레시 전략이 올바르게 구현되었는가?
- 낙관적인 업데이트가 적용되었는가?
- 에러 핸들링이 적절히 구현되었는가?
- 서버 상태와 클라이언트 상태가 명확히 분리되었는가?
- 코드가 간결하고 유지보수가 용이한 구조로 작성되었는가?
- TanStack Query의 Devtools가 정상적으로 작동하는가?
- 폴더구조와 나의 멘탈모데일이 일치하나요?
- 다른 사람이 봐도 이해하기 쉬운 구조인가요?
지금까지 과제 진행하면서 새로운 개념을 배웠을 때에는 번쩍하면서 이해가 가고 방향성이 보였다면, 이번 과제는 아무리 개념을 이해해도 울림이 안왔습니다.
저의 핵심적인 의문은 "개념은 알겠는데 어떻게 얼마나 편한건데?" 였습니다.
(1) 실무 프로젝트에 대입해보기 준형, 하늘님이 추천하신 방법은 지금 내가 작업하는 프로젝트 기준으로 FSD를 도입했을 때 as-is의 불편했던 점과 to-be에서 개선되는 점을 보면서 체감하는 것이었습니다.
지금 진행하고 있는 프로젝트는 작업할 때 화면설계서 상 ID를 기준으로 pages 폴더 하위에 ID명 그대로 채번해서 생성합니다.
기본 접두사 + BenefitMainIndex001 => benefit/main/전체 ID명.vue
그래서 전체적인 프로젝트 폴더 구조는 아래와 같이 되는데
src/
api/
assets/
components/
composables/
constants/
layouts/
middleware/
modules/
pages/
페이지 경로(monimo/asset)
plugins/
stores/
utils/
현재 코드 가이드 상 페이지 내부에 스크립트 로직을 다 짜서 그런지 솔직히 불편함을 겪어본 적은 없었습니다.
하지만 일단 기존 구조에 FSD를 도입해보면 아래와 같은 형식이 됩니다.
src/
app/ # 라우팅, providers, 전체 config
pages/
monimo/ # route entry
entities/
asset/ # asset이라는 핵심 모델이 있다면
features/
asset/ # asset 관련 인터랙션/기능
ui/
model/
lib/
api/
shared/
ui/ # 공용 버튼, toggle, modal etc
lib/ # utils
styles/ # global style variables, mixins
as-is의 문제점은 각기 다른 depth의 파일을 import하고 사용하면서 각 도메인별 분리가 명확하지 않다는 것입니다.
하지만 여전히 그렇게 큰 편리함이 있나?가 이해가 안가서 지피티랑 아래처럼 대화를 나눴는데
근데 내가 이해 안가는 건 as-is도 사실상 페이지 경로 기준으로 분리를 해둔거니까 as-is에서도 내가 수정하려는 페이지 경로 타고 들어가면 됐던 거 아니야?
결론은 UI적인 내용이나 페이지 경로 등은 큰 차이가 없어보일지 몰라도 as-is는 기능 자체가 사혼의 구슬처럼 여러군데 퍼져있으니까 그 의존성 추적이 쉽지 않다는 추가적인 문제점을 알 수 있었습니다.
하지만 FSD면 기능 기준으로 모아두기에 한 폴더 내에서 찾고 관리하면 되고 그로 인해 독립적인 확장이 가능해집니다. 또한 상위 레이어는 하위 레이어를 의존할 수 있지만 하위 레이어는 상위 레이어를 의존할 수 없기 때문에 단방향을 유지할 수 있습니다
이 규칙을 통해 코드의 구조가 명확해지고 계층 간의 결합도가 낮아지며, 특정 레이어에서 발생한 변경이 다른 레이어로 확산되지 않도록 방지할 수 있습니다.
✅ 응집도와 결합도가 뭘까?
- 응집도가 높다는 것은 관련 기능이 함께 모여있다는 의미이다.
- 결합도가 낮다는 것은 서로 다른 부분이 독립적으로 작동할 수 있다는 의미이다.
즉, 응집도는 비슷한 것끼리 모여있다!(응집) 결합도는 다른 요소들끼리 합쳐져있다!(결합) 이므로 응집도는 높을수록 좋고 결합도는 낮을수록(의존성이 줄어들수록) 좋다.
(2) 내 언어로 재정의하기
| 구분 | 설명 |
|---|---|
| app | - 전역적인 설정 담당 - 슬라이스가 없음 |
| pages | - 말 그대로 상품 목록, 관리자같은 페이지 ㄴ여기서 내부에 pages/product, pages/admin처럼 슬라이스 추가 |
| widgets | - 아이폰 위젯처럼 여러 페이지에서 재사용 가능한 기능 포함된 컴포넌트(상품 좋아요 버튼 위젯, 리뷰 작성 위젯처럼) ㄴ위 예시라면 widgets/product 슬라이스 추가 |
| features | - 장바구니 추가와 같이 widgets에게 제공할 사용자 인터랙션 중심의 개별 기능 단위(Action, Event) ㄴ위 예시라면 features/product 슬라이스 추가 |
| entities | - 비즈니스 엔티티 관련 로직(Data, Domain) ㄴentities/product, entities/admin |
| shared | - 전역적으로 사용되는 UI 컴포넌트, 타입 정의, 유틸 함수 |
- Widgets는 Entities, Features, Shared의 ui 컴포넌트 요소들을 조합하여 만들어지는 중간 단위 UI 블록
- Widgets의 조합으로 Pages 구성
즉, 아래와 같이 상위에서 하위로 의존 가능 / 역순서로는 불가능합니다!
app
└─ pages
└─ widgets
└─ features
└─ entities
└─ shared
위 흐름을 정리하다보니 FSD는 Composite 패턴 자체는 아니지만, 작은 기능 단위를 조합해 상위 구조를 구성하는 게 Composition 기반같아보인다고 생각했고
기존의 수평적인 방식이 크레이프 케이크라서 한겹씩 벗겨내서 먹지 않으면 속 재료(구조)를 알 수 없다면 FSD는 기능 단위를 세로로 자른 케이크 조각처럼, 단면으로 한번에 속 재료(구조)를 알 수 있고 원하는 부분만 잘라내서 수정이 가능하다고 생각했습니다!
결국 개발에서 중요한 건 개발 외에 불필요한 일에 소모되는 시간과 에너지를 최소화하는 것 같습니다.
1️⃣ 기존 폴더 구조의 문제점 파악 (1) components/index.ts
- components/index.ts 파일에 모든 UI 컴포넌트가 정의되어있다.
- type 정의가 되어있지 않다.
(2) pages/PostsManagerPage.tsx
한 파일 안에 모든 페이지별 컴포넌트가 정의되어있다.

(3) 모달 콘텐츠가 모달 영역을 벗어난다
=> class를 수정해서 벗어나지 않도록 개선했다!
(4) 1이 value로 지정되어있어 어떤 걸 기입해야 하는 항목인지 알 수 없다
=> id는 자동생성되므로 해당 내용에 대한 정보를 제공한다!
(5) 한 사람이 좋아요를 여러개 누를 수 있어도 되는가?
=> 좋아요 기능을 토글 형식으로 변경했다!
2️⃣ 나만의 FSD 규칙 정의 규칙이 있어야 적용할 수 있으니 규칙부터 정의해보았다. FSD 가이드
3️⃣ 위에서부터 아래로 내려가자 FSD의 특성을 활용해서 위에서부터 아래로 내려가면서 분리함으로써 아래 방향 의존성을 강제하자.
(3-1) Page 분리 기존 파일을 pages 폴더 하위로 옮긴다.
(3-2) UI 분리 => Widgets UI 블록 단위로 widgets로 분리한다.
(3-3) 사용자 행동별 분리 => Features 사용자 행동(Action) 기준으로 features로 분리한다.
(3-4) 비즈니스 단위 분리 => Entities 각 비즈니스 관련 api 로직을 entities 하위로 이동한다.
(3-5) 정말 범용적인 것 => shared formatting utils나 공용 UI는 shared 하위로 분리한다.
4️⃣ 고민 지점 (4-0) 너무 엔티티/도메인에 구애받는 것 같다 FSD는 기능 조립 아키텍쳐라는 걸 잊지 말자! 자꾸 그걸 잊고 공부할 때 배운 엔티티 기반 폴더 구조에 익숙해져서 도메인 규격화를 하려고 한다.
도메인으로 슬라이스를 나누라는 것은 규칙이 아니라 출발점이다. => 도메인과 무관한 기능이면 기능 중심으로 slice 생성
(4-1) 검색 및 필터 컨트롤 영역은 어디에 해당할까.. 위에서 재정의한 내용을 바탕으로 생각해보자.
검색 및 필터 영역은 게시물 관련이니 Post 폴더 내에 있어야할 것 같지만 기능에 집중해야 한다.
widgets/seachFilter.tsx
(4-2) 헤더랑 푸터는 어디에 해당할까.. 헤더랑 푸터는 기능이 하나도 없는 UI 요소이므로 아래에 위치할 수 있다.
widgets/layout/Header.tsx
widgets/layout/Footer.tsx
5️⃣ API URL 설정의 시행착오 GitHub Pages에 배포한 사이트에서 API 호출이 실패하고 있었고 브라우저 콘솔에는 다음과 같은 404 오류가 발생했다.
[Error] Failed to load resource: the server responded with a status of 404 () (tags, line 0)
[Error] Failed to load resource: the server responded with a status of 404 () (users, line 0)
[Error] Failed to load resource: the server responded with a status of 404 () (posts, line 0)
(5-1) 1단계: 초기 문제 발견 로컬 개발 환경에서는 Vite의 proxy 설정을 통해 /api 경로로 API를 호출하고 있었지만 GitHub Pages는 정적 사이트 호스팅이므로 서버 사이드 프록시가 없어, 배포 환경에서는 /api/posts 같은 상대 경로가 실제 API 서버로 연결되지 않았다.
// 모든 API 호출이 하드코딩된 상대 경로 사용
const response = await fetch("/api/posts")
(5-2) 2단계: 첫 번째 시도 - 환경 변수 기반 분기 환경 변수를 사용하여 개발/프로덕션 환경을 구분하려고 했다.
export const getApiBaseUrl = () => {
if (import.meta.env.VITE_API_BASE_URL) {
return import.meta.env.VITE_API_BASE_URL
}
if (import.meta.env.PROD) {
return "https://dummyjson.com"
}
return "/api"
}
하지만 이렇게 했을 때 아래 문제가 발생하여 다른 방법을 찾아봤다. [문제점]
- 함수 호출 오버헤드
- 코드가 복잡함
- 타입 추론이 명확하지 않음
(5-3) 3단계: 두 번째 시도 - 간단한 상수 분기 더 간단한 방식으로 변경했다.
const isDev = import.meta.env.DEV
export const BASE_URL = isDev ? "/api" : "https://dummyjson.com"
하지만 여전히 문제가 해결되지 않았다.🥲
[문제점]
- import.meta.env.DEV가 빌드 시점에 제대로 작동하지 않음
- 프로덕션 빌드에서도 /api로 설정되는 경우 발생
(5-4) 4단계: 세 번째 시도 - ApiClient 클래스 도입 API 호출을 중앙화하고 일관성 있게 관리하기 위해 ApiClient 클래스를 도입했다.
// ApiClient 클래스 생성
export class ApiClient {
private baseUrl: string
constructor(url: string) {
this.baseUrl = url
}
public async get<TResult>(endpoint: string, queryParams?: Record<string, string | number>) {
const response = await fetch(`${this.baseUrl}${endpoint}${queryString}`)
return this.handleResponse<TResult>(response)
}
// ... 다른 메서드들
}
export const apiClient = new ApiClient(API_URL)
[하지만 여전히 문제]
- API_URL이 빌드 시점에 올바르게 설정되지 않음
(5-5) 5단계: 네 번째 시도 - import.meta.env.MODE 사용 import.meta.env.DEV 대신 import.meta.env.MODE를 사용했다.
export const API_URL = import.meta.env.MODE === "production"
? "https://dummyjson.com"
: "/api"
[문제점]
- TypeScript 타입 오류 발생
Property 'env' does not exist on type 'ImportMeta'에러
(5-6) 6단계: 최종 해결 - 타입 정의 추가 Vite의 환경 변수 타입을 명시적으로 정의하여 문제를 해결했다.
// src/vite-env.d.ts
interface ImportMetaEnv {
readonly MODE: string
readonly DEV: boolean
readonly PROD: boolean
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
// src/shared/config/index.ts
export const API_URL = import.meta.env.MODE === "production"
? "https://dummyjson.com/"
: "/api"
✅ 최종 적용
1. 타입 정의 파일 생성
src/vite-env.d.ts에 ImportMetaEnv 인터페이스를 명시적으로 정의하여 TypeScript가 환경 변수를 인식하도록 했다.
2. 환경별 URL 설정
- 개발 환경:
/api(Vite proxy 사용) - 프로덕션 환경:
https://dummyjson.com(직접 API 호출)
3. ApiClient를 통한 일관된 API 호출
모든 API 호출을 ApiClient를 통해 처리하여
- URL 구성이 일관되게 처리됨
- 에러 처리가 중앙화됨
- 코드 유지보수성 향상
✅ 배운 점 1. Vite 환경 변수의 특성
import.meta.env.DEV: 개발 서버 실행 시에만trueimport.meta.env.PROD: 프로덕션 빌드 시에만trueimport.meta.env.MODE: 현재 모드 (development또는production)- 빌드 시점에 정적으로 치환되므로 런타임 오버헤드 없음
2. TypeScript 타입 정의의 중요성 Vite는 자동으로 타입을 제공하지만, 때로는 명시적인 타입 정의가 필요하다. 특히 프로젝트 설정에 따라 타입 인식이 안 될 수 있다.
3. 개발/프로덕션 환경 분리의 중요성
- 로컬 개발: 프록시를 통해 CORS 문제 해결
- 프로덕션: 직접 API 호출 (CORS 허용된 API인 경우)
4. 중앙화된 API 클라이언트의 장점
- 모든 API 호출이 일관된 방식으로 처리됨
- URL 구성 로직이 한 곳에 집중됨
- 향후 인증 토큰 추가, 인터셉터 설정 등이 용이함
API URL 설정은 단순해 보였지만 개발 환경과 프로덕션 환경의 차이, 빌드 시스템의 특성, TypeScript 타입 시스템 등을 고려해야 하는 복잡한 문제였다.😂
🤔 도메인 기준 vs 기능 기준 FSD를 이해했다고 생각했는데도, 막상 폴더 구조를 설계하려고 하면 다시 엔티티 중심 사고로 돌아가려는 제 자신을 발견했습니다. 😂
FSD가 정답이 있는 구조가 아니라 기능을 기준으로 조합 가능한 구조라는 건 알겠지만 실제 프로젝트에 대입하면 여전히 경계가 흐려지는 부분이 있습니다.
앞으로 다양한 작업을 FSD 구조로 만들어보면서 제 기준을 조금씩 다듬어가는 과정이 필요할 것 같습니다!!
페이지 단위 개발에서 기능 단위 개발로 사고 전환하기 이번 과제를 계기로 앞으로 개발할 때 다음과 같은 흐름으로 사고해보려고 합니다.
- 이 기능이 무엇을 하는가?(features)
- 이 기능이 어떤 데이터를 다루는가?(entities)
- 이 기능을 어떤 형태로 보여줄 것인가?(widgets/ui)
- 이 기능이 어느 페이지에서 사용되는가?(pages)
기존에는 이 화면에서 무엇을 보여주지?라는 페이지 중심 사고였다면, 앞으로는 이 기능은 어디서든 재사용 가능한가?라는 기능 모듈화 사고로 전환해보고 싶습니다.
또한, 이런 전환 과정을 통해 코드의 경계를 일관성 있게 볼 수 있는 능력을 만들고 싶습니다!
클린코드와 아키테쳑 챕터 함께 하느라 고생 많으셨습니다! 지난 3주간의 여정을 돌이켜 볼 수 있도록 준비해보았습니다. 아래에 적힌 질문들은 추억(?)을 회상할 수 있도록 도와주려고 만든 질문이며, 꼭 질문에 대한 대답이 아니어도 좋으니 내가 느꼈던 인사이트들을 자유롭게 적어주세요.
벌써 챕터3이 마무리되었습니다. 챕터3에서는 기능적인 부분보다는 프로젝트의 구조와 설계, 협업에서 고려하고 알아야할 사항들에 대해 배우는 과정같았습니다.
기능 구현을 좋아하는 입장으로서는 약간의 아쉬움도 없었다면 거짓말이지만, 과제를 진행하는 과정에서 배운 내용의 깊이가 매우 깊다는 걸 느꼈습니다.
실무에서 3주라는 과정에서 이 모든 내용을 나의 것으로 만들고 깊이있게 이해하기엔 무리가 있었을 것 같다고 생각하지만 극단적인 레거시 코드를 통한 과정이었기에 더욱 느낄 수 있는 바가 많았다고 생각합니다.
그리고 제일 좋았던 포인트는 실무를 진행하며 한번도 생각해본 적 없던 관점과 시각을 가질 수 있게 되었다는 겁니다.
예시로 아이콘 파일명에 대해서도, 이렇게까지 생각해보고 정해본 적이 없지만 교육이라는 목적이 있고 쳐내기 바쁜 일정+업무 재량권에 한계가 있는 실무와 다른 과제라는 포맷이기 때문에 가능했다고 생각합니다.
이 깨달음을 잊지 않고 유지해가고 싶습니다.
- 더티코드를 접했을 때 어떤 기분이었나요? ^^; 클린코드의 중요성, 읽기 좋은 코드란 무엇인지, 유지보수하기 쉬운 코드란 무엇인지에 대한 생각을 공유해주세요
Chapter3-1. UI 컴포넌트 모듈화와 디자인 시스템 처음 Before 패키지를 접했을 때 든 생각은 **UI가 이렇게 복잡할 수가 있나?**였습니다.
색상 값은 하드코딩되어 있고, 단위는 px과 rem이 뒤섞여 있고 모달은 접근성 기준을 거의 지키지 못한 채 동작하기만 하는 구조였고, Atom 단위로 나뉘어 있다지만 UI와 기능 로직이 섞인 컴포넌트들이 많았습니다.
특히 디자인 시스템으로 개편하라는 목표는 단순히 UI를 예쁘게 만드는 것이 아니라 혼란스러운 구조를 근본적으로 바꾸는 일이었기 때문에 처음엔 막막함이 더 크게 다가왔습니다.
1️⃣ 정돈되지 않은 상태의 UI를 분석하며 깨달은 것들 Before 패키지를 하나하나 뜯어보며 가장 먼저 느낀 것은 문제가 너무 많아서 어디서부터 시작해야 할지 모른다였습니다.
❗접근성 문제
- ESC로 모달을 닫을 수 없음
- ARIA 속성 및 role 미정의
- 닫기 버튼 위치가 잘못됨 이처럼 시각적으로만 작동하는 컴포넌트는 실무에서 매우 위험하다는 걸 체감했습니다.
❗스타일 문제
- 반복되는 색상 값
- 단위 혼재
- 반응형 기준 부재
- 컴포넌트 상태(variants)에 대한 명세 없음 그로 인해 결국 재사용이 불가능했고, 유지보수 또한 어려운 구조였습니다.
❗구조적 문제 UI 컴포넌트와 기능 로직이 분리되지 않아 UI가 지나치게 비대했고, Atomic 구조는 있으나 기준이 명확하지 않아 오히려 더 복잡한 폴더 구조가 되었습니다.
이 과정을 통해 UI가 깨지는 건 스타일 문제가 아니라, 구조 문제일 때가 더 많다는 사실을 깊게 이해하게 되었습니다.
2️⃣ 디자인 시스템을 설계하며 찾아온 아하! 모먼트 문제를 수집하고 나서 TailwindCSS·shadcn/ui·CVA·토큰 설계를 적용하면서 서서히 UI 시스템의 뼈대가 보이기 시작했습니다.
❗TailwindCSS + 디자인 토큰 설계 초기엔 TailwindCSS가 왜 디자인 시스템과 함께 쓰이는지 이해되지 않았지만 토큰을 만들고 실제 UI에 적용하는 순간 즉시 반영되는 변환성이 큰 장점임을 느꼈습니다.
이 구조를 잡는동안 디자인 시스템은 결국 팀의 공통 언어를 만드는 일임을 처음으로 실감했습니다.
3️⃣ shadcn/ui와 CVA를 사용하면서 느낀 구조적 해방감 shadcn/ui는 말 그대로 바로 사용 가능한 접근성 보장 소스 코드였습니다.
그리고 그 컴포넌트 위에 CVA로 variants를 정의하면서 유형(variant)이 추가되어도 UI는 흔들리지 않는 구조가 만들어졌습니다.
- Button 크기/종류/상태
- Input의 에러 상태
- Card의 타입 이런 것들이 스타일 조합의 혼란 없이 선언적으로 정의됐다는 건 저에게 매우 큰 안정감을 주는 개발 경험이었습니다.
4️⃣ 컴포넌트와 로직이 분리되며 구조가 선명해진 순간 로직을 모두 hooks로 이동하자 UI 컴포넌트는 props → UI 렌더링이라는 본연의 역할만 담당했고, 비즈니스 규칙은 도메인 훅에서 안정적으로 관리되었습니다.
이때 이런 게 진짜 재사용 가능한 컴포넌트구나라는 걸 알 수 있었습니다.
Chapter3-1의 가장 큰 배움은 UI를 디자인하는 것이 아니라, UI가 작동하는 방식을 설계하는 과정이었습니다. 처음엔 막막하고 혼란스러웠지만 이 과정을 거치며 UI 시스템을 하나의 구조로 정의하는 경험을 할 수 있었습니다.
- 거대한 단일 컴포넌트를 봤을때의 느낌! 처음엔 막막했던 상태관리, 디자인 패턴이라는 말이 어렵게만 느껴졌던 시절, 순수함수로 분리하면서 "아하!"했던 순간, 컴포넌트가 독립적이 되어가는 과정에서의 깨달음을 들려주세요
Chapter3-2. 디자인 패턴과 함수형 프로그래밍 그리고 상태 관리 설계 1️⃣ 처음엔 막막했던 상태관리와 디자인 패턴 초기 코드는 모든 게 한 곳에서 일어나는 구조였고, 어떤 상태는 엔티티를 다루고 있고, 어떤 상태는 단순 UI 상태였지만 한 화면 안에서 뒤섞여 있었습니다.
- cart, products 같은 엔티티 상태
- isShowPopup 같은 UI 상태
- 계산 로직
- 부수효과(Action) 모두가 한 컴포넌트에서 뒤엉켜 있었기 때문에 로직을 파악하는 데 시간이 많이 들었습니다.
이때부터 **도대체 어디까지가 관심사 분리의 기준일까?**가 큰 고민이었고, PR에서 정리했던 것처럼 Action / Calculation / Data 관점으로 전체 기능을 다시 바라보는 과정이 큰 전환점이 되었습니다.
그 덕분에 이게 함수형 사고구나가 실전에서 처음 체감됐습니다.
2️⃣ 순수함수로 분리하면서 찾아온 아하! 모먼트 처음 계산 로직을 순수 함수로 뽑아낼 때는 단순히 util 함수를 만드는 정도라고 생각했지만, 점점 분리할수록 전체 구조가 명확하게 보이기 시작하는 경험을 했습니다.
const total = calculateCartTotal(cart, selectedCoupon);
이 한 줄이 가능해지는 순간, 여러 파일에 흩어져 있던 할인 계산·아이템 계산·쿠폰 전략 로직이 한 눈에 이해 가능한 흐름을 가지게 되었습니다.
- 같은 입력이면 같은 출력
- 외부 상태에 의존하지 않는다 이 단순한 원칙 때문에 계산 함수들은 테스트가 쉬워지고 로직 변화가 전체 시스템에 어떤 영향을 주는지도 명확해졌습니다.
그리고 구조가 스스로 정돈되어 보이는 느낌이 들었습니다. 이게 바로 레고처럼 코드를 조합하는 느낌이구나라는 깨달음을 여기서 얻었습니다.
3️⃣ 컴포넌트를 독립시키는 과정에서 얻은 진짜 깨달음 엔티티 단위로 리팩토링하고, 기능을 domain hook / UI component / pure logic 으로 쪼개기 시작하면서 처음엔 오히려 props drilling이 더 심해지고 혼란스러웠습니다.
하지만 바로 그 과정이 **컴포넌트가 무엇을 알고 있어야 하는가?, 이 컴포넌트는 정말 이 엔티티를 다뤄야 하는가?**라는 질문을 던지게 만들었습니다.
그리고 나중에 Jotai를 도입해 props를 걷어냈을 때 코드가 가벼워지고 명확해졌습니다.
- 컴포넌트는 UI만 그린다.
- 엔티티 로직은 도메인 훅 또는 모델이 담당한다.
- 순수 함수가 비즈니스 규칙을 책임진다. 라는 구조가 체화되는 느낌이었습니다.
4️⃣ 큰 컴포넌트를 쪼개며 생긴 관점의 전환 리팩토링을 하면서 특히 크게 다가온 부분들은 다음과 같습니다.
❗역할 기반으로 개념을 재정의할 수 있게 됐다 X 아이콘 vs 쓰레기통 아이콘의 상황에서 과거라면 모양으로 구분했겠지만, 이번엔 역할로 구분했습니다. 도메인 기반 사고가 점점 몸에 익기 시작한 순간이었습니다.
5️⃣ 구조가 보이기 시작했을 때 생긴 해방감 프로젝트 전체를 시각화하면서 느낀 건 "화살표는 의존성이다. A → B는 곧 A가 B를 안다는 뜻이다." 이걸 이해한 뒤로, 시스템 전체가 선명하게 보이기 시작했고 내가 어떤 방향으로 구조를 개선해야 하는지도 명확해졌습니다.
이게 바로, 처음 막막하던 마음을 할 수 있겠다는 마음으로 바꿔준 가장 큰 순간이었습니다.
Chapter3-2를 진행하며 얻은 가장 큰 배움은 코드를 잘 만드는 것은 기능을 구현하는 것이 아니라, 구조를 설계하는 일이라는 것입니다. 그리고 그 구조 설계의 핵심은 결국 순수함수, 관심사의 분리, 도메인 중심 사고였습니다!
- "이 코드는 대체 어디에 둬야 하지?"라고 고민했던 시간, FSD를 적용해보면서의 느낌, 나만의 구조를 만들어가는 과정, TanStack Query로 서버 상태를 분리하면서 느낀 해방감(?)등을 공유해주세요
이번 챕터에서 가장 크게 느낀 점은 응집도를 높이고 결합도를 낮춘다는 게 이렇게 체감적으로 다가올 수 있구나였습니다.
1️⃣ TanStack Query로 서버 상태를 분리했을 때의 편리함 기존 방식에서는 API 로직이 컴포넌트에 섞여 있어서
- API 에러 핸들링 반복
- 로딩 상태 반복
- useEffect 반복
- data/state를 컴포넌트들이 공유하기 어려움 이런 비효율이 많았습니다.
하지만 React Query로 바꾸고 나니
- 캐싱 / 리트라이 / 에러 처리 / 리프레시가 자동화되고
- 단순히 useQuery만 부르면 되는 구조가 되어서 컴포넌트는 오직 UI와 이벤트 로직만 담당하게 되었습니다.
2️⃣ FSD로 폴더 구조를 정리했을 때의 정돈됨 FSD를 적용하기 전에는 컴포넌트·API·UI가 뒤죽박죽 섞여 있었기 때문에
- 한 기능을 수정하기 위해 여러 폴더를 왔다갔다
- 의존성도 뒤죽박죽
- 어떤 파일을 고쳐야 하는지 빠르게 판단하기 어려움 이런 상황이 많았습니다.
하지만 FSD로 구조를 바꾸고 나니,
- UI는 widget/ui
- 기능별 액션은 features
- 데이터 접근은 entities
- 공통은 shared 로 역할이 명확해졌습니다.
특히 위에서 아래로만 의존 가능하다는 단방향 규칙이 구조를 훨씬 선명하게 만들었습니다.
- 제가 정의한 FSD 슬라이스 기준(도메인 vs 기능)들이 적절한지 궁금합니다..!
- Query mutation 시 낙관적 업데이트 구조가 적절했는지 궁금합니다..! 적용은 했는데 실제 프로젝트에서 코드 작성 방식을 잘 모르다보니 어떤 걸 고려해야 좋고, 어떤 식으로 작성되면 좋을지 알고 싶습니다!
자세한 구현 과정과 회고는 아래 블로그에 정리했습니다! 😊 8주차_기능 중심 아키텍처와 프로젝트 폴더 구조_FSD(Feature-Sliced Design)_TanstackQuery WIL 8주차_Chapter3-3. 기능 중심 아키텍처와 프로젝트 폴더구조
