과제의 핵심취지
- React의 hook 이해하기
- 함수형 프로그래밍에 대한 이해
- 액션과 순수함수의 분리
과제에서 꼭 알아가길 바라는 점
- 엔티티를 다루는 상태와 그렇지 않은 상태 - cart, isCartFull vs isShowPopup
- 엔티티를 다루는 컴포넌트와 훅 - CartItemView, useCart(), useProduct()
- 엔티티를 다루지 않는 컴포넌트와 훅 - Button, useRoute, useEvent 등
- 엔티티를 다루는 함수와 그렇지 않은 함수 - calculateCartTotal(cart) vs capaitalize(str)
기본과제
-
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. 전체 아키텍처 개요
src/basic/
├── main.tsx
├── App.tsx
├── __tests__/
├── entities/ # 비즈니스 엔티티
│ ├── cart/ # 장바구니 계산 로직
│ ├── coupon/ # 쿠폰 데이터 및 로직
│ └── product/ # 상품 데이터
├── features/ # 도메인별 기능 모듈
│ ├── cart/ # 장바구니 기능
│ ├── coupon/ # 쿠폰 관리/선택
│ ├── order/ # 주문 처리
│ └── product/ # 상품 관리/표시
├── widgets/ # 복합 위젯
│ └── ShoppingSidebar/ # 쇼핑 사이드바
├── pages/ # 페이지 컴포넌트
│ ├── ShoppingPage.tsx
│ └── AdminPage.tsx
└── shared/ # 공통 모듈
├── hooks/ # 범용 커스텀 훅
├── ui/ # 공통 UI 컴포넌트
└── utils/ # 유틸리티 함수
2. 메인 엔트리 포인트 분석
App.tsx
const App = () => {
// 1. localStorage와 연동된 데이터 상태들
const [products, setProducts] = useLocalStorage('products', initialProducts);
const [cart, setCart] = useLocalStorage<CartItem[]>('cart', []);
const [coupons, setCoupons] = useLocalStorage('coupons', initialCoupons);
// 2. UI 상태 관리
const [selectedCoupon, setSelectedCoupon] = useState<Coupon | null>(null);
const [isAdmin, setIsAdmin] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
// 3. 알림 시스템
const { notifications, addNotification, removeNotification } = useNotification();
// 4. 장바구니 전체 금액 계산 (쿠폰 할인 포함)
const calculateCartTotalWithCoupon = useCallback(() => {
return calculateCartTotal(cart, selectedCoupon);
}, [cart, selectedCoupon]);
// 5. 렌더링: 헤더 + 메인 컨텐츠 (쇼핑/관리자 모드)
};
3. 공통 모듈 분석 (shared/)
A. 상태 관리 핵심 (hooks/)
useLocalStorage.ts - localStorage와 연동된 상태 관리
export function useLocalStorage<T>(
key: string,
defaultValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] {
// localStorage에서 초기값 복원
const [state, setState] = useState<T>(() => {
try {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : defaultValue;
} catch (error) {
return defaultValue;
}
});
// 상태 변경 시 localStorage에 자동 저장
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state));
}, [key, state]);
return [state, setState];
}
B. UI 컴포넌트 (ui/)
Button.tsx - 재사용 가능한 버튼 컴포넌트
const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size,
children,
...props
}) => {
const variantClasses = {
primary: 'bg-gray-900 text-white hover:bg-gray-800',
secondary: 'bg-indigo-600 text-white hover:bg-indigo-700',
danger: 'text-red-600 hover:text-red-800',
// ...
};
return <button className={classes} {...props}>{children}</button>;
};
C. 유틸리티 함수 (utils/)
stockUtils.ts - 재고 관련 계산
export const getRemainingStock = ({ stock, cartQuantity }) => {
return stock - cartQuantity;
};
export const getProductStockStatus = ({ stock, cartQuantity }) => {
return getRemainingStock({ stock, cartQuantity }) <= 0 ? 'SOLD OUT' : '';
};
4. 도메인별 기능 분석 (features/) - Product 도메인
Product 도메인 구조
features/product/
├── admin/ # 관리자용 상품 관리
│ ├── hooks/
│ │ ├── index.ts
│ │ └── useProducts.ts # 상품 CRUD 로직
│ └── ui/
│ ├── index.ts
│ ├── ProductForm.tsx # 상품 추가/수정 폼
│ ├── ProductManagement.tsx # 관리자 메인 컴포넌트
│ └── ProductTable.tsx # 상품 목록 테이블
└── shop/ # 고객용 상품 표시
├── hooks/
│ ├── index.ts
│ └── useProductSearch.tsx # 상품 검색/필터링
└── ui/
├── index.ts
├── ProductCard.tsx # 개별 상품 카드
└── ProductList.tsx # 상품 목록 그리드
A. Admin 도메인 상세 분석
┌─────────────────────────────────────────────────────────────────┐
│ Product Admin Domain Structure │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ProductManage-│───▶│ useProducts │───▶│ Product │ │
│ │ment.tsx │ │ Hook │ │ Entity │ │
│ │ │ │ │ │ │ │
│ │ • 탭 관리 │ │ • addProduct │ │ • validation │ │
│ │ • 폼 토글 │ │ • updateProd │ │ • formatting │ │
│ │ • 상태 관리 │ │ • deleteProd │ │ • business │ │
│ │ │ │ │ │ rules │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ProductTable │ │ProductForm │ │Product State │ │
│ │ │ │ │ │ │ │
│ │ • 목록 표시 │ │ • 입력 검증 │ │ • products[] │ │
│ │ • 수정/삭제 │ │ • 폼 제출 │ │ • editing │ │
│ │ • 정렬/필터 │ │ • 할인 관리 │ │ state │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
useProducts.ts - 관리자용 상품 관리 훅
export function useProducts({ products, setProducts, addNotification }) {
// 상품 추가
const addProduct = useCallback((newProduct: Omit<ProductWithUI, 'id'>) => {
const product: ProductWithUI = {
...newProduct,
id: `p${Date.now()}`, // 고유 ID 생성
};
setProducts(prev => [...prev, product]);
addNotification('상품이 추가되었습니다.', 'success');
}, [setProducts, addNotification]);
// 상품 업데이트
const updateProduct = useCallback((productId: string, updates: Partial<ProductWithUI>) => {
setProducts(prev =>
prev.map(product =>
product.id === productId ? { ...product, ...updates } : product
)
);
addNotification('상품이 수정되었습니다.', 'success');
}, [setProducts, addNotification]);
// 상품 삭제
const deleteProduct = useCallback((productId: string) => {
setProducts(prev => prev.filter(p => p.id !== productId));
addNotification('상품이 삭제되었습니다.', 'success');
}, [setProducts, addNotification]);
return { products, addProduct, updateProduct, deleteProduct };
}
B. Shop 도메인 상세 분석
┌─────────────────────────────────────────────────────────────────┐
│ Product Shop Domain Structure │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ProductList │───▶│useProductSea-│───▶│ Search/Filter│ │
│ │ │ │rch Hook │ │ Logic │ │
│ │ • 그리드 레이 │ │ │ │ │ │
│ │ 아웃 │ │ • debounced │ │ • name match │ │
│ │ • 반응형 │ │ search │ │ • description│ │
│ │ • 빈 상태 │ │ • filtering │ │ match │ │
│ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ProductCard │ │Stock Utils │ │Filtered │ │
│ │ │ │ │ │Products │ │
│ │ • 상품 이미지 │ │ • remaining │ │ │ │
│ │ • 가격 표시 │ │ stock │ │ • real-time │ │
│ │ • 할인 뱃지 │ │ • stock │ │ search │ │
│ │ • 재고 상태 │ │ status │ │ • debounced │ │
│ │ • 장바구니 │ │ │ │ results │ │
│ │ 버튼 │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
useProductSearch.ts - 상품 검색 및 필터링 훅
export function useProductSearch(products: ProductWithUI[], searchTerm: string) {
// 디바운스된 검색어 (500ms 지연)
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// 필터링된 상품 목록
const filteredProducts = useMemo(() => {
if (!debouncedSearchTerm) {
return products; // 검색어가 없으면 전체 상품 반환
}
return products.filter(product =>
// 상품명 또는 설명에서 검색어 매칭
product.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase()) ||
(product.description &&
product.description.toLowerCase().includes(debouncedSearchTerm.toLowerCase()))
);
}, [products, debouncedSearchTerm]);
return {
debouncedSearchTerm, // 실제 검색에 사용된 검색어
filteredProducts, // 필터링된 결과
};
}
5. 엔티티 계층 분석 (entities/) - Cart 도메인
A. 엔티티 구조 및 책임
entities/cart/
├── index.ts # 외부 노출 인터페이스
├── types.ts # 타입 정의
└── utils.ts # 순수 비즈니스 로직 함수들
- 순수 함수만 포함: 사이드 이펙트가 없는 계산 로직만
- 프레임워크 독립: React, DOM API 등에 의존하지 않음
- 재사용성: 다른 프레임워크에서도 사용 가능한 비즈니스 로직
B. 타입 정의 (types.ts)
import { CartItem, Coupon } from '../../../types';
// 장바구니 총액 계산 결과 타입
export interface CartTotal {
totalBeforeDiscount: number; // 할인 전 총액
totalAfterDiscount: number; // 할인 후 총액
}
// 장바구니 계산에 필요한 옵션들
export interface CartCalculationOptions {
cart: CartItem[];
selectedCoupon?: Coupon | null;
}
C. 핵심 비즈니스 로직 (utils.ts) - 상세 분석
- 개별 상품 할인율 계산
export const getProductDiscount = (item: CartItem): number => {
const { discounts } = item.product;
const { quantity } = item;
// 상품의 모든 할인 정책을 검토하여 최대 적용 가능한 할인율 반환
return discounts.reduce((maxDiscount, discount) => {
// 현재 수량이 할인 조건을 만족하고, 더 높은 할인율이면 적용
return quantity >= discount.quantity && discount.rate > maxDiscount
? discount.rate
: maxDiscount;
}, 0);
};
- 대량구매 할인 체크
export const getBulkPurchaseDiscount = (cart: CartItem[]): number => {
// 장바구니에 10개 이상 구매한 상품이 하나라도 있으면
// 모든 상품에 5% 추가 할인 적용
const hasBulkPurchase = cart.some((cartItem) => cartItem.quantity >= 10);
return hasBulkPurchase ? 0.05 : 0;
};
- 최대 적용 가능한 할인율 계산
export const getMaxApplicableDiscount = (
item: CartItem,
cart: CartItem[]
): number => {
const baseDiscount = getProductDiscount(item); // 개별 상품 할인
const bulkDiscount = getBulkPurchaseDiscount(cart); // 대량구매 할인
// 최대 50% 할인까지만 적용 (비즈니스 룰)
return Math.min(baseDiscount + bulkDiscount, 0.5);
};
- 장바구니 전체 금액 계산
export const calculateCartTotal = (
cart: CartItem[],
selectedCoupon: Coupon | null = null
): CartTotal => {
let totalBeforeDiscount = 0;
let totalAfterDiscount = 0;
// Step 1: 대량구매 할인은 한 번만 계산
const bulkDiscount = getBulkPurchaseDiscount(cart);
// Step 2: 각 상품별 금액 계산 및 누적
cart.forEach((item) => {
const itemPrice = item.product.price * item.quantity;
totalBeforeDiscount += itemPrice;
// 개별 상품 할인 + 대량구매 할인 조합
const productDiscount = getProductDiscount(item);
const totalDiscount = Math.min(productDiscount + bulkDiscount, 0.5);
const itemTotal = Math.round(itemPrice * (1 - totalDiscount));
totalAfterDiscount += itemTotal;
});
// Step 3: 쿠폰 할인 적용 (가장 마지막에 적용)
if (selectedCoupon) {
if (selectedCoupon.discountType === 'amount') {
// 정액 할인: 고정 금액 차감 (음수 방지)
totalAfterDiscount = Math.max(
0,
totalAfterDiscount - selectedCoupon.discountValue
);
} else {
// 정율 할인: 퍼센트 할인 적용
totalAfterDiscount = Math.round(
totalAfterDiscount * (1 - selectedCoupon.discountValue / 100)
);
}
}
return {
totalBeforeDiscount: Math.round(totalBeforeDiscount),
totalAfterDiscount: Math.round(totalAfterDiscount),
};
};
D. 비즈니스 룰 및 제약사항
할인 적용 우선순위:
- 개별 상품 할인 (수량별 할인율)
- 대량구매 할인 (10개 이상 시 5% 추가)
- 쿠폰 할인 (정액/정율)
할인 제한사항:
- 개별 상품 + 대량구매 할인 합계 최대 50%
- 쿠폰은 이미 할인된 금액에서 추가 적용
- 정액 할인 시 음수 결과 방지 (최소 0원)
계산 정확성:
- 모든 금액 계산 시 Math.round() 적용
- 부동소수점 오차 방지
- 일관된 반올림 정책
6. 아키텍처 설계 원칙
A. 도메인 분리
- Cart: 장바구니 상태와 계산 로직
- Product: 상품 정보와 재고 관리
- Coupon: 쿠폰 관리 및 적용
- Order: 주문 처리 및 완료
B. 계층 분리 (Clean Architecture)
UI Components ← Props/Events ← Custom Hooks
↑ ↑
Render Side Effects
↓ ↓
React State ← State Updates ← Entity Functions (Pure)
C. 데이터 흐름
- 사용자 인터랙션 → UI 컴포넌트에서 이벤트 발생
- 이벤트 핸들러 → 커스텀 훅의 함수 호출
- 비즈니스 로직 → Entity 순수 함수로 계산 처리
- 상태 업데이트 → React setState를 통한 상태 변경
- UI 리렌더링 → 변경된 상태 기반으로 컴포넌트 재렌더링
D. 핵심 설계 원칙
- 순수 함수: entities 레이어는 사이드 이펙트 없는 순수 함수만
- 단방향 의존성: pages → widgets → features → entities
- 관심사 분리: 비즈니스 로직과 UI 로직의 명확한 분리
- 재사용성: 공통 로직은 커스텀 훅으로, 공통 UI는 컴포넌트로 분리
FSD 아키텍처 원칙에 따라 상품 관리 기능을 비즈니스 로직과 UI 로직을 분리했습니다.
- 과제 요구사항을 단순히 구현하는 것보다 "왜 이렇게 구조화했는지?"에 대한 명확한 답을 갖고 싶었습니다!
// Before: 모든 로직이 컴포넌트에 혼재
// After: 역할별로 명확히 분리
// 📁 features/product/admin/hooks/useProducts.ts
// 상품 CRUD 비즈니스 로직만 담당
export function useProducts(props) {
const addProduct = useCallback(
(newProduct: Omit<ProductWithUI, 'id'>) => {
const product: ProductWithUI = { ...newProduct, id: `p${Date.now()}` };
setProducts((prev) => [...prev, product]);
addNotification('상품이 추가되었습니다.', 'success');
},
[setProducts, addNotification]
);
const updateProduct = useCallback(
(productId: string, updates: Partial<ProductWithUI>) => {
setProducts((prev) =>
prev.map((product) =>
product.id === productId ? { ...product, ...updates } : product
)
);
addNotification('상품이 수정되었습니다.', 'success');
},
[setProducts, addNotification]
);
const deleteProduct = useCallback(
(productId: string) => {
setProducts((prev) => prev.filter((p) => p.id !== productId));
addNotification('상품이 삭제되었습니다.', 'success');
},
[setProducts, addNotification]
);
return { addProduct, updateProduct, deleteProduct };
}
// 📁 features/product/admin/ui/ProductManagement.tsx
// UI 상태 관리와 사용자 인터랙션만 담당
export default function ProductManagement({ products, setProducts, addNotification }) {
// 비즈니스 로직을 훅에서 가져와 사용
const { addProduct, updateProduct, deleteProduct } = useProducts({
products,
setProducts,
addNotification,
});
// UI 상태만 관리
const [showProductForm, setShowProductForm] = useState(false);
const [editingProduct, setEditingProduct] = useState<string | null>(null);
const [productForm, setProductForm] = useState({...});
}
심화 과제
1. Props Drilling 발생 지점
1. 알림 시스템 (addNotification)
App → AdminPage/ShoppingPage → 거의 모든 하위 컴포넌트
거의 모든 사용자 액션에서 피드백이 필요하기 때문에 addNotification 함수가 앱의 모든 깊은 컴포넌트까지 전달되어야 했습니다.
- ProductManagement → ProductForm
- CouponManagement → CouponForm
- ShoppingSidebar → CartItemsList, CouponSelector, OrderSummary
- 총 8단계 이상의 props 전달
2. 장바구니 상태 (cart, setCart)
App → ShoppingPage → ShoppingSidebar → CartItemsList/OrderSummary
App → ShoppingPage → ProductList → ProductCard (cart 읽기용)
장바구니는 읽기와 쓰기가 여러 곳에서 필요한 전형적인 전역 상태입니다. 상품 카드에서는 재고 확인을 위해 읽기만 필요하지만 전체 상태를 props로 받아야 했습니다.
- 장바구니 데이터가 4-5단계를 거쳐 전달
- 상품 카드에서도 재고 확인을 위해 cart 필요
3. 상품 데이터 (products, setProducts)
App → AdminPage → ProductManagement → ProductTable/ProductForm
App → ShoppingPage → ShoppingSidebar (재고 확인용)
상품 데이터는 관리자 페이지에서의 CRUD 작업과 쇼핑 페이지에서의 재고 확인이라는 서로 다른 목적으로 사용되지만 동일한 상태를 공유해야 했습니다.
- 관리자 페이지: 4단계 전달
- 쇼핑 페이지: 재고 확인을 위해 3단계 전달
4. 쿠폰 상태 (coupons, selectedCoupon, setCoupons, setSelectedCoupon)
App → AdminPage → CouponManagement → CouponTable/CouponForm
App → ShoppingPage → ShoppingSidebar → CouponSelector
쿠폰은 관리와 적용이라는 두 가지 다른 컨텍스트에서 사용되지만 상태를 공유해야 해서 복잡한 props 전달이 필요했습니다.
- 쿠폰 관련 상태가 4단계씩 전달
- 쿠폰 선택 로직까지 props로 전달
5. 계산 함수 (calculateCartTotalWithCoupon)
App → ShoppingPage → ShoppingSidebar → CouponSelector
- 함수까지 props로 전달하는 상황
2. 전역 상태 설계
#### 각 도메인별로 atom을 설계하여 관심사를 명확히 분리했습니다. localStorage 연동이 필요한 상태와 세션별 상태를 구분하여 적용했습니다.Atom 구조 설계
// 핵심 상태 Atoms
export const cartAtom = atomWithStorage<CartItem[]>('cart', []);
export const productsAtom = atomWithStorage<ProductWithUI[]>('products', initialProducts);
export const couponsAtom = atomWithStorage<Coupon[]>('coupons', initialCoupons);
export const selectedCouponAtom = atom<Coupon | null>(null);
export const searchTermAtom = atom<string>('');
export const notificationsAtom = atom<Notification[]>([]);
localStorage 연동 (atomWithStorage) atomWithStorage를 사용하여 자동으로 localStorage와 동기화되도록 했습니다. 이를 통해 페이지 새로고침 후에도 사용자의 작업 상태가 유지됩니다.
// 자동 localStorage 동기화
- cartAtom → 'cart' 키
- productsAtom → 'products' 키
- couponsAtom → 'coupons' 키
// 세션별 상태 (동기화 제외)
- selectedCouponAtom (선택된 쿠폰)
- searchTermAtom (검색어)
- notificationsAtom (알림)
파생 상태 (cartTotalsAtom) 복잡한 계산 로직을 파생 상태로 분리하여 의존성이 변경될 때만 자동으로 재계산되도록 구현했습니다.
// 장바구니 총액 자동 계산
export const cartTotalsAtom = atom((get) => {
const cart = get(cartAtom);
const selectedCoupon = get(selectedCouponAtom);
return calculateCartTotal(cart, selectedCoupon);
});
3. Props Drilling 제거
Before - ShoppingSidebar (8개 props)
<ShoppingSidebar
cart={cart}
setCart={setCart}
coupons={coupons}
selectedCoupon={selectedCoupon}
setSelectedCoupon={setSelectedCoupon}
products={products}
addNotification={addNotification} // 8단계 전달
calculateCartTotalWithCoupon={calculateCartTotalWithCoupon}
/>
After - ShoppingSidebar (0개 props)
<ShoppingSidebar /> // 독립적인 컴포넌트
NotificationToast 컴포넌트
// Before: Props 의존
interface NotificationToastProps {
notifications: Notification[];
onRemove: (id: string) => void;
}
// After: Atom 직접 사용
export default function NotificationToast() {
const [notifications, setNotifications] = useAtom(notificationsAtom);
// props 없이 독립적으로 동작
}
useCart Hook 장바구니 로직을 캡슐화하여 어떤 컴포넌트에서든 일관된 방식으로 장바구니 기능을 사용할 수 있도록 구현했습니다.
export function useCart() {
const { addNotification } = useNotification();
const [cart, setCart] = useAtom(cartAtom);
const products = useAtomValue(productsAtom);
const addToCart = useCallback((product: ProductWithUI) => {
// 재고 확인 및 장바구니 추가 로직
// props 없이 atom에서 직접 상태 접근
}, [/* atom 의존성만 */]);
return { cart, addToCart, removeFromCart, updateQuantity, getCartQuantity };
}
4. useAtom vs useAtomValue vs useSetAtom
Jotai의 세분화된 훅을 활용하여 컴포넌트가 실제로 필요한 기능에만 구독하도록 최적화했습니다. 읽기 전용 최적화
// After: 읽기만 필요한 곳에서 최적화
const cart = useAtomValue(cartAtom); // 읽기 전용
const setCart = useSetAtom(cartAtom); // 쓰기 전용
파생 상태를 통한 계산 최적화
// Before: 매번 계산 함수 호출
const calculateCartTotalWithCoupon = useCallback(() => {
return calculateCartTotal(cart, selectedCoupon);
}, [cart, selectedCoupon]);
// After: 자동 메모이제이션
const totals = useAtomValue(cartTotalsAtom); // 의존성 변경시만 재계산
컴포넌트별 구독 최적화
// ProductCard: cart 읽기만 필요
const cart = useAtomValue(cartAtom);
// CartItemsList: cart 업데이트만 필요
const { removeFromCart, updateQuantity } = useCart();
// OrderSummary: 계산 결과만 필요
const totals = useAtomValue(cartTotalsAtom);
과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?
- 태그 내부에 인라인으로 함수 들어가는 부분을 분리하는 것
- 컴포넌트를 분리할 때 더 작은 단위로 분리하는 것
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문
1. FSD(Feature-Sliced Design) 폴더 구조에 대한 피드백
features/product/
├── admin/ # 관리자용 기능
│ ├── hooks/ # useProducts (CRUD)
│ └── ui/ # ProductManagement, ProductForm, ProductTable
└── shop/ # 고객용 기능
├── hooks/ # useProductSearch
└── ui/ # ProductList, ProductCard
의도:
- Product 도메인이 쇼핑몰/관리자 페이지 양쪽에서 사용됨
- 역할이 다르기 때문에 admin/shop으로 대분류를 나누어 구분
- 각각 다른 hooks와 UI 컴포넌트를 가짐
질문:
- 이런 방식의 도메인 내 컨텍스트별 분리가 FSD 관점에서 적절한가요?
- 더 나은 폴더 구조나 네이밍 컨벤션이 있을까요?
2. Widgets 레이어 사용에 대한 검토
widgets/ShoppingSidebar/
└── ui/
└── ShoppingSidebar.tsx # 복합 위젯
구성 요소:
├── CartItemsList (cart 도메인)
├── CouponSelector (coupon 도메인)
└── OrderSummary (order 도메인)
의도:
- 쇼핑몰 페이지 우측 사이드바는 3개 도메인(cart, coupon, order)이 조합된 복합 UI
- Features 레이어는 단일 도메인 책임이므로 다중 도메인 조합은 widgets에 위치
- 각 도메인별 features를 조합하여 하나의 사용자 시나리오 완성
질문:
- 여러 도메인을 조합한 복합 컴포넌트를 widgets 레이어에 두는 것이 FSD 설계 원칙에 맞나요?
- ShoppingSidebar처럼 특정 페이지에서만 사용되는 위젯의 위치가 적절한가요?
- 다른 대안적 구조(예: pages 레이어에서 직접 조합)와 비교했을 때 어떤 장단점이 있을까요?
과제 피드백
FSD관점으로 분리하는 과제를 미리 경험해주셨군요 :+1 각 레이어를 구분하고 레이어를 이렇게 분리했을 때 얻을 수 있는 장점들이 명확하게 있으셨으면 좋았겠네요! 과제도 잘 정리해주셨는데, 본격적인 규칙이나 FSD에 관련된 구체적인 내용들은 다음 주에 더 깊게 이야기 해보면 좋을 것 같네요.
질문 주신 부분 보면
- FSD(Feature-Sliced Design) 폴더 구조에 대한 피드백
좋은 접근인것 같아요. 해당 폴더에 대한 명확한 규칙은 사실 정하기 나름이라고 알고 있어요. 실제로 그냥 product로 묶어서 한 곳에 로직을 묶을수도 있고, admin을 파일 명에 붙이거나 하는 형태로 구분할 수 있겠죠. (사실 저라면 따로 구분을 하지 않고 적었을 것 같은데 ㅎㅎ) 추후에 공통 로직 같은 것들을 명확하게 할 수 있다면 지금의 방식도 명확하고 좋은 것 같아요 ㅎㅎ
- Widgets 레이어 사용에 대한 검토
"여러 도메인을 조합한 복합 컴포넌트를 widgets 레이어에 두는 것이 FSD 설계 원칙에 맞나요?"라는 질문에 있어서는 넵 입니다. 실제로 FSD설명을 보다보면 위젯에 대한 사용성이 약간 모호한 측면이 있는데, 해당 목적으로도 저희회사에서 많이 사용하는 편이에요! 결국 레이어간 간섭이 안되어야 하기 때문에 끌어올려서 조합해서 사용하고 페이지에 두기 애매한 경우 위젯에 두기 때문인데요. 그럼에도 대부분은 페이지에 위치시키는 경우가 적합한 경우가 많아 이 부분도 함께 생각해보시면 좋을것 같아요.
결국 위젯에서 조합을 하면 다른 페이지에서도 재사용을 할 수 있다는 장점이 생기니 이 부분도 같이 고려해보면 좋지 않을까 싶습니다!
고생하셨고 다음주차 과제는 쉬우실 것 같네요 ㅎㅎㅎ 화이팅입니다!