과제 체크포인트
배포 링크
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 구현
기존 제품의 프로퍼티가 어떤 의미인지 알아보기 힘든 축약 명칭을 사용하고 있어, 프론트엔드 앱 내부에서 명시적으로 데이터를 사용할 수 있게 가공하여 사용했습니다. 서버에서 주는 인터페이스를 프론트엔드 앱 내부에서 사용하기 적절하게 가공하여 사용할 수 있도록, 원본 ProductData와 Product 타입을 따로 정의하고, 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 사용 환경에 대한 최적화
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
- 확장성, 유지보수성을 고려하여 설계한 현재 할인 정책 관련 데이터 타입과 유틸의 구조가 적절한지 궁금합니다.
- 같은 맥락으로, 실제 이커머스 도메인에서는 복잡한 할인, 포인트, 쿠폰 등의 정책을 어떻게 관리하고 반영하는지 궁금합니다.
과제 피드백
수고했어요, 은지! 이번 과제는 레거시 코드를 React 기반의 현대적인 아키텍처로 고도화하는 것이 목표였어요. 바쁜 일정 속에서도 기본 과제는 AI 활용, 심화 과제는 직접 설계로 명확히 구분해서 접근한 전략이 정말 효율적이었습니다. 커밋 과정에 아주 촘촘하게 개선한 과정이 인상깊네요.
Zustnad 도입부터 관심사 분리, 특히 할인정책등을 고민하면서 확장 가능한 구조를 만들기 위해 많은 고민을 하면서 이러한 구조를 만든 부분 아주 좋습니다.
복잡한 할인, 적립 정책에 대해서 보다 좋은 데이터 구조를 설계하고 만들고 싶은 부분에 꽃혀서 고민이 정말 많았었군요. 이 과정에서도 AI와 함께 많은 고민들을 나눠본것 같아요. 아주 좋네요. 앞으로도 뭔가 해보고 싶은게 생겼을 때 고민 파트너 대화 파트너로써 AI를 충분히 활용해보면 좋겠다 생각을 합니다.
Q) 확장성, 유지보수성을 고려하여 설계한 현재 할인 정책 관련 데이터 타입과 유틸의 구조가 적절한지 궁금합니다.
좋은 고민입니다. 이번 과제는 리팩토링이고 리팩토링의 목표는 최대한 기존의 코드를 유지하면서 나은 구조를 만드는 것이죠. 지금 고민하는건 재설계에 가까운 고민이라고 생각하는데 함께 생각을 해보도록 하죠.
현재의 구조는 PointPolicy enum을 통해 모든 정책을 관리하면서 text, 포인트 계산등의 함수로 만드는 방식으로 분리한 점입니다. 우선 어떤 정책들이 있는지 한눈에 볼 수 있고 배열을 통해서 순차적으로 조립할 수 있도록 만든 부분은 좋았습니다.
반면 지금의 새로운 정책을 추가하거나 삭제를 할때마다 enum, calculator, text 등 여러 곳을 동시에 수정해야 하는 문제가 있네요. 삭제를 해야할 때 한번에 말끔하게 삭제 되는 구조가 아니라면 응집도를 높다고 말하기는 어려워요. 지금의 구조에서 조금만 더 낫게 고쳐본다면 다음과 같이 정책 자체를 하나로 묶어 버리는게 좋다고 생각합니다.
javascript
const POINT_POLICIES = {
tuesday: {
id: 'tuesday',
name: '화요일 2배',
calculator: (originalPoint) => originalPoint * POINT_MULTIPLIER_TUESDAY,
condition: () => new Date().getDay() === 2
},
keyboardSet: {
id: 'keyboard-set',
name: 키보드+마우스 세트 +${POINT_BONUS_KEYBOARD_MOUSE_SET}p,
calculator: (originalPoint) => originalPoint + POINT_BONUS_KEYBOARD_MOUSE_SET,
condition: (cartItems) => hasKeyboardAndMouse(cartItems)
}
};
Q) 같은 맥락으로, 실제 이커머스 도메인에서는 복잡한 할인, 포인트, 쿠폰 등의 정책을 어떻게 관리하고 반영하는지 궁금합니다.
사실 필요한만큼만 개발을 한다는 전제대로 지금 정도의 수준이라면 복잡한 설계보다는 위와 같이 적절한 객체나 클래스 정도의 수준으로도 충분합니다. 그러나 소스 코드의 수정으로 정책을 변경할 수 있다라는 것은 정책이 바뀔때마다 소스 코드 배포가 필요하다는 말이기에 해당 기능들은 전부 데이터화 되죠.
그렇게 정책을 다루는 코드들이 데이터 베이스화 되고 API등에서 해당 정책들을 모아서 하나의 계산으로 만들어내는 Rule이나 계산식을 만들어주는 코드가 되죠. 나중에는 우선순위라던가 더 큰 상위정책등 - 가령 더 할인율이 큰거 하나만 적용과 같은 - 도 코드화됩니다. 이런한 경우 모바일, 웹 등의 일원화를 위해서 주로 백엔드가 해당 기능들을 구현하게 되죠.
규모와 수준에 따라 달라지고 정책마다 달라지기에 정해진 답은 없는데 도움이 되는 답변이길 바래요.
수고하셨습니다. 다음 주차도 화이팅입니다!