Skip to content

Conversation

@hanseul524
Copy link

@hanseul524 hanseul524 commented Dec 2, 2025

과제의 핵심취지

  • 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는 분리되어 있나요?

  • 데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?

심화과제

  • 이번 심화과제는 Context나 Jotai를 사용해서 Props drilling을 없애는 것입니다.

  • 어떤 props는 남겨야 하는지, 어떤 props는 제거해야 하는지에 대한 기준을 세워보세요.

  • Context나 Jotai를 사용하여 상태를 관리하는 방법을 익히고, 이를 통해 컴포넌트 간의 데이터 전달을 효율적으로 처리할 수 있습니다.

  • Context나 Jotai를 사용해서 전역상태관리를 구축했나요?

  • 전역상태관리를 통해 domain custom hook을 적절하게 리팩토링 했나요?

  • 도메인 컴포넌트에 도메인 props는 남기고 props drilling을 유발하는 불필요한 props는 잘 제거했나요?

  • 전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?

과제 셀프회고

https://hanseul524.github.io/front_7th_chapter3-2/
굉장히 시간이 많이 들고 고민을 많이 하게 되는 과제 였습니다. basic을 진행할 때 부터 뭔가 명확하게 제 자신 스스로 기준이 안서는것 같아서 이랬다 저랬다 하면서 찾아갔던 것 같습니다 ... 일단 기준을 뚜렷하게 세운 뒤 그 기준을 따라가야 길을 잃지 않고 과제를 할 수 있다고 생각했습니다 ^^ .. 저는 사실 전역 상태관리를 써본적이 없어서 basic이 제가 공부했던 디폴트 값이였는데 advanced를 하면서 역체감을 하게 되서 공부가 많이 됬습니다.

폴더구조
힌트로 주신 구조를 기반으로 추가로 필요한 폴더를 추가하는 방식으로 진행했습니다. 페이지 컴포넌트와 재사용 가능한 컴포넌트를 분리해 페이지 → 하위 컴포넌트가 함께 위치하도록 했습니다.
이렇게 하면 페이지 수정 시 파일을 찾기 쉽고 해당 페이지에 기능 추가시 컴포넌트 파일의 위치가 명확해집니다.

src/basic/
├── components/       # 컴포넌트들
│   └── ui/           # 재사용 가능한 UI 컴포넌트
├── hooks/            # 커스텀 훅
├── models/           # 비즈니스 로직 (순수 함수)
├── utils/            # 유틸리티 함수
│   └── hooks/        # 범용 훅 (useDebounce 등)
├── constants/        # 상수 (초기 데이터)
├── pages/            # 도메인 (feature 단위)
│   └── admin/        # 관리자 컴포넌트
│   └── cart/         # 상품, 장바구니 컴포넌트
└── App.tsx           # 메인 앱 컴포넌트

계층별 책임분리
각 계층은 명확한 단일 책임만 가지도록 분리했습니다.

App.tsx (상태 통합)
  ↓ props 전달
Pages (페이지 조합 + 임시 폼 상태)
  ↓ props 전달
Components (UI 표시)

Hook (비즈니스 로직 + 상태)
  ↓ 호출
Models (순수 계산 함수)

1. App.tsx - 상태 통합 계층

  • 전역 상태 관리 Hook 호출 (useProducts, useCart, useCoupons)
  • 하위 페이지에 Hook 결과 전달
  • 페이지 전환 로직 (isAdmin)

2. Pages - 페이지 조합 계층

  • AdminPage: 상품/쿠폰 관리 페이지
  • CartPage: 장바구니 페이지
  • 임시 폼 상태 관리 (productForm, couponForm)
  • 하위 컴포넌트 조합 및 props 전달
  • 비즈니스 로직 호출 (addProduct, updateProduct 등)

3. Components - UI 표시 계층

  • ProductForm, CouponForm: 폼 UI 표시 및 validation
  • ProductTable, CouponList: 데이터 목록 표시
  • CartItem: 장바구니 아이템 표시
  • UI 컴포넌트 (Button, Input): 재사용 가능한 순수 UI

4. Hooks - 비즈니스 로직 계층

  • useProducts: 상품 CRUD + localStorage 연동
  • useCart: 장바구니 CRUD + localStorage 연동
  • useCoupons: 쿠폰 CRUD + localStorage 연동
  • useLocalStorage: localStorage 추상화
  • useNotification: 알림 관리
  • useDebounce: 입력 디바운싱
  • useSearch: 검색 로직

5. Models - 순수 계산 함수 계층

  • calculateCartTotal: 장바구니 총액 계산
  • getMaxApplicableDiscount: 최대 할인율 계산
  • applyCoupon: 쿠폰 적용 계산
  • calculateItemTotal: 개별 상품 총액 계산

의존성 방향

  • UI → Hook → Model (단방향)
  • 상위 계층은 하위 계층을 호출할 수 있지만, 하위는 상위를 알지 못함
  • 순수 함수(Model)는 어떤 계층에도 의존하지 않음

전역 상태 관리 전환

리액트를 사용하면서 전역 상태 관리 .. 라는 것을 처음 사용해보고 그 개념에 대해 잘 이해하지 못하다가 처음부터 사용해보면서 Custom Hooks + Props Drilling 방식에서 Zustand 전역 상태 관리로의 전환을 통해서 조금은 감을 잡은 것 같습니다.

문제점
처음 App.tsx 에서 store를 직접 사용하는 방식을 사용했습니다. props driling을 없애기 위해 전역 상태 관리를 사용하는 건데 똑같은 현상이 일어나고 있어서 각 컴포넌트에서 필요한 store를 가져오는 방식으로 변경했습니다ㅠㅠ .. 지금 생각하니까 완전 바보같은 실수였습니다.

// after
  return (
    <div className="min-h-screen bg-gray-50">
      <NotificationContainer
        notifications={notifications}
        onRemove={removeNotification}
      />

      <Header
        isAdmin={isAdmin}
        setIsAdmin={setIsAdmin}
      />

      <main className="max-w-7xl mx-auto px-4 py-8">
        {isAdmin ? <AdminPage /> : <CartPage />}
      </main>
    </div>
  );

과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?

Zustand와 Custom Hook의 차이점 체감

  • Basic에서 Custom Hook으로 상태를 관리할 때는 props drilling이 불가피했지만, Zustand를 사용하니 각 컴포넌트에서 필요한 store만 직접 가져올 수 있어 코드가 훨씬 간결해졌습니다.
// Before (Basic - Custom Hook)
// App.tsx에서 모든 hook 호출 후 props로 전달
const productsHook = useProducts();
const cartHook = useCart(addNotification);
<CartPage productsHook={productsHook} cartHook={cartHook} />

// After (Advanced - Zustand)
// 각 컴포넌트에서 필요한 store만 직접 사용
function CartItem({ product }) {
  const addToCart = useCartStore(state => state.addToCart);
  const addNotification = useNotificationStore(state => state.addNotification);

  const handleAddToCart = () => {
    const success = addToCart(product);
    if (success) {
      addNotification('장바구니에 담았습니다', 'success');
    }
  };
}

Zustand Persist의 동작 원리 이해

  • Zustand의 persist 미들웨어가 기본적으로 {state: {...}, version: 0} 형식으로 저장한다는 것을 알게 되었습니다.
  • 테스트 호환성을 위해 storage 옵션을 커스터마이징하는 방법을 배웠습니다.
// Zustand persist storage 커스터마이징
export const useCartStore = create<CartState>()(
  persist(
    (set, get) => ({...}),
    {
      name: 'cart',
      partialize: (state) => ({ cart: state.cart }), // cart만 persist
      storage: {
        getItem: (name) => {
          const str = localStorage.getItem(name);
          if (!str) return null;
          const data = JSON.parse(str);
          return Array.isArray(data) ? data : data.cart || [];
        },
        setItem: (name, value) => {
          // 배열만 저장 (Basic 버전과 호환)
          localStorage.setItem(name, JSON.stringify(value.state.cart));
        },
        removeItem: (name) => localStorage.removeItem(name)
      }
    }
  )
);

Store 분리와 조합의 중요성

  • 처음에는 모든 상태를 하나의 store에 넣으려고 했지만, domain별로 분리하니 유지보수가 훨씬 쉬워졌습니다.
  • 컴포넌트에서 여러 store를 조합해서 사용하는 패턴을 익혔습니다.
// Store 분리 전략
productStore: 상품 CRUD
cartStore: 장바구니 + selectedCoupon
couponStore: 쿠폰 목록 관리
notificationStore: 알림 (persist 없음)

// 컴포넌트에서 여러 store 조합
function CouponView() {
  const selectedCoupon = useCartStore(state => state.selectedCoupon);
  const applyCoupon = useCartStore(state => state.applyCoupon);
  const coupons = useCouponStore(state => state.coupons);
  const addNotification = useNotificationStore(state => state.addNotification);

  const handleApplyCoupon = (couponCode: string) => {
    const coupon = coupons.find(c => c.code === couponCode);
    if (coupon) {
      const success = applyCoupon(coupon);
      if (!success) {
        addNotification('쿠폰 적용 실패', 'error');
        return;
      }
      addNotification('쿠폰이 적용되었습니다', 'success');
    }
  };
}

Hook 분리 기준에 대한 명확한 이해

  • 처음에는 모든 로직을 Hook으로 만들어야 한다고 생각했지만, 실제로는 "재사용성"과 "책임"을 기준으로 판단해야 한다는 것을 배웠습니다.
  • 특히 폼 상태는 임시 데이터이므로 컴포넌트 내부 useState로 충분하고, 영속성 있는 엔티티 데이터만 Hook으로 분리하는 것이 적절하다는 것을 깨달았습니다.
  • useProductForm 같은 Hook을 만들 뻔 했지만, 이는 과도한 추상화라는 것을 알게 되었습니다. 특수한 상황에서만 사용되기 때문에 이런 상황에서는 컴포넌트 내부에서 구현하는 것이 나은 방법이라는것을 알게되었습니다.

순수 함수의 힘

  • calculateCartTotal, getMaxApplicableDiscount 같은 순수 함수를 분리하니 테스트가 쉬워지고 코드의 의도가 명확해졌습니다.
  • 특히 계산 로직이 Hook에서 분리되니 재사용성이 높아지고 유지보수가 편해졌습니다.

컴포넌트 분리의 실용성

  • 처음에는 컴포넌트를 어디까지 쪼개야 할지 고민이 많았는데, "단일 책임 원칙"과 "재사용성"을 기준으로 하니 명확해졌습니다.
  • ProductForm, CouponForm은 각자의 validation 로직을 가지고 있어 분리하는 것이 맞았고, Button, Input 같은 UI 컴포넌트는 재사용성이 높아 분리가 필수였습니다.

Props Drilling의 필요성 인식

  • basic 과제에서 props drilling을 경험하면서, advanced에서 Context나 Jotai를 사용하는 이유를 체감할 수 있었습니다.
  • 무조건 전역 상태가 답이 아니라, 상황에 맞는 적절한 선택이 중요하다는 것을 배웠습니다.

이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?

1. 명확한 기준 세우기

  • 처음부터 "Hook으로 분리할 것인가, 컴포넌트에 둘 것인가"의 기준을 세우는 데 가장 많은 시간을 투자했습니다. ai한테 물어보면서 기준을 세우고 그거에 맞게 분리하는 작업을 진행했습니다.
    ✅ Hook으로 분리해야 하는 경우
  • 여러 컴포넌트에서 재사용되는가?
  • 엔티티(도메인) 데이터를 다루는가?
  • 영속성이 필요한가? (localStorage, API 등)
  • 복잡한 비즈니스 로직이 포함되는가?
  • 상태와 로직이 밀접하게 결합되어 있는가?
    안티패턴 예시:
// 과도한 추상화
const useProductForm = (initialValue) => {
  const [form, setForm] = useState(initialValue);

  const handleNameChange = (name) => {
    setForm({ ...form, name });
  };

  const handlePriceChange = (price) => {
    setForm({ ...form, price });
  };

  // ... 

  return { form, setForm, handleNameChange, handlePriceChange, ... };
}

// 사용
const { form, setForm, handleNameChange, ... } = useProductForm(initialProduct);
<ProductForm form={form} onNameChange={handleNameChange} ... />

올바른 패턴:

// AdminPage에서 직접 관리
const [productForm, setProductForm] = useState(initialProduct);

// 간단한 상태 업데이트
<ProductForm
  productForm={productForm}
  setProductForm={setProductForm}  // 직접 전달
  onSave={() => addProduct(productForm)}
/>

2. 데이터 흐름의 단방향성

  • App.tsx → Pages → Components 방향으로 props가 흐르도록 일관되게 유지했습니다.
  • Hook은 비즈니스 로직만 담당하고, UI는 순수하게 props를 받아 표시하도록 분리했습니다.
  • 특히 addNotification을 props로 전달하는 방식을 선택한 이유를 문서화했습니다.

3. 과도한 추상화 지양

  • 초반에 useProductForm, useAdminTabs 같은 Hook을 만들 뻔 했지만, 재사용되지 않는 로직은 Hook으로 만들지 않았습니다.
  • "지금 당장 필요한 것만 추상화하고, 미래를 위한 추상화는 하지 않는다"는 원칙을 지켰습니다.

이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)

  • advanced 테스트 실패해서 원인을 찾아보니 localstorage 초기화 하는 부분이 있었는데 store를 사용하는것은 localstorage를 사용하지 않으려는것 아닌가 ..? 라는 생각이 들었습니다. 일단 테스트 코드를 통과하기 위해 App.tsx 파일에 코드를 추가했습니다. 이 방식이 틀린 것 같은데 찾아보니까 zustand persist가 localstorage를 공유하기 때문에 발생하는 문제라고 하는데 이럴 경우 어떻게 해결하는게 맞는 방식일까요ㅠㅠ?
  useEffect(() => {
    const productsData = localStorage.getItem('products');
    const cartData = localStorage.getItem('cart');
    const couponsData = localStorage.getItem('coupons');

    if (!productsData && !cartData && !couponsData) {
      useProductStore.setState({ products: initialProducts });
      useCartStore.setState({ cart: [], selectedCoupon: null });
      useCouponStore.setState({ coupons: initialCoupons });
      useNotificationStore.setState({ notifications: [] });
    }
  }, []);
  • props driling을 완전히 제거하는게 좋은방향인가요? 아니면 일부 props는 유지하는게 나은 방향인가요? 컴포넌트 내부에서 store를 직접 접근하는 방식이 제일 좋다! 인지 궁금합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant