jeongmingi123 님의 상세페이지[2팀 정민기] Chapter 2-1. 클린코드와 리팩토링

과제 체크포인트

배포 URL : https://jeongmingi123.github.io/front_6th_chapter2-1/

기본과제

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

심화과제

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

과제 셀프회고

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

단일 책임과 관련된 함수를 어떻게 넣어야할지 고민이 많았습니다. 또한 깃 커밋을 어떻게 작성해야할지도 고민이 많았고, 폴더를 어떻게 나눌까에 대한 고민을 많이 신경써서 작성했습니다.

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

시간이 조금 부족하여 함수를 잘개 단일 책임에 따라 쪼개지 못한 부분이 너무 아쉽습니다. 시간이 좀 더 있었으면 아쉬움이 있습니다.

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

service 폴더 관련하여 궁금한 부분이있습니다. 제가 요번에 보너스 계산 점수를 service 폴더에 넣었는데요.

import { PRODUCT_IDS } from '../data/products';
import { CartItem, Product } from '../types';

interface BonusResult {
  bonus: number;
  detail: string;
}

interface SetBonusResult {
  bonus: number;
  details: string[];
}

interface PointsResult {
  totalPoints: number;
  details: string[];
}

// 기본 포인트 계산 (구매액의 0.1%)
export function calculateBasePoints(totalAmount: number): number {
  return Math.floor(totalAmount / 1000);
}

// 화요일 포인트 배수 적용
export function applyTuesdayBonus(basePoints: number): number {
  const today = new Date();
  const isTuesday = today.getDay() === 2;
  return isTuesday ? basePoints * 2 : basePoints;
}

// 특정 제품이 장바구니에 있는지 확인
function hasProductInCart(cartItems: CartItem[], productList: Product[], productId: string): boolean {
  return cartItems.some((item) => {
    const product = productList.find((p) => p.id === item.productId);
    return product && product.id === productId;
  });
}

// 키보드+마우스 세트 보너스 계산
function calculateKeyboardMouseBonus(cartItems: CartItem[], productList: Product[]): BonusResult {
  const hasKeyboard = hasProductInCart(cartItems, productList, PRODUCT_IDS.KEYBOARD);
  const hasMouse = hasProductInCart(cartItems, productList, PRODUCT_IDS.MOUSE);

  if (hasKeyboard && hasMouse) {
    return { bonus: 50, detail: '키보드+마우스 세트 +50p' };
  }

  return { bonus: 0, detail: '' };
}

// 풀세트 보너스 계산 (키보드+마우스+모니터암)
function calculateFullSetBonus(cartItems: CartItem[], productList: Product[]): BonusResult {
  const hasKeyboard = hasProductInCart(cartItems, productList, PRODUCT_IDS.KEYBOARD);
  const hasMouse = hasProductInCart(cartItems, productList, PRODUCT_IDS.MOUSE);
  const hasMonitorArm = hasProductInCart(cartItems, productList, PRODUCT_IDS.MONITOR_ARM);

  if (hasKeyboard && hasMouse && hasMonitorArm) {
    return { bonus: 100, detail: '풀세트 구매 +100p' };
  }

  return { bonus: 0, detail: '' };
}

// 세트 구매 보너스 포인트 계산
export function calculateSetBonus(cartItems: CartItem[], productList: Product[]): SetBonusResult {
  let bonus = 0;
  const details: string[] = [];

  // 키보드+마우스 세트 보너스
  const keyboardMouseBonus = calculateKeyboardMouseBonus(cartItems, productList);
  bonus += keyboardMouseBonus.bonus;
  if (keyboardMouseBonus.detail) {
    details.push(keyboardMouseBonus.detail);
  }

  // 풀세트 보너스
  const fullSetBonus = calculateFullSetBonus(cartItems, productList);
  bonus += fullSetBonus.bonus;
  if (fullSetBonus.detail) {
    details.push(fullSetBonus.detail);
  }

  return { bonus, details };
}

// 대량구매 보너스 포인트 계산
export function calculateQuantityBonus(totalQuantity: number): BonusResult {
  let bonus = 0;
  let detail = '';

  switch (true) {
    case totalQuantity >= 30:
      bonus = 100;
      detail = '대량구매(30개+) +100p';
      break;
    case totalQuantity >= 20:
      bonus = 50;
      detail = '대량구매(20개+) +50p';
      break;
    case totalQuantity >= 10:
      bonus = 20;
      detail = '대량구매(10개+) +20p';
      break;
    default:
      bonus = 0;
      detail = '';
  }

  return { bonus, detail };
}

// 총 포인트 계산
export function calculateTotalPoints(totalAmount: number, cartItems: CartItem[], productList: Product[]): PointsResult {
  let basePoints = calculateBasePoints(totalAmount);
  let finalPoints = 0;
  const details: string[] = [];

  // 기본 포인트
  if (basePoints > 0) {
    finalPoints = basePoints;
    details.push(`기본: ${basePoints}p`);
  }

  // 화요일 보너스
  const tuesdayPoints = applyTuesdayBonus(basePoints);
  if (tuesdayPoints !== basePoints) {
    finalPoints = tuesdayPoints;
    details.push('화요일 2배');
  }

  // 세트 보너스
  const setBonus = calculateSetBonus(cartItems, productList);
  finalPoints += setBonus.bonus;
  details.push(...setBonus.details);

  // 대량구매 보너스
  const totalQuantity = cartItems.reduce((sum, item) => sum + item.quantity, 0);

  const quantityBonus = calculateQuantityBonus(totalQuantity);
  finalPoints += quantityBonus.bonus;
  if (quantityBonus.detail) {
    details.push(quantityBonus.detail);
  }

  return {
    totalPoints: finalPoints,
    details: details,
  };
}

위의 코드를 작성하면서, 관심사의 분리 관련하여 궁금한 부분이 생겼습니다. service 라는 폴더는 옛날에 제가 작성했을 떄 api를 사용할 때 주로 fetch와 같은 함수를 작성할 때 사용했습니다. 근데 리팩토링 진행하면서 service 폴더는 도메인과 관련된 로직을 넣어야 된다고하여 위의 보너스 계산 관련된 로직을 위의 service 폴더에 넣었는데요. service라는 폴더는 도메인 관련된 함수를 넣는게 맞을지 궁금합니다.

그리고 한가지 더 여쭤보고 싶은게있습니다. util 관련된 함수들인데요.

import { Product } from '../types';

/**
 * 상품의 표시 이름을 생성합니다.
 * 번개세일, 추천세일 등의 정보를 포함하여 반환합니다.
 *
 * @param product - 상품 정보
 * @returns 표시용 상품명
 */
export const formatProductName = (product: Product): string => {
  let displayName = product.name;

  if (product.lightningSale) {
    displayName += ' ⚡번개세일!';
  }
  if (product.suggestSale) {
    displayName += ' 💝추천세일!';
  }

  return displayName;
};

/**
 * 상품의 가격을 표시용 텍스트로 변환합니다.
 * 번개세일인 경우 원가와 할인가를 함께 표시하고,
 * 일반 상품인 경우 현재 가격만 표시합니다.
 *
 * @param product - 상품 정보
 * @returns 가격 표시용 텍스트 (예: "15,000원 → 12,000원" 또는 "12,000원")
 */
export const displayProductPriceText = (product: Product): string => {
  // 번개세일이고 원가가 있는 경우: 원가 → 할인가 형식으로 표시
  if (product.lightningSale && product.originalPrice) {
    return `${product.originalPrice.toLocaleString()}원 → ${product.price.toLocaleString()}`;
  }
  // 일반 상품 또는 번개세일이 아닌 경우: 현재 가격만 표시
  return `${product.price.toLocaleString()}`;
};

제가 이 함수를 만들면서 service 폴더에 안넣고, util 함수로 selectorUtil이라는 함수로 만들었는데요. 적절한 파일 이름일지, 그리고 service 폴더에 넣는게 맞았을지 궁금합니다. 아니면 따로 컴포넌트로 차라리 만들었어야할까?? 라는 고민도 있는데 뭐가 정답일지 여쭤보고싶습니다.

과제 피드백

고생하셨습니다 민기님! 아쉽게 테스트가 통과하지 않아 기본 과제는 불합격 처리 했습니다. 과제는 잘 진행해주셨는데요! 이미 알고계신것처럼 조금 더 개선할 여지는 있어보여요. 체크리스트에 남아있지 않은 부분들을 채워나가면서 좀 더 개선해보면 좋을것 같습니다 ㅎㅎ

질문 주신 부분 답변 드려보면요.

서비스 관점에서의 개선

우선 지금은 서비스 자체가 단순화 되어있는 구조이기도 하고 그러다보니 서비스 레이어에서 담당하는 일이 많아진 거 같아요. 그러다 보니 비즈니스 로직이나 네트워크 통신, 도메인 규칙 등등이 담기게 될 것 같은데요. 이런 부분을 세분화하고 더 나아가는 형태로 고민해보지 않았을까 싶네요.

우선 질문 주신 첫 번째 질문인 도메인 관련 함수들을 서비스 내에 둬야 하는게 맞냐 라는 건 맞아보입니다. 그리고 너무 많은 책임을 지는 것 같아보이면 별도의 레이어로 나누는 식으로 개선해야 할 것 같아요.

그리고 유틸 함수에 있어서도 지금 보여지는건 UI적인 곳에 보여지는 값들을 위치시키는 내용이니 formatter같은 이름으로 UI에 가깝게 두는 형태면 좋지 않을까 싶은 개인적인 의견이 있습니다 ㅎㅎ

고생하셨고 다음주도 화이팅입니다!