tomatopickles404 님의 상세페이지[9팀 권지호] Chapter 2-1. 클린코드와 리팩토링

과제 체크포인트

기본과제

  • 코드가 Prettier를 통해 일관된 포맷팅이 적용되어 있는가?
  • 적절한 줄바꿈과 주석을 사용하여 코드의 논리적 단위를 명확히 구분했는가?
  • 변수명과 함수명이 그 역할을 명확히 나타내며, 일관된 네이밍 규칙을 따르는가?
  • 매직 넘버와 문자열을 의미 있는 상수로 추출했는가?
  • 중복 코드를 제거하고 재사용 가능한 형태로 리팩토링했는가?
  • 함수가 단일 책임 원칙을 따르며, 한 가지 작업만 수행하는가?
  • 조건문과 반복문이 간결하고 명확한가? 복잡한 조건을 함수로 추출했는가?
  • 코드의 배치가 의존성과 실행 흐름에 따라 논리적으로 구성되어 있는가?
  • 연관된 코드를 의미 있는 함수나 모듈로 그룹화했는가?
  • ES6+ 문법을 활용하여 코드를 더 간결하고 명확하게 작성했는가?
  • 전역 상태와 부수 효과(side effects)를 최소화했는가?
  • 에러 처리와 예외 상황을 명확히 고려하고 처리했는가?
  • 코드 자체가 자기 문서화되어 있어, 주석 없이도 의도를 파악할 수 있는가?
  • 비즈니스 로직과 UI 로직이 적절히 분리되어 있는가?
  • 코드의 각 부분이 테스트 가능하도록 구조화되어 있는가?
  • 성능 개선을 위해 불필요한 연산이나 렌더링을 제거했는가?
  • 새로운 기능 추가나 변경이 기존 코드에 미치는 영향을 최소화했는가?
  • 코드 리뷰를 통해 다른 개발자들의 피드백을 반영하고 개선했는가?
  • (핵심!) 리팩토링 시 기존 기능을 그대로 유지하면서 점진적으로 개선했는가?

심화과제

  • 변경한 구조와 코드가 기존의 코드보다 가독성이 높고 이해하기 쉬운가?
  • 변경한 구조와 코드가 기존의 코드보다 기능을 수정하거나 확장하기에 용이한가?
  • 변경한 구조와 코드가 기존의 코드보다 테스트를 하기에 더 용이한가?
  • 변경한 구조와 코드가 기존의 모든 기능은 그대로 유지했는가?
  • (핵심!) 변경한 구조와 코드를 새로운 한번에 새로만들지 않고 점진적으로 개선했는가?

배포주소

https://tomatopickles404.github.io/front_6th_chapter2-1/

과제 셀프회고

1. 설계전 계획수립과 실제 수행한 과정에서의 회고

docs 문서들을 순차적으로 읽으면서 "아, 이 과제는 초기 계획이 중요하겠다" 라고 직감적으로 생각했습니다. 그래서 코드를 작성하기 전에 전략을 세우고자 cursor ai에 docs를 읽으라고 시키면서 우선순위와 방향성을 물었습니다. 그리고 이 사전 전략 수립에 대한 문서를 남겨두고, 실제로 제가 진행한 방향을 기록해서 비교해보면 의미가 있을 것 같다고 생각해 다른 주차보다 문서화를 자주 하려고 했습니다.

[초반 시도]

1. ai로 상태를 모듈화 하려고 시도 했습니다.

export const ShoppingCart = () => {
  // ===== Private State =====
  const state = {
    products: PRODUCTS, // prodList
    cart: [],

    totalAmount: 0, //totalAmt
    itemCount: 0, //itemCnt
    bonusPoints: 0, //bonusPts

    // UI 상태
    lastSelected: null, //lastSel

    // DOM 요소 참조 (sel, addBtn, cartDisp, sum, stockInfo)
    ui: {
      productSelector: null,
      addButton: null,
      cartDisplay: null,
      summaryElement: null,
      stockInformation: null,
      totalElement: null,
    },
  };

  // ===== Public Functions =====
  const addToCart = productId => { ... };

  const removeFromCart = productId => { ... };

  const updateQuantity = (productId, quantity) => { ... };

  // 할인이 적용된 최종 총 금액 계산
  const getTotalAmount = () => {
    // state.totalAmount = state.cart.reduce((acc, p) => acc + p.price * p.quantity, 0);
  };
  
  ... 
  
	return {
    /**
     * 모듈 초기화
     */
    init: initializeUI,
    /**
     * 장바구니 관련 함수
     */
    addToCart,
    removeFromCart,

    getStockTotal,
    getTotalAmount,

    updateSelectOptions,
    updateQuantity,
    updateStockInfo,
    updatePricesInCart,

    //TODO: 분리
    renderBonusPoints,
  };
};

  • 적용하고자 하니 너무 넓은 범위의 코드를 수정하는 상황이 되었고, ai의 결과물도 만족스럽지 않았습니다.

2. AI로 UI 부터 나누고자 했습니다.

/**
 * 헤더 요소 생성
 */
export function createHeader() {
  const header = document.createElement('div');
  header.className = 'mb-8';
  header.innerHTML = `
    <h1 class="text-xs font-medium tracking-extra-wide uppercase mb-2">🛒 Hanghae Online Store</h1>
    <div class="text-5xl tracking-tight leading-none">Shopping Cart</div>
    <p id="item-count" class="text-sm text-gray-500 font-normal mt-3">🛍️ 0 items in cart</p>
  `;
  return header;
}

/**
 * 상품 선택기 생성
 */
export function createProductSelector() {
  const selectorContainer = document.createElement('div');
  selectorContainer.className = 'mb-6 pb-6 border-b border-gray-200';

  const sel = document.createElement('select');
  sel.id = 'product-select';
  sel.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';

  const addBtn = document.createElement('button');
  addBtn.id = 'add-to-cart';
  addBtn.innerHTML = 'Add to Cart';
  addBtn.className =
    'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';

  const stockInfo = document.createElement('div');
  stockInfo.id = 'stock-status';
  stockInfo.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';

  selectorContainer.appendChild(sel);
  selectorContainer.appendChild(addBtn);
  selectorContainer.appendChild(stockInfo);

  return { selectorContainer, sel, addBtn, stockInfo };
}

/**
 * 장바구니 표시 영역 생성
 */
export function createCartDisplay() {
  const cartDisp = document.createElement('div');
  cartDisp.id = 'cart-items';
  return cartDisp;
}

...
  • 이 시도 역시 한번에 넓은 범위를 수정하게 되어 변경에 대한 파악이 어려웠습니다.
  • 이벤트 핸들러와 상태 변경이 혼합되어 있어 ui 요소만 분리하는 것이 어려웠습니다.

3. 직접 분리해보자!

  • 초반부터 ai가 세워준 전략으로 ai를 시켜서 설계를 시도하려고 하니 전반적인 코드베이스를 이해하기에는 한계가 있었습니다. 그래서 방향을 변경하여 main.basic.js의 구조부터 스스로 파악해야겠다고 생각했습니다.
  • 가장 먼저 함수들 사이에 spacing을 두어 어떤 함수가 있는지 파악하고, 그 함수들을 개별파일로 분리했습니다.
  • 분리를 하는 과정에서 각각의 함수가 어떤 일을 수행하는 함수인지 파악하고자 했습니다. 함수의 인자를 통해 의존성을 명시적으로 파악하고자 했습니다.
  • 인자로 받는 변수들이 파악 되면서 이 변수들을 상태로 관리해야겠다는 판단이 생겼습니다.
  • 함수 분리 후에는 다음에 할 일이 명확해 질 거라고 예상했습니다.

main.basic에 작성되어 있는 코드를 나누면서 생각해보니 이 과제는 스스로 생각하는게 많으면 많으수록 리팩토링에 대한 저만의 기준과 방향에 대해 많이 얻을 수 있겠다는 생각을 했습니다. 그래서 ai는 귀찮은 작업 위주로 사용하고 리팩토링에 대한 방향은 스스로 진행하고자 했습니다.

작은 단위부터 점진적으로 코드를 분리하려고 했고, 진도가 느리더라도 최대한 더티코드에 대한 감각을 직접적으로 체감해보려 했습니다. 그러다보니 리팩토링의 시작은 나쁜냄새 맡기라는 것을 깨닫게 되었습니다. 작은 단위부터 고쳐서 완성도 있게 고도화 시키는 과정을 최대한 스스로 해보는 경험이 훨씬 저에게 많이 남을 것 같다는 생각을 했습니다. 그렇게 진행하면서 제 예상보다 과제의 진행도가 느렸고, 또 한번 생산성에 대한 고민을 하게 되었습니다.

결론적으로 advanced의 React + TypeScript 구축과 코드 마이그레이션을 ai를 활용하여 빠르게 진행을 한 뒤, 고도화를 스스로 해보는 방향으로 변경했습니다.


나는 어떤 원칙을 토대로 구조화하였는가?

공유받은 아래의 문서가 테스트 주도 리팩토링에 도움이 많이 되었습니다.

절대 반드시 지켜야만 하는 절대적 원칙 (안지키면 다시해야함)

- 코드의 동작이나 구현이 바뀌면 안되고 반드시 구조 변경(리팩토링)만 해야해야만해
- 공통으로 쓰이는 파일만 공통 폴더에 넣어두고, 비즈니스 로직이 담긴 경우, 관심사끼리 묶어 폴더로 관리해야해
- src/basic/tests/basic.test.js, src/advanced/tests/advanced.test.js 테스트 코드가 모두 하나도 빠짐없이 통과해야해 (테스트는 npx vitest run 으로 watch 가 발생하지 않도록 해)
- 테스트 코드 검증 여부는 main.basic.js 를 기준으로 검사해야만해. origin 파일은 의미없어.
- > 결과파일은 main.basic.js 에 적용해줘
-> 작업 후 마지막으로 절대 원칙이 지켜졌는지 한번 더 컴토 후 올바르게 고치고 알려줘

그 밖에도

  1. 우선순위 기반 분류
  2. 토스 가이드라인 기반 원칙
  • Readability: 명확한 변수명, 단순한 로직
  • Predictability: 일관된 데이터 흐름, 명시적 타입 변환
  • Cohesion: 단일 책임 원칙, 관련 로직 그룹화
  • Coupling: Props Drilling 최소화, 컴포넌트 분리

을 적용하고자 했습니다.


2. 페르소나 사용경험

저는 cursor rules를 toss-frontend-rules.mdc 와 창준님이 공유해주셨던 커서를 클로드코드처럼 동작하게 하는 Rules 를 복사해서 붙여넣는 정도로만 경험했었습니다. 이번 과제에 toss-frontend-rules 가 도움이 될 것 같아 페어팀에 공유를 했었는데 준형님께서 다음과 같은 운을 띄우게 됩니다.

image

이 직전에 준형님께서 코드리뷰 에이전트를 만들어서 공유하셨는데, 그 에이전트와 토스 룰을 이용한 페르소나를 만들어서 페어팀에서 같이 사용하면 좋을 것 같다는 의견을 주셨습니다.


image

저는 덥석 미끼를 물어버렸습니다! 그래서 AI로 간단하게 합쳐보았고, 아직 본격적으로 관련 작업을 시작하지 않았지만 이번 과제에 제가 마루타가 되어 이 룰을 사용해 봤습니다. (Repo 보러가기)

토스 가이드라인을 따르는 클린 코드 리팩토링을 시켜보았습니다.

스크린샷 2025-07-31 오후 5 46 42
  • 단계적으로 문제점 분석과 함께 점진적인 개선을 진행했습니다.
  • 개선 후 테스트를 실행하며, 통과 된다면 문서화를 진행했습니다.

3. 테스트 주도 리팩토링, 주기적인 문서화에 대한 생각

테스트 주도 리팩토링 (?)

리팩토링은 코드를 개선하는 것이 아니라, 코드를 이해하는 것이다 - 마틴 파울러

  • Red-Green-Refactor 사이클을 통한 안전한 리팩토링 경험
  • 테스트가 있으면 "이 코드가 여전히 작동하는가?"라는 의심을 하지 않아도 된다.
  • 장바구니와 같은 복잡한 계산이 많이 요구되는 도메인에서 특히 중요함을 깨달았습니다.

주기적인 문서화의 중요성

문서화는 설계의 검증입니다. 문서를 쓰면서 아키텍처의 일관성을 확인하며, 설계의 결함을 조기에 발견할 수 있습니다. 리팩토링 과정에서 계획 단계의 문서화와 실행 과정의 문서화를 비교하면서 이를 체감했습니다.

[계획 단계의 문서화]

  • 이론적 접근, 이상적인 시나리오
  • 체계적인 구조 설계
  • 추상적이고 일반적인 원칙 적용

[실행 과정의 문서화]

  • 실제 문제 해결 과정, 구체적인 트러블 슈팅이 드러난다.
  • 예상치 못한 상황의 해결책을 찾는 과정에서의 사고 과정이 디테일하게 기록된다.

구체적인 비교

[모듈 패턴 설계]

계획 단계

const ShoppingCart = (() => {
  // ===== Private State =====
  let state = {
    products: [],
    cart: [],
    totalAmount: 0,
    itemCount: 0,
    lastSelected: null,
    bonusPoints: 0,
    ui: {
      productSelector: null,
      addButton: null,
      cartDisplay: null,
      summaryElement: null,
      stockInformation: null,
    },
  };

  // ===== Public API =====
  return {
    init,
    addToCart,
    removeFromCart,
    updateQuantity,
    calculateTotal,
    getState,
    destroy,
  };
})();
  • 모듈 패턴을 사용한 완전한 캡슐화 설계(계획 구상)
  • Private State와 Public API의 명확한 분리(설계 원칙)
  • 즉시 실행 함수(IIFE)로 상태 은닉(구현 방식)

실제 구현 단계 실제로는 점진적인 접근이 필요했습니다.

1단계: 기존 함수들을 modules로 분리

  • handleCalculateCartStuff → updateCartDisplay
  • onUpdateSelectOptions → updateProductOptions
  • doUpdatePricesInCart → updatePricesInCart

2단계: 매개변수 통일 과정에서 트러블 슈팅

  • 함수 시그니처 불일치(모든 호출 지점 수정과정에서의 누락이슈)
// 기존 호출 방식
handleCalculateCartStuff();  // 매개변수 없음

// 새로운 함수 시그니처
export function updateCartDisplay({ cartDisp, prodList }) {
  // 객체 destructuring을 기대하지만 매개변수가 없어서 에러!
}
  • 전역변수 의존성 문제
// 기존: 전역변수에 의존
function handleCalculateCartStuff() {
  cartItems = cartDisp.children;  // cartDisp는 전역변수
  // prodList도 전역변수
}

// 매개변수로 데이터 받음
export function updateCartDisplay({ cartDisp, prodList }) {
  const cartItems = Array.from(cartDisp.children);  // 매개변수 사용
}

3단계: 트러블슈팅 과정에서 발견한 문제들

// - 데이터 구조 불일치: 'price' vs 'val'
// - 상수 참조 불일치: PRODUCT_ONE vs p2 vs product_3
// - 함수 매개변수 destructuring 오류

그 밖에도 프로젝트의 설계적인 관점 뿐만 아니라 AI 토큰의 경제적 사용 관점에서도 주기적인 문서화는 중요했습니다. 매 컨텍스트가 끝날 때마다 문서화 해둔 것을 기반으로 새로운 대화에 주입시켜 토큰을 절약할 수 있었습니다.


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

1. 추상화 레벨의 일관성

advanced 과제에서 가장 신경을 많이쓰고자 했던 부분입니다.

[구체적 경험] Basic → Advanced 버전으로 발전시키면서 각 레이어(컴포넌트, 훅, 유틸리티)의 추상화 레벨을 일관성 있게 유지하려고 노력했습니다.

[어려웠던 점] useNewCart 훅이 너무 많은 책임을 가지게 되어 추상화 레벨이 불일치하는 문제를 겪었습니다.

[해결 방법] useNewCart, useCartActions, useCartItems로 책임을 분리하여 각각의 추상화 레벨을 명확히 했습니다.

관련 소스

컴포넌트 레이어에서의 추상화 (UI Layer)

// App.tsx - 최상위 컴포넌트 (높은 추상화)
export default function App() {
  return <AppContent />;
}

function AppContent() {
  useSaleEffects(); // 사이드 이펙트만 처리
  return (
    <div className="max-w-screen-xl h-screen max-h-800 mx-auto p-8 flex flex-col">
      <Header />
      <MainContent />
    </div>
  );
}

// MainContent - 레이아웃 컴포넌트 (중간 추상화)
function MainContent() {
  return (
    <div className="grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden">
      <SelectSection />
      <OrderSummary />
      <HelpButton />
    </div>
  );
}

// SelectSection - 구체적인 기능 컴포넌트 (낮은 추상화)
function SelectSection() {
  return (
    <div className="bg-white border border-gray-200 p-8 overflow-y-auto">
      <ProductSelector />
      <CartDisplay />
    </div>
  );
}

2. 토스 가이드라인 준수

  • README에 작성되어 있는 클린코드 준수 사항과 함께 토스 가이드라인을 고려했습니다.
  • 토스의 Frontend Fundamentals 문서는 클린코드에 대해 이해하기 쉽고 직관적으로 설명하고 있기에 이번 과제에 적용하는 연습을 함께 해보면 좋을 것 같다고 생각했습니다.

Frontend Fundamentals에서는 좋은 코드를 4가지 기준으로 나누어 설명합니다.

Readability (가독성)

  1. 명명 규칙: getCartTotals, updateProductQuantity 등 명확한 함수명
  2. 상수 분리: DISCOUNT_THRESHOLDS 등 매직 넘버 제거
  3. 함수 분리: 복잡한 로직을 작은 함수들로 분해

Predictability (예측 가능성)

  1. 일관된 반환 타입: 같은 종류의 함수나 Hook이 서로 다른 반환 타입을 가지면 코드의 일관성이 떨어져 협업에서 예측 가능성이 낮아짐 -> 같은 종류의 Hook이나 함수의 반환 타입을 동일한 기조로 유지
  2. 순수 함수: 부수 효과 없는 예측 가능한 함수들
  3. 에러 처리: 명확한 에러 상황 처리

Cohesion (응집도)

  1. 도메인별 분리: cart/, product/, order/ 등 기능별 모듈화
  2. 관련 로직 그룹화: 할인 계산, 포인트 계산 등 관련 기능 묶음

Coupling (결합도)

  1. 의존성 주입: 함수 파라미터로 의존성 전달
  2. 인터페이스 분리: 명확한 인터페이스 정의

3. 점진적 리팩토링 접근

위에서 언급했던 절대원칙 4가지를 매 컨텍스트마다 주입하며 점진적으로 리팩토링을 진행하고자 했습니다.

  • 절대 원칙(4가지)
    • 코드 동작 변경 금지: 구조 변경만 허용
    • 관심사별 분리: 공통 파일은 공통 폴더, 비즈니스 로직은 관심사별 폴더
    • 100% 테스트 통과: npx vitest run으로 모든 테스트 통과 필수
    • main.basic.js 기준: 테스트 검증은 main.basic.js 기준
  • 가장 작은 단위부터 점진적으로 개선하기
    • 코드 spacing 나누기 -> 함수 분리하기 -> 함수명 개선하기 -> 작은 단위로 나누기

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

1. 몇몇 코드의 아쉬운 단일책임 분리

[문제점]

  • calculateLoyaltyPoints.js의 단일책임 위반
  • 한 함수가 너무 많은 일을 수행하고 있습니다.
const calculateLoyaltyPoints = ({
  totalAmount,
  itemCount,
  cartItems,
  prodList,
}) => {
  const basePoints = Math.floor(totalAmount / POINTS.baseRate);
  let finalPoints = 0;
  const pointsDetail = [];

  if (basePoints > 0) {
    finalPoints = basePoints;
    pointsDetail.push(`기본: ${basePoints}p`);
  }

  // 화요일 보너스
  if (new Date().getDay() === DISCOUNT.tuesdayDay && basePoints > 0) {
    finalPoints = basePoints * POINTS.tuesdayMultiplier;
    pointsDetail.push('화요일 2배');
  }

  // 상품별 보너스 포인트
  const productBonus = calculateProductBonus(cartItems, prodList);
  finalPoints += productBonus.points;
  pointsDetail.push(...productBonus.details);

  // 수량별 보너스
  const quantityBonus = calculateQuantityBonus(itemCount);
  finalPoints += quantityBonus.points;
  pointsDetail.push(...quantityBonus.details);

  return { finalPoints, pointsDetail };
};

[개선 방안]

export const getLoyaltyPoints = (cartData) => {
  const basePoints = getBasePoints(cartData.totalAmount);
  const tuesdayBonus = getTuesdayBonus(basePoints);
  const productBonus = getProductBonus(cartData.cartItems, cartData.prodList);
  const quantityBonus = getQuantityBonus(cartData.itemCount);
  
  return {
    totalPoints: basePoints + tuesdayBonus + productBonus + quantityBonus,
    details: getPointDetails(basePoints, tuesdayBonus, productBonus, quantityBonus),
  };
};

  • 각 로직을 하나의 책임을 갖는 함수로 분리합니다.

2. 에러 처리 패턴 미흡

[문제점]

  • 하드코딩된 alert() 메시지
const addToCart = (productId: string) => {
  const product = getProduct(productId);
  
  if (!product) {
    alert('상품을 찾을 수 없습니다.'); 
    return;
  }
  
  if (!canAddToCart(product)) {
    alert('재고가 부족합니다.'); 
    return;
  }
  // ...
};

const startLightningSale = () => {
  // ...
  alert(`⚡ 번개세일! ${luckyItem.name}이(가) 20% 할인됩니다!`); // ❌ UI 로직이 훅에 포함
};
  • UI 로직인 alert() 가 비즈니스 로직에 섞여있습니다. (관심사 분리 위반)
  • 일관되지 않은 에러처리를 하고 있습니다. (어떤 곳은 alert, 어떤 곳은 return)

[개선 방안]

type Result<T, E = string> = 
  | { success: true; data: T }
  | { success: false; error: E };

const addToCart = (productId: string): Result<void> => {
  try {
    const product = getProduct(productId);
    if (!product) {
      return { success: false, error: 'PRODUCT_NOT_FOUND' };
    }
    
    if (!canAddToCart(product)) {
      return { success: false, error: 'INSUFFICIENT_STOCK' };
    }
    
    updateStock(productId, -1);
    addItem(productId, 1);
    return { success: true, data: undefined };
  } catch (error) {
    return { success: false, error: error.message };
  }
};

const handleAddToCart = (productId: string) => {
  const result = addToCart(productId);
  if (!result.success) {
    showNotification(result.error); // UI 로직은 컴포넌트에서
  }
};
  • Result 타입 패턴 적용
  • UI 컴포넌트에서 에러처리를 관리합니다.

3. 훅 내부의 Ui 요소 (alert)

[문제점]

export function useSaleManager({ products, updateSaleStatus }) {
  const startLightningSale = () => {
    // ...
    alert(`⚡ 번개세일! ${luckyItem.name}이(가) 20% 할인됩니다!`); // ❌ UI 로직
    updateSaleStatus(luckyItem.id, { /* ... */ });
  };
}
  • 관심사 분리 실패 (비즈니스 로직과 UI로직이 한 함수에 혼재)
  • 의존성 방향이 잘못 되었다.
useSaleManager → alert() // 훅이 UI에 의존

올바른 방향

Component → useSaleManager → 비즈니스 로직
Component → UI 렌더링

[개선 방향]

export function useSaleManager({ products, updateSaleStatus }) {
  const startLightningSale = () => {
    const luckyItem = selectRandomProduct(products);
    
    // 비즈니스 로직만 처리
    updateSaleStatus(luckyItem.id, { onSale: true, discountRate: 0.2 });
    
    // 이벤트 발생 (UI 로직은 컴포넌트에서 처리)
    return {
      type: 'LIGHTNING_SALE_STARTED',
      payload: {
        product: luckyItem,
        discountRate: 0.2,
        message: `⚡ 번개세일! ${luckyItem.name}이(가) 20% 할인됩니다!`
      }
    };
  };
  
  return { startLightningSale };
}

// 컴포넌트에서 UI 로직 처리
function SaleButton() {
  const { startLightningSale } = useSaleManager({ products, updateSaleStatus });
  const { showToast } = useNotification();
  
  const handleSaleStart = () => {
    const result = startLightningSale();
    
    if (result.type === 'LIGHTNING_SALE_STARTED') {
      showToast(result.payload.message);
    }
  };
  
  return <button onClick={handleSaleStart}>번개세일 시작</button>;
}
  • 훅은 순수한 비즈니스 로직만 담당
  • 다양한 알림 방식(토스트, 모달 등) 적용 가능

4. 하나의 context에 과도한 상태주입

[문제점]

CartProvider의 과도한 책임

interface CartContextType {
  // 상품 관련 (4개)
  products: Product[];
  getProduct: (productId: string) => Product | null;
  
  // 장바구니 아이템 관련 (2개)
  cartItems: Array<{ product: Product; quantity: number }>;
  getItemQuantity: (productId: string) => number;
  
  // 계산 관련 (3개)
  cartTotal: number;
  loyaltyPoints: number;
  discountInfo: string;
  
  // 재고 관련 (1개)
  stockStatus: string;
  
  // 액션 관련 (5개)
  addToCart: (productId: string) => void;
  handleRemove: (productId: string) => void;
  handleQuantityChange: (productId: string, change: number) => void;
  updateSaleStatus: (productId: string, saleInfo: SaleInfo) => void;
  clearCart: () => void;
}
  • 단일 책임 원칙 위반: 하나의 Context가 너무 많은 책임
  • 불필요한 리렌더링: 관련 없는 상태 변경이 전체 리렌더링 유발
  • 테스트 어려움: 너무 많은 의존성으로 테스트 복잡

[개선 방안]

// ProductContext - 상품 관련
interface ProductContextType {
  products: Product[];
  getProduct: (productId: string) => Product | null;
  stockStatus: string;
  updateSaleStatus: (productId: string, saleInfo: SaleInfo) => void;
}

// CartContext - 장바구니 관련
interface CartContextType {
  cartItems: Array<{ product: Product; quantity: number }>;
  getItemQuantity: (productId: string) => number;
  addToCart: (productId: string) => void;
  handleRemove: (productId: string) => void;
  handleQuantityChange: (productId: string, change: number) => void;
  clearCart: () => void;
}

// CalculationContext - 계산 관련
interface CalculationContextType {
  cartTotal: number;
  loyaltyPoints: number;
  discountInfo: string;
}
  • Context를 조금 더 세분화해 레이어를 나누어 책임을 분산시킵니다.

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

1. 디렉토리 구조에 대한 고민

src/
├── cart/                    # 장바구니 도메인
│   ├── components/         # 장바구니 관련 컴포넌트
│   ├── context/           # 장바구니 상태 관리
│   ├── hooks/             # 장바구니 관련 커스텀 훅
│   └── utils/             # 장바구니 유틸리티
├── order/                  # 주문 도메인
│   ├── components/         # 주문 관련 컴포넌트
│   └── hooks/             # 주문 관련 커스텀 훅
├── product/                # 상품 도메인
│   ├── components/         # 상품 관련 컴포넌트
│   ├── hooks/             # 상품 관련 커스텀 훅
│   └── utils/             # 상품 유틸리티
├── shared/                 # 공통 모듈
│   ├── components/         # 공통 컴포넌트
│   ├── hooks/             # 공통 커스텀 훅
│   └── utils/             # 공통 유틸리티
├── types/                  # TypeScript 타입 정의
├── App.tsx                 # 메인 앱 컴포넌트
├── main.tsx               # 앱 진입점
└── index.css              # 전역 스타일
  • 실제로 제가 최근까지 진행하고 있는 개인 프로젝트와 비슷한 도메인 기반의 디렉토리 구조로 설계했습니다.
  • 완전한 FSD 구조는 아니지만 어느정도 도메인 기반 Feature Sliced 한 구조라고 생각되어 이 구조를 선호하는데, 이 디렉토리 구조에 대해 코치님의 의견이 궁금합니다. 아쉬운점이 있다면 어떤 점을 추가적으로 고려하면 좋을지, 꼭 FSD가 아니어도 어떻게 하면 코드를 독립적이고 재사용 가능하게 관리할 수 있을지 궁금합니다.

2. 설계의 관점

Cart에 대한 상태나 로직을 작은 단위로 리팩토링 하는 것이 어려웠는데, 이렇게 복잡한 계산이 요구되는 경우에는 초기 설계가 중요할 것 같습니다. 초반에 어떤 것을 고려하며 코드를 작성하는 것이 좋을까요? 복잡한 상태를 훅으로 분리할 때, 어쩔 수 없이 훅 간의 의존성이 생기기도 합니다.

예를 들어, useNewCart, useCartActions, useCartItems와 같이 여러 커스텀 훅으로 분리하는 전략을 시도했습니다.

export function useNewCart({ initialProducts }: UseNewCartProps) {
  // 1단계: 상품 인벤토리 훅
  const { products, updateStock, updateSaleStatus, getProduct } =
    useProductInventory(initialProducts);
  
  // 2단계: 장바구니 아이템 훅 (products에 의존)
  const {
    cartItems,
    addItem,
    removeItem,
    updateItemQuantity,
    getItemQuantity,
    clearCart,
  } = useCartItems(products);

  // 3단계: 계산 훅 (cartItems에 의존)
  const totals = getCartTotals(cartItems);
  const stockStatus = getStockStatus(products);

  // 4단계: 액션 훅 (여러 의존성 주입)
  const { addToCart, handleRemove, handleQuantityChange } = useCartActions({
    products,        // useProductInventory에서
    getProduct,      // useProductInventory에서
    updateStock,     // useProductInventory에서
    addItem,         // useCartItems에서
    removeItem,      // useCartItems에서
    updateItemQuantity, // useCartItems에서
    getItemQuantity, // useCartItems에서
  });

  return {
    products,
    getProduct,
    cartItems,
    getItemQuantity,
    cartTotal: totals.cartTotal,
    loyaltyPoints: totals.loyaltyPoints,
    discountInfo: totals.discountInfo,
    stockStatus,
    addToCart,
    handleRemove,
    handleQuantityChange,
    updateSaleStatus,
    clearCart,
  };
}

이렇게 하나의 도메인을 위해 분리된 훅들이 서로를 의존해야 할 때, 이 관계를 어떻게 설계하는 것이 가장 이상적일까요?

3. 추상화 분리에 관하여

추상화 레벨의 일관성을 맞추기 위해 이런식으로 다른 훅을 조합하는 역할만 하는 훅이 만들어지는 경우가 생기는 것 같습니다. 이러한 분리도 의미가 있을까요?

과제 피드백

우선 지호님.. 회고문서 정말 대박이네요.. 이렇게 회고 열심히 작성해주신 분은 처음입니다. 열정이 보입니다!!

Q. FSD기반 슬라이스 구조..

A. 코드를 어떻게 하면 독립적이고 재사용가능성 있게 만들 수 있는지는 저도 아직까지 화두이고 항상 생각이 바뀌는 것 같아요. 어차피 정답은 없는 것일 것 같아요. "이 프로젝트에서는 현재의 구조가 최선으로 보인다" 정도의 믿음으로 필요한 부분들을 개선하려는 노력이 더 중요한 것 같습니다 끝판왕은 없으니까요.

저도 FSD의 슬라이스와 세그먼트구조는 괜찮다고 생각해요 컴포넌트 기반으로 개발하는 것이 일상화 된 상황에서는 어찌보면 자연스럽게 만들어지는 형태가 아닐까 생각합니다. 다만 레이어부분은 FSD가 정해놓은 것 보다는 프로덕트에 맞게 팀에서 적절히 다시 구성하는 편이 좋다는 생각정도만 하고 있습니다. 이렇게 프로젝트에 맞게 레이어를 구성해 위계를 만들고, 각 컴포넌트는 추상화된 개념으로 일관성있게 세그먼트 형태로 나눠서 개발하는 형태, 거기에 더해 수평적으로 공통적으로 사용할 수 있는 레이어정도만 추가되면 저는 일단 기본은 커버하는 구조라고 생각합니다. 물론 이것도 프로젝트마다 다를 것 같아요. 지금 지호님 이 잡으신 구조는 저는 오히려 FSD를 억지로 구현하려는 것보다, 장점만 취한 심플하고 좋은 구조라고 생각합니다. 어떻게 보면 제가 말씀드린 요건들을 다 갖춘것 같아요 다만 컴포넌트의 레이어 구분이 없긴하지만 사실 지금 과제를 기준으로 볼때는 레이어 구분은 오히려 오버엔지니어링일지도 모르겠네요.

Q. 훅들의 의존

A. 훅이 하나의 역할로만 잘 나눠지면 나줘질수록 오히려 의존성은 더 높아지는 것 같아요 어떻게보면 중복을 제거하는 과정이니 당연한 것 아닌 가 싶습니다. 의존은 피할 수 없을 것 같고 다만 훅도 모두 같은 훅으로 보지말고 훅도 적절한 개념의 레이어로 추상화해서 서로 위계를 만들고 의존의 방향을 설정해주 형태로 유지를 하는 편이 저는 옳은 것 같아요. 이것은 코드의 위치로도 구분할 수 있고 단순히 개념적으로 약속하고 네이밍으로 프리픽스나 서픽스를 다는형태로 구분할 수도 있을 것 같아요. 일단 레이어를 구분하기로 했다면 어떤형태로는 구분할 필요는 있어보입니다.

Q. 추상화 레벨의 일관성을 맞추기 위해 이런식으로 다른 훅을 조합하는 역할만 하는 훅이 만들어지는 경우가 생기는 것 같습니다. 이러한 분리도 의미가 있을까요?

A. 어떻게 보면 2번째 질문의 답변에서 같이 답변된 것 같은데요. 넵 이러한 분리도 당연히 의미가 있는 것 같아요. 기본 재료에 해당하는 훅이 있고 그런 레이어를 베이스훅 뭐 이런 이름으로 부르기로 "약속"할 수 있을 것 같아요. 그리고 그 재료들을 활용해서 더 추상화된 개념의 작업을 수행하는 훅도 있겠쭁. 다만 이때 의존의 방향을 한방향으로 흐르는 것으로 약속을 해야할 것 같아요. 레이어 안에서 의존을 허용할지 말지도 규칙을 정해야할 것이고요. 재사용성을 위해 어쩔수 없는 부분이라고 생각합니다.