-
Notifications
You must be signed in to change notification settings - Fork 47
[5팀 박수범] Chapter3-2. 디자인 패턴과 함수형 프로그래밍 그리고 상태 관리 설계 #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[구조 변경] - FSD 폴더 구조 초기화 (shared, entities, widgets) - 공통 타입과 유틸리티 함수를 `shared` 레이어로 이동 [Entities] - `ProductCard` UI 생성 및 도메인 로직(재고 확인) 캡슐화 - 장바구니 계산 로직을 `entities/cart/lib`으로 분리 [Widgets] - `Header` 위젯 추출: UI 렌더링과 상태 분리 - `ProductList` 위젯 추출: 목록 렌더링 및 검색 결과 없음 처리 분리 - `CartSidebar` 위젯 추출: 자율적인 가격 계산 로직 구현 [리팩토링] - App.tsx의 원시 JSX 코드를 위젯 컴포넌트 조합으로 교체 - 애플리케이션 전체의 가격 표기 방식 표준화 - UI 컴포넌트의 TSX 확장자 관련 에러 수정
- `src/features/product/model/useProducts.ts` 생성: 상품 CRUD 및 localStorage 동기화 로직 이관 - `src/features/coupon/model/useCoupons.ts` 생성: 쿠폰 CRUD 및 초기 데이터 로직 이관 - 의존성 역전(IoC)을 위해 알림 기능(`addNotification`)을 훅의 인자로 주입받도록 설계
- `src/features/cart/model/useCart.ts` 생성 - 장바구니 추가, 삭제, 수량 업데이트 로직을 훅으로 캡슐화 - `entities/product/lib`의 `getRemainingStock`을 사용하여 재고 검증 강화 - `entities/cart/lib`의 `calculateCartTotal`을 사용하여 쿠폰 적용 시 유효성 검사 로직 개선 - 재고 초과 방지 및 수량 0 이하 시 삭제 처리 등 엣지 케이스 처리 로직 보완
- `src/widgets/AdminDashboard` 생성 및 관리자 관련 UI 이관 - `App.tsx`에 흩어져 있던 `productForm`, `couponForm` 상태와 핸들러를 위젯 내부로 이동 (Local State) - 테스트 통과를 위해 `placeholder`, `required`, `type` 속성을 레거시 코드와 동일하게 유지 - 쿠폰 삭제 버튼 등의 SVG 아이콘 경로를 기존 테스트 코드 셀렉터와 일치시킴 - `alert` 대신 주입받은 `onNotification`을 사용하여 테스트 환경 지원
- `App.tsx`의 거대한 비즈니스 로직을 제거하고 Feature Hooks(`useProducts`, `useCart` 등) 연결 - UI 렌더링 부분을 위젯(`Header`, `ProductList`, `CartSidebar`, `AdminDashboard`) 조합으로 대체 - 기존 E2E/통합 테스트 통과를 위해 다음 로직 유지 및 보정: 1. `searchTerm` 디바운스(500ms) 로직 유지 2. `Notification` 상태 관리 로직 유지 (UI 테스트 의존성) 3. `AdminDashboard`의 Input 필드 속성(`placeholder="숫자만 입력"`) 복원 - 사용하지 않는 레거시 코드 및 임포트 정리
- `src/shared/lib/useLocalStorage.ts` 생성: 로컬 스토리지 읽기/쓰기 로직을 제네릭 훅으로 캡슐화 - Feature Hooks(`useCart`, `useProducts`, `useCoupons`)에서 중복되는 `useEffect` 및 저장소 접근 로직 제거 - `JSON.parse` 에러 처리를 공통 훅 내부로 통합하여 안정성 확보 - 비즈니스 로직에서 저장 매체(Implementation Detail)에 대한 의존성 제거
[UI 로직 추상화] - `src/shared/lib/useDebounce.ts` 생성: 값의 변경을 지연시키는 범용 훅 구현 - `App.tsx` 리팩토링: 명령형 `setTimeout` 타이머 로직을 제거하고 `useDebounce`를 사용하여 선언적으로 변경 [도메인 계산 분리] - `src/entities/coupon/lib/index.ts` 생성: `canApplyCoupon` 순수 함수 구현 - `useCart` 리팩토링: 훅 내부에 섞여있던 쿠폰 적용 유효성 검사(계산)를 외부 함수로 격리 - 액션(Hook)과 계산(Logic)의 책임을 명확히 분리하여 테스트 용이성 확보
- `src/widgets/AdminDashboard/ui/ProductListTable.tsx` 생성: 상품 목록 테이블 렌더링 분리 - `src/widgets/AdminDashboard/ui/CouponListGrid.tsx` 생성: 쿠폰 목록 그리드 렌더링 분리 - 테이블 헤더 및 데이터 매핑 로직을 별도 컴포넌트로 격리하여 가독성 향상
- `src/features/product/ui/ProductManagementForm.tsx` 생성: - 상품 추가/수정 폼 UI 분리 - 입력 필드 유효성 검사 및 `placeholder` 등 테스트 호환성 속성 유지 - `src/features/coupon/ui/CouponManagementForm.tsx` 생성: - 쿠폰 생성 폼 UI 분리 - 할인율/금액 유효성 검사(Validation) 로직 내재화 및 알림 연동 - 각 폼 컴포넌트에 JSDoc 주석 추가 및 Import 경로 정리
- `src/widgets/AdminDashboard/ui/index.tsx` 리팩토링: - 거대한 JSX 렌더링 로직을 하위 컴포넌트(`ProductManagementForm`, `ProductListTable` 등) 위임으로 대체 - 불필요한 폼 로컬 상태(`productForm`, `couponForm`) 제거 및 핸들러 간소화 - FSD 레이어별 Import 구문 정리 및 JSDoc 추가 - 의존성 주입된 `onNotification`을 하위 Feature 폼에 전달하여 기능 연동
- `src/shared/ui/NotificationSystem.tsx` 생성: - 알림 목록을 렌더링하는 순수 UI 컴포넌트 구현 - `App.tsx`에 하드코딩 되어있던 Tailwind 클래스와 JSX 제거 - `src/shared/lib/useNotificationSystem.ts` 생성: - 알림 상태 배열 관리 및 자동 삭제(Timer) 로직 캡슐화 - UI와 상태 관리를 분리하여 재사용성 및 테스트 용이성 확보
- `src/features/product/model/useProductFilter.ts` 생성: - 검색어 상태, Debounce 적용, 배열 필터링 로직을 하나의 훅으로 통합 - `App.tsx`에서 명령형 필터링 로직 제거 - `src/features/product/model/useProductForm.ts` 생성: - 상품 추가/수정 폼의 복잡한 상태 관리와 유효성 검사 로직을 훅으로 분리 (Headless UI 패턴 적용) - UI 컴포넌트(`ProductManagementForm`)는 렌더링에만 집중하도록 역할 축소
- `src/features/app/useShop.ts` (또는 `features/shop/useShop.ts`) 생성: - `useProducts`, `useCoupons`, `useCart`, `useNotification`을 하나로 묶는 통합 훅 구현 - 각 훅 간의 의존성 주입(Dependency Injection)을 내부에서 처리 - `App.tsx` 리팩토링: - 개별 훅 호출 코드를 `useShop` 하나로 대체 - 비즈니스 로직과 상태 선언 코드를 완전히 제거하고 View와 정리에만 집중
- `zustand` 라이브러리 설치 및 `src/shared/lib/notificationStore.ts` 생성 - `NotificationSystem` UI 컴포넌트가 Props 대신 전역 스토어를 구독하도록 변경 - `useCart` 훅 내부에서 알림 함수를 주입받지 않고 스토어 액션을 직접 사용하도록 리팩토링 - `App.tsx` 마운트 시 스토어 상태를 초기화하여 통합 테스트 간 상태 오염(State Pollution) 방지 - 불필요해진 `App.tsx` 내부의 알림 관리 로직 제거
- `src/features/coupon/ui/CouponManagementForm.tsx` 리팩토링 - FSD 계층 구조에 맞춰 Import 구문 그룹화 및 정리 - 컴포넌트 및 핸들러 함수에 JSDoc 주석을 추가하여 역할 명시 - JSX 구조 포맷팅 개선 및 불필요한 아이콘 제거로 가독성 향상
- `src/features/cart/model/cartStore.ts` 생성: 장바구니 상태 및 액션을 Zustand 스토어로 중앙 집중화 - `useCart` 훅 리팩토링: 로컬 상태 관리를 제거하고 스토어의 상태와 액션을 연결하는 Selector 역할로 변경 - `persist` 미들웨어 제거: 레거시 데이터 포맷 호환성 및 테스트 제어권 확보를 위해 순수 메모리 스토어 방식으로 변경 - `App.tsx` 동기화 로직 구현: 앱 마운트 시 로컬스토리지 데이터 주입(Hydration) 및 변경 사항 구독(Subscription) 로직 추가 - 테스트 환경 격리: `origin.test.tsx` 및 `App` 초기화 시점에 스토어 상태를 리셋하여 테스트 간 상태 오염 방지 [Bug Fix] - 리스트 렌더링 Key 중복 문제 해결: - `Date.now()` 사용 시 빠른 실행 속도로 인해 동일한 Key가 생성되는 문제 식별 - `crypto.randomUUID()`를 도입하여 고유한 식별자(UUID) 생성 보장 - (관련 파일: `notificationStore.ts`, `useProducts.ts`, `cartStore.ts`)
- `src/features/product/model/productStore.ts` 생성: 상품 CRUD 로직 및 상태를 Zustand로 중앙 집중화 - `useProducts` 훅 리팩토링: 스토어의 상태와 액션을 반환하는 Selector 역할로 변경 - ID 생성 로직 위임: `addProduct` 액션 내부에서 `crypto.randomUUID()`를 사용하여 고유 ID 생성 보장 (UI단에서 ID 생성 로직 제거) - `App.tsx` 동기화: 상품 데이터의 로컬스토리지 로드(Hydration) 및 구독(Subscription) 로직 구현 - 알림 연동: 액션 실행 시 `useNotificationStore`를 직접 호출하여 외부 의존성 제거
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
배포링크:advanced
[구조 변경]
shared레이어로 이동[Entities]
ProductCardUI 생성 및 도메인 로직(재고 확인) 캡슐화entities/cart/lib으로 분리[Widgets]
Header위젯 추출: UI 렌더링과 상태 분리ProductList위젯 추출: 목록 렌더링 및 검색 결과 없음 처리 분리CartSidebar위젯 추출: 자율적인 가격 계산 로직 구현[리팩토링]
과제의 핵심취지
과제에서 꼭 알아가길 바라는 점
기본과제
Component에서 비즈니스 로직을 분리하기
비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기
뷰데이터와 엔티티데이터의 분리에 대한 이해
entities -> features -> UI 계층에 대한 이해
Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
계산함수는 순수함수로 작성이 되었나요?
Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
계산함수는 순수함수로 작성이 되었나요?
특정 Entitiy만 다루는 함수는 분리되어 있나요?
특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?
데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?
심화과제
이번 심화과제는 Context나 Jotai를 사용해서 Props drilling을 없애는 것입니다.
어떤 props는 남겨야 하는지, 어떤 props는 제거해야 하는지에 대한 기준을 세워보세요.
Context나 Jotai를 사용하여 상태를 관리하는 방법을 익히고, 이를 통해 컴포넌트 간의 데이터 전달을 효율적으로 처리할 수 있습니다.
Context나 Jotai를 사용해서 전역상태관리를 구축했나요?
전역상태관리를 통해 domain custom hook을 적절하게 리팩토링 했나요?
도메인 컴포넌트에 도메인 props는 남기고 props drilling을 유발하는 불필요한 props는 잘 제거했나요?
전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?
과제 셀프회고
리팩토링 청사진
제가 집중한 이번 리팩토링의 핵심 목적은 수직적 분리 와 수평적 분리 를 통해 입체적인 코드 구조를 만드는 것입니다.
수직적 분리 (레이어 위계)
상위 레이어만 하위 레이어를 참조할 수 있는 단방향 의존성 규칙 을 세워 데이터 흐름을 통제했습니다.
수평적 분리 (내부 역할)
각 폴더 내부에서 책임을 다시 분리합니다:
이는 함수형 프로그래밍의 "데이터와 로직의 분리" 원칙을 파일 시스템에 적용한 것입니다.
입체적 메트릭스
수직과 수평을 조합하면, 코드의 위치만으로도 "어떤 계층의, 어떤 역할" 인지 즉시 파악할 수 있습니다:
widgets/ProductList/ui→ 상위 레이어의 UI 조립features/cart/model→ 중간 레이어의 상태 관리entities/cart/lib→ 하위 레이어의 순수 계산 로직이 구조는 파일 경로 자체가 명세서 역할을 합니다.
시행착오: 수평적 분리에 대한 오해
처음에는 FSD의 레이어 구조만 이해했습니다. 그래서 단순히
이렇게 함수형 프로그래밍의 3단계 분리(데이터, 계산, 액션)와 1:1로 매핑된다고 생각했습니다.
하지만 이건 잘못된 이해였습니다.
영서님과의 대화를 통해 깨달은 것은:
수직적 분리(Layer): '권력(의존성)'을 기준으로 나눈다
수평적 분리(Slice): '주제(도메인)'를 기준으로 나눈다
내부 분리(Segment): '직무(역할)'를 기준으로 나눈다
코드를 분리할 때는 "이건 어떤 Layer의 어떤 Slice에 있는 어떤 Segment가 해야 할 일인지" 를 생각하면 됩니다.
예를 들어, 장바구니 총합 계산 로직은:
entities(데이터 레이어)cart(장바구니 도메인)lib(순수 계산 로직)→
entities/cart/lib/calculateCartTotal.tsFSD 설계 + 함수형 프로그래밍은 3차원 좌표계 처럼 코드의 정확한 위치를 지정하는 시스템같다고 느껴졌습니다.
설계 의사결정 과정
폴더 구조의 큰 틀은 FSD 공식 문서와 레퍼런스를 통해 파악했지만, 실제로 중요한 것은 "어떤 설계를 선택할 것인가" 에 대한 의사결정이었습니다.
예를 들어:
calculateCartTotal로직을entities/cart/lib에 둘 것인가,shared/lib에 둘 것인가?ProductCard의 재고 확인 로직은 컴포넌트 내부에 둘 것인가, 별도 lib로 분리할 것인가?onAddToCart핸들러는 어느 레이어가 소유해야 하는가?이런 질문들은 정답이 명확하지 않았고, 각 선택마다 트레이드오프가 있었습니다.
함수형 프로그래밍 블로그 와 FSD 설계 블로그를 정독한 후 →
이런 인사이트를 얻을 수 있었고, 설계 결정의 타당성을 검증 할 수 있었습니다.
결국 좋은 아키텍처는 폴더를 어떻게 나누느냐의 문제가 아니라, "왜 이렇게 나눴는가"를 설명할 수 있는가 의 문제라는 것을 배웠습니다.
과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?
FSD 패턴: 폴더 구조가 곧 명세서다 [Screaming Architecture]
로버트 C. 마틴(Uncle Bob)이 주창한 "Screaming Architecture" 개념이 있습니다.
기술이 아닌 비즈니스가 보여야 한다
기존 구조:
이 구조는 "우리는 리액트를 씁니다!"라고 소리칩니다.
하지만 정작 중요한 질문에는 답하지 못합니다:
FSD 구조:
이 구조는 "우리는 쇼핑몰입니다!"라고 소리칩니다.
폴더 이름만 봐도 기획서의 목차가 보입니다.
FSD는 기술 스택이 아닌 비즈니스 도메인이 앞으로 나옵니다.
레이어 자체가 기능 명세서
FSD의 각 폴더는 기획서의 섹션과 정확히 매칭됩니다.
특히
features폴더는 그 자체로 "이 애플리케이션이 수행할 수 있는 동작 목록"입니다.신규 입사자가 왔을 때, 두꺼운 위키 문서를 던져주는 것보다
src/features폴더를 열어보라고 하는 게 훨씬 빠르고 정확합니다.강제된 의존성 규칙이 곧 설계도
일반적인 프로젝트에서는 개발자마다 "이 코드를 어디에 둘까?"에 대한 기준이 달라 스파게티가 되기 쉽습니다. 하지만 FSD는 상위 레이어만 하위 레이어를 참조할 수 있다는 엄격한 규칙이 있습니다.
이 규칙 자체가 "코드의 흐름도" 역할을 합니다:
폴더 위치만으로도 데이터의 흐름과 의존 관계를 파악할 수 있습니다. 폴더구조가 하나의 명세로 사용된다는 게 신기하고 충격이었습니다.
FSD 구조 + 함수형 프로그래밍 기반의 코드로 찢는과정
리팩토링을 진행하면서 가장 어려웠던 건 "이 코드를 어느 폴더에 넣을까?" 였습니다.
이 코드가 어느 레이어에 속해야 하며, 뷰데이터인지, 엔티티데이터인지 액션인가? 계산인가? 등 컴포넌트를 찢는 과정에 명확한 기준이 필요했습니다.
5가지 원칙을 세우고, 원칙에 의해서 판단 후 파일을 분리했습니다.
1. "이 코드가 외부환경을 바꾸는가?" (액션 vs 계산의 분리)
함수형 프로그래밍의 제1원칙
가장 먼저 적용한 기준입니다. 코드를 분리할 때 가장 중점적으로 적용한 원칙입니다.
질문:
분리 기준:
NO (순수함): 입력만 같으면 결과가 늘 같고, 아무것도 안 건드린다.
→ lib (Entities/Shared) 으로 격리
calculateCartTotal,formatCurrency,canApplyCouponYES (부수효과): 상태를 바꾸거나(
setState), 저장하거나(localStorage), 화면을 그린다.→ model (Hook) 또는 ui (Component) 에 남김
addToCart,useLocalStorage왜 중요한가?
순수 함수는 테스트하기 쉽고, 재사용하기 좋고, 버그가 적습니다.
lib 폴더만 열면 "아, 이건 어디든 가져다 써도 계산함수구나"라는 생각이 듭니다.
2. "이 코드는 '쇼핑몰'이라는 사실을 아는가?" (도메인 지식 유무)
FSD 레이어 결정 기준
코드가 비즈니스(도메인)와 얼마나 얽혀있는지 봤습니다.
질문:
분리 기준:
YES (작동함): 도메인을 모르는 멍청한 도구다.
→ Shared
useLocalStorage(저장만 함),useDebounce(시간만 끔),formatCurrency(숫자만 바꿈)NO (에러남): '상품', '쿠폰' 같은 단어를 알아야 한다.
→ Entities / Features
ProductCard,useCart왜 중요한가?
Shared는 다른 프로젝트에서도 복붙할 수 있는 "범용 인프라"입니다.
반면 Entities/Features는 "이 쇼핑몰만의 고유한 지식"입니다.
이 경계가 명확해야 코드를 재사용하거나 교체하기 쉬워집니다.
3. "책임이 너무 무겁지 않은가?" (단일 책임 원칙)
컴포넌트 분할 기준
AdminDashboard나App.tsx를 찢을 때 적용한 기준입니다.질문:
분리 기준:
App.tsx는 "라우팅/배치"가 바뀌면 수정되어야 하는데, "검색 로직" 때문에도 수정되어야 했다.→ 분리 대상
AdminDashboard는 "레이아웃" 때문에 수정되어야 하는데, "상품 입력 폼의 유효성 검사" 때문에도 수정되어야 했다.→ ProductManagementForm으로 분리
왜 중요한가?
하나의 파일이 여러 이유로 수정되면, 팀원들끼리 Git 충돌이 나고, 버그 수정 시 예상치 못한 부분이 깨집니다.
"한 가지 이유로만 변경되는 파일"은 안전하고 예측 가능합니다.
4. "구현(How)인가, 의도(What)인가?" (추상화 레벨)
훅(Hook) 분리 기준
useCart내부를 정리할 때 쓴 기준입니다.질문:
분리 기준:
Before:
localStorage.getItem('cart')... JSON.parse...→ 이건 "어떻게(How)"에 집착하는 저수준 코드
Action: 저수준 코드를
useLocalStorage로 숨김After:
usePersist("cart")→ "저장한다(What)"는 의도만 남김
→ 비즈니스 로직이 선명해짐
왜 중요한가?
비즈니스 로직에
JSON.parse,try-catch같은 저수준 코드가 섞이면 "진짜 중요한 로직"이 묻힙니다.추상화를 통해 "쿠폰을 적용한다", "장바구니에 담는다" 같은 비즈니스 언어만 남겨야 합니다.
5. "자주 함께 바뀌는가?" (응집도)
파일 위치 선정 기준
타입(
types.ts)을 찢어서 각 폴더에 넣을 때 쓴 기준입니다.질문:
분리 기준:
Product타입이 바뀌면ProductCard와useProducts가 귀찮아진다.→ 그들 곁(
entities/product)에 둔다.서로 관련 없는 코드들이 한 파일(
shared/types.ts)에 모여 있으면, 하나 고칠 때마다 부가적인 수정소요가 생긴다.→ 찢는다
왜 중요한가?
"함께 변경되는 것은 함께 둔다"는 원칙은 개발 속도를 올립니다.
Product관련 코드를 수정할 때,entities/product폴더만 열면 모든 게 거기 있습니다.여러 폴더를 헤매지 않아도 됩니다.
5가지 원칙 요약표
lib(순수함수)❌ →
hook(액션)Entities/Features❌ →
ShareduseLocalStorage)model)으로 이동1. 전역 상태 관리 도입 배경: "Props Drilling 지옥 탈출기"
① 마주한 한계 (The Pain Points)
FSD 아키텍처를 도입해 폴더 구조는 깔끔해졌지만, 데이터 흐름(Data Flow)은 여전히 복잡했습니다. 특히
App.tsx에서 정의된 상태와 핸들러가 말단 컴포넌트까지 전달되는 과정에서 'Props Drilling' 문제가 심각하게 대두되었습니다."택배 기사가 되어버린 중간 컴포넌트들"
가장 대표적인 예시는
addNotification함수였습니다.App.tsxProductManagementForm(상품 추가 실패 시 알림)App→AdminDashboard→ProductManagementForm이 과정에서
AdminDashboard는 본인이 알림 기능을 쓰지도 않으면서, 단지 자식에게 넘겨주기 위해 Props 인터페이스에onNotification을 정의해야 했습니다. 이는 불필요한 결합도(Coupling)를 높이고 코드를 지저분하게 만들었습니다.거대한 허브가 된 App.tsx
모든 도메인 상태(
cart,products,coupons)를App.tsx가 관리하다 보니,App컴포넌트는 UI 렌더링보다는 데이터를 엮어주는 작업에 치중하게 되었습니다. 이는 "페이지 레이아웃"이라는 본연의 책임에서 벗어난 것이었습니다.② 해결책: Zustand 도입과 제어의 역전
이러한 문제를 해결하기 위해 Zustand를 도입하여 상태 관리의 패러다임을 전환했습니다.
결과:
App.tsx는 더 이상 상태 관리자가 아닌, 순수한 조립자(Assembler)가 되었습니다.2. 기술적 도전: 전역 상태 도입과 테스트 격리 문제
하지만 전역 상태 관리를 도입하여 구조를 개선하자마자, 예상치 못한 새로운 문제가 발생했습니다.
① 문제 상황 (Problem)
알림 시스템(
Notification)을useState기반의 지역 상태에서Zustand기반의 전역 상태로 리팩토링한 직후, 기존에 잘 통과하던 통합 테스트(origin.test.tsx)가 실패하는 현상이 발생했습니다.② 원인 분석 및 추론 (Reasoning)
디버깅 결과, 원인은 지역 상태와 전역 상태의 '생명주기(Lifecycle)' 차이에 있었습니다.
전역 상태 관리 전 (
useState):App컴포넌트가 언마운트되면서 상태도 함께 메모리에서 소멸합니다.전역 상태 관리 후 (
Zustand):왜 문제가 되었는가?
③ 해결 방법 (Solution)
이 문제를 해결하기 위해 '마운트 시 초기화(Reset on Mount)' 전략을 선택했습니다.
접근법:
App.tsx가 마운트되는 시점(useEffect)에 전역 알림 스토어를 빈 배열로 강제 초기화했습니다.선택 근거:
언마운트 시점의
cleanup함수를 활용할 수도 있었으나, 테스트 도중 에러 발생 등으로 비정상 종료될 경우 뒷정리가 실행되지 않을 위험이 있습니다.반면, 마운트 시점의 초기화는 이전 상황과 관계없이 현재 실행 환경의 순수성을 확실하게 보장합니다.
부가적인 효과:
실제 사용자 경험에서도 페이지 새로고침 시 이전 세션의 불필요한 알림이 남지 않도록 하는 UX 개선 효과를 얻었습니다.
④ 배운 점 (Lesson Learned)
전역 상태 관리 라이브러리를 도입할 때는 단순히 "편리함"만 생각해서는 안 된다는 것을 깨달았습니다.
싱글톤은 자유로운 만큼 책임이 따른다:
전역 스토어는 컴포넌트 경계를 넘어 데이터를 공유할 수 있지만, 그만큼 생명주기 관리의 책임도 개발자에게 넘어옵니다.
테스트의 중요성:
"왜 예전엔 되던 테스트가 갑자기 깨지지?" 즉, 회귀 테스트를 실패하게 되면서 수정 직후 빠르게 문제를 파악할 수 있고 해당 문제에 대한 즉각적인 고민과 해결이 가능해서 너무 좋았습니다.
이 조치를 통해 전역 상태를 사용하면서도 각 테스트 케이스의 독립성을 보장할 수 있었고, 코드의 신뢰성을 유지할 수 있었습니다.
이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?
이번 과제를 진행하며 가장 신경쓰고, 또 고민했던 부분은 단순한 코드 이동이 아니라 "설계 원칙의 적용" 과정이었습니다. 특히 제가 가장 공을 들였던 부분은 'FP의 순수성' 과 'FSD의 위계질서' 를 실제 코드에 녹여내는 것이었습니다.
1. 액션(Action)과 계산(Calculation)의 엄격한 분리 (FP)
가장 공을 들인 부분은 컴포넌트와 훅에 뒤섞여 있던 비즈니스 로직을 '부수 효과가 없는 순수 함수(계산)' 와 '상태를 변경하는 함수(액션)' 로 명확히 구분하는 것이었습니다.
Before:
formatPrice나applyCoupon같은 함수들이 내부에서 전역 상태(isAdmin)나 컴포넌트 상태(cart)를 직접 참조(암묵적 입력)하고 있어 테스트가 불가능했습니다.After:
계산:
entities/**/lib폴더로 추출하여 외부 의존성을 모두 제거하고 인자(Explicit Input)만으로 동작하도록 리팩토링했습니다.예:
canApplyCoupon,calculateCartTotal액션: 상태 변경이나 사이드 이펙트는
features/**/model의 Custom Hook에 위임하여, 계산 로직을 호출하고 결과를 반영하는 역할만 수행하도록 했습니다.이를 통해 비즈니스 로직의 테스트 용이성을 확보 하고, UI 컴포넌트가 로직에 종속되지 않도록 만들었습니다.
2. FSD 아키텍처의 '의존성 규칙' 준수와 '응집도' 강화
단순히 파일을 폴더에 나누는 것을 넘어, "각 레이어가 서로를 어떻게 참조해야 하는가" 에 대한 규칙을 세우고 지키는 데 집중했습니다.
FSD 아키텍처로 보여지는것보다 각가의 레이어의 위계가 확실히 정립되고, 관련있는 파일끼리 최대한 모아서 높은 응집도의 구조가 되도록 노력했습니다.
Shared:
도메인을 모르는 순수 도구(
useLocalStorage,useDebounce)로 정의하여 범용성을 확보했습니다.Features:
useCart가useProducts의 데이터가 필요할 때, 인자로 주입받거나 전역 스토어를 구독하게 하여 결합도를 낮췄습니다.Widgets:
AdminDashboard같은 거대 컴포넌트를ProductManagementForm(Feature UI)과ProductListTable(Sub-Widget)로 쪼개어, 위젯이 '직접 그리기'보다는 '조립하기' 에 집중하도록 설계했습니다.3. 전역 상태(Zustand) 도입과 생명주기 관리
useState에서Zustand로 마이그레이션하며 발생한 테스트 격리 문제를 해결하는 과정에 깊은 고민을 담았습니다.App.tsx마운트 시점에 스토어를 강제로 초기화하는 전략을 사용하여, 전역 상태의 편리함과 테스트 환경의 독립성을 모두 확보했습니다.이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!
Server State와 Client State의 분리
이번 과제에서는
localStorage를 사용하여 클라이언트 사이드에서 데이터를 저장했습니다. 하지만 실제 서비스 환경을 고려하여, React Query(TanStack Query) 를 도입해 비동기 데이터와 UI 상태를 FSD 아키텍처 안에서 어떻게 효율적으로 관리할지 고민해보고 경험해보고 싶습니다.이번 과제를 통해 얻은 가장 큰 깨달음:
FSD와 FP 원칙을 적용하면서, 폴더 구조 자체가 하나의 명세서이자 설계도 가 될 수 있다는 것을 체감했습니다. 비록 모든 폴더 구조가 FSD 일수도 없고, 항상 정답도 아니지만 FSD 경험을 토대로 더 복잡한 비즈니스 로직과 사용자 경험을 다루는 프로젝트에 도전하고 싶습니다.
무엇보다 어떤 설계나 디자인 패턴이 좋아보여서 따라하고 적용하는게 중요한게 아닌, 어떤 관점과 기준에서 내 프로젝트에 적용했고 그 적용한 이유를 설명가능하며 그 이유가 나의 팀원과 조직을 설득시킬 수 있는지 여부가 더 중요하다고 느꼈습니다.
세상엔 정말 많은 패턴과 설계방법, 프로그래밍 패러다임이 존재하며 이번 FSD + FP는 좋은 경험이였지만 "항상 이 설계, 이 방법으로 프로그래밍 하겠어" 라는 생각으로 매몰되지는 말아야지 생각했습니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
1. 아키텍처 및 구조 관점 (FSD)
Q. Feature와 Entity의 경계 및 위젯의 역할 축소에 대해 의견이 궁금합니다.
이번 리팩토링에서 장바구니 계산 로직(
calculateCartTotal)은entities로, 상태 관리(useCart)는features로 분리했습니다.특히
AdminDashboard위젯이 가지고 있던 폼 상태와 유효성 검사 로직까지features레이어(ProductManagementForm등)로 내리면서 위젯이 오로지 조립 역할만 하게 되었는데요.이 구조가 FSD의 의도에 부합하는지, 혹은 위젯이 너무 껍데기만 남은 것은 아닌지 아키텍처 관점에서의 피드백이 궁금합니다.
2. 상태 관리 및 동기화 관점 (Zustand & Sync)
Q. App.tsx에서의 수동 데이터 동기화 패턴이 적절한지 궁금합니다.
Zustand의
persist미들웨어를 사용하는 대신,persist를 제거하고App.tsx에서useEffect를 통해 로컬스토리지와 스토어를 수동으로 동기화하는 방식을 택했습니다.이는 테스트 환경에서의 격리와 레거시 데이터 호환성을 확보하기 위함이었는데요. 이 패턴이 장기적인 유지보수 측면에서도 유효할지, 혹은 더 나은 대안이 있을지 의견이 궁금합니다.