이번에는 냅다 더티코드가 주어지고 더티코드를 클린코드로 바꾸는 주차였다.
어떤게 클린코드인지 왜 클린코드인지에 대해 고민하는 시간이었다.
// 과제 일부 발췌
var prodList, sel, addBtn, cartDisp, sum, stockInfo;
var lastSel, bonusPts=0, totalAmt=0, itemCnt=0;
function main() {
prodList=[
{id: 'p1', name: '상품1', val: 10000, q: 50 },
{id: 'p2', name: '상품2', val: 20000, q: 30 },
];
var root=document.getElementById('app');
let cont=document.createElement('div');
cartDisp.id='cart-items';
sum.id='cart-total';
stockInfo.id='stock-status';
wrap.className='max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl p-8';
hTxt.className='text-2xl font-bold mb-4';
hTxt.textContent='장바구니';
addBtn.textContent='추가';
updateSelOpts();
wrap.appendChild(hTxt);
wrap.appendChild(addBtn);
root.appendChild(cont);
calcCart();
setTimeout(function () {
setInterval(function () {
var luckyItem=prodList[Math.floor(Math.random() * prodList.length)];
if(Math.random() < 0.3 && luckyItem.q > 0) {
luckyItem.val=Math.round(luckyItem.val * 0.8);
alert('번개세일! ' + luckyItem.name + '이(가) 20% 할인 중입니다!');
updateSelOpts();
}
}, 30000);
}, Math.random() * 10000);
function calcCart() {
totalAmt=0;
itemCnt=0;
var cartItems=cartDisp.children;
var subTot=0;
for (var i=0; i < cartItems.length; i++) {
(function () {
var curItem;
for (var j=0; j < prodList.length; j++) {
if(prodList[j].id === cartItems[i].id) {
curItem=prodList[j];
break;
}
}
var q=parseInt(cartItems[i].querySelector('span').textContent.split('x ')[1]);
var itemTot=curItem.val * q;
var disc=0;
itemCnt += q;
subTot += itemTot;
if(q >= 10) {
if(curItem.id === 'p1') disc=0.1;
else if(curItem.id === 'p2') disc=0.15;
else if(curItem.id === 'p3') disc=0.2;
else if(curItem.id === 'p4') disc=0.05;
else if(curItem.id === 'p5') disc=0.25;
}
totalAmt += itemTot * (1 - disc);
})();
}
let discRate=0;
if(itemCnt >= 30) {
var bulkDisc=totalAmt * 0.25;
var itemDisc=subTot - totalAmt;
if(bulkDisc > itemDisc) {
totalAmt=subTot * (1 - 0.25);
discRate=0.25;
} else {
discRate=(subTot - totalAmt) / subTot;
}
} else {
discRate=(subTot - totalAmt) / subTot;
}
if(new Date().getDay() === 2) {
totalAmt *= (1 - 0.1);
discRate=Math.max(discRate, 0.1);
}
sum.textContent='총액: ' + Math.round(totalAmt) + '원';
if(discRate > 0) {
var span=document.createElement('span');
span.className='text-green-500 ml-2';
span.textContent='(' + (discRate * 100).toFixed(1) + '% 할인 적용)';
sum.appendChild(span);
}
updateStockInfo();
renderBonusPts();
}
main();
addBtn.addEventListener('click', function () {
var selItem=sel.value;
var itemToAdd=prodList.find(function (p) { return p.id === selItem; });
if(itemToAdd && itemToAdd.q > 0) {
var item=document.getElementById(itemToAdd.id);
if(item) {
var newQty=parseInt(item.querySelector('span').textContent.split('x ')[1]) + 1;
if(newQty <= itemToAdd.q) {
item.querySelector('span').textContent=itemToAdd.name + ' - ' + itemToAdd.val + '원 x ' + newQty;
itemToAdd.q--;
} else {
alert('재고가 부족합니다.');
}
} else {
var newItem=document.createElement('div');
newItem.id=itemToAdd.id;
newItem.className='flex justify-between items-center mb-2';
newItem.innerHTML='<span>' + itemToAdd.name + ' - ' + itemToAdd.val + '원 x 1</span><div>' +
'<button class="quantity-change bg-blue-500 text-white px-2 py-1 rounded mr-1" data-product-id="' + itemToAdd.id + '" data-change="-1">-</button>' +
'<button class="quantity-change bg-blue-500 text-white px-2 py-1 rounded mr-1" data-product-id="' + itemToAdd.id + '" data-change="1">+</button>' +
'<button class="remove-item bg-red-500 text-white px-2 py-1 rounded" data-product-id="' + itemToAdd.id + '">삭제</button></div>';
cartDisp.appendChild(newItem);
itemToAdd.q--;
}
calcCart();
lastSel=selItem;
}
});
처음 더티코드가 주어졌을 때 막막했다.
중복되고 주석과 들여쓰기가 불규칙적인데다 조건문도 매우매우 복잡하고, 무엇보다 네이밍센스가 말이 안됐다.
(ex. sel, cartDisp 등 무슨 내용의 변수인지 파악하기가 힘들다)
상상 이상의 코드에 뭐부터 해야 좋을지 과제 체크리스트를 살펴봤다
1. 의미 있는 이름 사용
2. 함수 추출 (단일 책임)
3. 매직 넘버 제거
4. 조건문 단순화
... 기타 등등
1. 의미 있는 이름 사용
참고 문서도 좀 읽어보다가 심각한 함수명 & 변수명부터 수술에 들어갔다
그동안은 크게 고민안하고 이름을 지었는데 '클린코드' 란 명목으로 이름을 지으려니 고려해야될게 생각보다 많았다
내가 그동안 사용했던 이름들은 나의 주관이 강하게 반영된 내가 알아보기 쉬운 이름이었다.
어떤 이름이 좋은 것인가 찾아보던 중
참 와닿은 내용이 있어서 발췌한다.
좋은 것은 특이한게 아니라 보편적이라서 좋은 것이다. 단, 그 의미를 분명하게 알고 있어야 한다 -테오
그럼 어떤게 보편적인가?
무언가를 배열에 넣는 함수는 add를 써야하는가 push를 써야하는가? insert는 안되나?
addProduct vs pushProduct vs insertProduct
나는 느낌적으로 addProduct가 맞는거 같다.
팀별로 변수명을 논의했는데 아래 변수명을 뭘로 바꿀지였다.
interface UserInfoData {
name: string;
age: number;
activeStatusFlag: boolean;
value: string;
detailsString: string;
}
UserInfo, UserData, User 정도 안건이 나왔는데 나름 타당한 이유들을 갖고있다
User) 간단하게 User로만 지어도 명확하다. 짧은게 좋다.
UserInfo) 그냥 User는 컴포넌트 이름과 겹치는 경우가 가끔있다 그럴때 User as 를 하느니 UserInfo로 하자
UserData) 그동안 컨벤션을 UserData로 가져가서 이게 더 맞는 느낌이다.
별거아닌 변수명에도 다양한 의견이 있었고 정말 '느낌'이 정답인 영역이었다.
그래서 합의의 과정이 필요했고 우리가 정한 roll대로 가는것이 정답이었다.
실제로 조금 안맞는 이름도 일관되게 적용해보고 아 이거 이래서 불편하네 싶은것들을 아는것도 클린코드의 과정이라고 그러더라 (4주차 끝나갈때쯤 들었다..)
2. 함수 추출 (단일 책임)
이전에는 리팩토링을 할 때 특히, 하나의 함수를 추출해서 단일 책임의 여러함수로 만들 때 내용이 파편화되고 가독성이 오히려 떨어진다고 생각한 적이 있었는데 이번 세션을 통해서 그 생각이 많이 깨졌다.
함수 추출에 layer를 고려해 단계별로 추상화를 통해 더티 코드가 상당히 읽기 좋은 코드가되었다.
3. 매직 넘버 제거
상품 별로 적용되는 할인율, 대량 구매 시 할인 (대량 구매의 수량) 등을 이해할 수 있는 변수로 분류했다
export const DISCOUNT_RATE = {
p1: 0.1,
p2: 0.15,
p3: 0.2,
p4: 0.05,
p5: 0.25,
};
export const NUMBER_OF_BULK = 30;
// productId를 변수로 받아 할인율 return
function getDiscountRate(productId) {
return DISCOUNT_RATE[productId] || 0;
}
// bulk 수량인 경우 bulk 할인율 적용
if (quantity >= NUMBER_OF_BULK) { // NUMBER_OF_BULK: number
discountRate = calculateBulkDiscountRate(quantity, discountedPrice, price);
}
아키텍쳐 적용
리팩토링을 어느정도 적용하고 1~2주차에 진행했던 컴포넌트 기반 아키텍쳐를 적용했다.
// App.js
export class AppComponent {
// private 변수 선언
#cartBox;
#totalPrice;
#select;
#addBtn;
#stockStatus;
#container;
#wrap;
#h1;
constructor(rootId) {
this.rootId = rootId;
// element 변수 선언
this.#cartBox;
this.#totalPrice;
this.#select;
this.#addBtn;
this.#stockStatus;
this.#container;
this.#wrap;
this.#h1;
// 마지막 선택한 상품 id
this.lastSelectedProductId;
}
// DOM 요소 생성
#createElements() {
this.#cartBox = document.createElement("div");
this.#totalPrice = document.createElement("div");
this.#select = document.createElement("select");
this.#addBtn = document.createElement("button");
this.#stockStatus = document.createElement("div");
this.#container = document.createElement("div");
this.#wrap = document.createElement("div");
this.#h1 = document.createElement("h1");
}
// DOM 요소 textContent 설정
#setTextContent() {
this.#totalPrice.textContent = "총액: 0원(포인트: 0)";
this.#h1.textContent = "장바구니";
}
// DOM 요소 property 설정
#setProperties() {
this.#cartBox.id = "cart-items";
this.#totalPrice.id = "cart-total";
this.#select.id = "product-select";
this.#addBtn.id = "add-to-cart";
this.#stockStatus.id = "stock-status";
this.#totalPrice.className = "text-xl font-bold my-4";
this.#select.className = "border rounded p-2 mr-2";
this.#addBtn.className = "bg-blue-500 text-white px-4 py-2 rounded";
this.#stockStatus.className = "text-sm text-gray-500 mt-2";
this.#addBtn.textContent = "추가";
this.#container.className = "bg-gray-100 p-8";
this.#wrap.className =
"max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl p-8";
this.#h1.className = "text-2xl font-bold mb-4";
}
// 상품 select 옵션 설정
#setSelectOptions() {
this.#select.innerHTML = "";
productList.forEach((item) => {
const $opt = document.createElement("option");
$opt.value = item.id;
$opt.textContent = item.name + " - " + item.val + "원";
if (item.quantity === 0) $opt.disabled = true;
this.#select.appendChild($opt);
});
}
// DOM 요소 appendChild 설정
#appendChilds() {
this.#wrap.appendChild(this.#h1);
this.#wrap.appendChild(this.#cartBox);
this.#wrap.appendChild(this.#totalPrice);
this.#wrap.appendChild(this.#select);
this.#wrap.appendChild(this.#addBtn);
this.#wrap.appendChild(this.#stockStatus);
this.#container.appendChild(this.#wrap);
const $root = document.getElementById(this.rootId);
$root.appendChild(this.#container);
}
// 이벤트 설정
#setEvent() {
// 상품 추가 버튼 이벤트
this.#addBtn.addEventListener("click", () => {
//생략
});
// 상품 + - 버튼 이벤트 (수량 변경)
this.#cartBox.addEventListener("click", (event) => {
//생략
});
// 장바구니 삭제 버튼 이벤트
this.#cartBox.addEventListener("click", (event) => {
//생략
});
}
// AppComponent 렌더링
render() {
this.#createElements();
this.#setProperties();
this.#setTextContent();
this.#setSelectOptions();
this.#appendChilds();
// DOM 요소 생성 후 이벤트 설정
this.#setEvent();
}
}
// main.js
const App = new AppComponent("app");
function init() {
App.render();
}
init();
1~2주차에서 배운 컴포넌트 아키텍처를 이런식으로 사용하는구나 싶어 좀 뿌듯했다
'하지만 현업에서 이런 코드 안쓰지않나..' 싶은 생각이 들때즈음 기본과제가 끝났고
심화과제는 위 내용을 고대로 React & Typescript 스펙으로 마이그레이션 하는 내용이었다.
기능 자체는 별게 없는데..
기존의 HTML 템플릿을 리액트 컴포넌트로 바꾸고 이벤트 핸들러를 리액트 방식으로 변경하는게 생각보다 까다로웠다
그냥 동일한 기능을 하는 코드를 새로 작성하는게 훨씬 나았다고 생각했는데 테오는 한번에 모든 코드를 변경하는게 아닌 점진적으로 리팩토링하는 감각을 기르는게 중요하다고한다
4주차 진행하면서 코딩은 정말 운동같은거다.. 라는걸 체감했다. 이전에도 클린코드에 대해서 문서나 블로그들을 많이 봤었는데
더티코드를 직접 클린코드로 변환하기 전까진 잘 모르는 내용이었다는걸 깨달았다.
이번주도 코딩근육이 조금 생긴거 같다. (아직 빈약한거 같다)
'항해➕플러스' 카테고리의 다른 글
항해 3주차 _React, Beyond the Basics (0) | 2025.04.14 |
---|---|
프레임워크 없이 SPA 만들기 (항해 2주차 회고) (0) | 2025.04.07 |
프레임워크 없이 SPA 만들기 (항해 1주차 회고) (0) | 2025.04.04 |