일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |
- tailwindcss
- 항해플러스프론트앤드
- 항해플러스 프론트앤드
- 노마드코더
- whispersofthestars
- 개발자북클럽
- niceselect사용법
- 타로엠비티아이
- book_club
- 프론트엔드사이드프로젝트
- jqueryplugin
- NextJs
- 사이드프로젝트
- 타로프로젝트
- corser챌린지
- 항해플러스
- 타로웹서비스
- 감성웹사이트
- select플러그인
- 프론트앤드개발
- plugin
- 프론트앤드
- 운세기록
- 노개북
- Supabase
- 항해99
- 프론트엔드
- 웹개발
- 개발회고
- 타로기록웹앱
- Today
- Total
ㅇㅅㅇ
[WIL] 항해플러스 프론트엔드 6기 5주차 회고 본문
놀랍다... 미뤘더니 생각나는게 많지 않네... 이래서 WIL 권장 작성 기간이 있나보다..
한편으로는 읽어보면서 이런걸 했구나 돌아보는 시간을 가지게 되었다.
그렇지만 역시 적을게 있으면 바로 적는게 좋을거 같다는 걸 밀린 회고를 적으면서 깨달아 본다...
다섯 번째 도전기 - 디자인패턴과 함수형 프로그래밍, 함수형 사고의 첫걸음
이번 과제는 단순히 기능을 구현하는 것을 넘어서 코드 구조와 설계에 대해 깊이 생각해볼 수 있는 기회였습니다. 평소 어느 정도 구조를 나누려 노력했지만 명확한 기준이나 패턴 없이 "그냥 이게 맞는 것 같은데?"라는 감각에 의존해온 부분이 있었습니다. 특히 컴포넌트를 언제 분리해야 할지, 상태 관리는 어떤 기준으로 해야 할지에 대해 체계적인 접근이 부족했음을 느꼈습니다.
제공된 관련 문서에서 본 "디자인패턴을 사용하기 위해 설계를 하는 것이 아니라, 문제를 잘 해결할 수 있는 디자인패턴이 있을 것입니다"라는 문장은 인상 깊었습니다. 이론이 단순한 장식이 아니라 실제 문제 해결 도구가 될 수 있음을 체감하고 싶었습니다.
항해의 절반을 지나오면서 과제 자체에는 익숙해졌지만, 매 과제마다 새로운 개념에 허우적거리는 것은 여전했습니다. 이번에 특히 고민했던 부분은 Entity와 UI의 역할 분리였습니다. 화요일 팀 토론에서 "엔티티는 비즈니스 주체다", "UI는 어디까지 상태를 가져야 하는가?"와 같은 실용적인 질문들을 함께 나누며, 혼자였다면 오래 헤맸을 문제들을 더듬더듬 풀어나갈 수 있었습니다.
1,100줄이 넘는 거대한 App.tsx
컴포넌트를 시작으로, 단일 책임 원칙을 적용하여 컴포넌트를 분리하고, 함수형 프로그래밍의 핵심인 순수함수와 액션을 구분하며, 마지막으로 Props Drilling 문제를 해결하기 위해 Jotai를 도입한 전역 상태 관리까지 진행했습니다.
과제를 통해 얻은 것들
1. 함수형 사고: "계산과 액션을 분리하는 명확함"
가장 인상적이었던 것은 계산 로직을 순수 함수로 분리했을 때의 효과였습니다.
Before
// 모든 로직이 하나의 컴포넌트에 집중
const CartPage = () => {
const [cart, setCart] = useState([]);
const handleQuantityChange = (productId, newQuantity) => {
setCart((prevCart) =>
prevCart.map((item) => {
if (item.product.id === productId) {
// 여기서 복잡한 할인 계산 로직
const discount = item.product.discounts.find((d) => d.quantity <= newQuantity);
const total = item.product.price * newQuantity * (1 - (discount?.rate || 0));
return { ...item, quantity: newQuantity, total };
}
return item;
})
);
};
};
After(함수형 사고 적용)
// models/cart.ts - 순수 함수들
export const getMaxApplicableDiscount = (item: CartItem, cart: CartItem[]): number => {
const { discounts } = item.product;
const { quantity } = item;
const baseDiscount = discounts.reduce((maxDiscount, discount) => {
return quantity >= discount.quantity && discount.rate > maxDiscount
? discount.rate
: maxDiscount;
}, 0);
// 대량 구매 시 추가 5% 할인
const hasBulkPurchase = cart.some((cartItem) => cartItem.quantity >= 10);
if (hasBulkPurchase) {
return Math.min(baseDiscount + 0.05, 0.5);
}
return baseDiscount;
};
export const calculateItemTotal = (item: CartItem, cart: CartItem[]): number => {
const { price } = item.product;
const { quantity } = item;
const discount = getMaxApplicableDiscount(item, cart);
return Math.round(price * quantity * (1 - discount));
};
이렇게 분리하고 나니:
- 계산 로직의 안정성 향상
- 코드의 재사용성 개선
- 디버깅 효율성 증대
의 효과를 알 수 있었습니다.
2. 엔티티 중심의 계층 분리와 VAC 패턴: "역할 분담의 명확성"
가장 중요한 깨달음은 "엔티티 중심의 사고"였습니다. 코드를 엔티티를 다루는 것과 그렇지 않은 것으로 명확히 구분하는 것이 핵심이었습니다.
엔티티를 다루는 것들:
- 상태:
cart
,products
,coupons
(비즈니스 데이터) - 컴포넌트:
CartItem
,ProductCard
,CouponSelector
(비즈니스 로직 포함) - 훅:
useCart()
,useProducts()
,useCoupons()
(도메인 로직) - 함수:
calculateCartTotal(cart)
,getMaxApplicableDiscount(product)
(순수함수)
엔티티를 다루지 않는 것들:
- 상태:
isShowPopup
,searchTerm
,isAdmin
(UI 상태) - 컴포넌트:
Button
,Input
,Badge
(재사용 가능한 UI) - 훅:
useDebounce()
,useLocalStorage()
(유틸리티) - 함수:
capitalize(str)
,formatPrice(amount)
(순수함수)
VAC 패턴을 적용하면서 느낀 것은, 각 컴포넌트가 하나의 명확한 책임을 가지게 된다는 점이었습니다.
View 컴포넌트 (순수 UI)
const ProductCard = ({ product, onAddToCart, isOutOfStock }) => (
<div className='border rounded-lg p-4'>
<img src={product.image} alt={product.name} />
<h3 className='text-lg font-semibold'>{product.name}</h3>
<p className='text-gray-600'>₩{product.price.toLocaleString()}</p>
{product.discounts.length > 0 && (
<p className='text-red-500'>최대 {Math.max(...product.discounts.map((d) => d.rate))}% 할인</p>
)}
<button
onClick={() => onAddToCart(product)}
disabled={isOutOfStock}
className={`mt-2 px-4 py-2 rounded ${
isOutOfStock ? 'bg-gray-300' : 'bg-blue-500 text-white'
}`}
>
{isOutOfStock ? '품절' : '장바구니 추가'}
</button>
</div>
);
Asset 훅 (비즈니스 로직)
// hooks/useCart.ts
export const useCart = (products: ProductWithUI[]) => {
const [cart, setCart] = useLocalStorage<CartItem[]>('cart', []);
const addToCart = (product: ProductWithUI, onNotification?: NotificationCallback) => {
const existingItem = cart.find((item) => item.product.id === product.id);
if (existingItem) {
updateQuantity(product.id, existingItem.quantity + 1, onNotification);
} else {
const newItem: CartItem = {
product,
quantity: 1,
total: calculateItemTotal({ product, quantity: 1 }, cart),
};
setCart((prev) => [...prev, newItem]);
onNotification?.('장바구니에 상품이 추가되었습니다.', 'success');
}
};
return { cart, addToCart /* ... */ };
};
이 구조의 가장 큰 장점은 각각을 독립적으로 개발하고 테스트할 수 있다는 것이었습니다. UI가 바뀌어도 로직은 그대로이고, 로직이 바뀌어도 UI는 영향받지 않았습니다.
3. Props Drilling 문제와 Jotai 해결책
이론적으로만 알고 있던 Props Drilling 문제를 컴포넌트를 분리하는 과정에서 실제로 불편함을 겪고, Jotai로 해결해보면서 깊이 있게 이해할 수 있었습니다.
// CartPage.tsx - 13개의 props를 받아야 함
<CartPage
debouncedSearchTerm={debouncedSearchTerm}
isAdmin={isAdmin}
products={products}
cart={cart}
// ... 9개 더
/>
이런 식으로 props를 계속 전달하다 보니 코드가 복잡해지고, 새로운 기능을 추가할 때마다 여러 컴포넌트를 수정해야 했습니다.
Jotai를 도입하면서 이 문제를 해결할 수 있었습니다.
// store/atoms.ts
export const productsAtom = atomWithStorage<ProductWithUI[]>('products', initialProducts);
export const couponsAtom = atomWithStorage<Coupon[]>('coupons', initialCoupons);
export const cartAtom = atomWithStorage<CartItem[]>('cart', []);
export const selectedCouponAtom = atom<Coupon | null>(null);
// 파생 상태도 쉽게 만들 수 있음
export const cartTotalAtom = atom((get) => {
const cart = get(cartAtom);
const selectedCoupon = get(selectedCouponAtom);
return calculateCartTotal(cart, selectedCoupon);
});
// 컴포넌트에서 직접 사용
const CartSummary = () => {
const cartTotal = useAtomValue(cartTotalAtom);
return (
<div>
<p>총액: ₩{cartTotal.totalBeforeDiscount.toLocaleString()}</p>
<p>할인액: ₩{cartTotal.totalDiscount.toLocaleString()}</p>
<p>최종 결제액: ₩{cartTotal.finalTotal.toLocaleString()}</p>
</div>
);
};
Context에 비해 불필요한 리렌더링도 적고, 코드도 훨씬 간결해졌습니다.
4. 엔티티 기반 구조: "찾기 쉬운 코드"
엔티티 기준으로 폴더를 구성하면서 가장 크게 느낀 것은 코드를 찾기가 쉬워졌다는 점입니다.
src/
├── hooks/
│ ├── useCart.ts # 장바구니 관련 모든 로직
│ ├── useProducts.ts # 상품 관련 모든 로직
│ ├── useCoupons.ts # 쿠폰 관련 모든 로직
│ └── useLocalStorage.ts # 범용 유틸리티
├── models/
│ ├── cart.ts # 장바구니 계산 함수들
│ ├── product.ts # 상품 관련 함수들
│ └── discount.ts # 할인 계산 함수들
└── components/
├── cart/ # 장바구니 관련 컴포넌트들
├── product/ # 상품 관련 컴포넌트들
└── admin/ # 관리자 관련 컴포넌트들
새로운 기능을 추가하거나 버그를 수정할 때, 어디서 찾아야 할지 고민할 시간이 확실히 줄어들었습니다.
5. 팀 토론의 가치: "함께 고민하는 힘"
화요일에 진행되었던 팀원들과의 토론을 통해 다양한 관점을 접할 수 있었고, 이는 큰 도움이 되었습니다. "엔티티는 비즈니스 주체다", "앱의 명사에 해당하는 데이터 덩어리다"와 같은 명확한 정의부터, "UI는 어디까지 상태를 가져야 하는가?", "엔티티가 절대 해서는 안 되는 것은 무엇인가?"와 같은 실용적인 부분도 나눌 수 있었습니다. 막히는 것에 대한 질문에 답해주고 함께 고민해주는 팀원들 덕분에, 혼자였다면 한참을 헤맸을 길을 더듬더듬 나아갈 수 있었습니다.
6. AI 활용의 새로운 가능성: "코치가 개발한 AI의 코드 리뷰"
이번 과제에서 가장 신기했던 경험 중 하나는 코치가 개발한 AI를 활용해서 제 PR을 보고 피드백을 해준 것이었습니다. 단순히 코드를 보는 것이 아니라, AI가 코드의 구조와 패턴을 분석해서 구체적인 개선 방향을 제시해주는 것을 보며 AI의 활용 가능성에 대해 새롭게 생각해볼 수 있었습니다.
특히 "App.tsx가 매우 얇아졌다"는 평가나 "모델(순수 함수)과 UI 분리가 잘 되어 테스트성과 유지보수성이 향상되었다"는 피드백을 받으며, AI가 단순한 도구를 넘어서 개발 과정에서 진정한 파트너가 될 수 있다는 것을 깨달았습니다.
도메인 훅 설계의 중요성도 AI를 통해 이해하게 되었습니다:
// AS-IS (컴포넌트에서 직접 Jotai 사용)
const [cart] = useAtom(cartAtom);
const [, addToCart] = useAtom(addToCartAtom);
// TO-BE (useCart 훅으로 추상화)
const { cart, addToCart } = useCart();
이런 추상화를 통해 상태 라이브러리 교체 시에도 useCart
내부만 바꾸면 되므로 컴포넌트 변경을 최소화할 수 있다는 점을 배웠습니다. 앞으로도 AI를 더 효과적으로 활용하는 방법을 찾아보고 싶습니다.
KPT 회고
Keep (계속 유지하고 싶은 점)
- 함수형 사고를 통한 액션, 계산, 데이터 분리
- VAC 패턴을 통한 컴포넌트 역할 분리
- 점진적 리팩터링으로 코드의 안정성 확보
- Jotai를 통한 상태 관리로 Props Drilling 해결
- 팀원들과의 토론을 통한 다양한 관점 학습
- AI 활용을 통한 개발 효율성 향상
Problem (개선이 필요한 문제점)
- 도메인 훅 추상화 미완성으로 인한 상태 라이브러리 의존성
- 액션 인터페이스 불일치로 인한 일관성 부족
- 파일 구조 개선 필요 (도메인별 재배치)
- 테스트 코드 부족으로 인한 코드 안정성 검증의 어려움
Try (앞으로 시도해 볼 점)
- 도메인 훅 완성:
useCart
,useProducts
,useCoupons
,useNotification
생성 - 액션 인터페이스 통일:
onSuccess
/onError
패턴 표준화 - 파일 구조 개선:
store/cart/*
,store/products/*
등 도메인별 재배치 - 테스트 코드 강화: 모델 함수(순수 함수) 단위 테스트 우선 → 도메인 훅 통합 테스트
이번 과제를 통해 "어떻게 코드를 구조화할 것인가"에 대한 나름의 기준과 철학을 세울 수 있었습니다. 완벽하지는 않지만, 적어도 "왜 이렇게 나누는지"에 대해 알 수 있게 되었다는 것이 가장 큰 수확입니다. 앞으로도 이런 고민을 계속해나가며 더 나은 개발자가 되고 싶습니다.
'항해99 플러스 프론트앤드 > 10주 과정' 카테고리의 다른 글
[WIL] 항해플러스 프론트엔드 6기 4주차 회고 (7) | 2025.08.04 |
---|---|
[WIL] 항해플러스 프론트엔드 6기 3주차 회고 (13) | 2025.07.28 |
[WIL] 항해플러스 프론트엔드 6기 2주차 회고 (9) | 2025.07.21 |
[WIL] 항해플러스 프론트엔드 6기 1주차 회고 (3) | 2025.07.13 |