
클린 코드: 동료를 위한 배려, 미래의 나를 위한 보험
나만 알아보는 코드는 쓰레기입니다. 변수명 짓기부터 함수 쪼개기, 그리고 주석을 달지 말아야 하는 이유까지. 6개월 뒤의 나를 살리는 리팩토링의 기술.

나만 알아보는 코드는 쓰레기입니다. 변수명 짓기부터 함수 쪼개기, 그리고 주석을 달지 말아야 하는 이유까지. 6개월 뒤의 나를 살리는 리팩토링의 기술.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

로버트 C. 마틴(Uncle Bob)이 제안한 클린 아키텍처의 핵심은 무엇일까요? 양파 껍질 같은 계층 구조와 의존성 규칙(Dependency Rule)을 통해 프레임워크와 UI로부터 독립적인 소프트웨어를 만드는 방법을 정리합니다.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

프로젝트를 시작한 지 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』를 읽고, 실제에 적용해보며, 수많은 시행착오를 겪었다. 결국 이거였다. 클린 코드는 남을 위한 것이 아니라 나 자신을 위한 것이다.
데드라인이 촉박할 때, 우리는 이렇게 생각한다. "일단 돌아가게 만들고, 나중에 리팩토링하자."
하지만 그 "나중"은 오지 않는다. 새로운 기능 요청이 쌓이고, 버그가 쏟아지고, "나중"은 영원히 미래로 미뤄진다. 그러다 6개월 뒤 내가 그 코드를 다시 열어보면... 위에 있는 프롤로그처럼 된다.
처음 이 사실을 받아들였다. 빠르게 가려면 클린 코드를 짜야 한다는 것을. 더러운 코드는 단기적으로는 빠를 수 있지만, 장기적으로는 팀 전체의 생산성을 무너뜨린다.
범죄학에 "깨진 유리창 법칙"이라는 게 있다. 건물에 깨진 유리창 하나를 방치하면, 사람들이 "여기는 관리가 안 되는구나"라고 생각해서 더 많은 유리창을 깨고, 결국 건물 전체가 황폐해진다는 이론이다.
코드도 마찬가지다. 한 파일에 더러운 코드가 있으면, 다음 개발자도 "아, 여기는 대충 짜도 되는구나"라고 생각한다. 그렇게 프로젝트 전체가 스파게티가 되고, 결국 Legacy Hell에 빠진다.
나는 이 비유가 너무 와닿았다. 실제로 내가 맡은 레거시 프로젝트가 정확히 그런 상태였다.
컴파일러는 변수명이 x든 calculateTotalPriceWithTaxAndDiscount든 상관하지 않는다. 빌드 결과는 똑같다.
하지만 코드는 기계가 실행하기 전에, 사람이 읽고 이해하고 유지보수해야 한다.
"프로그래밍은 사람에게 의도를 전달하는 행위이며, 기계는 그 부산물을 실행할 뿐이다." — Donald Knuth
"모든 코드는 코드를 짠 시간보다 읽히는 시간이 10배 더 길다." — Robert C. Martin
이 사실을 이해했다는 순간, 내 코딩 스타일이 완전히 바뀌었다. 나는 더 이상 "짧고 똑똑한" 코드를 짜려고 하지 않고, "읽기 쉬운" 코드를 짜려고 노력하게 됐다.
Phil Karlton이 말했다: "컴퓨터 과학에서 어려운 것은 단 두 가지뿐이다. 캐시 무효화와 이름 짓기."
이름 짓기는 쉬워 보이지만, 실제로는 프로그래밍에서 가장 중요한 스킬 중 하나다.
let d; // 경과 시간 (단위: 날짜)
let e; // 이메일
주석이 없으면 d가 data인지 date인지 distance인지 모른다.
주석이 있어도 시간이 지나면 코드는 변하는데 주석은 업데이트를 까먹는다.
let daysSinceCreation;
let daysSinceModification;
let elapsedTimeInDays;
이제 주석이 필요 없다. 변수명 자체가 주석이다.
let accountList = { /* Map 객체 */ };
let hp = "Hit Points"; // hp가 흔히 Hewlett-Packard를 의미하므로 혼란
프로그래머에게 List는 특정한 자료구조를 의미한다. 실제로 List가 아니라면 accountList라고 쓰면 안 된다.
let accountMap;
let accounts;
let accountCollection;
let hitPoints;
let playerHealth;
// 숫자 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로 검색하면 모든 사용처가 나온다. 나중에 학교 입학 나이가 바뀌면 상수 하나만 수정하면 된다.
let strName; // 타입을 변수명에 넣지 마라
let iCount;
let bIsValid;
TypeScript 같은 정적 타입 언어를 쓰면 이런 인코딩은 완전히 불필요하다.
Good:let name: string;
let count: number;
let isValid: boolean;
class Manager { } // 너무 모호함
class Process { }
function user() { } // 동사가 아님
Good:
class AccountManager { }
class OrderProcessor { }
function createUser() { }
function deleteAccount() { }
function validateInput() { }
함수 이름이 loginAndCreateSessionAndLogAccessAndSendEmail()이라면 이미 잘못된 것이다.
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}`);
}
이제 각 함수는 딱 한 가지 일만 한다. 테스트하기 쉽고, 재사용하기 쉽고, 이해하기 쉽다.
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);
}
이제 함수를 읽으면 마치 책의 목차처럼 읽힌다. 모든 문장이 같은 추상화 수준에 있다.
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);
객체를 넘기면 인수가 늘어나도 가독성이 좋고, 순서도 상관없다.
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;
}
이제 각 함수가 자신의 이름이 약속한 것만 한다.
로버트 C. 마틴은 이렇게 말했다:
"주석은 코드로 의도를 표현하지 못해 실패했음을 의미한다."
주석의 가장 큰 문제는 거짓말을 한다는 것이다. 코드는 변하지만 주석은 업데이트를 까먹는다. 그러면 주석은 코드를 이해하는 데 도움이 되기는커녕 방해가 된다.
// 사용자가 복지 혜택을 받을 자격이 있는지 확인
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: 에러 핸들링 개선 필요
하지만 대부분의 경우, 코드를 고쳐서 주석을 없애는 것이 최선이다.
Martin Fowler는 『Refactoring』에서 수십 가지 Code Smell을 정의했다. 여기서는 가장 자주 보이는 것들을 정리해본다.
함수가 한 화면을 넘어가면 쪼개야 한다. 이상적으로는 5-10줄, 최대 20줄 이내.
클래스가 너무 많은 책임을 지고 있다면 쪼개라.
인수가 3개를 넘어가면 객체로 묶어라.
똑같은 코드가 여러 곳에 있으면 하나로 합쳐라. 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;
}
사용하지 않는 코드는 과감하게 삭제하라. "나중에 필요할 수도 있어"라고 남겨두지 마라. Git이 있다.
Bad:function processOrder(order) {
// 주문 처리
processPayment(order);
// 아래 코드는 예전 버전. 지우면 안 될 것 같아서 주석 처리함
// if (order.isOldFormat) {
// convertOldFormat(order);
// }
}
Good:
function processOrder(order) {
processPayment(order);
}
의미 있는 데이터를 원시 타입으로만 표현하지 마라. 객체를 만들어라.
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) {
// 이제 주소는 하나의 객체로 처리
}
switch나 if-else가 끝없이 이어진다면? 다형성(Polymorphism)으로 해결하라.
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)이다. 확장에는 열려 있고, 수정에는 닫혀 있다.
내가 실제로 짰던 "쓰레기 코드"를 단계별로 고쳐보자.
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가 전부 축약형1, 0.9, 0.8, 50, 5"p", "v"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이 남아 있다.
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;
}
훨씬 좋아졌다! 이제 숫자들이 무엇을 의미하는지 명확하다.
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;
}
개선 결과:
만약 사용자 타입이 더 늘어난다면? 다형성을 사용하자.
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)을 완벽하게 지킨다.
"기분 나쁘지 않게 쓰레기 코드라고 말하는 법"
코드 리뷰는 비난의 장이 아니다. 함께 배우고 성장하는 장이다.
나쁜 리뷰 코멘트:리뷰어: "이 코드는 완전히 틀렸어요."
리뷰어: "이걸 왜 이렇게 짰죠?"
리뷰어: "다시 짜세요."
좋은 리뷰 코멘트:
리뷰어: "이 함수가 좀 길어 보이는데, 검증 로직을 별도 함수로 분리하면
테스트하기 더 쉬울 것 같습니다. 어떻게 생각하시나요?"
리뷰어: "Good catch! 다만 이 부분에서 예외 케이스가 하나 더 있을 것 같은데요.
사용자가 이메일 없이 가입하는 경우는 어떻게 처리하시나요?"
리뷰어: "이 알고리즘 선택 좋네요! 혹시 시간복잡도를 코멘트로 남겨주시면
나중에 최적화할 때 도움이 될 것 같습니다."
리뷰이의 좋은 반응:
리뷰이: "좋은 의견 감사합니다. 말씀하신 대로 리팩토링했습니다."
리뷰이: "아, 그 케이스를 놓쳤네요. 테스트 추가하겠습니다."
리뷰이: "데드라인이 촉박해서 일단 이렇게 머지하고, 다음 스프린트에
리팩토링 티켓 끊어서 개선하면 안 될까요?"
사람이 매번 들여쓰기와 스타일을 검사하는 건 시간 낭비다. 자동화하자.
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
코드 스타일 논쟁에 시간 쓰지 말고, 도구에 맡기자.
"캠핑장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라."
이 규칙을 코드에 적용하면?
"파일을 수정할 때, 처음 열었을 때보다 조금이라도 더 깨끗하게 만들고 커밋하라."버그를 고치러 들어갔나요? 그 김에:
이런 작은 개선이 쌓이면 시스템 전체가 점점 좋아진다. 반대로, 이걸 안 하면 시스템은 점점 더러워진다.
"나중에 고쳐야지" 하고 대충 짠 코드는 기술 부채가 된다. 부채는 이자가 붙는다.
기술 부채의 이자:클린 코드는 SOLID 원칙과 깊이 연결되어 있다. 간단히 정리해본다.
클래스나 함수는 딱 한 가지 이유로만 변경되어야 한다.
// 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) { /* ... */ }
}
확장에는 열려 있고, 수정에는 닫혀 있어야 한다. (위에서 본 다형성 예제가 바로 이것)
자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.
구체적인 것이 아니라 추상적인 것에 의존해야 한다.
"테스트 코드가 없는 리팩토링은 줄타기 곡예와 같다."
클린 코드를 유지하려면 심리적 안정감이 필요하다. "내가 이 코드를 고쳐도 망가지지 않는다"는 확신. 그 확신은 테스트 코드에서 나온다.
// 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);
}
테스트가 있으면 자신 있게 리팩토링할 수 있다. 뭔가 깨지면 테스트가 바로 알려주기 때문이다.
코드를 커밋하기 전에 스스로에게 물어보자:
이름 짓기:tmp, data, info 같은 모호한 이름을 쓰지 않았는가?if-else 지옥을 다형성으로 바꿀 수 있는가?처음에는 어색하다. 변수명 하나 짓는데 3분을 고민하고, 함수 하나 쪼개는데 10분을 쓰고...
"이렇게 하면 너무 느린 거 아냐?"
하지만 프로젝트 중반만 가도 달라진다. 코드를 읽는 시간이 쓰는 시간보다 압도적으로 많아지기 때문에, 클린 코드가 전체 개발 속도를 훨씬 빠르게 만든다.
6개월 뒤 내 코드를 열어봤을 때, "이거 누가 짰어? 잘 짰네"라고 생각하고 싶다면?
지금부터 클린 코드를 짜기 시작하자.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 지옥을 없앨 수 있다.
A: 초반에는 느립니다. 하지만 프로젝트 중반만 가도 코드를 읽는 시간이 쓰는 시간보다 압도적으로 많아지기 때문에, 클린 코드가 전체 개발 속도를 훨씬 빠르게 만듭니다. 더러운 코드로 짠 프로젝트는 시간이 지날수록 생산성이 0에 수렴합니다.
Q2: 주석은 정말 필요 없나요?A: "Why(왜 이렇게 짰는지)"를 설명하는 주석은 훌륭합니다. 하지만 "What(무슨 코드인지)"을 설명하는 주석은 코드가 실패했다는 증거입니다. 코드로 말하세요.
Q3: 리팩토링은 언제 해야 하나요?A: 기능을 추가할 때, 버그를 고칠 때, 코드 리뷰 할 때. 즉, 항상 해야 합니다. 따로 날 잡아서 하는 게 아닙니다. 보이스카우트 규칙을 따르세요.
Q4: 함수가 너무 많아지면 오히려 복잡해지지 않나요?A: 처음에는 그렇게 느껴질 수 있습니다. 하지만 각 함수의 이름이 잘 지어져 있다면, 코드는 마치 책의 목차처럼 읽힙니다. 디테일이 필요할 때만 함수 내부로 들어가면 됩니다.
Q5: Copy-Paste가 왜 나쁜가요?A: 로직이 변경되면 복사한 모든 곳을 찾아서 수정해야 합니다. 하나라도 놓치면 버그가 됩니다. DRY 원칙을 지키세요.
Q6: 짧은 코드가 항상 좋은 건가요?A: 아닙니다. a > b ? a : b는 괜찮지만, 100자짜리 정규식 원라이너는 읽을 수 없습니다. 명확함 > 간결함.
A: 코드 리뷰와 린팅 도구(ESLint, Prettier)를 사용하세요. 하지만 가장 중요한 건 팀 문화입니다. 리더가 먼저 실천해야 합니다.
Q8: 레거시 코드는 어떻게 개선하나요?A: 한 번에 다 고치려고 하지 마세요. 보이스카우트 규칙을 따라 조금씩 개선하세요. 그리고 무엇보다 테스트 코드를 먼저 작성하세요. 테스트 없는 리팩토링은 위험합니다.
Q9: 데드라인이 촉박할 때도 클린 코드를 짜야 하나요?A: 역설적이지만, 데드라인이 촉박할수록 클린 코드를 짜야 합니다. 더러운 코드는 디버깅과 버그 수정에 시간을 더 많이 쓰게 만들기 때문입니다. "빨리 가려면 제대로 가라."
Q10: 클린 코드의 가장 중요한 원칙 하나만 고르라면?A: 이름 짓기입니다. 좋은 이름은 주석을 대체하고, 의도를 명확히 하고, 코드를 문서처럼 만듭니다. 변수명과 함수명에 시간을 투자하세요.
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.
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.
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.
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.
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.
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.
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.
let daysSinceCreation;
let daysSinceModification;
let elapsedTimeInDays;
Now the name is the comment.
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.
let accountMap;
let accounts;
let accountCollection;
let hitPoints;
let playerHealth;
// 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.
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;
class Manager { } // Too vague
class Process { }
function user() { } // Not a verb
Good:
class AccountManager { }
class OrderProcessor { }
function createUser() { }
function deleteAccount() { }
function validateInput() { }
If your function is named loginAndCreateSessionAndLogAccessAndSendEmail(), you've already failed.
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.
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.
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.
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.
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.
// 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);
}
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.
Martin Fowler defined dozens of code smells in Refactoring. Here are the most common ones.
If a function doesn't fit on one screen, split it. Ideally 5-10 lines, max 20.
If a class has too many responsibilities, break it apart.
More than 3 parameters? Use an object.
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;
}
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);
}
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
}
Endless switch or if-else chains? Use polymorphism.
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.
Let me refactor actual "garbage code" I wrote.
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:
proc is meaninglessd, r, t, s, p, q are all abbreviated1, 0.9, 0.8, 50, 5"p", "v"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.
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.
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:
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.
"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?"
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.
"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:
These small improvements compound. Over time, the entire system gets better. Conversely, if you don't do this, the system rots.
Code you write hastily with the thought "I'll fix it later" becomes technical debt. Debt accrues interest.
Interest on technical debt:Clean code is deeply connected to SOLID principles. Here's a quick summary.
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) { /* ... */ }
}
Open for extension, closed for modification. (The polymorphism example above demonstrates this.)
Child classes should be substitutable for their parent classes.
Clients shouldn't depend on interfaces they don't use.
Depend on abstractions, not concretions.
"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.
// 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.
Before committing, ask yourself:
Naming:tmp, data, info?if-else hell be replaced with polymorphism?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.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-else chains.