nemobim 님의 상세페이지[2팀 정도은] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍

배포링크: https://nemobim.github.io/front_6th_chapter2-2/

과제의 핵심취지

  • 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과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

  • 엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

과제 셀프회고

최대한 ai 덜 쓰기를 도전했으나,,, 시간 부족으로 인해 이번에도 도움을 받았다. 지난 번에 사용했던 바닐라 전용 커서 클린코드 룰을 리액트로 수정하고 사용했다.

과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?

우선 기능은 그대로 유지한채, 점진적인 마이그레이션을 가장 신경썻습니다. 커밋을 상세하게 남겨서 어느 시점이든 돌아갈 수 있도록 했고 테스트를 진행하며 통과된 후에만 커밋을 진행했습니다.

내가 가장 집중했던 것: "책임을 나누자"

막막함을 해결하기 위해 일단 "하나씩 책임을 나누는 것" 부터 시작했습니다.

1. 거대한 단일 컴포넌트에서 책임 분리

  • Before (Origin): 1,124줄의 거대한 단일 컴포넌트
const App = () => {
  // 🚨 50개 이상의 상태가 한 곳에 집중
  const [products, setProducts] = useState<ProductWithUI[]>(...);
  const [cart, setCart] = useState<CartItem[]>(...);
  const [coupons, setCoupons] = useState<Coupon[]>(...);
  const [isAdmin, setIsAdmin] = useState(false);
  const [showCouponForm, setShowCouponForm] = useState(false);
  // ... 20개 이상의 상태

  // 🚨 비즈니스 로직이 컴포넌트에 직접 구현
  const calculateItemTotal = (item: CartItem): number => {
    const discount = getMaxApplicableDiscount(item);
    return Math.round(price * quantity * (1 - discount));
  };

  // 🚨 1000줄 이상의 JSX가 모두 인라인
  return <div>{/* 모든 UI가 여기에 */}</div>;
};
  • After (Basic): 역할별로 분리, basic 에서는 일단 의존성은 냅두고 꼬인 상태부터 정리했습니다.
const AppContent = () => {
  // ✅ UI 상태만 관리
  const [isAdmin, setIsAdmin] = useState(false);
  
  // ✅ 엔티티별 Hook으로 분리
  const { products } = useProduct();
  const { cart, addToCart } = useCart({ products });
  const { coupons } = useCoupon();

  // ✅ 페이지 컴포넌트로 분리
  return (
    <div>
      <Header />
      {isAdmin ? <AdminPage /> : <CustomerPage />}
    </div>
  );
};
  • After (Advanced): 더 높은 추상화, 상태 관리 로직까지 컴포넌트에서 분리하여 단순화
const AppContent = () => {
  // ✅ 전역 상태는 atom으로
  const [isAdmin] = useAtom(isAdminAtom);

  return (
    <div>
      <Header />
      {isAdmin ? <AdminPage /> : <CustomerPage />}
    </div>
  );
};

2. 비즈니스 로직을 순수함수로 분리

  • Before (Origin): 컴포넌트 내부에 비즈니스 로직 포함 컴포넌트가 계산 로직을 직접 가지고 있어 재사용이 어렵고 코드가 길어짐
// 🚨 컴포넌트 안에서 계산
const getMaxApplicableDiscount = (item: CartItem): number => {
  const hasBulkPurchase = cart.some(cartItem => cartItem.quantity >= 10);
  if (hasBulkPurchase) {
    return Math.min(baseDiscount + 0.05, 0.5);
  }
  return baseDiscount;
};
  • After: 순수함수로 분리 입력이 같으면 항상 같은 출력을 보장할 수 있는 순수함수로 분리하고, 공통된 로직을 쓰는 곳에서 재사용
// utils/cartCalculations.ts
export const calculateItemTotal = (item: CartItem, cart: CartItem[]): number => {
  const { price } = item.product;
  const { quantity } = item;
  const discount = calculateMaxApplicableDiscount(item, cart);
  
  return Math.round(price * quantity * (1 - discount));
};

// 작은 단위의 순수함수들
const checkBulkPurchase = (cart: CartItem[]): boolean => {
  return cart.some((cartItem) => cartItem.quantity >= BULK_PURCHASE_THRESHOLD);
};

3. 엔티티별 상태 관리 분리

  • Before (Origin): 모든 상태 로직이 컴포넌트에 기존에는 장바구니와 관련된 로직이 컴포넌트 여러곳에 섞여있어 구조파악하기 힘듦
// 🚨 localStorage 로직도 컴포넌트에
const [cart, setCart] = useState<CartItem[]>(() => {
  const saved = localStorage.getItem('cart');
  try {
    return saved ? JSON.parse(saved) : [];
  } catch {
    return [];
  }
});

// 🚨 복잡한 비즈니스 로직도 컴포넌트에
const addToCart = useCallback((product: ProductWithUI) => {
  const remainingStock = getRemainingStock(product);
  if (remainingStock <= 0) {
    addNotification('재고가 부족합니다!', 'error');
    return;
  }
  // ... 50줄의 복잡한 로직
}, [cart, addNotification]);
  • After (Basic): 엔티티별 Hook 분리 장바구니 관련 모든 로직이 한 곳에 응집되어 관리하기 쉬움
// hooks/useCart.ts - 장바구니만 담당
export const useCart = ({ products }: UseCartProps) => {
  const [cart, setCart] = useState<CartItem[]>(
    loadDataFromStorage<CartItem[]>("cart", [])
  );

  const addToCart = useCallback((product: ProductWithUI) => {
    if (!validateStockAvailability(product, cart)) {
      showToast("재고가 부족합니다!", "error");
      return;
    }
    setCart((prevCart) => addItemToCart(prevCart, product));
  }, [cart, showToast]);

  return { cart, addToCart, removeFromCart, updateCartQuantity };
};

과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?

아직 서로 의존하는 함수가 남아있는거같아서 그 부분을 개선하고싶습니다... 예를 들어

const formatCurrency = (price: number, isAdmin: boolean): string => {
  const formattedPrice = price.toLocaleString();
  return isAdmin ? `${formattedPrice}` : `${formattedPrice}`;
};

export const formatPrice = (price: number, isAdmin: boolean, stockChecker?: IStockChecker): string => {
  if (stockChecker && checkSoldOut(stockChecker)) {
    return "SOLD OUT";
  }
  
  return formatCurrency(price, isAdmin);
};

가격 포맷팅 하나 때문에 isAdmin을 여러 컴포넌트에 props로 계속 내려주기 싫었는데,,, 적절한 대안을 찾지 못해서 일단 내려받는 걸로 작성했습니다.

고려했던 대안들:

  • 함수 내부에서 isAdmin 직접 가져오기 (전역 상태 의존)
  • admin 전용 함수 따로 만들기 (함수 분리)
  • 현재처럼 매개변수로 받기 (props drilling)

그리고 추가적으로 jotai도 persist 옵션이 있었을텐데.. 시간이 없어서 적용하지 못했습니다.

// src/advanced/hooks/useCart.ts - localStorage 동기화가 Hook 내부에
useEffect(() => {
  if (cart.length > 0) {
    saveDataToStorage("cart", cart);
  } else {
    removeDataFromStorage("cart");
  }
}, [cart]);

atomWithStorage를 사용했다면 localStorage 동기화 로직을 완전히 제거할 수 있었텐데..싶습니다.

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

Jotai 설계 패턴

Basic 단계에서 custom hook으로 상태 관리를 구현했고 Advanced 단계에서 Jotai를 도입했습니다. 기존 hook 로직을 재사용하고 싶어서 atom은 순수 상태만 관리하도록 작성했는데, 이 방식이 적절한지 궁금합니다... Jotai를 사용할 때 순수 상태만 atom으로 관리하고 로직은 별도 hook에서 처리 vs 모든 것을 atom으로 통합해 관리

  • 현재 방식: 순수 Atom + 별도 Hook
// atoms/cartAtoms.ts - 상태만 관리
export const cartAtom = atom<CartItem[]>(initialCart);

// hooks/useCartActions.ts - 로직은 별도 hook에서
export const useCartActions = () => {
  const [cart, setCart] = useAtom(cartAtom);
  const { showToast } = useToast();

  const addToCart = useCallback((product: ProductWithUI) => {
    if (!validateStockAvailability(product, cart)) {
      showToast("재고가 부족합니다!", "error");
      return;
    }
    setCart((prevCart) => addItemToCart(prevCart, product));
  }, [cart, setCart, showToast]);

  return { addToCart, removeFromCart };
};
  • Atom에 로직까지 포함
// atoms/cartAtoms.ts
export const cartAtom = atom<CartItem[]>(initialCart);

export const addToCartAtom = atom(
  null,
  (get, set, product: ProductWithUI) => {
    const currentCart = get(cartAtom);
    
    if (!validateStockAvailability(product, currentCart)) {
     //대강 이런식으로,,?
      return;
    }
    
    set(cartAtom, addItemToCart(currentCart, product));
  }
);

컴포넌트 상태 전달 패턴

컴포넌트에서 상태를 어떻게 전달할지 고민됩니다. 여러 컴포넌트에서 같은 hook을 직접 사용하고 있을때 props로 처리 vs 컴포넌트에서 hook 사용하기 어떤 게 더 좋은 패턴일까요?,,,

  1. 같은 hook을 여러 컴포넌트에서 사용하는 것이 괜찮은지
  2. 언제는 props로 전달하고, 언제는 hook을 쓴다는 기준이 있는지 (뎁스가 깊을때 빼고)
const Header = () => {
  const { cart } = useCart();  // 개수 표시용
  return <div>장바구니 ({cart.length})</div>;
};

const ProductCard = () => {
  const { addToCart } = useCart();  // 추가용
  return <button onClick={() => addToCart(product)}>추가</button>;
};

과제 피드백

수고하셨습니다. 도은님!

Q. Basic 단계에서 custom hook으로 상태 관리를 구현했고 Advanced 단계에서 Jotai를 도입했습니다. 기존 hook 로직을 재사용하고 싶어서 atom은 순수 상태만 관리하도록 작성했는데, 이 방식이 적절한지 궁금합니다...

A. 저라면 도은님과 동일하게 구성했을 것 같습니다. 가급적 atom에 과한 로직을 두지 않고 훅에서 처리할 것 같아요. 오히려 atom에서 많은 로직을 처리하면 오히려 역할이 모호해지는 것 같아요. 최대한 순수한 상태로 유지되는 아톰과 validateStockAvailability과 같은 판단 함수 그리고 UI처리하는 훅을 조합하는 형태로 가는 것이 역할을 잘 분리하는 것이 아닐까 싶습니다.

Q. 컴포넌트에서 상태를 어떻게 전달할지 고민됩니다. 여러 컴포넌트에서 같은 hook을 직접 사용하고 있을때 props로 처리 vs 컴포넌트에서 hook 사용하기

A. 여러가지 이유가 있을 수 있지만 일단 뎊스가 깊지않고 훅을 통한 컴포넌트간 재사용이 필요하지 않다면 저는 기본적으로 프롭으로 전달할 것 같습니다.
훅을 고려하는 것은 해당 훅으로 분리(추상화) 해야할 작업(코드)의 양, 재사용성등을 고려할 것 같아요. 혹은 단순 데이터를 공유하는 용도는 스토어에서 데이터를 꺼낼때는 훅을 통해 레이어를 구성하자라고 팀에서 약속한 경우 그렇게 할 것 같아요.