1. 프롤로그 - "범인은 바로 나"
프로젝트를 시작한 지 6개월이 지났다. PM이 Slack으로 메시지를 보냈다. "이 기능에 필터링 하나만 추가해주세요. 간단한 거라 30분이면 될 것 같은데요?"
나는 자신있게 대답했다. "네, 금방 할게요!"
그리고 내가 6개월 전에 짠 코드를 열어봤다.
function doStuff(data) {
if(data.status == 1) {
if(data.type == "premium") {
// ...500줄의 중첩된 로직
} else {
// ...300줄
}
} else if(data.status == 2) {
// ...700줄
}
}
"...이게 뭐야?"
변수명은 d, tmp, x, arr2. 주석은 한 줄도 없고, 함수 하나가 1,500줄이다.
함수 이름은 doStuff, processData, handleThing 같은 것들뿐이다.
"도대체 누가 이런 쓰레기 코드를 짰지?"
나는 화가 났다. Git blame을 돌려봤다.
Author: Me <me@example.com>
Date: 6 months ago
Message: "quick fix"
범인은 바로 나였다.
그날 "간단한 수정"에 8시간이 걸렸다. 코드를 이해하는 데만 6시간을 썼기 때문이다. 그 순간 나는 깨달았다. 나쁜 코드의 가장 큰 피해자는 미래의 나 자신이라는 것을.
이 경험 이후, 나는 클린 코드에 대해 진지하게 공부하기 시작했다. 로버트 C. 마틴의 『Clean Code』를 읽고, 실제에 적용해보며, 수많은 시행착오를 겪었다. 결국 이거였다. 클린 코드는 남을 위한 것이 아니라 나 자신을 위한 것이다.
2. 고군분투 - 왜 우리는 더러운 코드를 짜는가?
2-1. "일단 돌아가게만 만들자"의 함정
데드라인이 촉박할 때, 우리는 이렇게 생각한다. "일단 돌아가게 만들고, 나중에 리팩토링하자."
하지만 그 "나중"은 오지 않는다. 새로운 기능 요청이 쌓이고, 버그가 쏟아지고, "나중"은 영원히 미래로 미뤄진다. 그러다 6개월 뒤 내가 그 코드를 다시 열어보면... 위에 있는 프롤로그처럼 된다.
처음 이 사실을 받아들였다. 빠르게 가려면 클린 코드를 짜야 한다는 것을. 더러운 코드는 단기적으로는 빠를 수 있지만, 장기적으로는 팀 전체의 생산성을 무너뜨린다.
2-2. 깨진 유리창 법칙 (Broken Windows Theory)
범죄학에 "깨진 유리창 법칙"이라는 게 있다. 건물에 깨진 유리창 하나를 방치하면, 사람들이 "여기는 관리가 안 되는구나"라고 생각해서 더 많은 유리창을 깨고, 결국 건물 전체가 황폐해진다는 이론이다.
코드도 마찬가지다. 한 파일에 더러운 코드가 있으면, 다음 개발자도 "아, 여기는 대충 짜도 되는구나"라고 생각한다. 그렇게 프로젝트 전체가 스파게티가 되고, 결국 Legacy Hell에 빠진다.
나는 이 비유가 너무 와닿았다. 실제로 내가 맡은 레거시 프로젝트가 정확히 그런 상태였다.
2-3. 코드는 기계가 아니라 사람을 위한 것
컴파일러는 변수명이 x든 calculateTotalPriceWithTaxAndDiscount든 상관하지 않는다. 빌드 결과는 똑같다.
하지만 코드는 기계가 실행하기 전에, 사람이 읽고 이해하고 유지보수해야 한다.
"프로그래밍은 사람에게 의도를 전달하는 행위이며, 기계는 그 부산물을 실행할 뿐이다." — Donald Knuth
"모든 코드는 코드를 짠 시간보다 읽히는 시간이 10배 더 길다." — Robert C. Martin
이 사실을 이해했다는 순간, 내 코딩 스타일이 완전히 바뀌었다. 나는 더 이상 "짧고 똑똑한" 코드를 짜려고 하지 않고, "읽기 쉬운" 코드를 짜려고 노력하게 됐다.
3. 깨달음 - 클린 코드의 핵심 원칙들
3-1. 이름 짓기 - 가장 기본이자 가장 어려운 것
Phil Karlton이 말했다: "컴퓨터 과학에서 어려운 것은 단 두 가지뿐이다. 캐시 무효화와 이름 짓기."
이름 짓기는 쉬워 보이지만, 실제로는 프로그래밍에서 가장 중요한 스킬 중 하나다.
원칙 1 - 의도를 분명히 밝혀라
Bad:
let d; // 경과 시간 (단위: 날짜)
let e; // 이메일
주석이 없으면 d가 data인지 date인지 distance인지 모른다.
주석이 있어도 시간이 지나면 코드는 변하는데 주석은 업데이트를 까먹는다.
Good:
let daysSinceCreation;
let daysSinceModification;
let elapsedTimeInDays;
이제 주석이 필요 없다. 변수명 자체가 주석이다.
원칙 2 - 그릇된 정보를 피하라
Bad:
let accountList = { /* Map 객체 */ };
let hp = "Hit Points"; // hp가 흔히 Hewlett-Packard를 의미하므로 혼란
프로그래머에게 List는 특정한 자료구조를 의미한다. 실제로 List가 아니라면 accountList라고 쓰면 안 된다.
Good:
let accountMap;
let accounts;
let accountCollection;
let hitPoints;
let playerHealth;
원칙 3 - 검색하기 쉬운 이름을 써라
Bad:
// 숫자 7이 뭘 의미하는지 찾기 힘듦 (Magic Number)
if (student.age > 7) {
enrollInSchool(student);
}
// 1을 검색하면 코드베이스 전체에서 수천 개가 검색됨
if (user.status == 1) {
// ...
}
Good:
const MINIMUM_SCHOOL_AGE = 7;
const STATUS_ACTIVE = 1;
const STATUS_INACTIVE = 0;
const STATUS_SUSPENDED = 2;
if (student.age >= MINIMUM_SCHOOL_AGE) {
enrollInSchool(student);
}
if (user.status === STATUS_ACTIVE) {
// ...
}
이제 MINIMUM_SCHOOL_AGE로 검색하면 모든 사용처가 나온다. 나중에 학교 입학 나이가 바뀌면 상수 하나만 수정하면 된다.
원칙 4 - 인코딩을 피하라
Bad (Hungarian Notation - 과거의 유물):
let strName; // 타입을 변수명에 넣지 마라
let iCount;
let bIsValid;
TypeScript 같은 정적 타입 언어를 쓰면 이런 인코딩은 완전히 불필요하다.
Good:
let name: string;
let count: number;
let isValid: boolean;
원칙 5 - 클래스명은 명사, 메서드명은 동사
Bad:
class Manager { } // 너무 모호함
class Process { }
function user() { } // 동사가 아님
Good:
class AccountManager { }
class OrderProcessor { }
function createUser() { }
function deleteAccount() { }
function validateInput() { }
3-2. 함수 - 작게, 더 작게!
원칙 1 - 한 가지 일만 해라 (Single Responsibility Principle)
함수 이름이 loginAndCreateSessionAndLogAccessAndSendEmail()이라면 이미 잘못된 것이다.
Bad:
function processUser(user) {
// 검증
if (!user.email) throw new Error("No email");
if (!user.password) throw new Error("No password");
// 비밀번호 해싱
const hashedPassword = bcrypt.hash(user.password, 10);
// DB 저장
db.users.insert({ email: user.email, password: hashedPassword });
// 환영 이메일 발송
sendEmail(user.email, "Welcome!");
// 로깅
logger.info(`User created: ${user.email}`);
// 슬랙 알림
slack.notify(`New user: ${user.email}`);
}
이 함수는 검증, 해싱, 저장, 이메일, 로깅, 알림... 무려 6가지 일을 한다. 테스트하기도 힘들고, 재사용하기도 힘들다.
Good:
function createUser(user) {
validateUser(user);
const hashedPassword = hashPassword(user.password);
const savedUser = saveUserToDatabase(user.email, hashedPassword);
sendWelcomeEmail(user.email);
logUserCreation(user.email);
notifyTeam(user.email);
return savedUser;
}
function validateUser(user) {
if (!user.email) throw new Error("Email is required");
if (!user.password) throw new Error("Password is required");
if (!isValidEmail(user.email)) throw new Error("Invalid email format");
}
function hashPassword(password) {
return bcrypt.hash(password, 10);
}
function saveUserToDatabase(email, hashedPassword) {
return db.users.insert({ email, password: hashedPassword });
}
function sendWelcomeEmail(email) {
sendEmail(email, "Welcome to our platform!");
}
function logUserCreation(email) {
logger.info(`User created: ${email}`);
}
function notifyTeam(email) {
slack.notify(`New user registered: ${email}`);
}
이제 각 함수는 딱 한 가지 일만 한다. 테스트하기 쉽고, 재사용하기 쉽고, 이해하기 쉽다.
원칙 2 - 추상화 수준을 맞춰라
Bad:
function renderPage() {
checkUserPermission(); // 높은 추상화
let html = ""; // 낮은 추상화 (구체적 구현)
html += "<div class='header'>";
html += "<h1>Welcome</h1>";
html += "</div>";
fetchDataFromDatabaseAndProcessIt(); // 높은 추상화
for (let i = 0; i < data.length; i++) { // 낮은 추상화
html += data[i].render();
}
return html;
}
추상화 수준이 뒤죽박죽이다. 어떤 줄은 높은 수준의 개념을 다루고, 어떤 줄은 낮은 수준의 구현 디테일을 다룬다.
Good:
function renderPage() {
if (!isAuthorizedUser()) {
return renderUnauthorizedPage();
}
const data = fetchPageData();
const content = buildPageContent(data);
return renderPageLayout(content);
}
이제 함수를 읽으면 마치 책의 목차처럼 읽힌다. 모든 문장이 같은 추상화 수준에 있다.
원칙 3 - 인수는 적을수록 좋다
이상적인 인수 개수:
- 0개 (niladic): 최고
- 1개 (monadic): 좋음
- 2개 (dyadic): 괜찮음
- 3개 (triadic): 피해야 함
- 4개 이상 (polyadic): 절대 피할 것
Bad:
makeCircle(10, 20, 5, "red", true, 0.8, "solid");
// 10이 x인지 y인지? 5가 반지름인지 지름인지?
Good:
const circle = {
x: 10,
y: 20,
radius: 5,
color: "red",
filled: true,
opacity: 0.8,
borderStyle: "solid"
};
makeCircle(circle);
객체를 넘기면 인수가 늘어나도 가독성이 좋고, 순서도 상관없다.
원칙 4 - 부수 효과(Side Effect)를 피하라
Bad:
function checkPassword(username, password) {
const user = db.findUser(username);
if (user.password === password) {
Session.initialize(); // 깜짝 놀랄 부수 효과!
return true;
}
return false;
}
함수 이름은 "비밀번호를 확인한다"인데, 실제로는 세션도 초기화한다. 이건 거짓말이다.
Good:
function checkPassword(username, password) {
const user = db.findUser(username);
return user.password === password;
}
function login(username, password) {
if (checkPassword(username, password)) {
Session.initialize();
return true;
}
return false;
}
이제 각 함수가 자신의 이름이 약속한 것만 한다.
3-3. 주석 - 실패를 자인하는 것
로버트 C. 마틴은 이렇게 말했다:
"주석은 코드로 의도를 표현하지 못해 실패했음을 의미한다."
주석의 가장 큰 문제는 거짓말을 한다는 것이다. 코드는 변하지만 주석은 업데이트를 까먹는다. 그러면 주석은 코드를 이해하는 데 도움이 되기는커녕 방해가 된다.
나쁜 주석의 예
Bad:
// 사용자가 복지 혜택을 받을 자격이 있는지 확인
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))
Good:
if (employee.isEligibleForFullBenefits())
함수 이름만으로 설명이 되므로 주석이 필요 없다.
Bad:
// i를 1씩 증가
i++;
이건 정말 최악이다. 코드를 그대로 반복한 것뿐이다.
Bad (오래된 주석, 거짓말하는 주석):
// 이 함수는 사용자 나이를 반환한다
function getUserData(userId) {
// 실제로는 전체 사용자 객체를 반환함
return db.users.find(userId);
}
좋은 주석의 예
주석이 정말로 필요한 경우도 있다:
1. 법적인 주석:
// Copyright (C) 2025 My Company. All rights reserved.
// Licensed under the MIT License
2. 정보 제공 주석 (단, 함수명으로 표현 못할 때만):
// 정규식 설명: "2025-01-31" 형식의 날짜 매칭
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
3. 의도를 설명하는 주석 (Why):
// 성능 최적화: 선형 검색 대신 해시맵 사용
// 벤치마크 결과 1000개 항목에서 10배 빠름
const userMap = new Map();
4. 경고 주석:
// 경고: 이 함수는 3초 이상 걸릴 수 있음.
// 메인 스레드에서 호출하지 말 것
function processLargeFile(file) {
// ...
}
5. TODO 주석:
// TODO: 캐싱 레이어 추가 (JIRA-1234)
// TODO: 에러 핸들링 개선 필요
하지만 대부분의 경우, 코드를 고쳐서 주석을 없애는 것이 최선이다.
4. 파헤치기 학습 - 코드 냄새(Code Smell)와 리팩토링
4-1. 대표적인 Code Smells
Martin Fowler는 『Refactoring』에서 수십 가지 Code Smell을 정의했다. 여기서는 가장 자주 보이는 것들을 정리해본다.
1) Long Function (긴 함수)
함수가 한 화면을 넘어가면 쪼개야 한다. 이상적으로는 5-10줄, 최대 20줄 이내.
2) Large Class (거대한 클래스)
클래스가 너무 많은 책임을 지고 있다면 쪼개라.
3) Long Parameter List (긴 인수 목록)
인수가 3개를 넘어가면 객체로 묶어라.
4) Duplicated Code (중복 코드)
똑같은 코드가 여러 곳에 있으면 하나로 합쳐라. DRY(Don't Repeat Yourself) 원칙.
Bad:
function calculateDiscountForPremiumUser(price) {
const discount = price * 0.2;
return price - discount;
}
function calculateDiscountForVIPUser(price) {
const discount = price * 0.3;
return price - discount;
}
function calculateDiscountForRegularUser(price) {
const discount = price * 0.1;
return price - discount;
}
Good:
const DISCOUNT_RATES = {
PREMIUM: 0.2,
VIP: 0.3,
REGULAR: 0.1
};
function calculateDiscount(price, userType) {
const discountRate = DISCOUNT_RATES[userType];
const discount = price * discountRate;
return price - discount;
}
5) Dead Code (죽은 코드)
사용하지 않는 코드는 과감하게 삭제하라. "나중에 필요할 수도 있어"라고 남겨두지 마라. Git이 있다.
Bad:
function processOrder(order) {
// 주문 처리
processPayment(order);
// 아래 코드는 예전 버전. 지우면 안 될 것 같아서 주석 처리함
// if (order.isOldFormat) {
// convertOldFormat(order);
// }
}
Good:
function processOrder(order) {
processPayment(order);
}
6) Primitive Obsession (기본 타입 집착)
의미 있는 데이터를 원시 타입으로만 표현하지 마라. 객체를 만들어라.
Bad:
function createUser(email, streetAddress, city, zipCode, country) {
// 주소 관련 매개변수가 4개나 됨
}
Good:
class Address {
constructor(street, city, zipCode, country) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
this.country = country;
}
getFullAddress() {
return `${this.street}, ${this.city} ${this.zipCode}, ${this.country}`;
}
}
function createUser(email, address) {
// 이제 주소는 하나의 객체로 처리
}
7) Switch Statement Hell (분기문 지옥)
switch나 if-else가 끝없이 이어진다면? 다형성(Polymorphism)으로 해결하라.
Bad:
function getAnimalSound(animal) {
if (animal.type === 'dog') {
return 'bark';
} else if (animal.type === 'cat') {
return 'meow';
} else if (animal.type === 'bird') {
return 'tweet';
} else if (animal.type === 'cow') {
return 'moo';
}
// 동물이 추가될 때마다 이 함수를 수정해야 함 (OCP 위반)
}
function getMovementSpeed(animal) {
if (animal.type === 'dog') {
return 15;
} else if (animal.type === 'cat') {
return 20;
} else if (animal.type === 'bird') {
return 50;
}
// 또 똑같은 분기문 반복...
}
Good (다형성 활용):
class Animal {
getSound() {
throw new Error("Must implement getSound()");
}
getMovementSpeed() {
throw new Error("Must implement getMovementSpeed()");
}
}
class Dog extends Animal {
getSound() { return 'bark'; }
getMovementSpeed() { return 15; }
}
class Cat extends Animal {
getSound() { return 'meow'; }
getMovementSpeed() { return 20; }
}
class Bird extends Animal {
getSound() { return 'tweet'; }
getMovementSpeed() { return 50; }
}
// 이제 동물이 100마리가 되어도 이 함수는 수정할 필요 없음
function printAnimalInfo(animal) {
console.log(`This animal says: ${animal.getSound()}`);
console.log(`Speed: ${animal.getMovementSpeed()} km/h`);
}
이것이 개방-폐쇄 원칙(Open-Closed Principle, OCP)이다. 확장에는 열려 있고, 수정에는 닫혀 있다.
4-2. 실제 리팩토링 워크샵
내가 실제로 짰던 "쓰레기 코드"를 단계별로 고쳐보자.
Before (Code Smell 천국)
function proc(d) {
// d는 주문 데이터 배열
let r = [];
for(let i=0; i<d.length; i++) {
// 상태가 1이고 가격이 있으면
if(d[i].s == 1 && d[i].p > 0) {
let t = d[i].p * d[i].q; // 가격 * 수량
if(d[i].t == "p") { // 프리미엄이면 할인
t = t * 0.9;
} else if(d[i].t == "v") { // VIP면 더 큰 할인
t = t * 0.8;
}
// 배송비 추가
if(t < 50) {
t += 5;
}
r.push({id: d[i].id, tot: t});
}
}
return r;
}
문제점:
- 함수명
proc가 의미 없음 - 변수명
d,r,t,s,p,q가 전부 축약형 - Magic Number:
1,0.9,0.8,50,5 - Magic String:
"p","v" - 주석에 의존하는 코드
- 추상화 수준이 뒤죽박죽
Step 1: 이름 바꾸기
function calculateOrderTotals(orders) {
let results = [];
for(let i=0; i<orders.length; i++) {
if(orders[i].status == 1 && orders[i].price > 0) {
let total = orders[i].price * orders[i].quantity;
if(orders[i].type == "p") {
total = total * 0.9;
} else if(orders[i].type == "v") {
total = total * 0.8;
}
if(total < 50) {
total += 5;
}
results.push({id: orders[i].id, total: total});
}
}
return results;
}
조금 나아졌다. 하지만 여전히 Magic Number와 Magic String이 남아 있다.
Step 2: 상수 추출하기
const ORDER_STATUS_ACTIVE = 1;
const USER_TYPE_PREMIUM = "p";
const USER_TYPE_VIP = "v";
const PREMIUM_DISCOUNT_RATE = 0.9;
const VIP_DISCOUNT_RATE = 0.8;
const FREE_SHIPPING_THRESHOLD = 50;
const SHIPPING_FEE = 5;
function calculateOrderTotals(orders) {
let results = [];
for(let i=0; i<orders.length; i++) {
if(orders[i].status === ORDER_STATUS_ACTIVE && orders[i].price > 0) {
let total = orders[i].price * orders[i].quantity;
if(orders[i].type === USER_TYPE_PREMIUM) {
total = total * PREMIUM_DISCOUNT_RATE;
} else if(orders[i].type === USER_TYPE_VIP) {
total = total * VIP_DISCOUNT_RATE;
}
if(total < FREE_SHIPPING_THRESHOLD) {
total += SHIPPING_FEE;
}
results.push({id: orders[i].id, total: total});
}
}
return results;
}
훨씬 좋아졌다! 이제 숫자들이 무엇을 의미하는지 명확하다.
Step 3: 함수 추출하기 (Extract Function)
const ORDER_STATUS_ACTIVE = 1;
const USER_TYPE_PREMIUM = "premium";
const USER_TYPE_VIP = "vip";
const PREMIUM_DISCOUNT_RATE = 0.9;
const VIP_DISCOUNT_RATE = 0.8;
const FREE_SHIPPING_THRESHOLD = 50;
const SHIPPING_FEE = 5;
function calculateOrderTotals(orders) {
return orders
.filter(isActiveOrder)
.map(calculateSingleOrderTotal);
}
function isActiveOrder(order) {
return order.status === ORDER_STATUS_ACTIVE && order.price > 0;
}
function calculateSingleOrderTotal(order) {
let subtotal = calculateSubtotal(order);
subtotal = applyUserDiscount(subtotal, order.userType);
subtotal = addShippingFee(subtotal);
return {
id: order.id,
total: subtotal
};
}
function calculateSubtotal(order) {
return order.price * order.quantity;
}
function applyUserDiscount(amount, userType) {
if (userType === USER_TYPE_VIP) {
return amount * VIP_DISCOUNT_RATE;
}
if (userType === USER_TYPE_PREMIUM) {
return amount * PREMIUM_DISCOUNT_RATE;
}
return amount;
}
function addShippingFee(amount) {
if (amount < FREE_SHIPPING_THRESHOLD) {
return amount + SHIPPING_FEE;
}
return amount;
}
개선 결과:
- 각 함수가 딱 한 가지 일만 한다
- 주석 없이도 코드가 스스로 설명한다
- 테스트하기 쉽다 (각 함수를 독립적으로 테스트 가능)
- 재사용하기 쉽다
- 버그를 찾기 쉽다
Step 4: 다형성 적용 (더 나아가기)
만약 사용자 타입이 더 늘어난다면? 다형성을 사용하자.
interface UserType {
getDiscountRate(): number;
}
class RegularUser implements UserType {
getDiscountRate() {
return 1.0; // 할인 없음
}
}
class PremiumUser implements UserType {
getDiscountRate() {
return 0.9; // 10% 할인
}
}
class VIPUser implements UserType {
getDiscountRate() {
return 0.8; // 20% 할인
}
}
class Order {
constructor(
public id: string,
public price: number,
public quantity: number,
public status: number,
public userType: UserType
) {}
isActive(): boolean {
return this.status === ORDER_STATUS_ACTIVE && this.price > 0;
}
calculateTotal(): number {
let subtotal = this.price * this.quantity;
subtotal *= this.userType.getDiscountRate();
if (subtotal < FREE_SHIPPING_THRESHOLD) {
subtotal += SHIPPING_FEE;
}
return subtotal;
}
}
function calculateOrderTotals(orders: Order[]) {
return orders
.filter(order => order.isActive())
.map(order => ({
id: order.id,
total: order.calculateTotal()
}));
}
이제 새로운 사용자 타입이 추가되어도 기존 코드를 수정할 필요가 없다. 개방-폐쇄 원칙(OCP)을 완벽하게 지킨다.
5. 적용 - 팀에서 클린 코드 문화 만들기
5-1. 코드 리뷰 - 문지기 역할
"기분 나쁘지 않게 쓰레기 코드라고 말하는 법"
코드 리뷰는 비난의 장이 아니다. 함께 배우고 성장하는 장이다.
나쁜 리뷰 코멘트:
리뷰어: "이 코드는 완전히 틀렸어요."
리뷰어: "이걸 왜 이렇게 짰죠?"
리뷰어: "다시 짜세요."
좋은 리뷰 코멘트:
리뷰어: "이 함수가 좀 길어 보이는데, 검증 로직을 별도 함수로 분리하면
테스트하기 더 쉬울 것 같습니다. 어떻게 생각하시나요?"
리뷰어: "Good catch! 다만 이 부분에서 예외 케이스가 하나 더 있을 것 같은데요.
사용자가 이메일 없이 가입하는 경우는 어떻게 처리하시나요?"
리뷰어: "이 알고리즘 선택 좋네요! 혹시 시간복잡도를 코멘트로 남겨주시면
나중에 최적화할 때 도움이 될 것 같습니다."
리뷰이의 좋은 반응:
리뷰이: "좋은 의견 감사합니다. 말씀하신 대로 리팩토링했습니다."
리뷰이: "아, 그 케이스를 놓쳤네요. 테스트 추가하겠습니다."
리뷰이: "데드라인이 촉박해서 일단 이렇게 머지하고, 다음 스프린트에
리팩토링 티켓 끊어서 개선하면 안 될까요?"
5-2. 린터와 포매터 - 자동화
사람이 매번 들여쓰기와 스타일을 검사하는 건 시간 낭비다. 자동화하자.
ESLint + Prettier 설정:
// .eslintrc.json
{
"rules": {
"max-lines-per-function": ["error", 50],
"max-params": ["error", 3],
"complexity": ["error", 10],
"no-magic-numbers": ["warn"],
"prefer-const": "error",
"no-var": "error"
}
}
Pre-commit Hook:
# .husky/pre-commit
npm run lint
npm run test
코드 스타일 논쟁에 시간 쓰지 말고, 도구에 맡기자.
5-3. 보이스카우트 규칙 (Boy Scout Rule)
"캠핑장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라."
이 규칙을 코드에 적용하면?
"파일을 수정할 때, 처음 열었을 때보다 조금이라도 더 깨끗하게 만들고 커밋하라."
버그를 고치러 들어갔나요? 그 김에:
- 지저분한 변수명 하나 고치기
- 긴 함수 하나 쪼개기
- 죽은 코드 하나 지우기
- 주석을 코드로 바꾸기
이런 작은 개선이 쌓이면 시스템 전체가 점점 좋아진다. 반대로, 이걸 안 하면 시스템은 점점 더러워진다.
5-4. 기술 부채(Technical Debt) 관리
"나중에 고쳐야지" 하고 대충 짠 코드는 기술 부채가 된다. 부채는 이자가 붙는다.
기술 부채의 이자:
- 버그 수정 시간 증가
- 새 기능 개발 속도 저하
- 팀원 온보딩 시간 증가
- 개발자 스트레스 증가
기술 부채 관리 전략:
-
의도적 부채 vs 무의도적 부채 구분
- 의도적: "데드라인 때문에 일단 이렇게 하고 다음 스프린트에 리팩토링"
- 무의도적: "그냥 몰라서 이렇게 짬"
-
부채 목록 관리 (Tech Debt Backlog)
- JIRA나 GitHub Issues에 기술 부채 라벨 달기
- 매 스프린트마다 20% 정도 시간을 부채 갚기에 할애
-
큰 부채는 쪼개서 갚기
- "전체 리팩토링" 같은 거대한 티켓은 절대 끝나지 않는다
- "User 클래스 리팩토링", "로그인 로직 단위 테스트 추가" 같이 작게 쪼개라
6. SOLID 원칙과의 연결
클린 코드는 SOLID 원칙과 깊이 연결되어 있다. 간단히 정리해본다.
1. Single Responsibility Principle (단일 책임 원칙)
클래스나 함수는 딱 한 가지 이유로만 변경되어야 한다.
// Bad: User 클래스가 너무 많은 책임을 가짐
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
saveToDatabase() { /* ... */ } // DB 책임
sendEmail() { /* ... */ } // 이메일 책임
generateReport() { /* ... */ } // 리포트 책임
validateData() { /* ... */ } // 검증 책임
}
// Good: 책임을 분리
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class UserRepository {
save(user) { /* ... */ }
}
class EmailService {
send(user, message) { /* ... */ }
}
class UserReportGenerator {
generate(user) { /* ... */ }
}
class UserValidator {
validate(user) { /* ... */ }
}
2. Open/Closed Principle (개방-폐쇄 원칙)
확장에는 열려 있고, 수정에는 닫혀 있어야 한다. (위에서 본 다형성 예제가 바로 이것)
3. Liskov Substitution Principle (리스코프 치환 원칙)
자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
4. Interface Segregation Principle (인터페이스 분리 원칙)
클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.
5. Dependency Inversion Principle (의존성 역전 원칙)
구체적인 것이 아니라 추상적인 것에 의존해야 한다.
7. TDD: 클린 코드의 방패막이
"테스트 코드가 없는 리팩토링은 줄타기 곡예와 같다."
클린 코드를 유지하려면 심리적 안정감이 필요하다. "내가 이 코드를 고쳐도 망가지지 않는다"는 확신. 그 확신은 테스트 코드에서 나온다.
TDD의 세 가지 법칙
- Red: 실패하는 테스트를 먼저 작성 (의도 정의)
- Green: 테스트를 통과하는 최소한의 코드 작성
- Refactor: 중복 제거 및 구조 개선 (이때 클린 코드가 됨!)
예제:
// 1. Red: 테스트 먼저 작성
describe('calculateDiscount', () => {
it('should apply 10% discount for premium users', () => {
const result = calculateDiscount(100, 'premium');
expect(result).toBe(90);
});
});
// 2. Green: 최소한의 코드로 통과
function calculateDiscount(price, userType) {
if (userType === 'premium') {
return price * 0.9;
}
return price;
}
// 3. Refactor: 구조 개선
const DISCOUNT_RATES = {
premium: 0.1,
vip: 0.2,
regular: 0
};
function calculateDiscount(price, userType) {
const discountRate = DISCOUNT_RATES[userType] || 0;
return price * (1 - discountRate);
}
테스트가 있으면 자신 있게 리팩토링할 수 있다. 뭔가 깨지면 테스트가 바로 알려주기 때문이다.
8. 실제 체크리스트
코드를 커밋하기 전에 스스로에게 물어보자:
이름 짓기:
- 변수/함수 이름이 의도를 명확히 드러내는가?
-
tmp,data,info같은 모호한 이름을 쓰지 않았는가? - Magic Number를 상수로 추출했는가?
함수:
- 함수가 한 가지 일만 하는가?
- 함수 길이가 20줄 이내인가?
- 인수가 3개 이하인가?
- 추상화 수준이 일관적인가?
주석:
- 주석 없이도 코드가 이해되는가?
- 주석이 거짓말하지 않는가?
- 주석 대신 코드를 고칠 수 있는가?
구조:
- 중복 코드가 없는가? (DRY)
- 죽은 코드를 삭제했는가?
-
if-else지옥을 다형성으로 바꿀 수 있는가?
테스트:
- 테스트 코드가 있는가?
- 리팩토링 후 모든 테스트가 통과하는가?
9. 마무리 - 클린 코드는 습관이다
처음에는 어색하다. 변수명 하나 짓는데 3분을 고민하고, 함수 하나 쪼개는데 10분을 쓰고...
"이렇게 하면 너무 느린 거 아냐?"
하지만 프로젝트 중반만 가도 달라진다. 코드를 읽는 시간이 쓰는 시간보다 압도적으로 많아지기 때문에, 클린 코드가 전체 개발 속도를 훨씬 빠르게 만든다.
6개월 뒤 내 코드를 열어봤을 때, "이거 누가 짰어? 잘 짰네"라고 생각하고 싶다면?
지금부터 클린 코드를 짜기 시작하자.
10. 핵심 용어 정리
-
Refactoring (리팩토링): 외부 동작(기능)은 바꾸지 않으면서 내부 구조(코드)를 개선하는 작업.
-
Code Smell (코드 냄새): 심각한 버그는 아니지만, 나중에 문제를 일으킬 수 있는 나쁜 코드 구조. 예: 긴 함수, 중복 코드, 긴 파라미터 목록, 거대한 클래스.
-
Technical Debt (기술 부채): "나중에 고쳐야지" 하고 대충 짜놓은 코드. 이자가 붙어서 나중에 수정하기 더 힘들어진다.
-
Magic Number: 코드 중간에 뜬금없이 등장하는 숫자. (
if (age > 18)). 상수로 이름을 붙여야 한다. -
KISS (Keep It Simple, Stupid): 불필요한 복잡성을 피하고 단순하게 짜라는 원칙.
-
YAGNI (You Ain't Gonna Need It): "나중에 필요할 것 같아서" 미리 짜지 마라. 지금 필요한 것만 짜라.
-
DRY (Don't Repeat Yourself): 똑같은 코드를 복사-붙여넣기 하지 마라. 로직이 중복되면 함수로 추출해라.
-
Boy Scout Rule (보이스카우트 규칙): "캠핑장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라." 코드를 수정할 때 주변도 조금씩 개선하고 나와라.
-
Side Effect (부수 효과): 함수가 자신의 이름이 약속한 것 외에 다른 일을 하는 것. 피해야 한다.
-
Polymorphism (다형성): 같은 인터페이스를 여러 타입이 구현해서, 타입에 따라 다른 동작을 하게 하는 것.
if-else지옥을 없앨 수 있다.
11. FAQ & 핵심 정리
Q1: 클린 코드를 짜면 개발 속도가 느려지지 않나요?
A: 초반에는 느립니다. 하지만 프로젝트 중반만 가도 코드를 읽는 시간이 쓰는 시간보다 압도적으로 많아지기 때문에, 클린 코드가 전체 개발 속도를 훨씬 빠르게 만듭니다. 더러운 코드로 짠 프로젝트는 시간이 지날수록 생산성이 0에 수렴합니다.
Q2: 주석은 정말 필요 없나요?
A: "Why(왜 이렇게 짰는지)"를 설명하는 주석은 훌륭합니다. 하지만 "What(무슨 코드인지)"을 설명하는 주석은 코드가 실패했다는 증거입니다. 코드로 말하세요.
Q3: 리팩토링은 언제 해야 하나요?
A: 기능을 추가할 때, 버그를 고칠 때, 코드 리뷰 할 때. 즉, 항상 해야 합니다. 따로 날 잡아서 하는 게 아닙니다. 보이스카우트 규칙을 따르세요.
Q4: 함수가 너무 많아지면 오히려 복잡해지지 않나요?
A: 처음에는 그렇게 느껴질 수 있습니다. 하지만 각 함수의 이름이 잘 지어져 있다면, 코드는 마치 책의 목차처럼 읽힙니다. 디테일이 필요할 때만 함수 내부로 들어가면 됩니다.
Q5: Copy-Paste가 왜 나쁜가요?
A: 로직이 변경되면 복사한 모든 곳을 찾아서 수정해야 합니다. 하나라도 놓치면 버그가 됩니다. DRY 원칙을 지키세요.
Q6: 짧은 코드가 항상 좋은 건가요?
A: 아닙니다. a > b ? a : b는 괜찮지만, 100자짜리 정규식 원라이너는 읽을 수 없습니다. 명확함 > 간결함.
Q7: 클린 코드를 어떻게 강제하나요?
A: 코드 리뷰와 린팅 도구(ESLint, Prettier)를 사용하세요. 하지만 가장 중요한 건 팀 문화입니다. 리더가 먼저 실천해야 합니다.
Q8: 레거시 코드는 어떻게 개선하나요?
A: 한 번에 다 고치려고 하지 마세요. 보이스카우트 규칙을 따라 조금씩 개선하세요. 그리고 무엇보다 테스트 코드를 먼저 작성하세요. 테스트 없는 리팩토링은 위험합니다.
Q9: 데드라인이 촉박할 때도 클린 코드를 짜야 하나요?
A: 역설적이지만, 데드라인이 촉박할수록 클린 코드를 짜야 합니다. 더러운 코드는 디버깅과 버그 수정에 시간을 더 많이 쓰게 만들기 때문입니다. "빨리 가려면 제대로 가라."
Q10: 클린 코드의 가장 중요한 원칙 하나만 고르라면?
A: 이름 짓기입니다. 좋은 이름은 주석을 대체하고, 의도를 명확히 하고, 코드를 문서처럼 만듭니다. 변수명과 함수명에 시간을 투자하세요.
Clean Code: Insurance for Your Future Self
1. Prologue: The Night I Couldn't Read My Own Code
Six months ago, I shipped a feature.
Today, my PM pinged me on Slack: "Hey, can you add a quick filter to that feature? Should take 30 minutes max."
I confidently replied, "Sure, on it!"
Then I opened the file I had written 6 months ago.
function doStuff(data) {
if(data.status == 1) {
if(data.type == "premium") {
// ...500 lines of nested logic
} else {
// ...300 lines
}
} else if(data.status == 2) {
// ...700 lines
}
}
"...what the hell is this?"
Variable names: d, tmp, x, arr2. Zero comments. A single 1,500-line function. Function names like doStuff, processData, handleThing.
"Who wrote this garbage?"
I was furious. I ran git blame.
Author: Me <me@example.com>
Date: 6 months ago
Message: "quick fix"
The culprit was me.
That "quick 30-minute task" took 8 hours. I spent 6 hours just trying to understand my own code. That's when it hit me: The biggest victim of bad code is your future self.
After that humbling experience, I took clean code seriously. I read Robert C. Martin's Clean Code, applied it at work, failed many times, and eventually understood: Clean code isn't for others—it's for me.
2. The Struggle: Why We Write Terrible Code
2-1. The "Make It Work First" Trap
When deadlines loom, we tell ourselves: "Let's just make it work now. I'll refactor later."
But "later" never comes. New features pile up. Bugs flood in. "Later" gets pushed to infinity. Then 6 months pass, and you open that file again... and you relive my prologue.
I learned this the hard way: To go fast, you must go clean.
Dirty code might seem fast in the short term, but it tanks team productivity over time.
2-2. The Broken Windows Theory
In criminology, there's a concept called the "Broken Windows Theory": If you leave one broken window in a building, people think "this place isn't maintained" and break more windows. Eventually, the whole building decays.
Code is the same. One messy file signals to the next developer: "Oh, we don't care about quality here." Soon, the entire codebase becomes spaghetti. You enter Legacy Hell.
This metaphor hit me hard. Our team's legacy project was exactly this.
2-3. Code Is for Humans, Not Machines
Compilers don't care if your variable is named x or calculateTotalPriceWithTaxAndDiscount. The compiled output is identical.
But code is read by humans before machines execute it.
"Programs must be written for people to read, and only incidentally for machines to execute." — Harold Abelson
"Indeed, the ratio of time spent reading versus writing is well over 10 to 1." — Robert C. Martin
Once I internalized this, my coding style changed completely. I stopped trying to write "short, clever" code and started writing "readable" code.
3. The Aha Moment: Core Principles of Clean Code
3-1. Naming: The Hardest Part of Programming
Phil Karlton once said: "There are only two hard things in Computer Science: cache invalidation and naming things."
Naming looks trivial but is actually one of the most critical skills in programming.
Rule 1: Reveal Intent
Bad:
let d; // elapsed time in days
let e; // email
Without comments, you can't tell if d means data, date, or distance. Even with comments, code changes but comments don't get updated, causing lies.
Good:
let daysSinceCreation;
let daysSinceModification;
let elapsedTimeInDays;
Now the name is the comment.
Rule 2: Avoid Disinformation
Bad:
let accountList = { /* Actually a Map */ };
let hp = "Hit Points"; // "hp" commonly means Hewlett-Packard
To programmers, List implies a specific data structure. If it's not a List, don't call it one.
Good:
let accountMap;
let accounts;
let accountCollection;
let hitPoints;
let playerHealth;
Rule 3: Use Searchable Names
Bad:
// What does 7 mean? (Magic Number)
if (student.age > 7) {
enrollInSchool(student);
}
// Searching for "1" returns thousands of results
if (user.status == 1) {
// ...
}
Good:
const MINIMUM_SCHOOL_AGE = 7;
const STATUS_ACTIVE = 1;
const STATUS_INACTIVE = 0;
const STATUS_SUSPENDED = 2;
if (student.age >= MINIMUM_SCHOOL_AGE) {
enrollInSchool(student);
}
if (user.status === STATUS_ACTIVE) {
// ...
}
Now searching for MINIMUM_SCHOOL_AGE finds every usage. If the age requirement changes, you update one constant.
Rule 4: Avoid Encodings
Bad (Hungarian Notation - relic of the past):
let strName; // Don't encode types in names
let iCount;
let bIsValid;
With modern static typing (TypeScript), encodings are pointless.
Good:
let name: string;
let count: number;
let isValid: boolean;
Rule 5: Classes Are Nouns, Methods Are Verbs
Bad:
class Manager { } // Too vague
class Process { }
function user() { } // Not a verb
Good:
class AccountManager { }
class OrderProcessor { }
function createUser() { }
function deleteAccount() { }
function validateInput() { }
3-2. Functions: Small, Then Smaller
Rule 1: Do One Thing (Single Responsibility Principle)
If your function is named loginAndCreateSessionAndLogAccessAndSendEmail(), you've already failed.
Bad:
function processUser(user) {
// Validation
if (!user.email) throw new Error("No email");
if (!user.password) throw new Error("No password");
// Hash password
const hashedPassword = bcrypt.hash(user.password, 10);
// Save to DB
db.users.insert({ email: user.email, password: hashedPassword });
// Send welcome email
sendEmail(user.email, "Welcome!");
// Log
logger.info(`User created: ${user.email}`);
// Slack notification
slack.notify(`New user: ${user.email}`);
}
This function does 6 things: validation, hashing, persistence, emailing, logging, and notifications. Hard to test, hard to reuse.
Good:
function createUser(user) {
validateUser(user);
const hashedPassword = hashPassword(user.password);
const savedUser = saveUserToDatabase(user.email, hashedPassword);
sendWelcomeEmail(user.email);
logUserCreation(user.email);
notifyTeam(user.email);
return savedUser;
}
function validateUser(user) {
if (!user.email) throw new Error("Email is required");
if (!user.password) throw new Error("Password is required");
if (!isValidEmail(user.email)) throw new Error("Invalid email format");
}
function hashPassword(password) {
return bcrypt.hash(password, 10);
}
function saveUserToDatabase(email, hashedPassword) {
return db.users.insert({ email, password: hashedPassword });
}
function sendWelcomeEmail(email) {
sendEmail(email, "Welcome to our platform!");
}
function logUserCreation(email) {
logger.info(`User created: ${email}`);
}
function notifyTeam(email) {
slack.notify(`New user registered: ${email}`);
}
Now each function does exactly one thing. Easy to test, easy to reuse, easy to understand.
Rule 2: Maintain Consistent Abstraction Levels
Bad:
function renderPage() {
checkUserPermission(); // High-level abstraction
let html = ""; // Low-level (implementation detail)
html += "<div class='header'>";
html += "<h1>Welcome</h1>";
html += "</div>";
fetchDataFromDatabaseAndProcessIt(); // High-level
for (let i = 0; i < data.length; i++) { // Low-level
html += data[i].render();
}
return html;
}
Abstraction levels are all over the place. Some lines handle high-level concepts, others deal with low-level details.
Good:
function renderPage() {
if (!isAuthorizedUser()) {
return renderUnauthorizedPage();
}
const data = fetchPageData();
const content = buildPageContent(data);
return renderPageLayout(content);
}
Now the function reads like a table of contents. All statements are at the same level of abstraction.
Rule 3: Fewer Arguments Is Better
Ideal argument counts:
- 0 arguments (niladic): Best
- 1 argument (monadic): Good
- 2 arguments (dyadic): OK
- 3 arguments (triadic): Avoid
- 4+ arguments (polyadic): Never
Bad:
makeCircle(10, 20, 5, "red", true, 0.8, "solid");
// Which is x? Which is y? Which is radius?
Good:
const circle = {
x: 10,
y: 20,
radius: 5,
color: "red",
filled: true,
opacity: 0.8,
borderStyle: "solid"
};
makeCircle(circle);
Passing an object makes arguments self-documenting and order-independent.
Rule 4: Avoid Side Effects
Bad:
function checkPassword(username, password) {
const user = db.findUser(username);
if (user.password === password) {
Session.initialize(); // Surprise side effect!
return true;
}
return false;
}
The function name says "check password," but it also initializes a session. This is a lie.
Good:
function checkPassword(username, password) {
const user = db.findUser(username);
return user.password === password;
}
function login(username, password) {
if (checkPassword(username, password)) {
Session.initialize();
return true;
}
return false;
}
Now each function does what its name promises.
3-3. Comments: Admitting Failure
Robert C. Martin wrote:
"Comments are always failures. We must have them because we cannot always figure out how to express ourselves without them, but their use is not a cause for celebration."
The biggest problem with comments? They lie. Code changes, but comments don't get updated. Then comments mislead rather than help.
Examples of Bad Comments
Bad:
// Check if employee is eligible for benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))
Good:
if (employee.isEligibleForFullBenefits())
No comment needed. The function name explains everything.
Bad:
// Increment i by 1
i++;
This is utterly useless. It just repeats what the code says.
Bad (outdated, lying comment):
// This function returns the user's age
function getUserData(userId) {
// Actually returns the entire user object
return db.users.find(userId);
}
When Comments Are Good
Comments are justified in some cases:
1. Legal comments:
// Copyright (C) 2025 My Company. All rights reserved.
// Licensed under the MIT License
2. Informative comments (only when the function name can't express it):
// Regex explanation: Matches dates in "YYYY-MM-DD" format
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
3. Explanation of intent (the "why"):
// Performance optimization: using hash map instead of linear search
// Benchmark: 10x faster with 1000+ items
const userMap = new Map();
4. Warning comments:
// WARNING: This function may take 3+ seconds to complete.
// Do NOT call it on the main thread.
function processLargeFile(file) {
// ...
}
5. TODO comments:
// TODO: Add caching layer (JIRA-1234)
// TODO: Improve error handling
But in most cases, fix the code instead of writing a comment.
4. Deep Dive: Code Smells and Refactoring
4-1. Common Code Smells
Martin Fowler defined dozens of code smells in Refactoring. Here are the most common ones.
1) Long Function
If a function doesn't fit on one screen, split it. Ideally 5-10 lines, max 20.
2) Large Class
If a class has too many responsibilities, break it apart.
3) Long Parameter List
More than 3 parameters? Use an object.
4) Duplicated Code
Same code in multiple places? Extract it into one function. DRY (Don't Repeat Yourself).
Bad:
function calculateDiscountForPremiumUser(price) {
const discount = price * 0.2;
return price - discount;
}
function calculateDiscountForVIPUser(price) {
const discount = price * 0.3;
return price - discount;
}
function calculateDiscountForRegularUser(price) {
const discount = price * 0.1;
return price - discount;
}
Good:
const DISCOUNT_RATES = {
PREMIUM: 0.2,
VIP: 0.3,
REGULAR: 0.1
};
function calculateDiscount(price, userType) {
const discountRate = DISCOUNT_RATES[userType];
const discount = price * discountRate;
return price - discount;
}
5) Dead Code
Delete unused code without mercy. Don't leave it "just in case." You have Git.
Bad:
function processOrder(order) {
processPayment(order);
// Old version. Not sure if I should delete it
// if (order.isOldFormat) {
// convertOldFormat(order);
// }
}
Good:
function processOrder(order) {
processPayment(order);
}
6) Primitive Obsession
Don't represent meaningful data only with primitives. Create objects.
Bad:
function createUser(email, streetAddress, city, zipCode, country) {
// 4 address-related parameters
}
Good:
class Address {
constructor(street, city, zipCode, country) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
this.country = country;
}
getFullAddress() {
return `${this.street}, ${this.city} ${this.zipCode}, ${this.country}`;
}
}
function createUser(email, address) {
// Address is now one cohesive object
}
7) Switch Statement Hell
Endless switch or if-else chains? Use polymorphism.
Bad:
function getAnimalSound(animal) {
if (animal.type === 'dog') {
return 'bark';
} else if (animal.type === 'cat') {
return 'meow';
} else if (animal.type === 'bird') {
return 'tweet';
} else if (animal.type === 'cow') {
return 'moo';
}
// Every new animal type requires modifying this function (OCP violation)
}
function getMovementSpeed(animal) {
if (animal.type === 'dog') {
return 15;
} else if (animal.type === 'cat') {
return 20;
} else if (animal.type === 'bird') {
return 50;
}
// Same switch repeated again...
}
Good (using polymorphism):
class Animal {
getSound() {
throw new Error("Must implement getSound()");
}
getMovementSpeed() {
throw new Error("Must implement getMovementSpeed()");
}
}
class Dog extends Animal {
getSound() { return 'bark'; }
getMovementSpeed() { return 15; }
}
class Cat extends Animal {
getSound() { return 'meow'; }
getMovementSpeed() { return 20; }
}
class Bird extends Animal {
getSound() { return 'tweet'; }
getMovementSpeed() { return 50; }
}
// Now even if we have 100 animal types, this function never changes
function printAnimalInfo(animal) {
console.log(`This animal says: ${animal.getSound()}`);
console.log(`Speed: ${animal.getMovementSpeed()} km/h`);
}
This is the Open-Closed Principle (OCP): Open for extension, closed for modification.
4-2. Real-World Refactoring Workshop
Let me refactor actual "garbage code" I wrote.
Before (Code Smell Paradise)
function proc(d) {
// d is order data array
let r = [];
for(let i=0; i<d.length; i++) {
// If status is 1 and price exists
if(d[i].s == 1 && d[i].p > 0) {
let t = d[i].p * d[i].q; // price * quantity
if(d[i].t == "p") { // if premium, discount
t = t * 0.9;
} else if(d[i].t == "v") { // if VIP, bigger discount
t = t * 0.8;
}
// Add shipping fee
if(t < 50) {
t += 5;
}
r.push({id: d[i].id, tot: t});
}
}
return r;
}
Problems:
- Function name
procis meaningless - Variable names
d,r,t,s,p,qare all abbreviated - Magic Numbers:
1,0.9,0.8,50,5 - Magic Strings:
"p","v" - Code relies on comments
- Mixed abstraction levels
Step 1: Rename Everything
function calculateOrderTotals(orders) {
let results = [];
for(let i=0; i<orders.length; i++) {
if(orders[i].status == 1 && orders[i].price > 0) {
let total = orders[i].price * orders[i].quantity;
if(orders[i].type == "p") {
total = total * 0.9;
} else if(orders[i].type == "v") {
total = total * 0.8;
}
if(total < 50) {
total += 5;
}
results.push({id: orders[i].id, total: total});
}
}
return results;
}
Better. But Magic Numbers and Strings remain.
Step 2: Extract Constants
const ORDER_STATUS_ACTIVE = 1;
const USER_TYPE_PREMIUM = "p";
const USER_TYPE_VIP = "v";
const PREMIUM_DISCOUNT_RATE = 0.9;
const VIP_DISCOUNT_RATE = 0.8;
const FREE_SHIPPING_THRESHOLD = 50;
const SHIPPING_FEE = 5;
function calculateOrderTotals(orders) {
let results = [];
for(let i=0; i<orders.length; i++) {
if(orders[i].status === ORDER_STATUS_ACTIVE && orders[i].price > 0) {
let total = orders[i].price * orders[i].quantity;
if(orders[i].type === USER_TYPE_PREMIUM) {
total = total * PREMIUM_DISCOUNT_RATE;
} else if(orders[i].type === USER_TYPE_VIP) {
total = total * VIP_DISCOUNT_RATE;
}
if(total < FREE_SHIPPING_THRESHOLD) {
total += SHIPPING_FEE;
}
results.push({id: orders[i].id, total: total});
}
}
return results;
}
Much clearer! Now we know what each number means.
Step 3: Extract Functions
const ORDER_STATUS_ACTIVE = 1;
const USER_TYPE_PREMIUM = "premium";
const USER_TYPE_VIP = "vip";
const PREMIUM_DISCOUNT_RATE = 0.9;
const VIP_DISCOUNT_RATE = 0.8;
const FREE_SHIPPING_THRESHOLD = 50;
const SHIPPING_FEE = 5;
function calculateOrderTotals(orders) {
return orders
.filter(isActiveOrder)
.map(calculateSingleOrderTotal);
}
function isActiveOrder(order) {
return order.status === ORDER_STATUS_ACTIVE && order.price > 0;
}
function calculateSingleOrderTotal(order) {
let subtotal = calculateSubtotal(order);
subtotal = applyUserDiscount(subtotal, order.userType);
subtotal = addShippingFee(subtotal);
return {
id: order.id,
total: subtotal
};
}
function calculateSubtotal(order) {
return order.price * order.quantity;
}
function applyUserDiscount(amount, userType) {
if (userType === USER_TYPE_VIP) {
return amount * VIP_DISCOUNT_RATE;
}
if (userType === USER_TYPE_PREMIUM) {
return amount * PREMIUM_DISCOUNT_RATE;
}
return amount;
}
function addShippingFee(amount) {
if (amount < FREE_SHIPPING_THRESHOLD) {
return amount + SHIPPING_FEE;
}
return amount;
}
Results:
- Each function does exactly one thing
- Code is self-documenting (no comments needed)
- Easy to test (each function can be tested independently)
- Easy to reuse
- Easy to find bugs
Step 4: Apply Polymorphism (Going Further)
If user types keep growing, use polymorphism.
interface UserType {
getDiscountRate(): number;
}
class RegularUser implements UserType {
getDiscountRate() {
return 1.0; // No discount
}
}
class PremiumUser implements UserType {
getDiscountRate() {
return 0.9; // 10% off
}
}
class VIPUser implements UserType {
getDiscountRate() {
return 0.8; // 20% off
}
}
class Order {
constructor(
public id: string,
public price: number,
public quantity: number,
public status: number,
public userType: UserType
) {}
isActive(): boolean {
return this.status === ORDER_STATUS_ACTIVE && this.price > 0;
}
calculateTotal(): number {
let subtotal = this.price * this.quantity;
subtotal *= this.userType.getDiscountRate();
if (subtotal < FREE_SHIPPING_THRESHOLD) {
subtotal += SHIPPING_FEE;
}
return subtotal;
}
}
function calculateOrderTotals(orders: Order[]) {
return orders
.filter(order => order.isActive())
.map(order => ({
id: order.id,
total: order.calculateTotal()
}));
}
Now new user types can be added without modifying existing code. Perfect Open-Closed Principle (OCP) compliance.
5. Real-World Application: Building Clean Code Culture
5-1. Code Reviews: The Gatekeeper
"How to tell someone their code is garbage without hurting feelings"
Code reviews aren't about blame. They're about learning together and growing together.
Bad review comments:
Reviewer: "This code is completely wrong."
Reviewer: "Why did you write it this way?"
Reviewer: "Rewrite it."
Good review comments:
Reviewer: "This function looks a bit long. Would splitting out the validation
logic into a separate function make it easier to test? What do you think?"
Reviewer: "Good catch! But I think there's one more edge case here.
What happens when a user signs up without an email?"
Reviewer: "Nice algorithm choice! Could you add a comment about time complexity?
It'll help when we optimize later."
Good responses from reviewee:
Reviewee: "Great suggestion. I've refactored it as you suggested."
Reviewee: "Oh, I missed that case. I'll add a test for it."
Reviewee: "The deadline is tight. Can I merge this for now and create a
refactoring ticket for the next sprint?"
5-2. Linters and Formatters: Automation
Having humans check indentation and style manually is a waste of time. Automate it.
ESLint + Prettier setup:
// .eslintrc.json
{
"rules": {
"max-lines-per-function": ["error", 50],
"max-params": ["error", 3],
"complexity": ["error", 10],
"no-magic-numbers": ["warn"],
"prefer-const": "error",
"no-var": "error"
}
}
Pre-commit Hook:
# .husky/pre-commit
npm run lint
npm run test
Don't waste time arguing about code style. Let tools handle it.
5-3. The Boy Scout Rule
"Always leave the campground cleaner than you found it."
Apply this to code:
"When you modify a file, leave it slightly cleaner than when you opened it."
Fixing a bug? While you're there:
- Rename one messy variable
- Split one long function
- Delete one piece of dead code
- Replace one comment with better code
These small improvements compound. Over time, the entire system gets better. Conversely, if you don't do this, the system rots.
5-4. Managing Technical Debt
Code you write hastily with the thought "I'll fix it later" becomes technical debt. Debt accrues interest.
Interest on technical debt:
- Longer bug fix times
- Slower feature development
- Longer onboarding times for new team members
- Increased developer stress
Technical debt management strategies:
-
Distinguish intentional vs. unintentional debt
- Intentional: "Rushing for deadline, will refactor next sprint"
- Unintentional: "Didn't know better, wrote it badly"
-
Maintain a Tech Debt Backlog
- Label tech debt issues in JIRA or GitHub
- Allocate ~20% of each sprint to paying down debt
-
Break big debt into small chunks
- Tickets like "Refactor entire codebase" never get done
- Break it down: "Refactor User class", "Add unit tests to login logic"
6. Connection to SOLID Principles
Clean code is deeply connected to SOLID principles. Here's a quick summary.
1. Single Responsibility Principle
A class or function should have only one reason to change.
// Bad: User class has too many responsibilities
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
saveToDatabase() { /* ... */ } // DB responsibility
sendEmail() { /* ... */ } // Email responsibility
generateReport() { /* ... */ } // Report responsibility
validateData() { /* ... */ } // Validation responsibility
}
// Good: Separate responsibilities
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class UserRepository {
save(user) { /* ... */ }
}
class EmailService {
send(user, message) { /* ... */ }
}
class UserReportGenerator {
generate(user) { /* ... */ }
}
class UserValidator {
validate(user) { /* ... */ }
}
2. Open/Closed Principle
Open for extension, closed for modification. (The polymorphism example above demonstrates this.)
3. Liskov Substitution Principle
Child classes should be substitutable for their parent classes.
4. Interface Segregation Principle
Clients shouldn't depend on interfaces they don't use.
5. Dependency Inversion Principle
Depend on abstractions, not concretions.
7. TDD: The Safety Net for Clean Code
"Refactoring without tests is like tightrope walking without a net."
To maintain clean code, you need psychological safety. The confidence that "I can change this code without breaking things." That confidence comes from tests.
The Three Laws of TDD
- Red: Write a failing test first (define intent)
- Green: Write minimal code to pass the test
- Refactor: Remove duplication and improve structure (this is when code becomes clean!)
Example:
// 1. Red: Write test first
describe('calculateDiscount', () => {
it('should apply 10% discount for premium users', () => {
const result = calculateDiscount(100, 'premium');
expect(result).toBe(90);
});
});
// 2. Green: Minimal code to pass
function calculateDiscount(price, userType) {
if (userType === 'premium') {
return price * 0.9;
}
return price;
}
// 3. Refactor: Improve structure
const DISCOUNT_RATES = {
premium: 0.1,
vip: 0.2,
regular: 0
};
function calculateDiscount(price, userType) {
const discountRate = DISCOUNT_RATES[userType] || 0;
return price * (1 - discountRate);
}
With tests, you can refactor confidently. If something breaks, tests catch it immediately.
8. Pre-Commit Checklist
Before committing, ask yourself:
Naming:
- Do variable/function names clearly reveal intent?
- Did I avoid vague names like
tmp,data,info? - Did I extract Magic Numbers into constants?
Functions:
- Does each function do one thing?
- Are functions under 20 lines?
- Are there 3 or fewer parameters?
- Are abstraction levels consistent?
Comments:
- Is the code understandable without comments?
- Are comments accurate (not lying)?
- Could I fix the code instead of writing a comment?
Structure:
- No duplicated code? (DRY)
- Dead code deleted?
- Could
if-elsehell be replaced with polymorphism?
Tests:
- Are there tests?
- Do all tests pass after refactoring?
9. Conclusion: Clean Code Is a Habit
At first, it feels awkward. You spend 3 minutes naming a variable. You spend 10 minutes splitting a function.
"Isn't this too slow?"
But by mid-project, it changes. You spend far more time reading code than writing it, so clean code makes the entire team much faster.
When you open your code 6 months later, do you want to think, "Who wrote this? They did a great job"?
Start writing clean code today.
10. Key Terms
-
Refactoring: Improving internal code structure without changing external behavior.
-
Code Smell: Surface indications that usually correspond to deeper problems. Examples: long functions, duplicated code, long parameter lists, large classes.
-
Technical Debt: Code written hastily with the plan to "fix it later." Accrues interest, making it harder to fix over time.
-
Magic Number: Numbers that appear without explanation (
if (age > 18)). Should be extracted into named constants. -
KISS (Keep It Simple, Stupid): Avoid unnecessary complexity. Keep things simple.
-
YAGNI (You Ain't Gonna Need It): Don't implement features "just in case." Only build what you need now.
-
DRY (Don't Repeat Yourself): Don't copy-paste code. Extract repeated logic into functions.
-
Boy Scout Rule: "Always leave the campground cleaner than you found it." When modifying code, improve it slightly before committing.
-
Side Effect: A function doing things beyond what its name promises. Avoid them.
-
Polymorphism: Multiple types implementing the same interface, enabling type-specific behavior without
if-elsechains.