[8팀 현지수] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#35
[8팀 현지수] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#35hyunzsu wants to merge 68 commits intohanghae-plus:mainfrom
Conversation
- ProductList - ProductCard
- useProductSearch
- ProductManagement
- useProducts
- useCart
- CartSidebar
- CouponManagement
- useCoupons
- useOrder
- ShoppingPage.tsx
- AdminPage.tsx
- Header.tsx
- Provider로 테스트 격리
There was a problem hiding this comment.
안녕하세요! 과제(리팩토링 + FSD 적용) 잘 진행하셨습니다. 구조 분리(entities / features / shared), 순수 함수로의 이동, jotai 기반 전역 상태 도입, 그리고 테스트 업데이트까지 깔끔하게 정리되어 있어 읽기 좋았습니다. 아래는 요청하신 흐름(시나리오 제시 → 각 시나리오에서 코드가 어떻게 바뀌는지 → 응집도/결합도 관점에서 평가 및 개선안)을 따라서 PR 리뷰 형식으로 정리한 피드백입니다. 이 코멘트를 그대로 GitHub PR Review로 붙여 쓰셔도 무방합니다.
안내: 아래 피드백은 코드 스냅샷(PullRequestFiles)과 PR 본문을 바탕으로 작성한 리뷰입니다.
요약(한줄)
- 장점: entities(순수 로직)와 UI/feature 훅의 분리, atom 기반 전역 상태로 Props Drilling 제거, 계산 함수의 추출(entities/cart) — 전형적인 FSD 적용이 잘 되어 있음.
- 개선 포인트(핵심): 상태관리 라이브러리 교체 / 패키지화(모듈화) 시 변경 범위를 줄이는 “추상층(어댑터)” 부재, 알림/사이드이펙트의 전역 의존성(결합), 일부 훅이 직접 useAtom을 호출해 라이브러리에 강결합된 상태.
종합 피드백
- PR 파일 전체로 본 핵심 키워드
- Props Drilling 제거
- atoms 기반 전역 상태 (jotai)
- entities 레이어: 순수함수 (calculateCartTotal 등)
- features/hook 분리 (useCart, useCoupons, useProducts 등)
- UI 컴포넌트 재사용성 (Button, CartItemsList, CouponForm 등)
- 결합도 문제: 훅 ↔ 상태관리 라이브러리(직접 useAtom 호출)
- 패키지화 준비: entities는 비교적 준비됨, 하지만 타입/외부 의존성으로 인한 떼어내기 비용 존재
- PR 본문(과제 셀프회고 / 신경 쓴 부분)에 대한 피드백 및 확장 질문
- 잘하신 점: “왜 이렇게 구조화했는지”를 문서화한 것은 매우 강점입니다. entities/ features/ shared 계층을 분명히 한 것도 적절합니다. cart 계산 로직을 entities로 이동한 결정도 합리적입니다.
- 인사이트 확장 질문:
- 현재 entities의 타입 의존성('../../../types')을 외부로 분리하면 더 재사용성이 높아집니다. 패키지화 시에는 어떻게 타입계를 설계할지 고민해보셨나요? (예: 엔티티 전용 타입 패키지)
- 알림(addNotification)과 같은 사이드 이펙트는 전역으로 쓰기 편리하지만, 패키지화/테스트/라이브러리 전환 시 걸림돌이 됩니다. '알림'을 추상화(인터페이스/서비스)하는 것이 어떨까요?
- 현재 atoms가 UI와 비즈니스 로직 사이를 잇는 "공통 접점" 역할을 합니다. atoms를 교체해야 할 때(예: zustand/Redux) 얼마나 많은 파일을 건드려야 할지 추적해보셨나요?
- 추가 질문 제안(스스로 더 생각해볼 거리)
- features 훅들이 전역 상태 API(예: useSetAtom, useAtomValue)를 직접 호출하는 대신 "store abstraction"을 사용하면 어떤 장점이 있을까?
- entities 레이어 테스트를 독립적으로 실행하려면 현재 타입과 데이터 종속성을 어떻게 정리해야 할까?
- 리뷰 받고 싶은 내용(마지막 PR 섹션이 비어있음)에 대한 답변/권장
- “상태관리 라이브러리 교체”와 “패키지화”가 걱정이라면, 우선 store-adapter 추상화를 만들고, 알림/로컬스토리지/atoms 노출을 표준화하는 작업부터 권장합니다. 아래 상세 섹션에서 AS-IS / TO-BE 코드로 제안합니다.
상세 피드백 — 개념 정의 & 문제 식별 -> AS-IS / TO-BE
먼저 개념 정의.
- 응집도(Cohesion) 정의 (요구하신 규칙을 반영)
- 응집도: 기능 단위(파일/모듈)가 변경되거나 추가/수정/삭제되어야 할 때 이동 경로(파일/코드의 수정 지점)가 얼마나 짧은가.
- 높은 응집도: 한 모듈에서 관련 로직(데이터 타입, 순수 로직, 테스트)이 함께 모여 있고, 변경 시 수정 범위가 좁음.
- 패키지화 관점: 모듈을 떼어낼 때 의존성이 적고, 외부 인터페이스(공식 API)가 명확하면 응집도가 높다.
- 결합도(Coupling) 정의
- 결합도: 모듈/훅/함수가 서로를 얼마나 강하게(구현 또는 특정 라이브러리에 직접 의존) 알고 있는가.
- 낮은 결합도: 함수가 인터페이스(인자/콜백)를 통해 통신해, 내부 구현(상태 라이브러리 등)을 바꿔도 호출 쪽 변경이 적음.
- 높은 결합도: 코드가 특정 전역 객체 또는 특정 라이브러리 API(useAtom 등)를 직접 호출함.
이제 파일 기반으로 문제/해결을 정리합니다.
1) 상태관리 라이브러리 변경 시나리오 (예: jotai → zustand / redux / tanstack-query)
목표: 상태 라이브러리 교체 시 수정범위를 최소화하도록 설계할 것.
문제 정의:
- 현재 훅들(e.g., useCart, useCoupons, useOrder, useProducts 등)이 직접 useAtom/useAtomValue/useSetAtom를 호출함.
- 이로 인해 다른 상태관리 라이브러리로 교체하면 훅 내부를 모두 수정해야 함(결합도 높음).
AS-IS (예: useCart 일부)
// src/advanced/features/cart/hooks/useCart.ts (현재)
import { useAtom, useAtomValue } from 'jotai';
import { cartAtom, productsAtom } from '../../../shared/store';
export function useCart() {
const [cart, setCart] = useAtom(cartAtom);
const products = useAtomValue(productsAtom);
// ... addToCart(), updateQuantity() 등 구현 (jotai API 사용)
}문제: useCart가 jotai API에 강결합. 교체 시 useCart 내부를 전부 바꿔야 함.
TO-BE (어댑터/추상화 추가) — 핵심 아이디어: shared/store에서 "공통 훅"을 제공하고, 그 구현부만 바꾼다.
- shared/store/index.ts (공개 인터페이스)
// shared/store/index.ts (interface)
export function useCartStore(): {
cart: CartItem[];
addToCart: (p: ProductWithUI) => void;
removeFromCart: (productId: string) => void;
updateQuantity: (productId: string, qty: number) => void;
getCartQuantity: (productId: string) => number;
} {
// 기본 구현은 jotai 어댑터로 제공 (아래에 연결)
}- shared/store/jotaiAdapter.ts (jotai 구현)
import { useAtom, useAtomValue } from 'jotai';
import { cartAtom, productsAtom } from './jotaiAtoms';
export function useCartStore() {
const [cart, setCart] = useAtom(cartAtom);
const products = useAtomValue(productsAtom);
const getCartQuantity = (id) => cart.find(i => i.product.id===id)?.quantity || 0;
const addToCart = (product) => { /* existing logic */ };
// ...
return { cart, addToCart, removeFromCart, updateQuantity, getCartQuantity };
}- features/hooks는 이제 아래처럼 변경 (전부 동일)
// src/advanced/features/cart/hooks/useCart.ts (TO-BE)
import { useCartStore } from '../../../shared/store';
export function useCart() {
return useCartStore(); // 내부 구현은 shared/store에서만 바꿔주면 됨
}이렇게 하면 상태 라이브러리 교체 시 변경 범위가 shared/store/* 어댑터 파일들만 바꾸면 됩니다.
예: zustand로 바꿀 때는 shared/store/zustandAdapter.ts 만 작성하고 index.ts가 zustandAdapter를 export 하도록 바꾸면 됩니다.
추가로
- 테스트도 변경 지점이 adapter만(Provider 대체 등)으로 축소됩니다. 현재 test에서 Provider를 import한 부분도 shared/setupTest나 shared/store/testAdapter로 대체 가능.
특이사항: TanStack Query는 서버 상태용이므로 전역 client-side state(장바구니 등) 교체 수단으로는 적합하지 않습니다. 다만 서버 동기화(useQuery/mutation)를 병행하는 패턴은 고려할 수 있습니다.
2) 모듈화/패키지로 배포해야 하는 경우 (엔티티 레이어를 패키지로 떼어내기)
검토: entities 폴더(특히 cart utils)는 비교적 잘 분리되어 있습니다. 다만 현재 entities/utils imports '../../../types' 등으로 app 내 타입에 의존하고 있어 그대로 떼어내면 의존성 문제가 발생합니다.
문제 정의:
- entities/cart/* 가 프로젝트공통 타입('../../../types')에 의존 → 패키지화 시 소비자(앱)가 동일 타입을 제공해야 함.
- features/hooks가 atoms 등 앱 상태에 의존(이를 떼어내려면 public API만 남겨야 함).
AS-IS (entities util)
// src/advanced/entities/cart/utils.ts
import { CartItem, Coupon } from '../../../types';
export const calculateCartTotal = (cart: CartItem[], selectedCoupon: Coupon|null) => { ... }TO-BE (패키지화 관점)
- entities 패키지는 자체 types를 정의하거나 제네릭 타입을 사용하여 앱종속을 제거.
- 외부(앱)에서 매핑해주는 레이어를 둠.
옵션 A — entities 패키지에 자체 타입 정의 & 변환 레이어 제공
// entities-cart-package/src/types.ts (패키지 내부 타입, 앱 중립)
export interface ProductDTO {
id: string;
price: number;
discounts: Array<{ quantity:number; rate:number }>;
// 필요한 최소 필드만 노출
}
export interface CartItemDTO {
product: ProductDTO;
quantity: number;
}
export interface CouponDTO {
discountType: 'amount'|'percentage';
discountValue: number;
}
// entities-cart-package/src/index.ts
export function calculateCartTotal(cart: CartItemDTO[], coupon?: CouponDTO) { ... }앱에서는 변환기(adapter)를 둠:
// app/src/adapters/entitiesAdapter.ts
import { calculateCartTotal } from 'entities-cart-package';
import { ProductWithUI, CartItem } from '../types';
function toDTO(cart: CartItem[]): CartItemDTO[] { ... }
const totals = calculateCartTotal(toDTO(cart), toCouponDTO(selectedCoupon));옵션 B — 패키지가 제네릭 타입을 수용 (TS 제네릭)
export function calculateCartTotal<TProduct extends { price:number; discounts:any }>(cart: Array<{product:TProduct;quantity:number}>, coupon?: { ... }) { ... }권장: 패키지화 시 entities 패키지가 의존할 최소한의 타입 표준(또는 DTO 변환 함수를 제공)과 명확한 public API(index.ts)를 준비하세요. 또한 패키지에 React / DOM 의존성이 전혀 없어야 합니다.
추가: 패키지용 package.json에는 react/jotai를 peerDependencies로 두지 마세요(entities는 프레임워크 독립이어야 함). 현재 pnpm-lock.yaml에 jotai가 들어가 있는데, 패키지화를 위해서는 jotai 의존은 feature 레이어로 유지해야 합니다.
3) 알림/사이드이펙트(예: addNotification) 결합 완화
문제 정의:
- addNotification이 여러 훅에서 직접 import/useNotification으로 호출됩니다(전역 사이드이펙트 의존).
- 전역 알림 구현을 바꾸거나 라이브러리 해체 시 모든 훅이 영향받음.
안좋은 예시(결합 높음):
// current
const { addNotification } = useNotification();
addNotification('상품이 추가되었습니다');더 나은 설계(의존성 주입/콜백 사용):
- 훅이 외부 콜백(onSuccess/onError)을 인자로 받게 하면 훅 자체는 사이드 이펙트에 무관하게 재사용 가능.
AS-IS:
export function useProducts() {
const { addNotification } = useNotification();
const addProduct = (payload) => {
setProducts(...);
addNotification('상품 추가');
};
}TO-BE (옵션A: 인자 주입)
export function useProducts({ onSuccess, onError } = {}) {
const addProduct = (payload) => {
setProducts(...);
onSuccess?.('상품 추가');
};
return { addProduct };
}
// 사용하는 쪽
const { addProduct } = useProducts({ onSuccess: (msg) => addNotification(msg) });TO-BE (옵션B: 서비스 추상화)
// shared/services/notification.ts (인터페이스)
export type NotificationService = {
addNotification: (msg:string, type?:string) => void;
};
let notifier: NotificationService | null = null;
export function setNotificationService(svc: NotificationService) { notifier = svc; }
export function getNotificationService(): NotificationService {
if (!notifier) throw new Error('No notifier set');
return notifier;
}
// 앱 초기화 시 실제 구현 주입
setNotificationService({ addNotification: (m,t) => {/* implement with atom or toasts */} });
// 훅 내부에서 사용
import { getNotificationService } from '../../shared/services/notification';
getNotificationService().addNotification('상품 추가');장점: 테스트 시 mock service 주입 가능, 패키지/라이브러리 분리 용이.
4) 응집도 & 결합도 평가 (현재 코드에 적용)
응집도(현재)
- entities/cart: 매우 높음(순수 함수, 독립적) — 패키지화 적합.
- features/*: 훅과 UI 컴포넌트가 분리되어 있음 → 좋은 응집도(기능별 묶음).
- shared/ui: Button 등 공통 컴포넌트 응집도 양호.
- shared/store: 현재 atoms가 흩어져 있음(jotaiAtoms, cartTotalsAtom, couponsAtom 등). 응집도는 중간. “store 어댑터”를 만들어 응집도를 더 끌어올릴 수 있음.
결합도(현재)
- 훅들이 jotai API에 직접 의존 → 결합도가 높음.
- useNotification 및 localStorage 접근 등 전역 구현에 의존 → 결합도 증가.
- entities는 비교적 낮은 결합도(좋음).
따라서:
- entities는 패키지화 우선순위 1순위(응집도 높고 결합도 낮음).
- features와 shared는 adapter/추상화 레이어 추가로 결합도를 낮춰야 함.
구체적 개선 제안 (우선순위 & 코드 스니펫)
우선순위(권장 순서)
- shared/store에 "store-adapter" 레이어 만들기 (최소 수정으로 다른 상태관리로 교체 가능)
- notification을 서비스/인터페이스로 추상화(의존성 주입/설정)
- entities 패키지화 준비: DTO 또는 자체 타입 분리 + public API(index.ts)
- 테스트 초기화 코드 정리: Provider 감싸는 부분을 공통 helper로 이동
- 리팩토링된 훅에서 onSuccess/onError 형태의 옵션 사용 검토(특히 비즈니스 훅)
중요 코드 예시(요약):
- store adapter (shared/store/index.ts)
// shared/store/index.ts (public shim)
export * from './cartStore'; // 내부에서 jotai/zustand 구현을 교체
export * from './productStore';
export * from './couponStore';- cartStore (jotai adapter) — 현재 jotai 구현을 한 곳으로 집중
// shared/store/cartStore.jotai.ts
import { useAtom, useAtomValue } from 'jotai';
import { cartAtom, productsAtom } from './jotaiAtoms';
export function useCartStore() {
const [cart, setCart] = useAtom(cartAtom);
const products = useAtomValue(productsAtom);
// ...wrap logic (addToCart, removeFromCart, updateQuantity)
return { cart, addToCart, removeFromCart, updateQuantity, getCartQuantity };
}- cartStore (zustand adapter) — 대체할 때 바꾸면 됨
// shared/store/cartStore.zustand.ts
import create from 'zustand';
const useZustand = create(set=>({
cart: [],
addToCart: (product)=> set(state => { /* ... */}),
// ...
}));
export function useCartStore() {
return {
cart: useZustand(state=>state.cart),
addToCart: useZustand(state=>state.addToCart),
// ...
}
}- useCart hook (features) 간단화
// features/cart/hooks/useCart.ts
import { useCartStore } from '../../../shared/store';
export function useCart() {
return useCartStore();
}- notification service
// shared/services/notification.ts
export type NotificationService = { add: (msg:string, type?:string)=>void };
let svc: NotificationService | null = null;
export const setNotificationService = (s:NotificationService) => (svc = s);
export const getNotificationService = () => {
if (!svc) throw new Error('No notification service set');
return svc;
}앱 진입점에서:
// app init
import { setNotificationService } from './shared/services/notification';
setNotificationService({
add: (m,t) => { /* atom으로 push or toast lib */ }
});- useProducts: onSuccess/onError 옵션 패턴
// useProducts.ts
export function useProducts({ onSuccess, onError } = {}) {
const addProduct = (p) => {
try {
// mutate
onSuccess?.();
} catch (err) {
onError?.(err);
}
}
}파일별(주요 모듈) 코멘트 요약
-
src/advanced/entities/cart/*
- 매우 잘 분리되어 있고 순수 함수로 구성되어 있음. Math.round 정책 등 비즈니스 규칙도 깔끔. 패키지화 준비로 가장 적합.
- 권장: types 분리(entities 내 자체 타입 혹은 DTO) — '../../../types' 의존성 제거.
-
src/advanced/features/* (cart, coupon, order)
- Hook / UI 분리 좋음. useCart/useCoupons 등 기능 훅들이 store를 직접 사용하지 말고 shared/store의 어댑터를 사용하면 더 유연해짐.
- 사용 예시: CartItemsList 컴포넌트는 Prop으로 cart를 받도록 되어 있는데(현재는 props 받음), 앱 전체에서 atom을 사용한다면 CartItemsList를 pure 컴포넌트로 유지하면서 소비자에 의해 주입되는 형태가 재사용성에 유리함.
-
src/advanced/shared/store/*
- atoms를 한 군데로 모아두고, store adapter 패턴을 적용하세요. 현재 cartTotalsAtom 같이 파생 상태를 entities.calculateCartTotal에 의존해 만든 점은 좋음.
-
src/advanced/tests/origin.test.tsx
- Provider로 감싼 테스트로 변경한 것은 적절. 향후 store adapter를 만들면 테스트 헬퍼(renderWithProviders)을 공유할 수 있음(현재 renderWithProvider가 테스트 파일에 중복되어 있어 shared/testing-utils로 추출 권장).
-
package.json / pnpm-lock.yaml
- jotai가 종속성에 추가되었음. 패키지/라이브러리로 분리시 peerDependencies, optionalDeps 고려 필요.
체크리스트(실행 가능한 권장 작업)
- shared/store에 adapter 레이어 추가 (useCartStore, useProductStore, useCouponStore 등) — 모든 기능 훅이 여기만 참조하게 변경
- notification을 서비스로 추상화하고 앱 초기화에서 구현 주입(setNotificationService)
- entities 패키지화 준비: entities 내부에서 외부 types에 의존하지 않도록 DTO/내부 타입 정의 또는 제네릭 적용
- tests: render helper(shared/testing-utils)로 Provider 주입 로직 및 공통 세팅 통합
- 코드 스타일: useAtom vs useAtomValue/useSetAtom 사용에 따른 렌더링 최적화 점검(읽기 전용으로 useAtomValue 사용하는 것은 좋음)
- 문서: public API(공개 훅, entities 계산함수) README로 정리 → 패키지 출시에 유리
맺음말(권장 의사결정 가이드)
- 단기(빠른 개선): shared/store 어댑터 추가 + notification 서비스 추상화. 이 둘만 해도 상태 라이브러리 교체 비용과 결합도를 크게 낮출 수 있습니다.
- 중기(패키지화): entities 패키지화 → DTO/타입 정리 → CI 배포 파이프라인 추가.
- 장기: features 훅들의 옵션(onSuccess/onError) 표준화로 재사용성 확대, 테스트 시나리오(서비스 주입) 자동화.
원하시면:
- shared/store adapter의 실제 코드(현재 atoms를 기반으로 한 기본 어댑터)를 PR로 제안해 드리겠습니다.
- 또는 entities 패키지화를 위한 구체적 리포지토리 구조 및 package.json 샘플을 만들어 드릴게요.
필요하신 방향 알려주세요(예: "zustand로 바꾸는 예시 코드" 또는 "entities/cart를 npm 패키지로 분리하는 PR 초안").
unseoJang
left a comment
There was a problem hiding this comment.
안녕하세요 지수님
2-2 과제도 고생 하셨습니다.
FSD로 디렉토리를 잘 나눠주셨고 구조화, 명확성, 디자인 패턴을 기준으로 코드작성을 너무 잘해주셨네요.
중간 중간 상세하게 함수형 업데이트를 사용한다든가, 정규화를 세밀하게 구성한다든가, 절때 바뀌지 않는 값은 ts enum으로 작업한다든가의 디테일만 잡아주면 큰 틀은 너무 잘 잡아주셔서 따로 작업할 부분은 더 안보이는 것 같아요!!
해당 과제 하면서 부족했다고 느껴지는 부분들, AI가 도와줫던 부분들 본인이 복습하면서 추가적으로 학습하게 된다면 훨씬 깔끔한 코드가 될것같습니다.
이번 과제도 고생 많았어요!
| import { CartItem, Coupon } from '../../../types'; | ||
| import { CartTotal } from './types'; | ||
|
|
||
| /** |
There was a problem hiding this comment.
util 파일 전체적으로 주석 잘 달아주시고 구조, 의도는 잘 잡혀 있네요 detail 하게 Math.min, Math.min. Math.route만 잘 묶어서 해당 부분도 헬퍼로 따로 모아서 처리해준다면 훨씬 깔끔해질수 있다는 생각도 들어요
|
|
||
| export function useCoupons() { | ||
| const { addNotification } = useNotification(); | ||
| const [coupons, setCoupons] = useAtom(couponsAtom); |
There was a problem hiding this comment.
해당 부분의 coupns의 의존성이 크다고 ai가 알려주네요
// 읽기
const coupons = useAtomValue(couponsAtom);
const cartTotals = useAtomValue(cartTotalsAtom);
const selectedCoupon = useAtomValue(selectedCouponAtom);이런식으로 의존성을 줄여 콜백의 횟수를 줄이면 좀더 효율성을 확인할수 있다고 합니다!
|
|
||
| export function useCart() { | ||
| const { addNotification } = useNotification(); | ||
| const [cart, setCart] = useAtom(cartAtom); |
There was a problem hiding this comment.
setCart로 업데이트 할때 레이디스컨디션이 이슈가 생길수 있어요
setCart(prev => next(prev)) 형태로 바꾸면 항상 최신 상태 기준으로 안전하게 업데이트됩니다.
There was a problem hiding this comment.
아래 코드 가독성, 안정성 모두 좋게 작성이 되어 있네요
과제를 진행함에 있어서 중점을 두면 좋은 코드인데 함수형 업데이트와 셀렉터 분리, 중복 검증 통합 정도만 적용해도 경합 안전성/성능/가독성이 확 좋아질것 같아요
| return ( | ||
| <section className='bg-white rounded-lg border border-gray-200 p-4'> | ||
| <h2 className='text-lg font-semibold mb-4 flex items-center'> | ||
| <svg |
과제의 핵심취지
과제에서 꼭 알아가길 바라는 점
기본과제
Component에서 비즈니스 로직을 분리하기
비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기
뷰데이터와 엔티티데이터의 분리에 대한 이해
entities -> features -> UI 계층에 대한 이해
Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
계산함수는 순수함수로 작성이 되었나요?
Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?
주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
계산함수는 순수함수로 작성이 되었나요?
특정 Entitiy만 다루는 함수는 분리되어 있나요?
특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?
데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?
심화과제
재사용 가능한 Custom UI 컴포넌트를 만들어 보기
재사용 가능한 Custom 라이브러리 Hook을 만들어 보기
재사용 가능한 Custom 유틸 함수를 만들어 보기
그래서 엔티티와는 어떤 다른 계층적 특징을 가지는지 이해하기
UI 컴포넌트 계층과 엔티티 컴포넌트의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
엔티티 Hook과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
과제 셀프회고
https://hyunzsu.github.io/front_6th_chapter2-2/
과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?
기본 과제
📍 기본 과제
1. 전체 아키텍처 개요
2. 메인 엔트리 포인트 분석
App.tsx
3. 공통 모듈 분석 (shared/)
A. 상태 관리 핵심 (hooks/)
useLocalStorage.ts - localStorage와 연동된 상태 관리
B. UI 컴포넌트 (ui/)
Button.tsx - 재사용 가능한 버튼 컴포넌트
C. 유틸리티 함수 (utils/)
stockUtils.ts - 재고 관련 계산
4. 도메인별 기능 분석 (features/) - Product 도메인
Product 도메인 구조
A. Admin 도메인 상세 분석
useProducts.ts - 관리자용 상품 관리 훅
B. Shop 도메인 상세 분석
useProductSearch.ts - 상품 검색 및 필터링 훅
5. 엔티티 계층 분석 (entities/) - Cart 도메인
A. 엔티티 구조 및 책임
B. 타입 정의 (types.ts)
C. 핵심 비즈니스 로직 (utils.ts) - 상세 분석
D. 비즈니스 룰 및 제약사항
할인 적용 우선순위:
할인 제한사항:
계산 정확성:
6. 아키텍처 설계 원칙
A. 도메인 분리
B. 계층 분리 (Clean Architecture)
C. 데이터 흐름
D. 핵심 설계 원칙
FSD 아키텍처 원칙에 따라 상품 관리 기능을 비즈니스 로직과 UI 로직을 분리했습니다.
심화 과제
1. Props Drilling 발생 지점
1. 알림 시스템 (addNotification)
거의 모든 사용자 액션에서 피드백이 필요하기 때문에 addNotification 함수가 앱의 모든 깊은 컴포넌트까지 전달되어야 했습니다.
2. 장바구니 상태 (cart, setCart)
장바구니는 읽기와 쓰기가 여러 곳에서 필요한 전형적인 전역 상태입니다. 상품 카드에서는 재고 확인을 위해 읽기만 필요하지만 전체 상태를 props로 받아야 했습니다.
3. 상품 데이터 (products, setProducts)
상품 데이터는 관리자 페이지에서의 CRUD 작업과 쇼핑 페이지에서의 재고 확인이라는 서로 다른 목적으로 사용되지만 동일한 상태를 공유해야 했습니다.
4. 쿠폰 상태 (coupons, selectedCoupon, setCoupons, setSelectedCoupon)
쿠폰은 관리와 적용이라는 두 가지 다른 컨텍스트에서 사용되지만 상태를 공유해야 해서 복잡한 props 전달이 필요했습니다.
5. 계산 함수 (calculateCartTotalWithCoupon)
2. 전역 상태 설계
#### 각 도메인별로 atom을 설계하여 관심사를 명확히 분리했습니다. localStorage 연동이 필요한 상태와 세션별 상태를 구분하여 적용했습니다.Atom 구조 설계
localStorage 연동 (atomWithStorage)
atomWithStorage를 사용하여 자동으로 localStorage와 동기화되도록 했습니다. 이를 통해 페이지 새로고침 후에도 사용자의 작업 상태가 유지됩니다.
파생 상태 (cartTotalsAtom)
복잡한 계산 로직을 파생 상태로 분리하여 의존성이 변경될 때만 자동으로 재계산되도록 구현했습니다.
3. Props Drilling 제거
Before - ShoppingSidebar (8개 props)
After - ShoppingSidebar (0개 props)
NotificationToast 컴포넌트
useCart Hook
장바구니 로직을 캡슐화하여 어떤 컴포넌트에서든 일관된 방식으로 장바구니 기능을 사용할 수 있도록 구현했습니다.
4. useAtom vs useAtomValue vs useSetAtom
Jotai의 세분화된 훅을 활용하여 컴포넌트가 실제로 필요한 기능에만 구독하도록 최적화했습니다.
읽기 전용 최적화
파생 상태를 통한 계산 최적화
컴포넌트별 구독 최적화
과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문
1. FSD(Feature-Sliced Design) 폴더 구조에 대한 피드백
의도:
질문:
2. Widgets 레이어 사용에 대한 검토
의도:
질문: