Monday, September 22, 2025

동료에게 환영받는 코드의 비밀: 실무 중심 리팩토링

"이 코드 대체 누가 짰어?"

개발자라면 누구나 한 번쯤 들어봤을, 그리고 가장 듣기 싫은 말 중 하나일 것입니다. 특히 경력이 많지 않은 주니어 개발자에게는 이 한마디가 심리적인 압박감과 불안감으로 다가오기도 합니다. 내가 작성한 코드가 미래의 나 혹은 동료에게 기술 부채(Technical Debt)라는 짐을 지우게 될까 봐 걱정하는 것은 지극히 자연스러운 일입니다. 우리는 모두 '좋은 코드'를 작성하고 싶어 합니다.

그렇다면 '좋은 코드'란 무엇일까요? 이 질문에 대한 답은 다양하지만, 여러 전문가와 서적에서 공통적으로 강조하는 핵심은 바로 '가독성(Readability)''유지보수성(Maintainability)'입니다. 즉, 컴퓨터가 이해하는 것을 넘어, 코드를 처음 보는 사람도 그 의도와 흐름을 쉽게 파악하고, 향후 요구사항 변경에 유연하게 대처할 수 있는 코드가 바로 좋은 코드입니다. 전설적인 서적 '클린 코드(Clean Code)'는 이러한 원칙들을 집대성했지만, 방대한 분량과 철학적인 내용 때문에 당장 눈앞의 코드를 개선해야 하는 주니어 개발자에게는 다소 막막하게 느껴질 수 있습니다.

이 글은 '클린 코드'의 모든 철학을 한 번에 습득하려는 시도 대신, 오늘 당장 당신의 코드에 적용하여 "이 코드, 참 깔끔하네요!"라는 칭찬을 들을 수 있는 7가지 실용적인 원칙을 제시합니다. 단순히 이론을 나열하는 것이 아니라, 우리가 흔히 작성하는 '나쁜 코드' 예시를 통해 문제점을 진단하고, 점진적으로 개선해 나가는 과정을 구체적으로 보여줄 것입니다. 이 원칙들을 습관으로 만든다면, 당신은 더 이상 코드 리뷰를 두려워하지 않고, 팀의 생산성에 기여하는 신뢰받는 동료로 성장할 수 있을 것입니다.


원칙 1. 이름 속에 의도를 담아라: 명확한 변수와 함수 네이밍

코드는 주석보다 이름으로 스스로를 설명해야 합니다. 변수, 함수, 클래스의 이름은 해당 요소의 존재 이유, 역할, 사용 방법을 명확하게 드러내야 합니다. 모호하고 축약된 이름은 당장의 타이핑 시간을 조금 줄여줄지는 몰라도, 코드를 읽는 모든 사람(미래의 당신을 포함하여)의 시간을 빼앗고 오해를 불러일으키는 주범이 됩니다.

나쁜 코드 예시: 무엇을 하는지 알 수 없는 이름들


// 사용자 데이터 처리 함수
function process(data) {
  // data는 사용자 정보 배열이라고 가정
  const d = 30; // 이건 뭘까?
  
  for (let i = 0; i < data.length; i++) {
    if (data[i].age > 19 && data[i].d > d) { // d가 또 나오네?
      const user = data[i];
      // ... 어떤 로직 수행
    }
  }
}

const list1 = [
  { name: 'John', age: 25, d: 40 },
  { name: 'Jane', age: 17, d: 10 },
];
process(list1);

개선된 코드 예시: 이름만으로 역할이 보이는 코드


/**
 * 활성 사용자 목록을 필터링하여 반환합니다.
 * 활성 사용자: 20세 이상 성인이며, 마지막 접속일로부터 30일이 지나지 않은 사용자
 * @param {Array<Object>} userList - 사용자 정보 배열
 * @returns {Array<Object>} 필터링된 활성 사용자 목록
 */
function filterActiveUsers(userList) {
  const INACTIVE_DAYS_THRESHOLD = 30; // '비활성'으로 간주하는 임계 기간(일)
  const LEGAL_ADULT_AGE = 20; // 법적 성인으로 간주하는 나이

  const activeUsers = userList.filter(user => 
    user.age >= LEGAL_ADULT_AGE && user.daysSinceLastLogin <= INACTIVE_DAYS_THRESHOLD
  );
  
  return activeUsers;
}

const users = [
  { name: 'John', age: 25, daysSinceLastLogin: 40 },
  { name: 'Jane', age: 17, daysSinceLastLogin: 10 },
  { name: 'Alex', age: 30, daysSinceLastLogin: 5 },
];
const activeUsers = filterActiveUsers(users);
console.log(activeUsers); // [{ name: 'Alex', age: 30, daysSinceLastLogin: 5 }]

왜 이렇게 개선해야 할까요?

첫 번째 코드의 문제점은 명확합니다. process라는 함수 이름은 '처리한다'는 막연한 의미 외에 아무런 정보를 주지 못합니다. 무엇을, 어떻게 처리한다는 것일까요? data, list1과 같은 변수명 역시 마찬가지입니다. 이것이 사용자 목록인지, 상품 목록인지, 아니면 단순 숫자 배열인지 알 수 없습니다. 최악은 d라는 변수입니다. 첫 번째 d는 숫자 30을 담고 있고, 두 번째 d는 객체의 속성으로 사용됩니다. 이 둘이 어떤 관계인지, 각각 무엇을 의미하는지 파악하려면 코드의 다른 부분을 샅샅이 뒤지거나, 코드를 작성한 원작자에게 물어봐야만 합니다.

반면, 개선된 코드는 이름만으로도 전체적인 맥락이 그려집니다. filterActiveUsers는 '활성 사용자를 필터링한다'는 명확한 역할을 설명합니다. 매개변수 이름은 userList로 바뀌어 사용자 정보의 '목록'이라는 형태까지 암시합니다. 가장 모호했던 dINACTIVE_DAYS_THRESHOLD(비활성 임계일)와 daysSinceLastLogin(마지막 로그인 후 경과일)이라는 구체적인 이름으로 재탄생했습니다. 이제 코드를 처음 보는 사람도 user.daysSinceLastLogin <= INACTIVE_DAYS_THRESHOLD 라는 조건식을 보고 "아, 마지막으로 로그인한 지 30일이 넘지 않은 사용자를 찾는구나"라고 즉시 이해할 수 있습니다.

좋은 이름 짓기 Tip:

  • 구체적으로 작성하세요: temp, data, info, handle과 같은 막연한 이름 대신 temperatureInCelsius, customerData, productInfo, handleLoginButtonClick처럼 구체적인 맥락을 담으세요.
  • 불리언(Boolean) 값은 `is`, `has`, `can`으로 시작하세요: 변수 이름만 봐도 참/거짓 값을 담고 있음을 알 수 있습니다. (예: isLoggedIn, hasPermission, canEdit)
  • 함수 이름은 동사로 시작하세요: 함수는 어떤 동작을 수행하므로, 그 동작을 나타내는 동사로 시작하는 것이 자연스럽습니다. (예: getUser, calculateTotalPrice, validateInput)
  • 단위나 자료구조를 명시하세요: width 보다는 widthInPixels, users 보다는 userArray 또는 userMap 처럼 단위를 포함하거나 자료구조를 암시하면 오해의 소지를 줄일 수 있습니다.

명확한 이름 짓기는 단순히 코드를 예쁘게 꾸미는 작업이 아닙니다. 이는 코드의 가독성을 높여 버그 발생 가능성을 줄이고, 동료와의 협업을 원활하게 만드는 가장 기본적이고 강력한 도구입니다.


원칙 2. 함수는 한 가지 일만 잘하게 하라: 단일 책임 원칙 (SRP)

소프트웨어 공학의 중요한 원칙 중 하나인 단일 책임 원칙(Single Responsibility Principle, SRP)은 함수에도 그대로 적용됩니다. 좋은 함수는 이름 그대로 '한 가지' 기능만 수행하고, 그 기능을 완벽하게 해내야 합니다. 하나의 함수가 사용자 입력 유효성 검사, 데이터베이스 조회, 비즈니스 로직 처리, 결과 포맷팅, UI 업데이트 등 여러 책임을 동시에 지고 있다면 그 함수는 '만능 칼'이 아니라 '괴물 함수'가 될 가능성이 높습니다.

이런 함수는 길이가 길고 복잡해 이해하기 어렵고, 작은 수정이 예기치 않은 곳에서 버그를 유발하며(사이드 이펙트), 재사용이 거의 불가능하고, 단위 테스트를 작성하기 매우 까다롭습니다.

나쁜 코드 예시: 모든 것을 다 하려는 거대 함수


function createReport(data) {
  // 1. 입력 데이터 유효성 검사
  if (!data || !Array.isArray(data) || data.length === 0) {
    console.error("오류: 유효하지 않은 데이터입니다.");
    // 2. 오류 발생 시 특정 DOM 요소에 메시지 표시 (UI 로직)
    const errorDiv = document.getElementById('error-message');
    errorDiv.innerText = "보고서를 생성할 데이터가 없습니다.";
    errorDiv.style.display = 'block';
    return;
  }
  
  // 3. 보고서 헤더 생성
  let report = "월간 판매 보고서\n";
  report += "====================\n";
  
  let totalRevenue = 0;
  
  // 4. 핵심 비즈니스 로직: 데이터 순회 및 총 수익 계산
  for (const item of data) {
    if (item.price > 0 && item.quantity > 0) {
      const revenue = item.price * item.quantity;
      totalRevenue += revenue;
      // 5. 보고서 내용 포맷팅
      report += `${item.name}: ${item.quantity}개 * ${item.price}원 = ${revenue}원\n`;
    }
  }
  
  // 6. 보고서 푸터 생성 및 포맷팅
  report += "====================\n";
  report += `총 수익: ${totalRevenue}원\n`;
  
  // 7. 최종 보고서를 콘솔에 출력하고 DOM에 렌더링 (UI 로직)
  console.log(report);
  const reportContainer = document.getElementById('report-container');
  reportContainer.innerText = report;
  
  return report;
}

개선된 코드 예시: 각자의 역할에 충실한 작은 함수들


// 1. 데이터 유효성 검사 책임
function isValidReportData(data) {
  return data && Array.isArray(data) && data.length > 0;
}

// 2. 총 수익 계산 책임 (핵심 비즈니스 로직)
function calculateTotalRevenue(data) {
  return data
    .filter(item => item.price > 0 && item.quantity > 0)
    .reduce((total, item) => total + (item.price * item.quantity), 0);
}

// 3. 보고서 텍스트 생성 책임 (포맷팅)
function formatReportText(data, totalRevenue) {
  const header = "월간 판매 보고서\n====================\n";
  const footer = `====================\n총 수익: ${totalRevenue}원\n`;
  
  const body = data
    .filter(item => item.price > 0 && item.quantity > 0)
    .map(item => {
      const revenue = item.price * item.quantity;
      return `${item.name}: ${item.quantity}개 * ${item.price}원 = ${revenue}원`;
    })
    .join('\n');
    
  return `${header}${body}\n${footer}`;
}

// 4. UI 업데이트 책임
function displayReport(reportText) {
  console.log(reportText);
  const reportContainer = document.getElementById('report-container');
  reportContainer.innerText = reportText;
}

function displayError(message) {
  const errorDiv = document.getElementById('error-message');
  errorDiv.innerText = message;
  errorDiv.style.display = 'block';
}

// 5. 함수들을 조합하여 전체 프로세스를 제어하는 메인 함수
function generateAndDisplayReport(data) {
  if (!isValidReportData(data)) {
    displayError("보고서를 생성할 데이터가 없습니다.");
    return;
  }
  
  const totalRevenue = calculateTotalRevenue(data);
  const reportText = formatReportText(data, totalRevenue);
  displayReport(reportText);
}

왜 이렇게 개선해야 할까요?

나쁜 예시의 createReport 함수는 최소 5가지 이상의 책임을 가지고 있습니다: 데이터 검증, 비즈니스 로직(수익 계산), 문자열 포맷팅, 콘솔 출력, DOM 조작. 만약 보고서의 포맷만 바꾸고 싶어도, 우리는 수익 계산 로직과 UI 업데이트 로직까지 함께 들여다봐야 합니다. 실수로 다른 부분을 건드려 버그를 만들 위험도 커집니다. 또한, '총 수익 계산' 로직만 따로 떼어 다른 곳에서 사용하고 싶어도, 이 거대 함수 때문에 재사용이 불가능합니다.

개선된 코드는 각 책임을 명확하게 분리된 함수로 나누었습니다. isValidReportData는 오직 데이터 유효성만 검사하고 boolean 값을 반환합니다. calculateTotalRevenue는 순수하게 계산 로직에만 집중합니다. formatReportText는 데이터를 받아 예쁜 문자열로 만드는 역할만 합니다. displayReportdisplayError는 UI를 업데이트하는 책임만 집니다. 마지막으로 generateAndDisplayReport 함수는 이 작은 부품들을 조립하여 전체적인 흐름을 조율하는 '지휘자' 역할을 합니다.

이렇게 함수를 분리했을 때 얻는 이점은 명확합니다.

  • 가독성 향상: 각 함수의 이름과 코드가 짧고 명확하여, 무엇을 하는지 즉시 파악할 수 있습니다.
  • 유지보수 용이성: 보고서 제목만 바꾸고 싶다면? formatReportText 함수만 수정하면 됩니다. 다른 함수는 건드릴 필요가 없어 안전합니다.
  • 재사용성 증가: 웹페이지뿐만 아니라, 이메일로도 총 수익을 보내야 하는 기능이 추가되었다고 상상해보세요. calculateTotalRevenue 함수를 그대로 가져다 쓸 수 있습니다.
  • 테스트 용이성: calculateTotalRevenue([ { price: 10, quantity: 2 } ])가 20을 반환하는지 테스트하기는 매우 쉽습니다. 하지만 거대한 createReport 함수를 테스트하려면 DOM 환경까지 모킹(mocking)해야 하는 등 훨씬 복잡한 준비가 필요합니다.

함수를 작성할 때 "이 함수가 한 가지 이상의 일을 하고 있나?"라고 스스로 질문하는 습관을 들이세요. 만약 그렇다면, 주저하지 말고 더 작은 함수로 분리하세요. 코드가 조금 길어지는 것처럼 느껴질 수 있지만, 장기적으로는 훨씬 더 깨끗하고 견고한 코드를 만들 수 있습니다.


원칙 3. 마법을 부리지 말라: 의미 없는 숫자와 문자열을 상수로 대체

코드 중간에 뜬금없이 나타나는 숫자나 문자열을 '매직 넘버(Magic Number)' 또는 '매직 스트링(Magic String)'이라고 부릅니다. 이러한 값들은 당장 코드를 작성할 때는 그 의미를 명확히 알고 있겠지만, 몇 주, 몇 달 뒤에 다시 보면 '이 숫자 86400이 대체 뭐였지?'라며 스스로에게 되묻게 됩니다. 동료 개발자는 말할 것도 없습니다. 이는 코드의 의도를 파악하는 데 심각한 방해 요소가 됩니다.

나쁜 코드 예시: 곳곳에 숨어있는 마법의 숫자와 문자열


function checkUserAccess(user) {
  // 사용자의 상태 코드가 'ACTIVE'이고, 등급이 3 이상이어야 접근 가능
  if (user.status === 'ACTIVE' && user.grade > 2) {
    // ... 접근 허용 로직
    return true;
  }
  return false;
}

function setTokenExpiration() {
  const token = generateToken();
  // 토큰 만료 시간을 24시간으로 설정
  const expirationTime = new Date().getTime() + 86400000; 
  saveToken(token, expirationTime);
}

// 상품 가격에 10% 부가세 추가
function calculateFinalPrice(price) {
  return price * 1.1;
}

개선된 코드 예시: 이름으로 의미를 설명하는 상수


// 사용자 관련 상수
const USER_STATUS = {
  ACTIVE: 'ACTIVE',
  DORMANT: 'DORMANT',
  BANNED: 'BANNED',
};
const MINIMUM_ACCESS_GRADE = 3;

function checkUserAccess(user) {
  const isUserActive = user.status === USER_STATUS.ACTIVE;
  const hasSufficientGrade = user.grade >= MINIMUM_ACCESS_GRADE;
  
  if (isUserActive && hasSufficientGrade) {
    // ... 접근 허용 로직
    return true;
  }
  return false;
}

// 시간 관련 상수 (단위: 밀리초)
const ONE_SECOND_IN_MS = 1000;
const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
const ONE_HOUR_IN_MS = 60 * ONE_MINUTE_IN_MS;
const ONE_DAY_IN_MS = 24 * ONE_HOUR_IN_MS; // 86400000

function setTokenExpiration() {
  const token = generateToken();
  const expirationTime = new Date().getTime() + ONE_DAY_IN_MS;
  saveToken(token, expirationTime);
}

// 세금 관련 상수
const VAT_RATE = 0.10; // Value Added Tax (부가가치세)

function calculateFinalPrice(price) {
  const vat = price * VAT_RATE;
  return price + vat;
}

왜 이렇게 개선해야 할까요?

나쁜 코드에서 `user.grade > 2` 라는 조건은 "등급이 2보다 커야 한다"는 의미로, "3등급부터 가능하다"는 비즈니스 규칙을 담고 있습니다. 하지만 왜 3등급부터인지, 2는 무엇을 의미하는지 코드만 봐서는 알 수 없습니다. 만약 정책이 바뀌어 4등급부터 접근 가능하게 하려면, 코드에서 `2`를 `3`으로 바꿔야 합니다. 이 규칙이 여러 곳에서 사용된다면 모든 곳을 찾아 수정해야 하며, 하나라도 놓치면 버그가 됩니다.

마찬가지로 숫자 `86400000`은 24시간을 밀리초로 변환한 값입니다. 이 숫자를 보고 즉시 '24시간'을 떠올릴 수 있는 개발자는 많지 않습니다. `1.1` 역시 '10% 부가세'라는 중요한 비즈니스 로직을 함축하고 있지만, 그 의미가 전혀 드러나지 않습니다.

개선된 코드는 이러한 '마법의 값'들을 모두 이름이 있는 상수로 추출했습니다. `MINIMUM_ACCESS_GRADE = 3`는 그 자체로 "최소 접근 등급은 3이다"라는 규칙을 설명합니다. 이제 조건식은 `user.grade >= MINIMUM_ACCESS_GRADE`가 되어 훨씬 명확해졌습니다. 정책이 4등급으로 변경되면 상수 값만 `4`로 수정하면, 이 상수를 사용하는 모든 곳에 일괄적으로 반영됩니다. 이는 유지보수성을 극적으로 향상시킵니다.

`ONE_DAY_IN_MS`라는 상수는 `86400000`이라는 모호한 숫자에 '하루(밀리초 단위)'라는 명확한 의미를 부여합니다. 심지어 `24 * ONE_HOUR_IN_MS`와 같이 그 값이 어떻게 계산되었는지 과정을 보여줌으로써 실수를 방지하고 이해를 돕습니다. `VAT_RATE = 0.10` 역시 마찬가지입니다. `price * 1.1`보다 `price + (price * VAT_RATE)`가 조금 더 길지는 몰라도, "원가에 부가세를 더한다"는 계산 과정을 명확하게 보여주어 코드의 의도를 훨씬 잘 설명합니다.

또한, `USER_STATUS`처럼 관련된 문자열들을 객체로 묶어 관리하면, 오타로 인한 버그를 방지할 수 있습니다. `user.status === 'ACITVE'`(오타) 와 같은 실수는 컴파일러나 린터가 잡아내기 어렵지만, `user.status === USER_STATUS.ACITVE`(존재하지 않는 속성에 접근)는 개발 환경에서 즉시 오류를 표시해 줄 가능성이 높습니다.

코드에 숫자나 문자열 리터럴을 직접 작성하기 전에 잠시 멈추고 생각해보세요. "이 값에 이름을 붙여줄 수 있는가?" 대부분의 경우, 답은 '그렇다'일 것입니다. 그 작은 노력이 당신의 코드를 마법이 아닌 논리로 만들어 줄 것입니다.


원칙 4. 반복되는 코드는 하나로 묶어라: DRY 원칙 (Don't Repeat Yourself)

DRY는 "스스로를 반복하지 말라(Don't Repeat Yourself)"의 약자로, 소프트웨어 개발에서 가장 유명하고 중요한 원칙 중 하나입니다. 이 원칙의 핵심은 '모든 지식은 시스템 내에서 단일하고, 모호하지 않으며, 권위 있는 표현을 가져야 한다'는 것입니다. 쉽게 말해, 똑같은 로직이나 코드가 여러 곳에 복사-붙여넣기 되어 있다면, 이를 하나의 함수나 모듈로 추상화하여 중복을 제거해야 한다는 의미입니다.

코드 중복은 여러 가지 문제를 야기합니다. 첫째, 로직을 수정해야 할 때 중복된 모든 곳을 찾아 빠짐없이 수정해야 합니다. 하나라도 놓치면 시스템 전체의 일관성이 깨지고 예상치 못한 버그가 발생합니다. 둘째, 코드베이스의 전체적인 양이 불필요하게 늘어나 가독성과 유지보수성을 떨어뜨립니다. 셋째, 중복된 코드 조각들은 각기 조금씩 다르게 변형될 가능성이 높아져, 결국 서로 다른 동작을 하는 미묘한 버그의 온상이 됩니다.

나쁜 코드 예시: 미묘하게 반복되는 유효성 검사 로직


function saveUser(name, email) {
  // 사용자 이름 유효성 검사
  if (name === null || name === undefined || name.trim() === '') {
    throw new Error('이름은 필수 항목입니다.');
  }
  if (name.length > 20) {
    throw new Error('이름은 20자를 초과할 수 없습니다.');
  }
  
  // 이메일 유효성 검사
  if (email === null || email === undefined || email.trim() === '') {
    throw new Error('이메일은 필수 항목입니다.');
  }
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    throw new Error('유효하지 않은 이메일 형식입니다.');
  }
  
  // ... 사용자 저장 로직 ...
  console.log('사용자 저장 성공:', { name, email });
}

function updateUserProfile(userId, name) {
  // 프로필 이름 유효성 검사
  if (name === null || name === undefined || name.trim() === '') {
    throw new Error('이름은 필수 항목입니다.');
  }
  if (name.length > 20) {
    throw new Error('이름은 20자를 초과할 수 없습니다.');
  }
  
  // ... 프로필 업데이트 로직 ...
  console.log('프로필 업데이트 성공:', { userId, name });
}

개선된 코드 예시: 재사용 가능한 유틸리티 함수로 추상화


// validator.js - 유효성 검사 로직을 모아놓은 유틸리티 모듈

const MAX_NAME_LENGTH = 20;

function isNullOrWhitespace(value) {
  return value === null || value === undefined || String(value).trim() === '';
}

function validateName(name) {
  if (isNullOrWhitespace(name)) {
    throw new Error('이름은 필수 항목입니다.');
  }
  if (name.length > MAX_NAME_LENGTH) {
    throw new Error(`이름은 ${MAX_NAME_LENGTH}자를 초과할 수 없습니다.`);
  }
}

function validateEmail(email) {
  if (isNullOrWhitespace(email)) {
    throw new Error('이메일은 필수 항목입니다.');
  }
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    throw new Error('유효하지 않은 이메일 형식입니다.');
  }
}


// userService.js - 실제 비즈니스 로직을 처리하는 서비스

// import { validateName, validateEmail } from './validator.js';

function saveUser(name, email) {
  validateName(name);
  validateEmail(email);
  
  // ... 사용자 저장 로직 ...
  console.log('사용자 저장 성공:', { name, email });
}

function updateUserProfile(userId, name) {
  validateName(name);
  
  // ... 프로필 업데이트 로직 ...
  console.log('프로필 업데이트 성공:', { userId, name });
}

왜 이렇게 개선해야 할까요?

나쁜 예시에서는 '사용자 이름 유효성 검사' 로직이 saveUser 함수와 updateUserProfile 함수에 거의 동일하게 반복되고 있습니다. 지금은 두 군데뿐이지만, 시스템이 커지면서 사용자 이름을 입력받는 기능이 추가될 때마다 이 코드는 계속해서 복사-붙여넣기 될 것입니다. 만약 이름 정책이 '20자'에서 '15자'로 변경된다면 어떻게 될까요? 개발자는 이 로직이 사용된 모든 곳을 기억해내고, 하나도 빠짐없이 수정해야 합니다. 이는 매우 번거롭고 실수를 유발하기 쉬운 작업입니다.

개선된 코드는 이 중복되는 로직을 validateName이라는 명확한 이름의 함수로 추출했습니다. 이제 이름 유효성 검사가 필요한 모든 곳에서는 이 함수를 호출하기만 하면 됩니다. 이름 정책이 변경되면, 우리는 오직 validator.js 파일의 MAX_NAME_LENGTH 상수와 관련 로직만 수정하면 됩니다. 이 한 번의 수정으로 validateName을 사용하는 모든 기능에 새로운 정책이 자동으로 적용됩니다. 이것이 바로 DRY 원칙의 힘입니다.

더 나아가, `isNullOrWhitespace`와 같은 더 작은 단위의 공통 로직을 추출하여 재사용성을 극대화했습니다. validateNamevalidateEmail 모두 '값이 비어있는지'를 검사해야 하는데, 이 로직을 별도 함수로 만들어 양쪽에서 모두 활용하고 있습니다. 이렇게 잘게 쪼개진 유틸리티 함수들은 시스템 전반에서 레고 블록처럼 조합하여 강력하고 일관된 기능을 만들어내는 기반이 됩니다.

코드를 작성하다가 '어, 이 로직 어디선가 본 것 같은데?'라는 생각이 든다면, 그 즉시 중복을 의심하고 추상화를 고민해야 합니다. 세 번 이상 반복되는 코드는 반드시 함수나 클래스로 분리하는 것을 원칙으로 삼는 것이 좋습니다. 처음에는 조금 번거롭게 느껴질 수 있지만, 이러한 노력은 결국 미래의 당신과 동료들을 반복 작업과 잠재적 버그의 늪에서 구해줄 것입니다.


원칙 5. 코드가 최고의 주석이다: 좋은 코드는 스스로를 설명한다

주니어 개발자들이 흔히 하는 오해 중 하나는 '주석이 많을수록 좋은 코드'라는 생각입니다. 물론 주석은 복잡한 비즈니스 로직의 배경을 설명하거나, 특정 기술적 선택의 이유(Why)를 밝히는 데 유용하게 사용될 수 있습니다. 하지만 코드의 동작 방식(How)을 설명하는 주석은 대부분 코드 자체를 더 명확하게 개선함으로써 제거할 수 있으며, 오히려 나쁜 코드의 냄새(Code Smell)일 경우가 많습니다.

주석은 코드와 별개로 존재하기 때문에, 코드가 수정될 때 함께 관리되지 않으면 금방 낡고 사실과 다른 정보가 되어버리기 쉽습니다. 잘못된 주석은 없는 것보다 해롭습니다. 코드를 읽는 사람에게 혼란을 주고, 버그의 원인이 되기도 합니다. 따라서 우리의 최우선 목표는 주석 없이도 쉽게 읽히는 코드를 작성하는 것이어야 합니다. 주석은 코드로 도저히 표현할 수 없는 마지막 수단으로 남겨두어야 합니다.

나쁜 코드 예시: 코드를 번역하는 불필요한 주석들


// 사용자 목록과 플래그를 받아서 처리하는 함수
function procUsers(list, f) {
  // 루프를 돌면서 각 사용자를 확인
  for (let i = 0; i < list.length; i++) {
    // 플래그가 true이고, 사용자의 타입이 1이면
    if (f && list[i].type === 1) {
      // 해당 사용자를 활성화시킨다
      list[i].status = 'active';
    }
  }
}

개선된 코드 예시: 주석을 제거하고 코드를 명확하게 리팩토링


const USER_TYPE = {
  PREMIUM: 1,
  GENERAL: 2,
};

function activatePremiumUsers(users) {
  for (const user of users) {
    if (user.type === USER_TYPE.PREMIUM) {
      user.status = 'active';
    }
  }
}

// 만약 조건부 활성화가 필요하다면, 아래와 같이 의도를 명확히 드러낼 수 있다.
function activatePremiumUsersIfEnabled(users, isActivationEnabled) {
  if (!isActivationEnabled) {
    return; // 아무것도 하지 않고 즉시 종료 (Guard Clause)
  }

  // isActivationEnabled가 true일 때만 아래 로직이 실행됨
  for (const user of users) {
    if (user.type === USER_TYPE.PREMIUM) {
      user.status = 'active';
    }
  }
}

왜 이렇게 개선해야 할까요?

나쁜 코드 예시의 주석들은 코드 한 줄 한 줄을 한국어로 번역하고 있을 뿐, 새로운 정보를 전혀 제공하지 않습니다. // 루프를 돌면서 각 사용자를 확인 이라는 주석은 for문을 모르는 사람이 아니라면 아무런 의미가 없습니다. 오히려 procUsers, list, f, `type === 1`과 같이 모호한 이름과 매직 넘버 때문에 코드를 이해하기 어려우니, 주석으로라도 억지로 설명을 덧붙이고 있는 상황입니다.

이런 주석은 근본적인 문제를 해결하는 것이 아니라, 임시방편으로 덮어두는 것에 불과합니다. 만약 나중에 프리미엄 사용자 타입이 `1`에서 `100`으로 바뀐다면, 개발자는 코드의 `list[i].type === 1`은 수정하겠지만 `// 사용자의 타입이 1이면` 이라는 주석을 함께 수정하는 것을 잊어버릴 가능성이 매우 높습니다. 결국 코드와 주석은 서로 다른 이야기를 하게 됩니다.

개선된 코드는 주석을 모두 삭제했습니다. 그 대신, 주석이 필요했던 이유, 즉 코드의 모호함을 근본적으로 해결했습니다.

  1. 명확한 이름 사용: procUsersactivatePremiumUsers로, listusers로, fisActivationEnabled와 같이 의도를 명확히 드러내는 이름으로 변경했습니다. 이제 함수 시그니처만 봐도 "프리미엄 사용자를 활성화하는구나" 혹은 "활성화 옵션이 켜져 있을 때 프리미엄 사용자를 활성화하는구나"라고 이해할 수 있습니다.
  2. 매직 넘버 제거: `type === 1`은 `user.type === USER_TYPE.PREMIUM`으로 변경하여, 숫자 1이 '프리미엄 회원'이라는 비즈니스 의미를 가짐을 명확히 했습니다.

이러한 리팩토링을 통해 코드는 스스로를 설명하는 '자기-문서화(Self-Documenting)' 코드가 되었습니다. 이제 더 이상 동작 방식을 설명하는 주석은 필요하지 않습니다.

그렇다면 언제 주석을 사용해야 할까요?

  • '왜(Why)'를 설명할 때: 코드만 봐서는 알 수 없는 비즈니스 결정이나 기술적인 트레이드오프를 설명할 때 사용합니다.
    // HACK: IE11에서 발생하는 특정 렌더링 버그를 우회하기 위해 강제로 리플로우를 발생시킴
  • TODO 주석: 지금 당장 처리할 수는 없지만, 미래에 개선해야 할 부분을 명시할 때 사용합니다.
    // TODO: 현재는 임시 하드코딩된 데이터 사용 중. 추후 API 연동 필요.
  • 공개 API에 대한 문서화 주석: JSDoc, JavaDoc 등 정해진 형식을 따라 라이브러리나 모듈의 사용법을 설명하는 주석은 매우 유용합니다.

주석을 달기 전에 항상 "이 주석 없이 코드를 더 명확하게 만들 수는 없을까?"라고 먼저 질문하세요. 대부분의 경우, 답은 '있다'일 것이고, 그 고민의 과정이 당신을 더 나은 개발자로 만들어 줄 것입니다.


원칙 6. 예측 가능하게 만들어라: 부수 효과(Side Effect) 줄이기

함수의 부수 효과(Side Effect)란, 함수가 자신의 스코프(scope) 바깥에 있는 변수나 상태를 변경하거나, 함수 외부의 세상과 상호작용(네트워크 요청, 데이터베이스 쓰기, 콘솔 출력 등)하는 것을 의미합니다. 함수가 return 값을 반환하는 것 외에 다른 일을 할 때 부수 효과가 발생했다고 말합니다.

모든 부수 효과가 나쁜 것은 아닙니다. 애플리케이션이 의미 있는 작업을 하려면 결국 화면에 무언가를 그리거나(부수 효과), 서버에 데이터를 저장(부수 효과)해야 합니다. 하지만 제어되지 않는 부수 효과는 코드의 동작을 예측하기 어렵게 만드는 주범입니다. 어떤 함수를 호출했는데, 그 함수가 내가 전혀 예상치 못한 곳의 데이터를 바꿔버린다면 어떨까요? 그런 코드는 디버깅하기 매우 까다롭고, 버그의 원인을 추적하기 어렵게 만듭니다.

따라서 좋은 코드는 부수 효과를 최대한 격리하고, 불가피한 경우 명확하게 드러내도록 설계해야 합니다. 특히, 함수의 입력값(인자)을 직접 수정하는 것은 매우 위험한 부수 효과 중 하나입니다.

나쁜 코드 예시: 입력받은 배열을 직접 수정하는 함수


function sortAndAddRank(users) {
  // 1. 원본 배열을 직접 정렬하여 순서를 바꿔버린다. (부수 효과!)
  users.sort((a, b) => b.score - a.score); 
  
  // 2. 원본 배열의 각 요소에 새로운 속성을 추가하여 수정한다. (부수 효과!)
  for (let i = 0; i < users.length; i++) {
    users[i].rank = i + 1;
  }
  
  return users;
}

const userList = [
  { name: 'Bob', score: 85 },
  { name: 'Alice', score: 92 },
  { name: 'Charlie', score: 78 },
];

console.log('함수 호출 전 원본 배열:', JSON.parse(JSON.stringify(userList)));
// 출력: 함수 호출 전 원본 배열: [{name:"Bob",score:85},{name:"Alice",score:92},{name:"Charlie",score:78}]

const rankedUsers = sortAndAddRank(userList);

console.log('함수 호출 후 반환된 배열:', rankedUsers);
// 출력: 함수 호출 후 반환된 배열: [{name:"Alice",score:92,rank:1},{name:"Bob",score:85,rank:2},{name:"Charlie",score:78,rank:3}]

console.log('함수 호출 후 원본 배열:', userList);
// 출력: 함수 호출 후 원본 배열: [{name:"Alice",score:92,rank:1},{name:"Bob",score:85,rank:2},{name:"Charlie",score:78,rank:3}]
// 앗! 원본 배열이 예기치 않게 변경되었다!

개선된 코드 예시: 원본을 보존하고 새로운 배열을 반환하는 순수 함수


function createRankedUserList(users) {
  // 1. 원본 배열의 복사본을 만든다. (얕은 복사)
  // 전개 연산자(...)나 slice()를 사용하여 원본을 보존한다.
  const usersCopy = [...users]; 
  
  // 2. 복사본을 정렬한다. 원본 배열은 영향을 받지 않는다.
  usersCopy.sort((a, b) => b.score - a.score);
  
  // 3. map을 사용하여 기존 객체에 rank 속성이 추가된 '새로운' 객체로 구성된 '새로운' 배열을 생성한다.
  const rankedUsers = usersCopy.map((user, index) => ({
    ...user, // 기존 user 객체의 속성을 복사
    rank: index + 1, // rank 속성을 새로 추가
  }));
  
  return rankedUsers;
}


const userList = [
  { name: 'Bob', score: 85 },
  { name: 'Alice', score: 92 },
  { name: 'Charlie', score: 78 },
];

console.log('함수 호출 전 원본 배열:', JSON.parse(JSON.stringify(userList)));
// 출력: 함수 호출 전 원본 배열: [{name:"Bob",score:85},{name:"Alice",score:92},{name:"Charlie",score:78}]

const rankedUsers = createRankedUserList(userList);

console.log('함수 호출 후 반환된 배열:', rankedUsers);
// 출력: 함수 호출 후 반환된 배열: [{name:"Alice",score:92,rank:1},{name:"Bob",score:85,rank:2},{name:"Charlie",score:78,rank:3}]

console.log('함수 호출 후 원본 배열:', userList);
// 출력: 함수 호출 후 원본 배열: [{name:"Bob",score:85},{name:"Alice",score:92},{name:"Charlie",score:78}]
// 원본 배열이 안전하게 보존되었다!

왜 이렇게 개선해야 할까요?

나쁜 예시의 sortAndAddRank 함수는 매우 위험합니다. 이 함수를 호출하는 개발자는 단순히 순위가 매겨진 사용자 목록을 '받기'를 기대할 뿐, 자신이 전달한 userList 원본 데이터가 '변경'될 것이라고 예상하기 어렵습니다. 만약 이 userList가 애플리케이션의 다른 부분에서도 사용되고 있었다면, 예를 들어 원래 순서대로 사용자 목록을 화면에 보여주는 부분에서 심각한 버그가 발생할 것입니다. 이처럼 함수의 동작이 외부 상태에 미치는 영향을 추적해야 하는 코드는 복잡성이 기하급수적으로 증가합니다.

개선된 코드의 createRankedUserList 함수는 '순수 함수(Pure Function)'에 가깝게 동작합니다. 순수 함수의 두 가지 주요 특징은 다음과 같습니다.

  1. 동일한 입력에 대해 항상 동일한 출력을 반환한다.
  2. 부수 효과가 없다. (외부 상태를 변경하지 않는다.)

이 함수는 먼저 `[...users]`를 통해 전달받은 배열의 복사본을 만듭니다. 그 후 모든 정렬과 수정 작업을 이 '복사본'에 대해서만 수행합니다. 마지막으로 `map` 메서드를 사용하여 각 사용자 객체 역시 복사본을 만들고(`...user`) 거기에 새로운 `rank` 속성을 추가한 뒤, 이 새로운 객체들로 이루어진 완전히 새로운 배열을 만들어 반환합니다. 결과적으로 원본 userList는 함수 호출 전과 후에 전혀 변하지 않습니다.

이렇게 부수 효과를 제거하면 다음과 같은 엄청난 이점을 얻을 수 있습니다.

  • 예측 가능성: 함수가 어떤 일을 할지 명확하게 예측할 수 있습니다. 입력값을 망가뜨릴 걱정 없이 안심하고 함수를 호출할 수 있습니다.
  • 테스트 용이성: 순수 함수는 테스트하기 가장 쉬운 형태의 함수입니다. 외부 환경에 의존하지 않으므로, 특정 입력을 주고 기대하는 출력이 나오는지 확인하기만 하면 됩니다.
  • 조합 용이성: 부수 효과 없는 함수들은 레고 블록처럼 안전하게 조합하여 더 복잡한 로직을 만들 수 있습니다.
  • 디버깅 용이성: 데이터가 예기치 않게 변경되는 일이 없으므로, 버그가 발생했을 때 데이터의 흐름을 추적하기가 훨씬 수월합니다.

물론 모든 함수를 순수 함수로 만들 수는 없습니다. 하지만 "이 함수가 꼭 부수 효과를 가져야만 하는가?"라고 질문하고, 가능하다면 부수 효과가 없는 순수한 로직 부분과, 어쩔 수 없이 부수 효과를 발생시키는 부분을 명확히 분리하려는 노력만으로도 코드의 품질은 크게 향상될 수 있습니다.


원칙 7. 하나의 목소리를 내라: 일관성 있는 코드 스타일과 패턴

지금까지 다룬 6가지 원칙이 개별 코드 라인과 함수의 품질에 대한 것이었다면, 마지막 원칙은 프로젝트 전체의 조화와 통일성에 대한 이야기입니다. 아무리 개별 함수가 잘 작성되었다고 하더라도, 프로젝트 전체의 코드 스타일이 중구난방이라면 가독성은 떨어지고 유지보수는 어려워집니다. 좋은 코드는 마치 한 사람이 작성한 것처럼 일관된 스타일과 패턴을 유지합니다.

일관성은 개발자가 코드를 읽고 이해하는 데 필요한 '인지 부하(Cognitive Load)'를 줄여줍니다. 예를 들어, 어떤 파일에서는 변수명을 카멜 케이스(camelCase)로 쓰고, 다른 파일에서는 스네이크 케이스(snake_case)로 쓴다면, 코드를 읽는 사람은 계속해서 스타일에 적응해야 하므로 피로감을 느끼고 핵심 로직을 파악하는 데 방해를 받습니다. 들여쓰기, 중괄호 위치, 변수 선언 방식 등 사소해 보이는 모든 것들이 모여 코드의 전반적인 인상을 결정합니다.

나쁜 코드 예시: 통일성 없는 코드 조각들


// userController.js
// 변수 선언: var, let 혼용
var user_name = "Alice"; 
// 함수 선언: 함수 선언식 사용, 중괄호가 다음 줄에 위치
function getUser(id) 
{
    // 문자열: 작은따옴표 사용
    console.log('Fetching user...');
    return fetch(`/api/users/${id}`);
}

// productController.js
// 변수 선언: const 사용
const productName = "Clean Code Book";
// 함수 선언: 화살표 함수 표현식 사용, 중괄호가 같은 줄에 위치
const getProduct = (id) => {
    // 문자열: 큰따옴표 사용
    console.log("Fetching product...");
    return fetch(`/api/products/${id}`);
};

개선된 코드 예시: 린터와 포맷터로 강제된 일관성


// .prettierrc (포맷터 설정 파일)
// {
//   "singleQuote": true,
//   "semi": true,
//   "tabWidth": 2,
//   "trailingComma": "all"
// }

// .eslintrc.js (린터 설정 파일)
// {
//   "rules": {
//     "no-var": "error",
//     "prefer-const": "error",
//     "func-style": ["error", "expression"]
//   }
// }


// --- 아래 코드는 위 설정에 따라 자동으로 교정됨 ---

// userController.js
const userName = 'Alice';

const getUser = (id) => {
  console.log('Fetching user...');
  return fetch(`/api/users/${id}`);
};

// productController.js
const productName = 'Clean Code Book';

const getProduct = (id) => {
  console.log('Fetching product...');
  return fetch(`/api/products/${id}`);
};

왜 이렇게 개선해야 할까요?

나쁜 예시는 두 파일이 마치 다른 언어로 작성된 것처럼 보입니다. 변수 선언 방식, 함수 정의 스타일, 문자열 표기법, 중괄호 위치 등 거의 모든 면에서 다릅니다. 이런 코드는 팀 프로젝트에서 여러 개발자가 각자의 스타일대로 코드를 작성할 때 흔히 발생합니다. 이는 코드 리뷰를 어렵게 만들고, 새로운 팀원이 프로젝트에 적응하는 시간을 늘립니다. 어떤 스타일이 '더 좋은가'에 대한 논쟁은 소모적일 뿐입니다. 중요한 것은 '하나의 스타일을 정하고 모두가 따르는 것'입니다.

개선된 코드 예시는 이러한 문제를 사람이 아닌 '도구'를 통해 해결합니다.

  • 린터(Linter, 예: ESLint): 코드의 잠재적인 오류나 버그, 스타일 문제를 찾아내고 경고해주는 도구입니다. 'var 대신 constlet을 사용하라'와 같은 규칙을 설정하여 팀 전체가 일관된 문법을 사용하도록 강제할 수 있습니다.
  • 포맷터(Formatter, 예: Prettier): 코드의 '모양'을 정해진 규칙에 따라 자동으로 정리해주는 도구입니다. 들여쓰기, 줄 바꿈, 따옴표 종류, 쉼표 위치 등 미적인 부분을 모두 통일시켜 줍니다. 개발자는 더 이상 포맷팅에 신경 쓸 필요 없이 로직에만 집중할 수 있습니다.

이러한 도구들을 프로젝트에 도입하고, git hook 등을 이용해 커밋하기 전에 자동으로 검사 및 수정을 거치도록 설정하면, 모든 팀원이 자연스럽게 일관된 스타일의 코드를 작성하게 됩니다. 이는 개인의 취향이나 습관에 대한 불필요한 논쟁을 없애고, 팀의 생산성을 크게 향상시킵니다.

코드 스타일의 일관성은 단순히 미적인 문제를 넘어, 프로젝트의 건강과 직결됩니다. 깨진 유리창 하나를 방치하면 다른 유리창도 계속 깨지듯이(깨진 유리창 이론), 일관성 없는 코드는 다른 개발자들에게 '이 프로젝트는 관리가 잘 안 되는구나'라는 인상을 주어 코드 품질을 점차 악화시키는 원인이 될 수 있습니다. 깔끔하고 일관된 코드는 그 자체로 '우리 프로젝트는 품질을 중요하게 생각한다'는 강력한 메시지를 전달합니다.


결론: 클린 코드는 습관이자 태도입니다

지금까지 동료에게 환영받는 코드를 작성하기 위한 7가지 실용적인 원칙을 살펴보았습니다. 의도를 드러내는 이름 짓기부터, 함수를 작게 나누고, 마법의 숫자를 상수로 바꾸며, 중복을 제거하고, 코드로써 설명하고, 부수 효과를 경계하며, 마지막으로 일관성을 유지하는 것까지. 이 원칙들은 서로 동떨어진 규칙이 아니라 '코드의 명확성과 유지보수성'이라는 하나의 목표를 향해 유기적으로 연결되어 있습니다.

클린 코드를 작성하는 것은 하루아침에 달성할 수 있는 기술이 아닙니다. 그것은 매일 코드를 작성할 때마다 '어떻게 하면 더 나은 코드가 될까?'를 고민하는 습관이자, 미래의 동료와 나 자신을 배려하는 태도에 가깝습니다. 처음에는 조금 더 시간이 걸리고 번거롭게 느껴질 수 있습니다. 하지만 이 작은 노력들이 쌓여 당신의 코드를 견고하게 만들고, 당신을 더 유능하고 신뢰받는 개발자로 성장시킬 것입니다.

오늘부터 당신의 코드에 이 원칙들을 하나씩 적용해보세요. 완벽하지 않아도 괜찮습니다. 어제보다 조금 더 읽기 좋은 코드를 작성했다면, 그것만으로도 훌륭한 첫걸음입니다. 더 이상 "이 코드 누가 짰어?"라는 말을 두려워하지 마세요. 당신의 코드가 "이 코드 정말 깔끔하네요. 덕분에 이해하기 쉬웠어요."라는 칭찬의 대상이 되는 날이 머지않았습니다.


0 개의 댓글:

Post a Comment