과제의 핵심취지
- 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의 책임에 맞도록 코드가 분리가 되었나요?
-
계산함수는 순수함수로 작성이 되었나요?
-
주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?
-
계산함수는 순수함수로 작성이 되었나요?
-
특정 Entitiy만 다루는 함수는 분리되어 있나요?
-
특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?
-
데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?
심화과제
-
재사용 가능한 Custom UI 컴포넌트를 만들어 보기
-
재사용 가능한 Custom 라이브러리 Hook을 만들어 보기
-
재사용 가능한 Custom 유틸 함수를 만들어 보기
-
그래서 엔티티와는 어떤 다른 계층적 특징을 가지는지 이해하기
-
UI 컴포넌트 계층과 엔티티 컴포넌트의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
-
엔티티 Hook과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
-
엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?
과제 셀프회고
과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?
1️⃣ 엔티티 기준 계층분리를 중점적으로 구현하는 것
과제를 진행하면서 가장 많은 고민과 시간을 투자한 부분은 엔티티별로 Component-Hook-Function 3계층을 명확하게 분리하는 것이었습니다. 처음에는 단순히 "큰 컴포넌트를 작은 컴포넌트로 나누기"라고 생각했던 저로서는 조금 신기하고, 새롭다고 느꼈습니다. 이런 개념을 잘 모르고 있던 터라 hint폴더와 과제 요구사항 그리고 테오의 발제영상을 보면서 엔티티가 Product,cart,coupon을 관련해서 컴퍼넌트를 분리부터 시작했습니다.
아래와 같은 내용으로 구분을 했습니다.
📁 Cart 엔티티
├── Function: calculateCartTotal, calculateItemTotal
├── Hook: cartAtom, addToCartAtom, updateQuantityAtom
└── Component: CartItem, CartSection, EmptyCart
📁 Product 엔티티
├── Function: getMaxApplicableDiscount, getRemainingStock
├── Hook: productAtom, addProductAtom, updateProductAtom
└── Component: ProductCard, ProductArea
2️⃣ Jotai를 활용한 Props Drilling 완전 제거
atomWithStorage를 통해 데이터 지속성까지 확보하면서도 각 엔티티별로 독립적인 상태 관리가 되도록 했습니다.
export const cartAtom = atomWithStorage<CartItem[]>('cart', [], undefined, {
getOnInit: true,
});
export const addToCartAtom = atom(null, (get, set, product: ProductWithUI) => {
// 재고 검증, 수량 관리, 알림 처리 등
// 장바구니 관련 모든 비즈니스 로직을 여기서 처리
});
비즈니스 로직의 중앙화 기존 Hook에 분산되어 있던 로직들을 store 파일로 이관했습니다. 이를 통해 장바구니 관련 모든 규칙과 상태 변경 로직이 cart.store.ts 한 곳에서 관리됩니다.
Basic vs Advanced Basic의 경우는 hooks에서 props에 로직들이 내려오면서 되게 보기도 불편했습니다. 그리고 의존성도 복잡하고, 컴퍼넌트 태그에 프롭스가 길게 내려오는것이 가독성을 오히려 해치는거 같았습니다.
이러한, 구조 변경을 통해서 설계원칙을 달성한거 같습니다.
- 관심사 분리
- 높은 응집도
- 소소하게 낮은 결합도를 달성한거 같습니다.
유지 보수성도 좋고 가독성도 챙겼다고 생각하는 결과를 만들어낸거 같습니다. 기본
/// 베이직
const App = () => {
// 각종 카트,상품, 쿠폰등 관련 훅들
// cartItemCount
return (
<div className="min-h-screen bg-gray-50">
{notifications.length > 0 && (
<ToastContainer
notifications={notifications}
onClose={handleCloseToast}
/>
)}
<AppHeader
.... 각종 props~
/>
<AppMain
isAdmin={isAdmin}
products={products}
cart={cart}
coupons={coupons}
selectedCoupon={selectedCoupon}
productActions={{ addProduct, updateProduct, deleteProduct }}
cartActions={{ addToCart, removeFromCart, updateQuantity, onResetCart }}
couponActions={{ addCoupon, deleteCoupon, applyCoupon, resetCoupon }}
searchState={{ debouncedSearchTerm, }}
commonActions={{ addNotification }}
/>
</div>
);
};
심화
const App = () => {
return (
<div className="min-h-screen bg-gray-50">
<ToastContainer />
<AppHeader />
<AppMain />
</div>
);
};
한눈에 들어오게 최대한 신경 썼던거 같습니다.
과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?
1️⃣ 디테일과 세심함의 부족
과제를 제출하고 나서 코드를 다시 훑어보다가 아차싶던 순간들이 있었습니다. 큰 틀에서는 나름 잘 만들었다고 생각했는데, 자세히 들여다보니 "어? 이게 왜 여기 있지?" 싶은 부분들이 여러 곳에서 눈에 띄었어요. 좀 황당했던게 ProductWithUI 인터페이스였습니다. 분명히 models 폴더에 타입들을 정리해두었는데, 어느 순간 App.tsx에도 같은 역할을 하는 타입이 생겨있었어요. "아 저거 안지웠구나.." WebStorm에서 저걸 사용하는 목록을 보는데 너무 많았습니다. 하... 한숨만 나왔습니다. 과제를 다 제출하고 아침에 회고를 쓰면서 알게 되서 고치지는 못했습니다.
// App.tsx에 있던 타입 정의 (문제!)
export interface ProductWithUI extends Product {
description?: string;
isRecommended?: boolean;
}
// models/entities/product.ts에도 유사한 타입들이 있음
// 다른 여러 파일에서 App.tsx로부터 import
import { ProductWithUI } from '../../App.tsx';
두 번째로는 ProductArea 컴포넌트에서 발견한 중복 로직이었어요. 분명히 재고 계산하는 함수를 utils에 깔끔하게 만들어두었는데, 같은 일을 하는 코드가 컴포넌트 안에 또 있었습니다.
// utils/formatters.ts에 이미 구현된 함수
export const getRemainingStock = (product, quantity) => {
return product.stock - (quantity || 0);
};
// ProductArea.tsx에 똑같은 로직이 중복으로 존재 (문제!)
const ProductArea = () => {
// ... 중복된 재고 계산 로직이 컴포넌트 내부에 있음
const remainingStock = product.stock - (cartItem?.quantity || 0);
// getRemainingStock 함수가 있는데도 직접 계산해버림
};
getRemainingStock이라는 함수를 이미 만들어놨으면서 왜 또 만들었을까요? 아마 그때는 그 함수가 있다는 걸 까먹고 있었던 것 같습니다.
그리고 Basic은 네이밍 실수로.. CheckOut/Shopping 으로 나누려 했는데 실수로 Cart/Checkout으로 잘못 네이밍을 해서 나눠서 basic 폴더 구조가 살짝 꼬여버렸습니다. 이런 실수가 좀 많았습니다. ㅠㅠ
왜 이런 실수를 했을까
돌이켜보니 우선, 너무 여유롭게 생각한 나머지 시간관리를 재대로 못한점이 컸던거 같습니다. 코딩을 하는 시간도 잘 못했고, 하는 내내 집중을 못했던 순간도 많았던거 같습니다.
한 파트만 맡아서 정리하고, 넘어가던지 비중을 작업하는 파트에 둬야했는데, 카트정리하다가 product정리하가 꼬이고 이런점떄문에 다시 롤백하고 시간을 너무 소비하는게 문제가 됐었습니다.
UI 컴포넌트를 만들면서 필요성에 대해서 너무 고민했던 것 같습니다. 만들어놓고도 사용성이 좋은가 수십 번 고민했던 것 같아요. 결국 다 삭제하고 기존 코드를 이용했던 내용도 있었습니다. 타이포그래피(텍스트) UI도 제목 따로 description 따로 해서 눈에 들어올 때 "이게 설명란이구나" 이런 직관적으로 보이게 하고 싶었는데, 그냥 사용성 자체도 불편하고 들어오는 props들이 많아지고 오히려 더 불편해지기에 그냥 삭제했습니다. 이거 고민하고 만드는 시간이 또 지나다 보니 본질적으로 해야 하는 걸 시간이 부족해지면서 놓치는 점도 생기게 됐습니다.
앞으로는 어떻게 할까
- 구현 하기전에는 항상 코드를 잘 살펴보는 습관을 기르고
- 바로바로 주석으로 TODO을 달거나 메모장으로 할 목록을 정리하는 습관을 들여야 할거 같습니다.
다시 한다면
- 제출 전에 전체 코드를 체계적으로 점검하는 시간을 꼭 확보하겠습니다. 특히 타입 정의와 유틸 함수의 중복 여부를 꼼꼼히 확인하는 체크리스트를 만들어서 활용하겠어요.
- 먼저 전체 기능을 작은 단위로 나누고, 우선순위를 명확히 정해서 하나씩 완성해나가겠습니다. 카트 기능이면 카트를 완전히 끝내고 다음 기능으로 넘어가는 식으로요. 물론 연결되거나 응집성이 있으면, 돌아갈수있게 임시방편으로 조치를 해놓는 식으로 우선 그래도 하나의 기능을 우선으로.
- 완벽한 컴포넌트를 처음부터 만들려고 하지 말고, 핵심 기능 구현에 집중한 후에 점진적으로 개선해나가는 방식을 택하겠어요.
작은 실수들이 주는 느낀점
이런 작은 실수들 때문에 전체적인 코드 품질이 떨어진 느낌이 듭니다. 좋은 코드란 단순히 동작하는 코드가 아니라 읽기 쉽고, 유지보수하기 쉽고, 일관성 있는 코드라는 걸 다시 한번 깨달았어요. 앞으로는 이런 디테일한 부분들을 놓치지 않도록 더 세심하게 코딩해야겠습니다. 작은 습관들이 쌓여서 더 나은 개발자가 되기 위해 노력해야겠습니다. 과제를 다시 한다면 가장 중요하게 생각할 것들 :
- 시간관리와 우선순위 설정 - 핵심 기능부터 확실히
- 한 번에 하나씩 - 멀티태스킹보다는 집중
- 제출 전 체계적 점검 - 중복 코드, 타입 정리 등
- 완벽보다는 완성 - 일단 동작하게 만들고 개선하기
2️⃣ 관심사별 Props 그룹화 - "일관성을 끝까지 가져가지 못한 아쉬움"
관심사별로 props를 더 깔끔하게 정리할 수 있었는데 시간이 부족해서 중간에 멈춘 게 아쉬웠습니다. AppMain에서는 cartActions, productActions, couponActions 이런 식으로 관심사별 그룹화를 잘 시작했는데, 하위 컴포넌트들에서는 다시 개별 props로 풀어서 받는 일관성 없는 구조가 되어버렸어요. 물론 순서가 뒤죽박죽이여서 관리자 Form형식들은 handler,state,action으로 분리를 했었습니다. 또 마지막에 AppMain하고 다른거도 했으면 했는데 시간이 좀 없어서.. 완성을 미쳐 못한게 아쉬웠습니다.
한 번 정한 패턴(관심사별 그룹화)을 끝까지 일관성 있게 가져가겠습니다. 전체적은 구조에도 통일감이 있어야 보는 입장에서도, 파악이 쉬우니까요.. 그리고 시간이 부족하더라도 이런 구조적인 부분은 나중에 수정하기 어려우니까 처음부터 제대로 설계하는 게 중요하다는 걸 깨달았어요.
그만큼 클린하게 코드를 짜는게 중요하다는걸 더욱 깨닫게 되는 순간이였던거 같습니다.
3️⃣ 인터페이스 정리..
interface CartViewProps {
cart: CartItem[];
products: ProductWithUI[];
coupons: Coupon[];
addNotification: (message: string, type?: 'error' | 'success' | 'warning') => void;
addToCart: (product: Product) => void;
onRemoveFromCart: (productId: string) => void;
onUpdateQuantity: (productId: string, quantity: number) => void;
onApplyCoupon: (coupon: Coupon) => void;
onResetCart: () => void;
selectedCoupon: Coupon | null;
debouncedSearchTerm: string;
onResetCoupon: () => void;
}
// 뭐가 먼지 하나도 정리가 안됨..
관심사별로 분류를 재대로 못했던게 아쉽습니다. 코드를 다시 보니 인터페이스들도 정말 아무 생각 없이 한 줄로 죄다 박아서 썼더라고요. 11개의 props가 한 인터페이스에 쭉 나열되어 있으니 뭐가 뭔지 파악하기도 어렵고, 재사용하기도 어려운 구조가 되어버렸어요. 다시 이걸 혼자 해본다면 무조건 관심별로 분리를 해보는걸 해야겠습니다.
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
- Jotai atom에 이렇게 복잡한 비즈니스 로직을 넣는 게 괜찮은건가요? 아니면 별도 service 가 있어야 하나요? 우선 큰 틀안에서 하나에 넣고 관리를 했는데... 뭔가 상태와 로직을 따로 분리해야할까 고민을 했었습니다.
과제 피드백
안녕하세요 성진님! 5주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!
Jotai atom에 이렇게 복잡한 비즈니스 로직을 넣는 게 괜찮은건가요? 아니면 별도 service 가 있어야 하나요? 우선 큰 틀안에서 하나에 넣고 관리를 했는데... 뭔가 상태와 로직을 따로 분리해야할까 고민을 했었습니다.
service라기보단, atom의 로직을 별도의 순수함수로 분리해서 다루면 어떨까 싶어요 ㅎㅎ
component -> hook -> atom -> function
더 극단적으로 생각해보자면... jotai대신 zustand나 redux로 바꾼다고 했을 때 어떻게 해야 최소한의 비용, 최소한의 수정으로 교체가 가능할지 고민해보시면 좋답니다 ㅎㅎ