ㅇㅅㅇ

항해플러스 프론트엔드 6기 5주차 회고 본문

항해99 플러스 프론트앤드/10주 과정

항해플러스 프론트엔드 6기 5주차 회고

소 아 2025. 9. 23. 01:40

놀랍다... 미뤘더니 생각나는게 많지 않네... 이래서 WIL 권장 작성 기간이 있나보다..
한편으로는 읽어보면서 이런걸 했구나 돌아보는 시간을 가지게 되었다.
그렇지만 역시 적을게 있으면 바로 적는게 좋을거 같다는 걸 밀린 회고를 적으면서 깨달아 본다...

다섯 번째 도전기 - 디자인패턴과 함수형 프로그래밍, 함수형 사고의 첫걸음

이번 과제는 단순히 기능을 구현하는 것을 넘어서 코드 구조와 설계에 대해 깊이 생각해볼 수 있는 기회였습니다. 평소 어느 정도 구조를 나누려 노력했지만 명확한 기준이나 패턴 없이 "그냥 이게 맞는 것 같은데?"라는 감각에 의존해온 부분이 있었습니다. 특히 컴포넌트를 언제 분리해야 할지, 상태 관리는 어떤 기준으로 해야 할지에 대해 체계적인 접근이 부족했음을 느꼈습니다.

제공된 관련 문서에서 본 "디자인패턴을 사용하기 위해 설계를 하는 것이 아니라, 문제를 잘 해결할 수 있는 디자인패턴이 있을 것입니다"라는 문장은 인상 깊었습니다. 이론이 단순한 장식이 아니라 실제 문제 해결 도구가 될 수 있음을 체감하고 싶었습니다.

항해의 절반을 지나오면서 과제 자체에는 익숙해졌지만, 매 과제마다 새로운 개념에 허우적거리는 것은 여전했습니다. 이번에 특히 고민했던 부분은 Entity와 UI의 역할 분리였습니다. 화요일 팀 토론에서 "엔티티는 비즈니스 주체다", "UI는 어디까지 상태를 가져야 하는가?"와 같은 실용적인 질문들을 함께 나누며, 혼자였다면 오래 헤맸을 문제들을 더듬더듬 풀어나갈 수 있었습니다.


리팩토링 여정

과제로 주어진 정리되지 않는 거대한 App.tsx 컴포넌트로, 다음과 같은 여정을 거쳤습니다:

  1. 단일 책임 원칙 적용하여 컴포넌트 분리
  2. 함수형 프로그래밍의 핵심인 순수함수와 액션 구분
  3. 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. 비즈니스 로직과 UI 로직의 분리: "역할 분담의 명확성"

가장 중요한 깨달음은 "비즈니스 로직과 UI 로직의 명확한 분리"였습니다. 코드를 핵심 비즈니스 규칙을 다루는 것과 그렇지 않은 것으로 명확히 구분하는 것이 핵심이었습니다.

비즈니스 로직을 다루는 것들

  • 상태: 사용자 데이터, 상품 정보, 주문 내역 등 (핵심 비즈니스 데이터)
  • 컴포넌트: 특정 도메인의 비즈니스 규칙을 포함한 컴포넌트
  • : 비즈니스 로직을 캡슐화한 커스텀 훅
  • 함수: 계산, 검증, 변환 등 순수한 비즈니스 로직 함수

UI와 유틸리티를 다루는 것들

  • 상태: 팝업 표시 여부, 검색어, 관리자 모드 등 (UI 상태)
  • 컴포넌트: 버튼, 입력창, 배지 등 재사용 가능한 UI 컴포넌트
  • : 디바운스, 로컬 스토리지 등 범용 유틸리티
  • 함수: 텍스트 변환, 날짜 포맷팅 등 순수한 유틸리티 함수

컴포넌트 역할 분리 패턴을 적용하면서 느낀 것은, 각 컴포넌트가 하나의 명확한 책임을 가지게 된다는 점이었습니다.

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가 바뀌어도 로직은 그대로, 로직이 바뀌어도 UI는 영향받지 않음
  • 테스트 용이성: 각 계층을 독립적으로 검증 가능

3. Props Drilling 문제와 Jotai 해결책

이론적으로만 알고 있던 Props Drilling 문제를 컴포넌트를 분리하는 과정에서 실제로 불편함을 겪고, Jotai로 해결해보면서 깊이 있게 이해할 수 있었습니다.

🔴 Before: Props Drilling 지옥

// CartPage.tsx - 13개의 props를 받아야 함 😱
<CartPage
  debouncedSearchTerm={debouncedSearchTerm}
  isAdmin={isAdmin}
  products={products}
  cart={cart}
  // ... 9개 더 props 전달
/>

이런 식으로 props를 계속 전달하다 보니 코드가 복잡해지고, 새로운 기능을 추가할 때마다 여러 컴포넌트를 수정해야 했습니다.

🟢 After: 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);
});
// 컴포넌트에서 직접 사용 - props 전달 불필요! 🎉
const CartSummary = () => {
  const cartTotal = useAtomValue(cartTotalAtom);

  return (
    <div>
      <p>총액: ₩{cartTotal.totalBeforeDiscount.toLocaleString()}</p>
      <p>할인액: ₩{cartTotal.totalDiscount.toLocaleString()}</p>
      <p>최종 결제액: ₩{cartTotal.finalTotal.toLocaleString()}</p>
    </div>
  );
};

Jotai의 장점

  • 성능 최적화: Context에 비해 불필요한 리렌더링 최소화
  • 간결한 코드: props 전달 없이 필요한 상태에 직접 접근
  • 유연한 구조: 파생 상태 계산과 의존성 관리가 쉬움

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의 활용 가능성에 대해 새롭게 생각해볼 수 있었습니다.

AI가 준 피드백

  • "App.tsx가 매우 얇아졌다" - 컴포넌트 분리의 효과
  • "모델(순수 함수)과 UI 분리가 잘 되어 테스트성과 유지보수성이 향상되었다" - 아키텍처 개선의 가시적 효과

AI를 통한 학습
AI가 단순한 도구를 넘어서 개발 과정에서 진정한 파트너가 될 수 있다는 것을 깨달았습니다. 특히 도메인 훅 설계의 중요성을 AI를 통해 이해하게 되었습니다:

// 🔴 AS-IS: 컴포넌트에서 직접 Jotai 사용
const [cart] = useAtom(cartAtom);
const [, addToCart] = useAtom(addToCartAtom);

// 🟢 TO-BE: useCart 훅으로 추상화
const { cart, addToCart } = useCart();

추상화의 가치
이런 추상화를 통해 상태 라이브러리 교체 시에도 useCart 내부만 바꾸면 되므로 컴포넌트 변경을 최소화할 수 있다는 점을 배웠습니다. 앞으로도 AI를 더 효과적으로 활용하는 방법을 찾아보고 싶습니다.


과제 완료 후 지금까지의 변화

과제를 통해 얻은 것들

  • 함수형 사고를 통한 액션, 계산, 데이터 분리
  • 컴포넌트 역할 분리 패턴을 통한 명확한 책임 분담
  • 점진적 리팩터링으로 코드의 안정성 확보
  • 전역 상태 관리로 Props Drilling 문제 해결
  • 팀원들과의 토론을 통한 다양한 관점 학습
  • AI 활용을 통한 개발 효율성 향상

과제 완료 후 발견한 한계점

  • 도메인 추상화 미완성으로 인한 상태 라이브러리 의존성
  • 액션 인터페이스 불일치로 인한 일관성 부족
  • 파일 구조 개선 필요 (도메인별 재배치)
  • 테스트 코드 부족으로 인한 코드 안정성 검증의 어려움

앞으로 더 발전시키고 싶은 부분

  • 도메인 훅 완성: 비즈니스 로직을 완전히 캡슐화한 커스텀 훅들
  • 일관된 인터페이스: 성공/실패 콜백 패턴 표준화
  • 도메인 중심 구조: 비즈니스 도메인별 파일 재배치
  • 테스트 코드 강화: 순수 함수 단위 테스트 → 통합 테스트

마무리

이번 과제를 통해 "어떻게 코드를 구조화할 것인가"에 대한 나름의 기준과 철학을 세울 수 있었습니다. 완벽하지는 않지만, 적어도 "왜 이렇게 나누는지"에 대한 생각할 수 있는 것에 대해 알게되어 좋았습니다. 앞으로도 이런 고민을 계속해나가며 더 나은 개발자가 되도록 노력해야겠다 생각했습니다.


추천인 코드

제 후기를 보고 프로그램에 관심이 생기셨다면, 감사한 마음으로 수강료 할인을 받을 수 있는 추천인 코드를 드립니다.

수강 신청 시 추천인 코드 WlHJDO 입력하면 20만 원 할인이 적용되며, 자세한 커리큘럼과 일정은 아래 링크에서 확인하실 수 있습니다.

추천인 코드 : WlHJDO

https://hanghae99.spartacodingclub.kr/plus/fe

 

항해 플러스 | 도전을 넘어 개발자 커리어 도약으로

빅테크 시니어 코치진이 사수가 되어 드립니다. 시니어 개발자의 멘토링, 실무 포트폴리오, 동료 개발자들과의 인사이트 나눔까지.

hanghae99.spartaclub.kr