과제 체크포인트
기본과제
- 코드가 Prettier를 통해 일관된 포맷팅이 적용되어 있는가?
- 적절한 줄바꿈과 주석을 사용하여 코드의 논리적 단위를 명확히 구분했는가?
- 변수명과 함수명이 그 역할을 명확히 나타내며, 일관된 네이밍 규칙을 따르는가?
- 매직 넘버와 문자열을 의미 있는 상수로 추출했는가?
- 중복 코드를 제거하고 재사용 가능한 형태로 리팩토링했는가?
- 함수가 단일 책임 원칙을 따르며, 한 가지 작업만 수행하는가?
- 조건문과 반복문이 간결하고 명확한가? 복잡한 조건을 함수로 추출했는가?
- 코드의 배치가 의존성과 실행 흐름에 따라 논리적으로 구성되어 있는가?
- 연관된 코드를 의미 있는 함수나 모듈로 그룹화했는가?
- ES6+ 문법을 활용하여 코드를 더 간결하고 명확하게 작성했는가?
- 전역 상태와 부수 효과(side effects)를 최소화했는가?
- 에러 처리와 예외 상황을 명확히 고려하고 처리했는가?
- 코드 자체가 자기 문서화되어 있어, 주석 없이도 의도를 파악할 수 있는가?
- 비즈니스 로직과 UI 로직이 적절히 분리되어 있는가?
- 코드의 각 부분이 테스트 가능하도록 구조화되어 있는가?
- 성능 개선을 위해 불필요한 연산이나 렌더링을 제거했는가?
- 새로운 기능 추가나 변경이 기존 코드에 미치는 영향을 최소화했는가?
- 코드 리뷰를 통해 다른 개발자들의 피드백을 반영하고 개선했는가?
- (핵심!) 리팩토링 시 기존 기능을 그대로 유지하면서 점진적으로 개선했는가?
심화과제
- 변경한 구조와 코드가 기존의 코드보다 가독성이 높고 이해하기 쉬운가?
- 변경한 구조와 코드가 기존의 코드보다 기능을 수정하거나 확장하기에 용이한가?
- 변경한 구조와 코드가 기존의 코드보다 테스트를 하기에 더 용이한가?
- 변경한 구조와 코드가 기존의 모든 기능은 그대로 유지했는가?
- (핵심!) 변경한 구조와 코드를 새로운 한번에 새로만들지 않고 점진적으로 개선했는가?
배포 링크
https://ldhldh07.github.io/front_6th_chapter2-1/
과제 셀프회고
과제를 해결하면서 얻고자 한것은 다음과 같습니다.
- 흐릿하게 알고 있는 클린 코드의 개념들을 선명하게 언어화하자.
- 모르고 있던 클린코드의 개념을 학습하자.
- 팀(이 과제의 경우 나 자신)의 스타일이 담긴 클린 코드를 구현하자
정돈되지 않았던 논리로 진행하던 클린코드의 지침들을 본격적으로 설명할 수 있고자 했습니다. 클린코드의 개념중 모르고 지키지 않고 있던 것을 새롭게 얻어가는 것 또한 의미가 있습니다.
특히 이번 과제에서는 스타일이 묻어있는 클린코드를 제대로 정립하고 싶은 욕심이 들었습니다.
클린 코드에는 어느정도 확실히 정답이 있는 부분도 있습니다. 그럼에도 어느정도 개인적인 색깔이 묻어있는, 하지만 클린코드의 기본 방향성에는 들어맞는 코드를 짜고 싶었습니다.
Original 코드
original -> basic -> advanced의 구성과 연동되는 실무 경험은 두 가지입니다.
- basic : 바닐라 JS의 더티 코드를 보고 클린 코드로 리팩터링 하는 경험
- advanced: 바닐라 기반의 레거시 코드를 React + TypeScript로 마이그레이션하는 경험
리팩토링을 위해 오리지널 코드의 문제부터 분석해봤습니다.
var prodList
var bonusPts = 0
...
// var를 통해 마구잡이로 선언된 전역변수들
function main() {
var root;
var header;
var gridContainer;
var leftColumn;
...
// 마찬가지로 var를 통해 선언된 변수들
var root = document.getElementById('app')
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>
`;
// DOM으로 만들어져 하드코딩으로 구성된 UI
for (var i = 0; i < prodList.length; i++) {
initStock += prodList[i].q;
}
onUpdateSelectOptions();
handleCalculateCartStuff();
// for 반복문 위주의 동작과 흐름의 전후가 뒤집힌 코딩
function handleCalculateCartStuff() {
var cartItems;
var subTot;
...
itemDiscounts = [];
lowStockItems = [];
for (idx = 0; idx < prodList.length; idx++) {
if (prodList[idx].q < 5 && prodList[idx].q > 0) {
lowStockItems.push(prodList[idx].name);
}
}
// 지나치게 한 함수가 많은 책임을 가진 형태
- 변수 및 스코프 관리 문제
- DOM 조작 및 UI 구성 문제
- 함수 설계 및 책임 분리 문제
- 흐름 및 로직 구조 문제
- 비즈니스 로직 및 데이터 관리 문제
- 유지보수성 및 확장성 문제
- 모던 JavaScript 패턴 미적용
거의 모든 문제가 종합되어있으며 이를 해결해야했습니다.
프로젝트의 구조
기본 구조
- 상품: 기본적인 CRUD 데이터
- 장바구니: 상품 + 수량의 단순한 조합
변칙적인 시스템
- 할인 정책: 개별/대량/화요일/번개세일/추천할인의 5단계 중첩
- 포인트 적립 시스템: 기본/화요일/콤보/대량구매의 4가지 보너스 조합
- 재고 시스템: 실시간 차감, 경고 임계값, UI 상태 연동
복합적으로 적용되는 시스템이 많습니다. 이 때 시스템간의 의존성, 우선순위를 고려해야 했습니다. 이 서비스의 특징적인 점은 규모에 비해 데이터 간 의존성의 복잡도가 높다는 점입니다. 프로젝트 내의 함수들을 리팩토링하는데 있어서 이 의존성을 유지하고, 리팩토링 범위 안에서 최적화시키는 것에 신경을 썼습니다.
지침 만들기
지침을 만드는 것이 이번 과제의 핵심이라고 생각이 들었습니다. 이 과정에서 기존의 스탠다드들을 이해하고 이 프로젝트에 '일관적'으로 적용될 지침을 만들었습니다.
그중에서도 우선순위에 윗선에 들어갈 클린코드 요소들을 고려했습니다.
- 명시성
- 인접성
- 일관성
시기별로 코딩하는 데 있어서 꽃혀있는 특성들이 있는데 지금 시즌에는 이 특성들을 충족하는 데에 많은 고민을 하고 있습니다.
위 특성들은 모두 '인지 부하'와 관련이 되어있습니다. 실무에서는 생각보다 인지 에너지를 절약해야 하는 경우가 많아 인지 부하를 최소화하는 것이 생각보다 더 중요했습니다.
이전까지 중요하다고 생각했던 것
- 일단 분리하고 보기
- 파일은 한번에 보기 쉽게 짧아야 한다
- 관심사에 따라 파일 단위로 나뉘어야 한다.
- 연산자 등을 이용한 잔기술로 코드 짧게 만들기
- 재사용 한번만 되어도 분리해버리기
작업을 반복해가면서 이런 방식이 생산성이 떨어질 뿐더러 파일을 여러개 읽어야 하는 경우 다시 파악할때 인지하기가 힘들다는걸 느꼈습니다. 그래서 이 반작용으로 명시적이고 흐름을 인근에서 파악할 수 있는 코드를 쓰고자 했습니다.
명시성
영리한 코드보다 명확한 코드가 좋은 코드다
행동 지침
- 복잡한 조건은 변수로 추출하여 이름을 붙이기
- 부정 조건보다 긍정 조건을 사용
- 조기 반환(early return)으로 중첩을 줄이기
- 의도가 드러난 명확한 변수명 사용
function handleCalculateCartStuff() {
var q = parseInt(qtyElem.textContent);
var disc = 0;
var curItem = /* 복잡한 검색 */;
if (q >= 10) {
if (curItem.id === PRODUCT_ONE) {
disc = 10 / 100;
} else {
if (curItem.id === p2) {
disc = 15 / 100;
} else {
if (curItem.id === product_3) {
disc = 20 / 100;
}
}
}
}
totalAmt += itemTot * (1 - disc);
}
```ts
export const applyProductDiscount = (product, quantity, threshold) => {
// 복잡한 조건을 변수로 추출
const qualifiesForDiscount = quantity >= threshold;
if (qualifiesForDiscount) {
return getProductDiscountRate(product.id);
}
return 0;
};
// 상수로 의미를 명확히 표현
const PRODUCT_DISCOUNT_MAP = {
[PRODUCT_ONE]: KEYBOARD_DISCOUNT_RATE, // 0.10 (10%)
[PRODUCT_TWO]: MOUSE_DISCOUNT_RATE, // 0.15 (15%)
[PRODUCT_THREE]: MONITOR_ARM_DISCOUNT_RATE, // 0.20 (20%)
[PRODUCT_FOUR]: SUGGESTION_DISCOUNT_RATE, // 0.05 (5%)
[PRODUCT_FIVE]: SPEAKER_DISCOUNT_RATE, // 0.25 (25%)
};
// 부정 조건보다 긍정 조건 사용
export const applyBulkDiscount = (itemCount, subtotal) => {
const qualifiesForBulkDiscount = itemCount >= BULK_DISCOUNT_THRESHOLD;
if (qualifiesForBulkDiscount) {
return {
totalAmount: subtotal * (1 - BULK_DISCOUNT_RATE),
discRate: BULK_DISCOUNT_RATE,
type: "bulk",
};
}
return null;
};
인접성
관련된 코드는 물리적으로 가깝게 배치하라
행동 지침
- 같은 목적의 코드는 연속된 블록으로 작성
- 다른 관심사가 섞이면 즉시 함수로 분리
- 빈 줄로 논리적 블록을 구분하라
- 같은 기능과 관련된 코드는 같은 폴더 안에 포함하라
original
function main() {
var cartItems = []; // 상태 정의
// ... 100줄 뒤
function addToCart() { /* 추가 로직 */ }
// ... 50줄 뒤
function updateCartDisplay() { /* UI 업데이트 */ }
// ... 80줄 뒤
function calculateTotal() { /* 계산 로직 */ }
}
변경후
export const calculateCompleteCartTotals = (cartItems, productList, constants) => {
const cartItemsData = cartItems.map(cartItem => calculateItemData(cartItem, productList));
const { totalAmount, itemCount, subtotal } = cartItemsData.reduce(...);
const bulkDiscount = applyBulkDiscount(itemCount, subtotal);
const tuesdayDiscount = calculateTuesdayDiscount(...);
return { totalAmount: finalAmount, itemCount, ... };
};
AI 협업을 통한 지침 생성
AI에게 이런 식으로 질문했습니다:
"지금부터 지침을 만들 것이고 공인된 자료를 제공할 게, 이 자료들을 종합하되 선택의 여지가 있는 부분은 물어봐주면 답해줄게" 이 과정을 통해 공인된 클린코드 원칙들 (SRP, DRY, KISS 등)을 기반으로 하되 그중에서도 특히 강조하는 부분이 있는 팀(개인) 스타일이 담긴 구체적인 지침을 만들었습니다
특히 강조한 부분은 다음과 같습니다.
- 변수명을 지을 때 작동 로직이 아닌 의도가 담긴 변수명을 사용할 것 clickBlueButton -> showModal
- let 사용을 최소화 하고 선언형으로 결과를 사용할 것
- 파일을 과도하게 분리하지 말 것
ESLint와 Prettier 활용
ESLint와 Prettier를 사용했습니다.
import js from "@eslint/js";
import prettier from "eslint-config-prettier";
export default [
js.configs.recommended,
prettier,
{
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
globals: {
document: "readonly",
window: "readonly",
console: "readonly",
alert: "readonly",
setTimeout: "readonly",
setInterval: "readonly",
Math: "readonly",
Date: "readonly",
parseInt: "readonly",
Object: "readonly",
},
},
rules: {
"no-var": "error",
"prefer-const": "error",
"no-unused-vars": "warn",
"no-console": "off",
semi: ["error", "always"],
},
},
];
작업의 흐름
기본적으로는 작은 단위의 수정 -> 큰 단위의 수정으로 작업을 진행했습니다. 먼저 프로젝트의 큰 골자를 파악하는 게 우선이라고 생각했습니다.
그러기 위해 가장 미시적인 부분부터 리팩토링을 해서 일단 읽을 수 있는 코드를 만들었습니다.
A. 변수명 수정, 매직 넘버 분리
변경전
var q = parseInt(qtyElem.textContent);
var disc = 0;
var curItem;
var itemTot = curItem.val * q;
변경후
const quantity = parseInt(quantityElement.textContent);
const discountRate = 0;
const selectedProduct = findProductById(productId, productList);
const itemTotal = selectedProduct.price * quantity;
B. 기능별 코드 분리
이후 코드를 분산시켰습니다. 분산시킨 기준은 기능이었습니다.
구조
과제에서 제공하는 서비스의 규모를 봤을 때 각 기능별로 4개의 파일이면 나눌 수 있겠다고 판단했습니다. AI에 코딩을 맡겼을 떄 처음에는 전부 모듈화해서 폴더를 쪼개려고 했지만, 더 이야기를 나눠보고 기능당 파일 하나로 응집되게 설계했습니다.
쪼개는 기준 또한 레이어 단위가 아닌 피쳐 단위로 설계했습니다.
basic
// 레이어 분리
src/
├── models/ # 모든 도메인의 데이터 모델
├── services/ # 모든 도메인의 비즈니스 로직
├── components/ # 모든 UI 컴포넌트
└── utils/ # 공통 유틸리티
// 기능 우선 분리
src/
├── features/
│ ├── cart/ # 장바구니 관련 모든 것
│ ├── products/ # 상품 관련 모든 것
│ └── discounts/ # 할인 관련 모든 것
└── shared/ # 공통 기능만
advanced
src/basic/
├── features/
│ ├── cart.js
│ │ # calculateCompleteCartTotals()
│ │ # addCartItem(), removeCartItem()
│ │ # updateCartDisplay()
│ │
│ ├── products.js
│ │ # initializeProducts()
│ │ # findProductById()
│ │ # getLowStockProducts()
│ │ # getOptionData()
│ │
│ ├── discounts.js
│ │ # calculateProductDiscount()
│ │ # applyBulkDiscount()
│ │ # calculateTuesdayDiscount()
│ │
│ ├── points.js
│ │ # calculateEarnedPoints()
│ │ # updatePointsDisplay()
│ │ # hidePointsIfEmpty()
│ │ (104줄, 3KB)
│ # setupLightningEvent()
│ # setupSuggestionEvent()
│
├── constants.js
└── main.basic.js
많이 쪼개지 않는 코드
이번 리팩토링의 추구미는 많이 쪼개지 않는 코드였습니다. 앞서 언급한 맥락과도 이어집니다.
페이지 하나짜리 서비스인 만큼 파일 분리를 최소화하고 대신 같이 작동하는 코드들은 한 파일에서 인접해있게 만들고자 했습니다. 동시에 데이터 동작 흐름대로 코드를 배치하고자 했습니다.
export const calculateCompleteCartTotals = (cartItems, productList, constants) => {
// 1. 데이터 준비
const cartItemsData = cartItems.map(cartItem => calculateItemData(cartItem, productList));
// 2. 계산
const { totalAmount, itemCount, subtotal, itemDiscounts } = cartItemsData.reduce((acc, itemData) => {
const { quantity, itemTotal, discount, product } = itemData;
const discountedTotal = itemTotal * (1 - discount);
return {
totalAmount: acc.totalAmount + discountedTotal,
itemCount: acc.itemCount + quantity,
subtotal: acc.subtotal + itemTotal,
itemDiscounts: discount > 0 ? [...acc.itemDiscounts, { name: product.name, discount: discount * 100 }] : acc.itemDiscounts,
};
}, { totalAmount: 0, itemCount: 0, subtotal: 0, itemDiscounts: [] });
// 3. 할인 정책 적용
const originalTotal = subtotal;
const bulkDiscount = applyBulkDiscount(itemCount, subtotal);
const afterBulkAmount = bulkDiscount ? bulkDiscount.totalAmount : totalAmount;
const tuesdayDiscount = calculateTuesdayDiscount(afterBulkAmount, originalTotal, TUESDAY_DAY_NUMBER, TUESDAY_ADDITIONAL_DISCOUNT_RATE);
// 4. 최종 계산 및 반환
const finalTotalAmount = tuesdayDiscount.totalAmount;
const earnedPoints = Math.floor(finalTotalAmount / POINTS_CALCULATION_BASE);
return {
totalAmount: finalTotalAmount,
itemCount,
earnedPoints,
discountInfo: { bulkDiscount, tuesdayDiscount, itemDiscounts },
lowStockItems: getLowStockProducts(productList, LOW_STOCK_THRESHOLD)
};
};
C. 함수 단위의 리팩토링/데이터 흐름 기준 재배치
- 선언형으로 결과 정의
- 조건문 로직 조정
- 3번 이상의 중복되는 유틸 함수 분리
- 추상화 레벨 조정
변경전
// 어떻게 할지에 집중
var initStock = 0;
for (var i = 0; i < prodList.length; i++) {
initStock += prodList[i].q;
}
변경후
const PRODUCT_DISCOUNT_MAP = {
[PRODUCT_ONE]: 0.10,
[PRODUCT_TWO]: 0.15,
[PRODUCT_THREE]: 0.20,
};
export const applyProductDiscount = (product, quantity, threshold) => {
if (quantity < threshold) return 0;
return PRODUCT_DISCOUNT_MAP[product.id] || 0;
};
변경전 - 중첩 if
if (q >= 10) {
if (curItem.id === PRODUCT_ONE) {
disc = 10 / 100;
} else {
if (curItem.id === p2) {
disc = 15 / 100;
} else {
if (curItem.id === product_3) {
disc = 20 / 100;
}
}
}
}
변경후 - 매핑 or early return
const PRODUCT_DISCOUNT_MAP = {
[PRODUCT_ONE]: 0.10,
[PRODUCT_TWO]: 0.15,
[PRODUCT_THREE]: 0.20,
};
export const applyProductDiscount = (product, quantity, threshold) => {
if (quantity < threshold) return 0;
return PRODUCT_DISCOUNT_MAP[product.id] || 0;
};
변경전
// 상품 찾기가 5군데에서 반복
for (var j = 0; j < prodList.length; j++) {
if (prodList[j].id === cartItems[i].id) {
curItem = prodList[j];
break;
}
}
변경후
export const findProductById = (productId, productList) => {
return productList.find(product => product.id === productId);
};
이 1->2->3 단계를 거친후에는 3단계를 계속해서 반복하는 위주로 리팩토링을 진행했습니다.
새롭게 배운 점
단순히 하나의 함수는 하나의 책임만을 수행해야 한다는 개념을 추상화 레벨의 일관성이라는 개념으로 확장시킬 수 있었습니다.
function calcCart() {
for (let i = 0; i < cartItems.length; i++) {
// === 데이터 검색 ===
var curItem;
for (var j = 0; j < prodList.length; j++) {
if (prodList[j].id === cartItems[i].id) {
curItem = prodList[j];
break;
}
}
// === DOM 직접 조작 ===
var qtyElem = cartItems[i].querySelector('.quantity-number');
var q = parseInt(qtyElem.textContent);
// === 비즈니스 로직 ===
var disc = 0;
if (q >= 10) {
if (curItem.id === PRODUCT_ONE) {
disc = 10 / 100; // 할인율 계산
}
}
// === 수치 계산 ===
itemTot = curItem.val * q;
totalAmt += itemTot * (1 - disc);
}
}
original : 비즈니스 로직에서 단순한 수치를 계산하는 로직이나 할인율을 계산하는 로직이 같은 함수 안에 위치하고 있다.
// App.tsx
function App() {
const { cartItems, products } = useCart();
const { discountResult } = useDiscounts();
const { pointsResult } = usePoints();
const { totalAmount, itemCount } = useCartTotals(cartItems, products);
return (
<CartLayout>
<ProductSelector />
<CartItems />
<OrderSummary />
</CartLayout>
);
}
반면 리팩토링 이후에는 훅으로 반환받는 값들이나 UI 데이터 같이 같은 추상화 레벨에 있는 요소들로 하나의 함수가 구성되어있습니다. 어떤 단위로 함수를 분리하고 호출해야하는지 기준을 잡을 수 있었습니다.
AI 코딩
리팩토링은 AI가 잘하는 작업입니다. 작업량 또한 많아서 이번 과제를 통해 AI 코딩을 적극적으로 활용했습니다.
- 태스크 쪼개기
- 회귀테스트
- 문서화를 통한 히스토리 및 룰 관리
- ai의 코드도 컨트롤 아래에 두기
AI 코딩의 부작용을 유발시키는 가장 큰 원인이 한번에 이뤄지는 많은 양의 변경입니다.
그래서 변경을 작게 하도록 룰을 설정했습니다. 여러 파일을 한번에 바꾸지 말고, 한번의 하나의 변화만 한 후 변경 직후 테스트를 통해 검증받도록 했습니다.
AI가 코딩하는 내용을 놓치지 않고 따라가기에도 적은 변화를 유도하는 것이 유리했습니다.
장점
- 진행상황 및 청사진을 알 수 있다.
- 태스크 쪼갬으로서 해야할 작업을 더 명확하게 ai가 인식하고 해결법을 제시한다.
문서화를 이용해서 히스토리를 유지하는 전략은 이전 과제들에서도 유용하게 사용했습니다.
점점 그 활용법도 늘어났는데, 이번 과제에서는 문서의 양이 지나치게 많아져 안좋은 예에 더 가까워졌습니다. 지침 부분에 대한 자세한 초반 세팅과 이후 최신화해나가는 과정은 좋았습니다. 하지만 진행상황에 대한 문서화의 양이 많아져서 효율이 떨어졌습니다.
정리된 문서를 읽으면서 이 과정에 대해 다시 이해하고 과정을 돌아볼 수 있는 점은 좋았습니다.
최종 회고
- Basic 코드에서는 뷰와 비즈니스 로직을 구분하고 기능별로 응집된 코드를 작성했습니다.
- advanced는 basic에서 만든 구조와 대응을 통해 마이그레이션 작업에서의 사이드 이펙트를 최소화했습니다.
코딩을 처음 접하면서 클린 코딩하는 데에 과하게 집착했던 시기가 있었습니다. 어느 순간 코딩을 일정 수준 이상으로 클린하게 작성하는 것이 그렇게 중요한가하는 생각이 점점 강해지고 있었습니다. 그런 시기에 그 중심을 잡아줄 수 있는 개념을 학습했던 과제였습니다.
과제 피드백
아니 두현님.. MR 회고내용이 너무 좋네요! 와 9팀은 정말 인재들이 많네요 ㅎㅎ
내용중에 객체 맵을 활용한 전략패턴 비스무리한 코드는 정말 특 참 좋은 전략이라고 생각해요. 복잡한 if-else를 먼저 고려하기전에 비교 조건이 단순화 가능하다면 객체 맵을 활용하는 형태가 자바스크립트 다운 선택이라고 생각합니다. 성능을 떠나서 코드 자체가 확줄고 가독성이 확 올라가거든요 :)
수고많으셨습니다!