Skip to content

[1팀 이은지] Chapter 2-1. 클린코드와 리팩토링#55

Open
angielxx wants to merge 55 commits intohanghae-plus:mainfrom
angielxx:main
Open

[1팀 이은지] Chapter 2-1. 클린코드와 리팩토링#55
angielxx wants to merge 55 commits intohanghae-plus:mainfrom
angielxx:main

Conversation

@angielxx
Copy link

@angielxx angielxx commented Jul 31, 2025

과제 체크포인트

배포 링크

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

기본과제

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

심화과제

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

과제 셀프회고

주요 변경사항

이번 주 회사 일이 바쁜 탓에 절대적인 시간 부족으로 기본 과제의 경우 AI를 최대한 활용하여 진행했습니다. 심화과제의 경우 아주 일부를 제외하곤 직접 설계하고 구현하려고 노력했습니다.

1. 컴포넌트 기반 UI 구조

UI로직을 컴포넌트 기반으로 모듈화하여 분리했습니다.

src/
├── basic/ # Vanilla JS 구현
│   ├── components/ # UI 컴포넌트
│   │   ├── cart/ # 장바구니 관련 컴포넌트
│   │   ├── product/ # 상품 관련 컴포넌트ㅌ
│   │   └── order/ # 주문 관련 컴포넌트

재사용 가능한 컴포넌트들:

  • CartItem.js: 장바구니 아이템 렌더링
  • ProductPrice.js: 가격 표시 (할인가 계산 포함)
  • OrderSummary.js: 주문 요약 섹션
  • BasicDiscount.js: 기본 할인 정보 표시
  • SpecialDiscount.js: 특별 할인 정보 표시

2. Hook 기반 비즈니스 로직 분리

계산하는 로직은 각 관심사 별로 훅을 만들어 사용했습니다.

Before:

// src/main.original.js

// 계산 로직과 UI 업데이트가 혼재
var qtyElem = cartItems[i].querySelector('.quantity-number');
var q = parseInt(qtyElem.textContent); // 📊 계산: 수량 파싱
itemTot = curItem.val * q; // 📊 계산: 아이템 총액
disc = 0; // 📊 계산: 할인 초기화
itemCnt += q; // 📊 계산: 총 수량 누적
subTot += itemTot; // 📊 계산: 소계 누적

var itemDiv = cartItems[i];
var priceElems = itemDiv.querySelectorAll('.text-lg, .text-xs');
priceElems.forEach(function (elem) {
  // 🎨 UI: DOM 요소 스타일 변경
  if (elem.classList.contains('text-lg')) {
    elem.style.fontWeight = q >= 10 ? 'bold' : 'normal';
  }
});

//...

After:

// src/basic/hooks/ - 비즈니스 로직 분리

hooks
├── useDiscount.js        // 할인 계산 로직을 담당하는 커스텀 훅
├── useOrderSummary.js    // 주문 요약(총액, 할인 등) 계산 및 제공 훅
├── usePoint.js           // 포인트 적립/사용 관련 비즈니스 로직 훅
└── useStock.js           // 상품 재고 관리 및 확인 로직 훅

3. 유틸리티 함수 모듈화

관심사 별로 매직넘버를 상수화하고 유틸함수를 분리하여 관리했습니다.

상수 및 설정값 분리:

// src/basic/data/
├── discount.data.js     # 할인율 상수
├── product.data.js      # 상품 데이터
├── quantity.data.js     # 수량 관련 상수
└── point.data.js        # 포인트 정책 상수

순수 함수 유틸리티:

// src/basic/utils/
├── cart.util.js         # 장바구니 조작 함수들
├── discount.util.js     # 할인 계산 함수들
├── product.util.js      # 상품 관련 함수들
├── point.util.js        # 포인트 계산 함수들
└── stock.util.js        # 재고 관리 함수들

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

Advanced 버전에서 TypeScript를 도입하며 현대적 아키텍처 기반의 확장성, 유지보수성을 고민했습니다.

1. 타입 시스템 및 API 구현

기존 제품의 프로퍼티가 어떤 의미인지 알아보기 힘든 축약 명칭을 사용하고 있어, 프론트엔드 앱 내부에서 명시적으로 데이터를 사용할 수 있게 가공하여 사용했습니다. 서버에서 주는 인터페이스를 프론트엔드 앱 내부에서 사용하기 적절하게 가공하여 사용할 수 있도록, 원본 ProductDataProduct 타입을 따로 정의하고, API를 구현하여 데이터 요청 후 프론트엔드 앱에 맞게 가공하여 데이터를 저장하여 사용하도록 설계했습니다.

타입 정의 (product.type.ts):

// src/advanced/types/product.type.ts

// 서버 응답 데이터 타입
export interface ProductData {
  id: string;
  name: string;
  val: number; // 서버에서 사용하는 가격 필드명
  originalVal: number; // 서버에서 사용하는 원가 필드명
  q: number; // 서버에서 사용하는 수량 필드명
  onSale: boolean;
  suggestSale: boolean;
}

// 프론트엔드에서 사용하는 정제된 타입
export interface Product {
  id: string;
  name: string;
  price: number; // 정제된 필드명
  originalPrice: number; // 정제된 필드명
  stock: number; // 정제된 필드명
  onSale: boolean;
  suggestSale: boolean;
  discountRate: number;
}

// 상품 상태 열거형
export enum ProductStatus {
  OUT_OF_STOCK = 'outOfStock',
  SUPER_SALE = 'superSale',
  LIGHTNING_SALE = 'lightningSale',
  SUGGESTION_SALE = 'suggestionSale',
  NORMAL = 'normal',
}

API 구현 (getProducts.ts):

// src/advanced/api/getProducts.ts
export default async function getProducts(): Promise<Product[]> {
  return Promise.resolve(formatProductData(PRODUCT_LIST));
}

// 어플리케이션 내부에서 필요로하는 인터페이스에 맞게 가공
function formatProductData(productData: ProductData[]): Product[] {
  return productData.map(product => ({
    id: product.id,
    name: product.name,
    price: product.val,
    originalPrice: product.originalVal,
    stock: product.q,
    onSale: product.onSale,
    suggestSale: product.suggestSale,
    discountRate: 0,
  }));
}

2. 관심사에 따른 분리

컴포넌트 및 스토어, 타입, 유틸 함수 등을 관심사에 따라 분리했습니다. UI의 근본이 되는 데이터인 제품 목록, 장바구니 목록은 전역 상태로 저장하고 그 외의 계산하는 로직은 커스텀 훅으로 분리하여 사용했습니다.

타입 기반 아키텍처:

// src/advanced/types/
├── product.type.ts     # 상품 관련 타입
├── cart.type.ts        # 장바구니 관련 타입
├── point.type.ts       # 포인트 정책 타입
└── discount.type.ts    # 할인 관련 타입

상태 관리 (Zustand):

// src/advanced/store/
├── useProductStore.ts  # 상품 목록 전역 상태
├── useCartStore.ts     # 장바구니 전역 상태
└── useLayoutStore.ts   # UI 상태 관리

관심사별 커스텀 훅

// src/advanced/hooks/
├── useDiscount.ts              # 할인 정책 계산 
├── useLightningSaleTimer.ts    # 번개 세일 타이머 관리 
├── useOrderSummary.ts          # 주문 요약(합계 ) 계산 
├── usePoint.ts                 # 포인트 정책 계산 
├── useSuggestionSaleTimer.ts   # 제안 세일 타이머 관리 
└── useTimer.ts                 # 범용 타이머 

3. 확장 가능한 구조 (할인 정책)

할인 및 포인트 정책 관리와 적용 시 정책이 추가되거나 삭제되어도 확장 가능하도록 설계하고자 했습니다. 코드를 읽기만해도 어떤 정책 종류가 있고, 각 정책별 로직을 파악할 수 있도록 작성하고자 했습니다.

포인트 정책 타입 정의:

// src/advanced/types/point.type.ts

enum PointPolicy {
  DEFAULT = 'default',
  TUESDAY = 'tuesday',
  KEYBOARD_SET = 'keyboard-set',
  FULL_SET = 'full-set',
  BULK_BONUS_10 = 'bulk-bonus-10',
  BULK_BONUS_20 = 'bulk-bonus-20',
  BULK_BONUS_30 = 'bulk-bonus-30',
}

포인트 정책 타입별 계산 로직 유틸

// src/advanced/utils/point.util.ts

export const getPointCalculator = (policy: PointPolicy) => {
  const calculator: Record<PointPolicy, (originalPoint: number, totalPrice: number) => number> = {
    [PointPolicy.DEFAULT]: (_, totalPrice) => Math.floor(totalPrice / POINT_RATE_BASE),
    [PointPolicy.TUESDAY]: originalPoint => originalPoint * POINT_MULTIPLIER_TUESDAY,
    [PointPolicy.KEYBOARD_SET]: originalPoint => originalPoint + POINT_BONUS_KEYBOARD_MOUSE_SET,
    [PointPolicy.FULL_SET]: originalPoint => originalPoint + POINT_BONUS_FULL_SET,
    [PointPolicy.BULK_BONUS_10]: originalPoint => originalPoint + POINT_BONUS_QUANTITY_TIER1,
    [PointPolicy.BULK_BONUS_20]: originalPoint => originalPoint + POINT_BONUS_QUANTITY_TIER2,
    [PointPolicy.BULK_BONUS_30]: originalPoint => originalPoint + POINT_BONUS_QUANTITY_TIER3,
  };

  return calculator[policy];
};

복수의 포인트 정책 적용

포인트 정책이 새롭게 추가될 것을 고려하여 적용할 포인트 정책을 배열에 저장하고, 해당 배열을 순회하며 포인트 정책별 로직을 결과값에 적용할 수 있도록 설계했습니다.

// src/advanced/hooks/usePoint.ts - 실제 구현

const calculateTotalPoint = () => {
  let currentPoint = defaultPoint;

  // 각 정책을 순차적으로 적용
  applicablePolicies.forEach(policy => {
    currentPoint = getPointCalculator(policy)(currentPoint, totalPrice);
  });

  return currentPoint;
};

// 정책 결정 로직
const getPointPolicies = () => {
  const policies: PointPolicy[] = [];

  if (isTuesday) {
    policies.push(PointPolicy.TUESDAY);
  }

  if (hasFullSet) {
    policies.push(PointPolicy.FULL_SET);
  } else if (hasKeyboard) {
    policies.push(PointPolicy.KEYBOARD_SET);
  }

  // 대량 구매 보너스 정책
  if (totalCount >= MIN_QUANTITY_FOR_POINT_BONUS_TIER3) {
    policies.push(PointPolicy.BULK_BONUS_30);
  } else if (totalCount >= MIN_QUANTITY_FOR_POINT_BONUS_TIER2) {
    policies.push(PointPolicy.BULK_BONUS_20);
  } else if (totalCount >= MIN_QUANTITY_FOR_POINT_BONUS_TIER1) {
    policies.push(PointPolicy.BULK_BONUS_10);
  }

  return policies;
};

const totalPoint = calculateTotalPoint();

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

1. 데이터 구조 설계의 아쉬움

할인, 적립 정책을 실제 이커머스 서비스를 관찰하여 데이터 구조를 설계했으면 좋았을 것 같다.

2. 더욱 효과적인 AI 활용

천재적인 항해 동료들 덕분에 AI 활용 꿀팁들을 많이 얻었지만 아직 다른 분들에 비해 AI를 효과적으로 활용하지 못하고 있는 것 같아 아쉽습니다.

AI 활용에 대한 발전 방향

  • 막연한 요청 대신 구체적이고 체계적인 질문으로 빠르게 원하는 결과물 얻기
  • 설계 검토 파트너로서 아키텍처 결정에 대한 검증과 대안 탐색
  • cursor 환경이나 CLI 등 AI 사용 환경에 대한 최적화

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

  • 확장성, 유지보수성을 고려하여 설계한 현재 할인 정책 관련 데이터 타입과 유틸의 구조가 적절한지 궁금합니다.
  • 같은 맥락으로, 실제 이커머스 도메인에서는 복잡한 할인, 포인트, 쿠폰 등의 정책을 어떻게 관리하고 반영하는지 궁금합니다.

angielee0304 and others added 30 commits July 28, 2025 17:49
@Hwirin-Kim
Copy link

폴더 구조가 정말 가독성이 좋네요...! 한 주간 고생 많으셨습니다!


const isBulkDiscount = getCartTotalCount(cartItems) >= 30;

const isTuesday = new Date().getDay() === TUESDAY_DAY_OF_WEEK;
Copy link

@creco-hanghae creco-hanghae Aug 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isTuseday 를 utils 함수로 빼는 것도 좋을 것 같아요

e.g.)

import { isTuesday } from '@/advanced/utils/day';

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.

4 participants