과제 체크포인트
기본과제
- 코드가 Prettier를 통해 일관된 포맷팅이 적용되어 있는가?
- 적절한 줄바꿈과 주석을 사용하여 코드의 논리적 단위를 명확히 구분했는가?
- 변수명과 함수명이 그 역할을 명확히 나타내며, 일관된 네이밍 규칙을 따르는가?
- 매직 넘버와 문자열을 의미 있는 상수로 추출했는가?
- 중복 코드를 제거하고 재사용 가능한 형태로 리팩토링했는가?
- 함수가 단일 책임 원칙을 따르며, 한 가지 작업만 수행하는가?
- 조건문과 반복문이 간결하고 명확한가? 복잡한 조건을 함수로 추출했는가?
- 코드의 배치가 의존성과 실행 흐름에 따라 논리적으로 구성되어 있는가?
- 연관된 코드를 의미 있는 함수나 모듈로 그룹화했는가?
- ES6+ 문법을 활용하여 코드를 더 간결하고 명확하게 작성했는가?
- 전역 상태와 부수 효과(side effects)를 최소화했는가?
- 에러 처리와 예외 상황을 명확히 고려하고 처리했는가?
- 코드 자체가 자기 문서화되어 있어, 주석 없이도 의도를 파악할 수 있는가?
- 비즈니스 로직과 UI 로직이 적절히 분리되어 있는가?
- 코드의 각 부분이 테스트 가능하도록 구조화되어 있는가?
- 성능 개선을 위해 불필요한 연산이나 렌더링을 제거했는가?
- 새로운 기능 추가나 변경이 기존 코드에 미치는 영향을 최소화했는가?
- 코드 리뷰를 통해 다른 개발자들의 피드백을 반영하고 개선했는가?
- (핵심!) 리팩토링 시 기존 기능을 그대로 유지하면서 점진적으로 개선했는가?
심화과제
- 변경한 구조와 코드가 기존의 코드보다 가독성이 높고 이해하기 쉬운가?
- 변경한 구조와 코드가 기존의 코드보다 기능을 수정하거나 확장하기에 용이한가?
- 변경한 구조와 코드가 기존의 코드보다 테스트를 하기에 더 용이한가?
- 변경한 구조와 코드가 기존의 모든 기능은 그대로 유지했는가?
- (핵심!) 변경한 구조와 코드를 새로운 한번에 새로만들지 않고 점진적으로 개선했는가?
과제 셀프회고
https://hyunzsu.github.io/front_6th_chapter2-1/
과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?
| 구분 | 기본과제 (Vanilla JS) | 심화과제 (React + TS) |
|---|---|---|
| 상태관리 | 전역 상태 객체 | useReducer + 도메인별 상태 |
| UI 렌더링 | DOM 직접 조작 | React 컴포넌트 |
| 타입 안정성 | JSDoc 주석 | TypeScript |
| 패턴 | MVC (Controller) | Flux (Hooks + Reducers) |
| 데이터 흐름 | 명령형 | 선언형 |
📍 기본 과제
1. 전체 아키텍처 개요
src/basic/
├── main.basic.js
├── __tests__/
├── features/ # 도메인별 기능 모듈
│ ├── cart/
│ ├── order/
│ ├── product/
│ ├── promotion/
│ └── index.js
└── shared/ # 공통 모듈
├── components/
├── constants/
├── core/
├── utils/
└── index.js
2. 메인 엔트리 포인트 분석
main.basic.js
function main() {
// 앱 데이터 초기화
initializeAppData();
// DOM 구조 생성
createDOMStructure();
// 초기 UI 업데이트
ProductController.updateSelectOptions();
CartController.updateCartDisplay();
// 이벤트 핸들러 초기화
initializeEvents();
// 프로모션 시스템 시작
LightningSaleService.startLightningSaleSystem(/* ... */);
SuggestedSaleService.startSuggestedSaleSystem(/* ... */);
}
main();
- 절차적 초기화: 순서대로 앱을 초기화
- 전역 상태 기반: 비즈니스 상태를 전역으로 관리
- DOM 직접 조작: React 없이 순수 DOM API 사용
3. 공통 모듈 분석 (shared/)
A. 상태 관리 핵심 (core/)
business-state.js - 전역 상태 저장소
export const businessState = {
products: [],
bonusPoints: 0,
itemCount: 0,
totalAmount: 0,
lastSelectedProductId: null,
};
export function getProducts() {
return businessState.products;
}
export function getProductById(id) {
return businessState.products.find((product) => product.id === id);
}
dom-refs.js - DOM 요소 참조 관리
export const domRefs = {
stockInfo: null,
productSelect: null,
addButton: null,
cartDisplay: null,
totalDisplay: null,
// ... 기타 DOM 요소들
};
export function initializeDOMReferences() {
domRefs.stockInfo = document.getElementById('stock-status');
domRefs.productSelect = document.getElementById('product-select');
// ... DOM 요소 참조 설정
}
B. DOM 구조 생성 (core/)
dom.js - 전체 DOM 구조를 프로그래밍 방식으로 생성
export function createDOMStructure() {
const root = document.getElementById('app');
// 컴포넌트 생성
const header = Header();
const mainGrid = MainGrid();
const leftColumn = LeftColumn();
const productPanel = ProductPanel();
// DOM 조립
productPanel.appendChild(ProductSelect());
productPanel.appendChild(AddToCartButton());
leftColumn.appendChild(productPanel);
leftColumn.appendChild(CartContainer());
root.appendChild(header);
root.appendChild(mainGrid);
}
C. 이벤트 관리 (core/)
events.js - 중앙화된 이벤트 핸들러
export function initializeEvents() {
setupAddButtonHandler();
setupCartClickHandler();
setupHelpManualEvents();
}
function setupAddButtonHandler() {
getAddButtonElement().addEventListener('click', () => {
const selItem = getProductSelectElement().value;
const itemToAdd = getProductById(selItem);
if (!itemToAdd || itemToAdd.stock <= 0) return;
// 비즈니스 로직 실행
if (existingItem) {
updateExistingItemQuantity(existingItem, itemToAdd);
} else {
addNewItemToCart(itemToAdd);
}
CartController.updateCartDisplay();
});
}
D. 컴포넌트 (components/)
UI 컴포넌트를 함수로 생성
// Header.js
export function Header() {
const header = document.createElement('header');
header.className = 'mb-8';
header.innerHTML = `
<h1>🛒 Hanghae Online Store</h1>
<div class="text-5xl">Shopping Cart</div>
<p id="item-count">🛍️ 0 items in cart</p>
`;
return header;
}
4. 도메인별 기능 분석 (features/) - Cart 도메인
Cart 도메인 구조
features/cart/
├── CartController.js # 장바구니 오케스트레이션
├── CartRenderer.js # UI 렌더링 담당
├── CartService.js # 비즈니스 로직
└── components/ # UI 컴포넌트 생성 함수
├── CartContainer.js
├── CartItem.js
└── OrderSummary.js
A. Model 폴더 상세 분석
┌──────────────────────────────────────────────────────────────┐
│ Cart Domain Structure │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │Controller.js│───▶│ Service.js │───▶│Renderer.js │ │
│ │ │ │ │ │ │ │
│ │ • update │ │ • calculate │ │ • render │ │
│ │ Display │ │ Cart │ │ Items │ │
│ │ • update │ │ • extract │ │ • render │ │
│ │ Prices │ │ Data │ │ Summary │ │
│ │ • render │ │ • isEmpty │ │ • render │ │
│ │ Items │ │ │ │ Points │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Business State │ │
│ │ │ │
│ │ • products[] │ │
│ │ • totalAmount │ │
│ │ • itemCount │ │
│ │ • bonusPoints │ │
│ └─────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
CartController.js - 장바구니 전체 오케스트레이션
export function updateCartDisplay() {
const cartItems = getCartDisplayElement().children;
// 1. 상태 초기화
setTotalAmount(0);
setItemCount(0);
// 2. DOM에서 장바구니 데이터 추출
const cartData = CartService.extractCartData(cartItems);
// 3. 장바구니 전체 계산
const cartResult = CartService.calculateCart(cartData, getProductById);
// 4. 상태 업데이트
setTotalAmount(cartResult.finalAmount);
setItemCount(cartResult.totalQuantity);
// 5. UI 렌더링
CartRenderer.renderOrderSummary(cartResult);
CartRenderer.renderCartCount(cartResult.totalQuantity);
// 6. 포인트 계산 및 표시
updateBonusPoints();
}
B. Service 레이어 분석
CartService.js - 순수 함수로 비즈니스 로직 처리
export function calculateCartSummary(cartData, getProductById) {
let subtotal = 0;
let totalQuantity = 0;
let discountedTotal = 0;
const individualDiscountInfo = [];
for (const item of cartData) {
const product = getProductById(item.id);
if (!product) continue;
const quantity = item.quantity;
const itemSubtotal = product.price * quantity;
totalQuantity += quantity;
subtotal += itemSubtotal;
// 개별 상품 할인 계산
const discountRate = getProductDiscount(product.id, quantity);
if (discountRate > 0) {
individualDiscountInfo.push({
name: product.name,
discountPercent: discountRate * 100,
});
}
discountedTotal += itemSubtotal * (1 - discountRate);
}
return { subtotal, totalQuantity, discountedTotal, individualDiscountInfo };
}
C. Renderer 레이어 분석
CartRenderer.js - DOM 조작을 통한 UI 렌더링
export function renderCartItems(cartData) {
const cartDisplay = getCartDisplayElement();
if (!cartDisplay) return;
if (!cartData || cartData.length === 0) {
cartDisplay.innerHTML = '';
return;
}
cartDisplay.innerHTML = cartData
.map((item) => {
const product = getProductById(item.id);
return CartItem(item, product);
})
.join('');
}
D. Components 폴더 분석
CartItem.js - 장바구니 아이템 HTML 생성
export function CartItem(item, product) {
if (!product) return '';
// 할인 아이콘 결정
const discountIcon = product.isOnSale && product.isSuggestedSale
? '⚡💝' : product.isOnSale ? '⚡' : product.isSuggestedSale ? '💝' : '';
// 가격 표시 결정
const priceDisplay = product.isOnSale || product.isSuggestedSale
? `<span class="line-through">₩${product.originalPrice.toLocaleString()}</span>
<span class="${getDiscountColor(product)}">₩${product.price.toLocaleString()}</span>`
: `₩${product.price.toLocaleString()}`;
return `
<div id="${product.id}" class="grid grid-cols-[80px_1fr_auto] gap-5 py-5">
<div class="w-20 h-20 bg-gradient-black"></div>
<div>
<h3>${discountIcon}${product.name}</h3>
<p class="text-xs">${priceDisplay}</p>
<!-- 수량 조절 버튼들... -->
</div>
<div class="text-right">
<div class="text-lg">${priceDisplay}</div>
<a class="remove-item" data-product-id="${product.id}">Remove</a>
</div>
</div>
`;
}
5. 아키텍처 설계 원칙
A. 도메인 분리
- Cart: 장바구니 상태와 계산 로직
- Product: 상품 정보와 재고 관리
- Promotion: 번개세일, 추천할인 타이머
- Order: 할인율 계산, 포인트 적립 등
B. 레이어 분리 (MVC 패턴)
View (DOM Elements) ← 렌더링 ← Renderer
↑ ↑
이벤트 결과
↓ ↓
Controller (오케스트레이션) → Service (비즈니스 로직)
↓ ↓
Business State (전역 상태) ← 상태 변경 ←
C. 데이터 흐름
- 사용자 이벤트 발생 → 이벤트 핸들러 실행
- Controller 메서드 호출 → 비즈니스 로직 오케스트레이션
- Service 함수 실행 → 순수 함수로 계산 처리
- Business State 업데이트 → 전역 상태 변경
- Renderer 함수 호출 → DOM 직접 조작으로 UI 업데이트
📍 심화 과제
1. 전체 아키텍처 개요
src/advanced/
├── App.tsx
├── main.tsx
├── main.advanced.js
├── __tests__/
├── features/ # 도메인별 기능 모듈
│ ├── cart/
│ ├── order/
│ ├── product/
│ └── promotion/
└── shared/ # 공통 모듈
├── components/
├── constants/
├── core/
├── hooks/
├── services/
├── types/
└── utils/
2. 메인 컴포넌트 분석
App.tsx
function App() {
const { state, actions, getProductById } = useShoppingCart();
return (
<div className="flex flex-col h-full">
<Header itemCount={state.itemCount} />
<MainGrid>
<LeftColumn>
<ProductPanel>...</ProductPanel>
<CartContainer {...cartProps} />
</LeftColumn>
<OrderSummary {...orderProps} />
</MainGrid>
<HelpToggleButton />
</div>
);
}
- 단일 커스텀 훅 사용:
useShoppingCart()로 모든 상태와 액션을 관리 - 컴포넌트 조합: 작은 컴포넌트들을 조합해서 UI 구성
- Props Drilling 최소화: 필요한 props만 전달
3. 공통 모듈 분석 (shared/)
A. 상태 관리 핵심 (core/)
combinedReducer.ts - 모든 도메인의 리듀서를 하나로 결합
export interface RootState {
cart: CartState;
product: ProductState;
promotion: PromotionState;
}
export type RootAction = CartAction | ProductAction | PromotionAction;
export function combinedReducer(state: RootState, action: RootAction): RootState {
return {
cart: cartReducer(state.cart, action as CartAction),
product: productReducer(state.product, action as ProductAction),
promotion: promotionReducer(state.promotion, action as PromotionAction),
};
}
B. 통합 훅 (hooks/)
useShoppingCart.ts - 모든 도메인을 하나로 묶는 최상위 훅
export function useShoppingCart() {
const [rootState, dispatch] = useReducer(combinedReducer, initialRootState);
// 각 도메인별 hook 사용
const cart = useCart(rootState, dispatch);
const products = useProducts(rootState, dispatch);
const promotion = usePromotion(rootState, dispatch);
// 계산된 값들을 메모이제이션
const calculatedTotals = useMemo(() => calculateTotals(rootState), [rootState]);
// 통합된 상태와 액션 반환
return {
state: { ...cartState, ...productState, ...calculatedTotals },
actions: { ...cartActions, ...productActions },
getProductById: products.getProductById
};
}
C. 서비스 레이어
stockService.ts - 재고 검증과 에러 처리:
export class StockValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'StockValidationError';
}
}
export const stockService = {
validateAddToCart: (product: Product | undefined): void => {
if (!product) throw new StockValidationError('상품을 찾을 수 없습니다.');
if (product.stock === 0) throw new StockValidationError('재고가 부족합니다.');
},
// ... 기타 검증 메서드들
};
calculationService.ts - 전체 주문 계산을 담당:
export function calculateTotals(state: RootState): CalculatedTotals {
const cartItems = cartSelectors.getItems(state.cart);
if (cartItems.length === 0) {
return { itemCount: 0, totalAmount: 0, /* ... */ };
}
const cartResult = calculateCart(cartItems, getProductById);
const pointsResult = calculateTotalPoints(/* ... */);
return {
itemCount: cartResult.totalQuantity,
totalAmount: cartResult.finalAmount,
bonusPoints: pointsResult.finalPoints,
// ... 기타 계산 결과
};
}
D. 타입 시스템 (types/)
타입들을 도메인별로 분리하고 중앙에서 관리
types/
├── entities.ts # Product, CartItem 등 기본 엔티티
├── calculations.ts # 계산 관련 타입들
├── hooks.ts # 훅들의 반환 타입
├── legacy.ts # 기존 코드 호환용
└── index.ts
기본 엔티티 예시:
// entities.ts
export interface Product {
id: string;
name: string;
price: number;
originalPrice: number;
stock: number;
isOnSale: boolean;
isSuggestedSale: boolean;
}
export interface CartItem {
id: string;
quantity: number;
}
4. 도메인별 기능 분석 (features/) - Cart 도메인 중심
Cart 도메인 구조
features/cart/
├── components/ # UI 컴포넌트
│ ├── CartContainer.tsx
│ ├── CartItem.tsx
│ ├── OrderSummary.tsx
│ └── index.ts
├── hooks/
│ └── useCart.ts # 장바구니 비즈니스 로직
├── model/ # 상태 관리
│ ├── actions.ts # 액션 정의
│ ├── reducer.ts # 리듀서
│ └── selectors.ts # 상태 선택자
└── services/
└── CartService.ts # 장바구니 계산 로직
A. Model 폴더 상세 분석
┌──────────────────────────────────────────────────────────────┐
│ Cart Domain Model │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ actions.ts │───▶│ reducer.ts │───▶│selectors.ts │ │
│ │ │ │ │ │ │ │
│ │ • addToCart │ │ • ADD_TO_ │ │ • getItems │ │
│ │ • updateQty │ │ CART │ │ • getCount │ │
│ │ • remove │ │ • UPDATE_ │ │ • getById │ │
│ │ • clear │ │ QUANTITY │ │ • isEmpty │ │
│ │ │ │ • REMOVE │ │ • hasItem │ │
│ │ │ │ • CLEAR │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ CartState │ │
│ │ │ │
│ │ { │ │
│ │ items: [] │ │
│ │ } │ │
│ └─────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
actions.ts - 장바구니에서 발생할 수 있는 모든 액션들
export type CartAction =
| { type: 'cart/ADD_TO_CART'; payload: string }
| { type: 'cart/UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'cart/REMOVE_FROM_CART'; payload: string }
| { type: 'cart/CLEAR_CART' };
export const cartActions = {
addToCart: (productId: string): CartAction => ({
type: 'cart/ADD_TO_CART',
payload: productId,
}),
// ... 기타 액션 생성자들
};
reducer.ts - 순수 함수로 상태 변화를 관리
export interface CartState {
items: CartItem[];
}
export function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'cart/ADD_TO_CART': {
const productId = action.payload;
const existingItemIndex = state.items.findIndex(
(item) => item.id === productId
);
if (existingItemIndex >= 0) {
// 기존 아이템 수량 증가
const newItems = state.items.map((item, index) =>
index === existingItemIndex
? { ...item, quantity: item.quantity + 1 }
: item
);
return { ...state, items: newItems };
} else {
// 새 아이템 추가
return { ...state, items: [...state.items, { id: productId, quantity: 1 }] };
}
}
// ... 기타 케이스들
}
}
selectors.ts - 상태에서 필요한 데이터를 효율적으로 추출
export const cartSelectors = {
getItems: (state: CartState): CartItem[] => state.items,
getItemCount: (state: CartState): number =>
state.items.reduce((total, item) => total + item.quantity, 0),
getItemById: (state: CartState, id: string): CartItem | undefined =>
state.items.find((item) => item.id === id),
isEmpty: (state: CartState): boolean => state.items.length === 0,
hasItem: (state: CartState, id: string): boolean =>
state.items.some((item) => item.id === id),
};
B. Hooks 폴더 분석
useCart.ts - 장바구니 비즈니스 로직
export function useCart(state: RootState, dispatch: (action: RootAction) => void) {
const addToCart = useCallback(
(productId: string) => {
try {
const product = productSelectors.getProductById(state.product, productId);
stockService.validateAddToCart(product);
// 트랜잭션처럼 두 액션을 함께 실행
dispatch(cartActions.addToCart(productId));
dispatch(productActions.updateStock(productId, -1));
} catch (error) {
if (error instanceof StockValidationError) {
notificationService.showError(error.message);
}
}
},
[state.product, dispatch]
);
// updateQuantity, removeFromCart 등 기타 메서드들...
return {
cartItems: cartSelectors.getItems(state.cart),
itemCount: cartSelectors.getItemCount(state.cart),
isEmpty: cartSelectors.isEmpty(state.cart),
addToCart,
updateQuantity,
removeFromCart,
};
}
C. Services 폴더 분석
CartService.ts - 순수 함수로 장바구니 계산 담당
export function calculateCartSummary(cartItems: CartItem[], getProductById: Function): CartSummary {
let subtotal = 0;
let totalQuantity = 0;
const individualDiscountInfo: DiscountInfo[] = [];
for (const item of cartItems) {
const product = getProductById(item.id);
if (!product) continue;
const itemSubtotal = product.price * item.quantity;
totalQuantity += item.quantity;
subtotal += itemSubtotal;
// 개별 할인 계산 로직...
}
return { subtotal, totalQuantity, /* ... */ };
}
export function calculateCart(cartItems: CartItem[], getProductById: Function): CartCalculation {
const cartSummary = calculateCartSummary(cartItems, getProductById);
const bulkDiscounts = applyBulkDiscounts(/* ... */);
return { /* 최종 계산 결과 */ };
}
D. Components 폴더 분석
CartContainer.tsx - 장바구니 전체를 렌더링하는 컨테이너
export function CartContainer({ cartItems, getProductById, onUpdateQuantity, onRemoveItem }) {
if (cartItems.length === 0) {
return (
<div id="cart-items">
<p className="text-gray-500 text-center py-8">🛍️ 0 items in cart</p>
</div>
);
}
return (
<div id="cart-items">
{cartItems.map((item) => {
const product = getProductById(item.id);
if (!product) return null;
return <CartItemComponent key={item.id} /* props */ />;
})}
</div>
);
}
5. 아키텍처 설계 원칙
A. 도메인 분리
- Cart: 장바구니 상태와 아이템 관리
- Product: 상품 정보와 재고 관리
- Promotion: 번개세일, 추천할인 등 프로모션
- Order: 할인율 계산, 포인트 적립 등
B. 레이어 분리
UI Components (렌더링만 담당)
↓
Hooks (비즈니스 로직)
↓
Services (도메인 로직)
↓
State Management (순수한 상태 관리)
C. 데이터 흐름
- UI에서 액션 발생 → Hook 호출
- Hook에서 비즈니스 로직 처리 → Service 호출 + Validation
- Service에서 도메인 로직 실행 → 필요시 여러 액션 디스패치
- Reducer에서 상태 변경 → 새로운 상태 반환
- Selector로 필요한 데이터 추출 → UI 리렌더링
실제 개발하면서 가장 고민했던 부분들
1. 레거시 코드 점진적 개선
270줄짜리 거대한 main() 함수를 보고 어디서부터 손대야 할지 막막했습니다.
테스트코드를 참고해 단계별로 접근했어요:
- 1차: 전역변수들을
appState객체로 모으기 - 2차: 큰 함수를 작은 함수들로 쪼개기
- 3차: 비슷한 기능끼리 폴더로 묶기
매번 리팩토링 후 테스트가 모두 통과하는지 확인하는 게 필수였어요.. 🥲
2. React 마이그레이션을 고려한 설계
기본과제 진행 중에도 "이 구조가 React로 전환할 때 자연스러울까?"를 계속 고민했습니다.
CartController.updateCartDisplay()→useCart()훅으로 바뀔 거고CartRenderer.renderOrderSummary()→<OrderSummary />컴포넌트가 될 거고CartService.calculateCart()→ 그대로 비즈니스 로직으로 쓸 수 있고
그래서 기본과제에서도 Controller-Service-Renderer 패턴으로 나누어서 React 전환이 쉽도록 했습니다.
3. 상태관리 설계
기존 코드의 10개 넘는 전역변수를 어떻게 정리할지가 큰 과제였습니다. appState 객체로 통합하고 접근자 함수를 만든 후, 심화과제에서 useReducer로 자연스럽게 전환할 수 있었습니다. 기본과제 단계에서 React 전환을 염두에 둔 구조 설계가 심화과제 진행에 큰 도움이 되었습니다.
과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?
- 타입스크립트를 제대로 활용하지 못했어요.
- 심화 과제에서 컴포넌트를 더 작고 명확하게 나누지 못한 점이 아쉬워요.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
1. 도메인 중심 설계 + MVC 패턴의 적절성
src/basic/domains/
├── cart/
│ ├── CartController.js # 오케스트레이션
│ ├── CartService.js # 비즈니스 로직
│ └── CartRenderer.js # UI 렌더링
Controller-Service-Renderer 3계층으로 나눈 구조가 적절적절했는지, 그리고 React로 마이그레이션할 때 이런 레이어 분리가 도움이 되었는지 궁금합니다!
2. React 상태 관리
// useShoppingCart.ts - 모든 도메인을 하나로 묶는 최상위 훅
export function useShoppingCart() {
const [rootState, dispatch] = useReducer(combinedReducer, initialRootState);
const cart = useCart(rootState, dispatch);
const products = useProducts(rootState, dispatch);
const promotion = usePromotion(rootState, dispatch);
// ...
}
하나의 combinedReducer로 모든 상태를 관리하는 방식이 적절한지 궁금합니다. 장바구니만 변경되어도 전체 상태가 업데이트되는데, Context 분리나 다른 상태 관리 패턴을 고려해야 할까요?
3. 전체적인 개선 방향
심화과제 React+TypeScript로 마이그레이션하면서 useReducer로 재설계했는데, 현재 구조가 React다운 설계인지 아니면 더 React스러운 패턴으로 개선할 부분이 있는지 피드백 받고 싶습니다! 🤔 그리고 Javascript → React 마이그레이션 과정에서 놓친 React 베스트 프랙티스가 있다면 알고 싶습니다!
과제 피드백
지수님 고생하셨습니다. 작성하신 문서를 읽는데도 시간이 굉장히 오래걸렸는데요. 꼼꼼하게 잘 작성해주신걸 보니 과제를 어떤 방향으로 진행할 수 있었는지 명확하게 이해할 수 있었습니다. 시각화한 다이어그램도 좋았어요 :+1 과제는 전반적으로 매우 잘 작성해주셨어요! 크게 피드백 드릴만한 내용이 없는 것 같습니다.
궁금하셨던 내용 답변드려보고 마무리 해볼게요.
도메인 중심 설계 + MVC 패턴의 적절성
좋은 접근법이였어요! 비즈니스 로직들을 미리 나눠둔 덕분에 리액트로 마이그레이션 할 때 별도 분리를 진행하면서 코드를 재작성하지 않아도 되었던것으로 생각이 됩니다. :+1
- React 상태 관리
단일 진실 공급원 관점에서는 한 곳에서 관리를 하면 일관성이 보장된다는 점도 있고 상태 변화를 한 곳에서 추적할 수 있기 때문에 함께 업데이트 되어야 하는 값들이 자연스럽게 처리될 수 있긴 한 것 같아요. 지금의 단계에서는 상품의 양이나 성능 이슈가 발생할 만큼은 걱정하지 않아도 괜찮은 수준인 것 같고, 최적화는 문제가 발생하는 그 시점에 고민을 하면 되기 때문에 갠적으로는 분리를 하면 오히려 복잡도만 높아질수도 있을 것 같네요!
전체적인 개선 방향
걱정하실 필요없이 잘 작성해주셨어요. useReducer같은 것들을 사용해서 명확하게 상태를 관리해주시고 관심사의 분리나 훅을 구성하는 부분들도 너무 잘 해주셨습니다. 더 나아간다면 앞선 주차때 했던 메모이제이션이나 에러 바운더리, suspense, 합성 패턴 같은것들이나... 포탈같은 것들을 활용해보는것?들 정도이지 않을까 싶네요!
이번 주 과제 너무 잘해주셨고 다음주도 이번주처럼 잘 하시길 바랍니다!