Tidy First? - 코드 정리법

BO·2024년 10월 13일
14

tidy-first

목록 보기
1/2
post-thumbnail

글을 시작하며

지난 여름 처음 Kent Beck의 Tidy First?를 접했을 때, 그 단순하면서도 실용적인 내용에 크게 공감되었습니다.

최근 지인들과 이 책을 다시 읽으면서 처음 놓쳤던 부분들도 새롭게 발견하고, 개발자들이 꼭 한 번쯤은 읽어봐야 할 책이라는 것을 다시금 느꼈습니다. 하지만 모든 개발 서적이 그렇듯이, 이 책도 프런트엔드에서 바로 이해하고 적용하기에는 조금 어려운 부분이 있었습니다.

그래서 Tidy First?의 핵심 개념들을 프런트엔드에서 어떻게 적용할 수 있을지 정리한 글과 예시, 그리고 몇 가지 액션 아이템을 준비했습니다.

이번 글은 첫 번째 파트인 코드 정리를 다룹니다.

Tidy First?

Kent BeckTidy First?는 코드를 정리하고 리팩터링하는 과정을 통해 시스템의 복잡성을 줄이고, 코드를 더 명료하게 만드는 것을 목표로 합니다. 이 책에서 가장 강조되는 점은 기능을 추가하기 전에 먼저 코드를 깔끔하게 정리하라는 원칙입니다. 복잡한 코드베이스에 새로운 기능을 추가하려다 보면, 코드의 구조가 더 엉망이 되고, 유지보수성이 떨어지는 경우가 많습니다. 이를 방지하기 위해서는 코드를 먼저 정리한 후에 기능을 추가함으로써, 코드의 일관성을 유지하고 더 나은 품질의 소프트웨어를 만들 수 있습니다.

또한 이 책은 작은 리팩터링 단위의 중요성도 강조합니다. 코드 정리는 작은 단계에서 시작해야 하며, 큰 변화를 한꺼번에 시도하기보다는 일관된 방식으로 점진적으로 개선해야 한다는 것이 핵심입니다. 이렇게 해야 팀원들이 변경사항을 이해하기 쉽고, 버그가 발생할 가능성도 줄일 수 있습니다.

"코드 정리를 리팩터링의 부분집합으로도 볼 수 있습니다. 코드 정리는 작은 리팩터링으로 누구도 싫어할 수 없을 정도로 사랑스럽고 포근합니다."

보호구문

보호구문이라는 표현은 한 번쯤 들어보았을 법하지만, 정확히 무엇을 의미하는지 설명하기는 막상 쉽지 않습니다. 책에서는 보호구문을 적용한 예시를 통해 보호구문을 다음과 같이 설명하고 있습니다:

"코드의 세부 사항을 살펴보기 전에 염두에 두어야 할 몇 가지 전제 조건이 있습니다"라고 말하는 것처럼 보입니다.

즉, 보호구문은 특정한 조건이 만족되지 않을 경우, 더 이상의 처리가 필요하지 않다는 것을 명시하는 방식입니다. 이를 통해 코드의 흐름을 명확하게 하고, 불필요한 깊이를 줄여 코드의 가독성을 높일 수 있습니다.

그럼 예제를 보며 보호구문을 이해해 보겠습니다.

보호구문을 사용하지 않는 경우

보호구문을 사용하지 않고 작성된 코드는 다음과 같습니다. 이 경우 중첩된 조건문들이 계속해서 깊어지고, 읽기 어려워집니다.

function processOrder(order) {
  if (order) {
    if (order.items && order.items.length > 0) {
      if (order.paymentInfo) {
        if (order.shippingAddress) {
          // 주문 처리 로직 실행
          console.log("주문을 처리합니다.");
        } else {
          console.log("배송 주소가 없습니다.");
        }
      } else {
        console.log("결제 정보가 없습니다.");
      }
    } else {
      console.log("주문 항목이 없습니다.");
    }
  } else {
    console.log("주문 정보가 없습니다.");
  }
}

보호구문을 사용하는 경우

보호구문을 적용한 코드는 조건을 미리 확인하고, 조건이 만족되지 않는 경우에는 일찍 반환하여 코드의 깊이를 줄이고 가독성을 높입니다. 이렇게 하면 조건이 맞지 않을 때 더 이상 불필요한 처리를 하지 않게 되어 코드가 간결해집니다. 아래 예제는 보호구문을 적용한 코드입니다.

function processOrder(order) {
if (!order) {
    console.log("주문 정보가 없습니다.");
    return;
  }

  if (!order.items || order.items.length === 0) {
    console.log("주문 항목이 없습니다.");
    return;
  }

  if (!order.paymentInfo) {
    console.log("결제 정보가 없습니다.");
    return;
  }

  if (!order.shippingAddress) {
    console.log("배송 주소가 없습니다.");
    return;
  }

  // 주문 처리 로직 실행
  console.log("주문을 처리합니다.");
}

책에서 언급한 것처럼, 몇 가지 전제 조건을 미리 체크하여, 조건이 충족되지 않는 경우에는 바로 반환하는 방식입니다. 이를 통해 코드를 한 눈에 이해하기 쉽고, 로직 흐름도 간결하게 정리됩니다.

보호구문이 주는 이점

보호구문을 사용함으로써 얻을 수 있는 가장 큰 이점은 코드의 가독성과 유지보수성입니다. 복잡한 중첩 조건을 사용하면 코드의 흐름을 파악하기 어렵고, 어디에서 어떤 로직이 실행되는지 혼란스럽기 쉽습니다. 보호구문을 사용하면, 코드를 읽는 사람은 불필요한 로직을 생략하고 중요한 로직에만 집중할 수 있습니다.

또한, 보호구문은 코드의 예측 가능성을 높입니다. 코드의 흐름이 명확해지기 때문에, 특정 조건이 충족되지 않는 경우에는 즉시 반환되어야 함을 쉽게 알 수 있습니다. 이로 인해 코드가 더 단순해지고, 디버깅도 쉬워집니다. 특히, 조건이 복잡하거나 여러 개일 때, 중복된 코드 없이 명확하게 로직을 표현할 수 있습니다.

주의해야 할 것

그러나 보호구문을 남용해서는 안 됩니다. 지나치게 많은 보호구문이 사용된 코드는 가독성을 높이려다 오히려 코드가 너무 잘게 쪼개지고 복잡해질 수 있습니다. 보호구문은 주로 필수적인 조건을 미리 확인하고, 코드의 흐름을 단순화하는 데 사용해야 하며, 너무 많은 조건문이 포함된 코드라면 다시 한 번 코드의 구조를 고민해볼 필요가 있습니다.

Action Item

  1. 조건문은 보호구문으로 정리하자
    코드의 실행을 결정하는 조건문은 보호구문을 사용해 로직을 간결하게 만들어 코드의 흐름을 명확히 하자.
  2. 불필요한 조건 중첩을 피하자
    중첩된 조건문을 줄이고, 불필요한 로직을 미리 걸러내어 코드의 가독성과 유지보수성을 높이자.
  3. 필요할 때만 보호구문을 사용하자
    남용하지 않고, 필수적인 전제 조건만 보호구문으로 처리해 코드가 지나치게 세분화되지 않도록 하자.

참고

참고로, Swift에서는 이를 위해 특별한 문법인 guard문을 제공합니다. guard문은 조건이 충족되지 않을 때 특정 동작을 수행하고, 조건이 충족될 때만 계속해서 코드를 실행할 수 있게 도와줍니다. Swift의 guard문을 사용한 예시는 다음과 같습니다.

func processOrder(order: Order?) {
    guard let order = order else {
        print("주문 정보가 없습니다.")
        return
    }

    guard let items = order.items, !items.isEmpty else {
        print("주문 항목이 없습니다.")
        return
    }

    guard let paymentInfo = order.paymentInfo else {
        print("결제 정보가 없습니다.")
        return
    }

    guard let shippingAddress = order.shippingAddress else {
        print("배송 주소가 없습니다.")
        return
    }

    // 주문 처리 로직 실행
    print("주문을 처리합니다.")
}

대칭으로 맞추기

코드는 유기체처럼 성장합니다. 시간이 지남에 따라 새로운 기능과 요구 사항에 맞추어 변화하고 확장되며, 이러한 과정에서 대칭성을 유지하는 것은 유지보수성과 가독성을 높이는 중요한 요소가 됩니다. 코드의 대칭성은 코드의 일관성을 유지하고, 코드를 읽는 사람에게 직관적인 구조를 제공하여 효율적인 개발을 가능하게 합니다.

대칭적인 코드란, 동일한 동작을 하는 코드를 여러 가지 방식으로 작성하기보다는, 하나의 일관된 방식으로 통일하여 작성하는 것을 의미합니다. 이로 인해 코드는 읽기 쉬워지고, 확장하기 용이해지며, 팀원 간의 협업 효율성도 높아집니다.

예시: 사용자의 나이에 따라 다른 메시지를 표시하는 기능

다양한 방식으로 동일한 동작을 구현할 수 있지만, 코드 스타일의 일관성은 코드의 품질에 직결됩니다. 아래 예시는 사용자의 나이에 따라 다른 메시지를 출력하는 코드의 여러 구현 방식을 보여줍니다.

1. if…else 문을 사용한 구현

function getUserCategory(age) {
  if (age >= 18) {
    return "성인입니다.";
  } else if (age >= 13) {
    return "청소년입니다.";
  } else {
    return "어린이입니다.";
  }
}

console.log(getUserCategory(20)); // 출력: 성인입니다.

2. 삼항 연산자를 사용한 구현

function getUserCategory(age) {
  return age >= 18
    ? "성인입니다."
    : age >= 13
    ? "청소년입니다."
    : "어린이입니다.";
}

console.log(getUserCategory(20)); // 출력: 성인입니다.

3. switch 문을 사용한 구현

function getUserCategory(age) {
  switch (true) {
    case age >= 18:
      return "성인입니다.";
    case age >= 13:
      return "청소년입니다.";
    default:
      return "어린이입니다.";
  }
}

console.log(getUserCategory(20)); // 출력: 성인입니다.

위 세 가지 예시는 모두 동일한 결과를 반환하지만, 각각 다른 방식으로 구현되어 있습니다. 대칭적으로 맞춘 코드는 이러한 다양한 구현 방식 중 하나를 선택하여 일관된 코드 스타일을 유지하는 것을 목표로 합니다.

대칭적인 코드가 중요한 이유

대칭적인 코드 작성은 코드베이스에서 중요한 역할을 합니다. 코드의 대칭성은 코드의 일관성을 높여 유지보수가 쉬워지고, 확장성이 커지며, 협업 시 불필요한 충돌을 줄입니다.

  1. 가독성 향상: 대칭적인 코드는 일정한 패턴을 따르므로, 코드가 어떻게 동작할지 쉽게 예측할 수 있습니다. 이는 코드를 빠르게 이해하는 데 도움이 됩니다.
  2. 유지보수성 향상: 일관된 코드 구조는 수정이나 기능 추가 시 혼란을 줄이고, 코드의 흐름을 유지하는 데 도움이 됩니다. 대칭적으로 작성된 코드는 한눈에 파악할 수 있기 때문에 버그 발생 가능성도 줄어듭니다.
  3. 팀 생산성 증가: 팀 내에서 합의된 코드 스타일을 따르는 것은 협업의 효율성을 높입니다. 서로 다른 방식으로 작성된 코드는 이해하는 데 시간이 걸리지만, 대칭적으로 작성된 코드는 모든 팀원이 동일한 규칙을 적용하므로 작업 속도가 빨라집니다.

Action Item

  1. 일관된 코드 스타일을 유지하자
    코드 작성 시, 여러 가지 구현 방식을 혼합하지 않고 팀 내에서 합의된 스타일 가이드를 따르자. 이를 통해 코드가 일관된 구조를 유지하고, 가독성을 높이자.
  2. 동일한 로직은 하나의 방식으로 통일하자
    같은 기능을 수행하는 코드가 여러 방식으로 작성되어 있을 때, 한 가지 일관된 방식을 선택해 통일하자. 특히 조건문이나 반복문에서 사용할 패턴을 미리 정의해 두는 것이 중요하다.
  3. 확장성과 가독성을 고려한 선택을 하자
    간결하면서도 확장성 있는 코드 작성 방식을 선택하여, 코드의 가독성과 유지보수성을 높이자. 복잡한 로직에서도 간단한 구문을 사용해 복잡도를 낮추자.
  4. ESLint와 Prettier로 코드 스타일을 강제하자
    대칭성을 유지하려면 ESLint와 Prettier 같은 도구를 활용하여 자동으로 코드 스타일을 일관되게 맞추자. 팀 내에서 설정된 규칙에 맞춰 ESLint를 설정하고, Prettier로 코드 형식을 일관되게 적용하여 대칭성을 유지하자.
  5. 코드 리뷰를 통해 일관성을 체크하자
    코드 리뷰는 대칭적인 코드 스타일을 유지하는 데 중요한 역할을 한다. 팀원들이 작성한 코드가 스타일 가이드와 일치하는지 확인하고, 일관성을 유지할 수 있도록 피드백을 주고받자.

읽는 순서

읽기 좋은 순서로 다시 정렬하기

코드를 작성할 때 읽기 좋은 순서로 정렬하는 것은 매우 중요합니다. 코드를 읽는 사람이 논리적인 흐름에 따라 코드를 쉽게 이해할 수 있기 때문입니다. 하지만 주의할 점은 순서를 정렬하면서 함수의 동작이나 세부 구현을 변경하지 않아야 한다는 것입니다. 코드를 정리할 때는 코드의 기능을 그대로 유지하면서 순서만 변경해야 합니다.

예시: 유효성 검사 순서 정렬

차례대로 이름, 이메일, 주소에 대한 정보를 수집하는 form이 있다고 가정해 보겠습니다. 각 필드에 대해 유효성 검사를 수행하는 코드가 있을 때, 입력 순서에 맞춰 코드를 정리하는 것이 가독성에 좋습니다.

순서가 맞지 않는 코드

const isEmailValid = validateEmail(email);
const isAddressValid = validateAddress(address);
const isNameValid = validateName(name);

// 유효성 검사 로직
if (isEmailValid) {
  console.log("이메일이 유효합니다.");
}

if (isAddressValid) {
  console.log("주소가 유효합니다.");
}

if (isNameValid) {
  console.log("이름이 유효합니다.");
}

문제점:

  • 입력받는 순서는 이름 → 이메일 → 주소인데, 유효성 검사 순서는 이메일 → 주소 → 이름으로 코드가 작성되어 있습니다.
  • 입력 순서와 유효성 검사 순서가 맞지 않으면, 코드를 읽는 사람이 혼란스러워질 수 있습니다.

순서를 맞춘 코드

const isNameValid = validateName(name);
const isEmailValid = validateEmail(email);
const isAddressValid = validateAddress(address);

// 유효성 검사 로직
if (isNameValid) {
  console.log("이름이 유효합니다.");
}

if (isEmailValid) {
  console.log("이메일이 유효합니다.");
}

if (isAddressValid) {
  console.log("주소가 유효합니다.");
}

개선점:

  • 입력 순서와 코드의 순서를 일치시켜 가독성을 높였습니다.
  • 이름 → 이메일 → 주소의 순서대로 유효성 검사를 수행하여 코드의 흐름이 더 자연스러워졌습니다.
  • 동작은 그대로 유지하면서 코드의 논리적인 흐름을 명확하게 정리했습니다.

함수 정의 순서 맞추기

추가로, 각 필드에 대한 유효성 검사를 함수로 정의한다면, 함수 정의 순서도 입력 순서에 맞추는 것이 좋습니다. 함수의 정의 순서가 입력 흐름과 일치하면, 전체 코드의 일관성이 더해져 읽기 쉽고 직관적인 코드가 됩니다.

function validateName(name: string): boolean {
  // 이름 유효성 검사 로직
  return name.length > 0;
}

function validateEmail(email: string): boolean {
  // 이메일 유효성 검사 로직
  const emailRegex = /\S+@\S+\.\S+/;
  return emailRegex.test(email);
}

function validateAddress(address: string): boolean {
  // 주소 유효성 검사 로직
  return address.length > 5;
}

주의해야 할 것

코드를 보기 좋은 순서로 바꾸는 것이 중요하지만, 순서에 민감한 코드의 동작을 변경해 사이드 이펙트가 발생하지 않도록 주의해야 합니다. 코드의 기능은 그대로 유지하고 순서만 정리하는 것이 핵심입니다.

이 과정에서 코드의 정리를 지나치게 복잡하게 하거나 순서를 맞추려는 욕심 때문에 기존 코드의 동작을 바꾸는 실수를 범하지 않도록 해야 합니다. Tidy First?에서 강조하는 정리의 원칙은, 코드의 동작을 변경하지 않고 일관된 기준에 따라 구조를 정돈하는 것입니다. 따라서 본인의 기준을 명확히 정하고 그 기준에 따라 일관되게 정리하는 것이 중요합니다.

Action Item

  1. 코드를 읽기 좋은 순서로 정렬하자
    입력 순서나 처리 순서와 일치하도록 코드를 정렬하여 가독성을 높이자. 논리적인 흐름에 맞게 코드를 작성하면 유지보수성과 협업 효율이 향상됩니다.
  2. 코드를 정리할 때 함수의 동작은 변경하지 말자
    순서를 정렬하면서 코드의 기능이 변경되지 않도록 주의하자. 순서는 정리하되, 동작은 그대로 유지해야 합니다.
  3. 함수 정의 순서도 일관성을 맞추자
    함수의 정의 순서도 입력과 처리 흐름에 맞춰 정리하여, 전체적인 코드의 일관성을 유지하자. 코드를 읽는 사람이 쉽게 흐름을 파악할 수 있도록 코드를 구성하자.
  4. 사이드 이펙트를 일으키지 않도록 주의하자
    순서 정리 중 코드 동작을 바꾸지 말고, 불필요한 변화가 발생하지 않도록 해야 합니다. 정리의 핵심은 순서만 변경하는 것이지, 코드를 새로 작성하는 것이 아닙니다.

설명하는 변수와 상수

코드를 더 명확하고 가독성 좋게 만들기 위해서는 설명하는 변수와 상징적인 상수를 사용하는 것이 중요합니다. 이는 코드의 의도를 더 명확하게 표현하여 유지보수성을 높이고, 코드 수정 시에도 쉽게 이해할 수 있도록 돕습니다. 이 과정에서 표현식의 의도와 리터럴 값을 설명하는 이름으로 치환하는 것이 핵심입니다.

설명하는 변수

설명하는 변수란, 복잡하거나 의도가 분명하지 않은 표현식을 변수로 추출하여 이름을 통해 그 목적을 명확히 하는 것을 의미합니다. 코드가 복잡해질수록 설명하는 변수는 가독성을 크게 향상시킵니다.

예시

다음 코드는 query가 비어있는지를 확인하는 로직입니다. 하지만 코드만으로는 그 의도를 명확히 파악하기 어렵습니다. 이 복잡한 표현을 설명하는 변수로 치환해봅시다.

의도가 드러나지 않는 코드

useEffect(() => {
  if (Object.keys(query).length === 0) {
    return;
  }

  foo();
}, [query]);

의도가 드러나는 코드

useEffect(() => {
  const isInitialPageLoad = Object.keys(query).length === 0;

  if (isInitialPageLoad) {
    return;
  }

  foo();
}, [query]);

변경으로 인한 개선점

  1. 가독성 향상:
    isInitialPageLoad라는 변수명을 사용하여 query가 비어있는 상태가 페이지의 첫 진입을 의미한다는 의도를 명확히 했습니다. 이를 통해 코드를 읽는 사람은 이 조건이 페이지 첫 로드를 확인하는 것임을 쉽게 파악할 수 있습니다.
  2. 유지보수성:
    코드가 더 복잡해지거나 조건이 변경되어야 할 때, 표현식 대신 isInitialPageLoad 변수만 수정하면 되므로 변경이 더 쉬워집니다.
  3. 의미 전달:
    복잡한 조건식을 설명하는 변수로 분리함으로써 코드의 의도를 명확하게 전달할 수 있습니다.

설명하는 상수

설명하는 상수는 숫자나 문자열과 같은 리터럴 값을 의미를 명확하게 드러내는 상수로 변환하는 것을 말합니다. 리터럴 값을 설명하는 상수로 바꾸면 가독성과 유지보수성이 크게 향상되며, 코드 곳곳에서 같은 값을 반복적으로 사용할 때 발생하는 오류도 방지할 수 있습니다.

사례

리터럴 상수인 4_8006_240을 설명하는 상수로 변경하여 코드의 의도를 명확히 드러내 보겠습니다.

의도가 드러나지 않는 코드

function calculateTaxiFare(isNight: boolean): number {
  if (isNight) {
    return 6_240;
  }

  return 4_800;
}

의도가 드러나는 코드

const TAXI_BASE_FARE = 4_800;
const TAXI_NIGHT_SURCHARGE_FARE = 6_240;

function calculateTaxiFare(isNight: boolean): number {
  if (isNight) {
    return TAXI_NIGHT_SURCHARGE_FARE;
  }

  return TAXI_BASE_FARE;
}

변경으로 인한 개선점

  1. 가독성 향상:
    상수명을 통해 숫자 값이 의미하는 바를 명확히 알 수 있습니다. TAXI_BASE_FARETAXI_NIGHT_SURCHARGE_FARE는 각각 기본 요금과 야간 할증 요금임을 바로 파악할 수 있습니다.
  2. 유지보수성:
    요금이 변경되어야 할 경우, 상수만 수정하면 되므로 여러 곳의 코드를 변경할 필요 없이 유지보수가 수월해집니다.
  3. 의미 전달:
    상수를 통해 코드가 택시 요금을 계산하고 있음을 명확히 전달할 수 있습니다. 상수화된 값은 설명적이기 때문에 코드의 의도를 쉽게 파악할 수 있습니다.

상수화의 한계

모든 값을 상수화하는 것이 좋은 것은 아닙니다. 예를 들어, const ONE = 1;과 같은 상수는 의미 전달에 전혀 도움이 되지 않으므로 지양해야 합니다. 상수화는 코드에서 의미를 명확하게 전달할 수 있을 때만 적용해야 하며, 단순히 리터럴 값을 치환하는 것이 상수화의 목적이 되어서는 안 됩니다.

또한, 관련된 상수는 한 곳에 모아서 관리하는 것도 고려해보면 좋습니다. 이렇게 정리한다면 같이 변경되거나 이해해야 할 상수들을 쉽게 관리하고 유지보수할 수 있도록 도와줍니다.

Action Item

  1. 설명하는 변수를 적극적으로 사용하자
    복잡한 조건식이나 표현식은 설명하는 변수로 추출하여 코드의 의도를 명확히 하고 가독성을 높이자.
  2. 리터럴 상수는 상징적인 상수로 바꾸자
    코드에 자주 등장하는 리터럴 값들은 의미를 명확하게 설명하는 상수로 변환하여 유지보수성과 가독성을 향상시키자.
  3. 코드 정리와 동작 변경은 별도로 관리하자
    코드 정리에 대한 커밋과 동작 변경에 대한 커밋을 명확히 분리하여 관리하자. 이를 통해 변경 사항을 추적하고 코드 리뷰를 효율적으로 진행할 수 있다.
  4. 관련된 상수를 한곳에 모으자
    같이 변경되거나 관련된 상수는 한곳에 모아 관리하여, 나중에 수정할 때 코드 전체에 미치는 영향을 최소화하자.

도우미 함수 추출

코드를 더 깔끔하고 목적에 맞게 작성하려면 도우미 함수를 추출하는 것이 중요합니다. 도우미 함수는 목적이 명확하고, 다른 코드와 상호작용이 적은 코드 블록을 함수로 분리하는 방법입니다. 중요한 점은 도우미 함수의 이름은 작동 방식이 아니라 그 목적을 설명하는 이름으로 짓는 것입니다. 이를 통해 코드의 가독성이 향상되고, 유지보수와 코드 재사용이 용이해집니다.

이 작업은 리팩터링의 기본적인 원칙인 메서드 추출과 유사하며, 코드의 복잡성을 줄이고 변경에 유연한 설계를 가능하게 합니다.

예시: 시간적 결합을 표현하는 코드

시간적 결합이란, 특정 로직이 특정 순서로 실행되어야 할 때 나타나는 상황을 의미합니다. 이러한 로직은 순서가 매우 중요하기 때문에 코드를 도우미 함수로 추출하여 그 순서 의도를 명확하게 표현하는 것이 좋습니다.

흐름이 명확하게 드러나지 않는 코드

function handleFileUpload(file) {
  const isValid = validateFile(file);

  if (!isValid) {
    throw new Error("Invalid file");
  }

  uploadFile(file)
    .then(() => {
      console.log("File uploaded successfully");
      finalizeUpload();
    })
    .catch((error) => {
      console.error("Upload failed", error);
    });
}

function validateFile(file) {
  // 파일 유효성 검사 로직
  return file.size < 5000000; // 5MB 미만 파일만 허용
}

function uploadFile(file) {
  // 파일 업로드 로직
  return new Promise((resolve, reject) => {
    // 비동기 업로드 시뮬레이션
    setTimeout(() => {
      resolve();
    }, 2000);
  });
}

function finalizeUpload() {
  console.log("Upload finalized");
}

흐름이 드러나는 코드

async function handleFileUpload(file) {
  try {
    if (!isFileValid(file)) {
      throw new Error("Invalid file");
    }

    await processFileUpload(file);
    onFileUploadSuccess();
  } catch (error) {
    onFileUploadFailure(error);
  }
}

function isFileValid(file) {
  return file.size < 5000000; // 5MB 미만 파일만 허용
}

async function processFileUpload(file) {
  await uploadFile(file);
}

function uploadFile(file) {
  return new Promise((resolve, reject) => {
    // 비동기 업로드 시뮬레이션
    setTimeout(() => {
      resolve();
    }, 2000);
  });
}

function onFileUploadSuccess() {
  console.log("File uploaded successfully");
  finalizeUpload();
}

function onFileUploadFailure(error) {
  console.error("Upload failed", error);
}

function finalizeUpload() {
  console.log("Upload finalized");
}

변경으로 인한 개선점

  1. 시간적 결합 표현:
    파일 검증 → 파일 업로드 → 성공 또는 실패 처리 → 완료 과정이 순차적으로 진행되어 시간적 결합의 흐름이 더 분명하게 드러납니다.

예시: 복잡한 메인 로직을 헬퍼 함수로 분리하는 경우

도우미 함수는 거대한 함수에서 복잡한 로직을 분리해 함수의 가독성과 유지보수성을 크게 향상시킬 수 있습니다. 거대한 함수에 여러 로직이 혼합되어 있으면 함수의 흐름을 파악하기 어려워지는데, 이를 같은 추상화 레벨로 맞춘 헬퍼 함수로 분리하면 함수가 훨씬 명확해집니다.

복잡하고 가독성이 낮은 메인 함수

function processOrder(order) {
  // 1. 유효성 검사
  if (order.items.length === 0) {
    throw new Error("Empty order");
  }

  // 2. 주문 처리
  console.log("Processing order:", order);

  // 3. 주문 완료 처리
  console.log("Order completed");
}

명확하고 가독성이 높은 메인 함수

function processOrder(order) {
  validateOrder(order);
  handleOrderProcessing(order);
  completeOrder();
}

function validateOrder(order) {
  if (order.items.length === 0) {
    throw new Error("Empty order");
  }
}

function handleOrderProcessing(order) {
  console.log("Processing order:", order);
}

function completeOrder() {
  console.log("Order completed");
}

변경으로 인한 개선점

  1. 가독성 향상:
    processOrder 함수는 이제 세부 구현을 신경 쓰지 않고 주문 처리 흐름을 쉽게 이해할 수 있습니다. 각 로직을 헬퍼 함수로 분리해 메인 함수가 간결해졌습니다.
  2. 유지보수성:
    validateOrder, handleOrderProcessing, completeOrder와 같은 헬퍼 함수로 분리하여 각 부분을 독립적으로 수정할 수 있습니다. 예를 들어, 유효성 검사 로직만 변경해야 할 경우 validateOrder 함수만 수정하면 됩니다.
  3. 메인 로직 가독성 개선:
    헬퍼 함수로 복잡한 로직을 분리하면 메인 함수 상단에 메인 로직을 배치할 수 있습니다. 이렇게 하면 메인 로직이 바로 드러나고, 헬퍼 함수들은 아래에 배치되어 있어 파일을 열었을 때 로직의 흐름을 쉽게 파악할 수 있습니다. 큰 프로젝트일수록 이 방식은 메인 로직을 빠르게 이해하는 데 유용합니다.

도우미 함수의 장점

  1. 목적이 명확한 함수 추출:
    도우미 함수는 특정 코드 블록을 분리하여 그 목적에 맞는 이름을 부여함으로써 코드의 의도를 명확히 드러냅니다. 이를 통해 코드를 읽는 사람은 각 함수가 무엇을 위해 존재하는지를 쉽게 이해할 수 있습니다.
  2. 코드의 유연성 향상:
    도우미 함수는 특정 로직을 분리해 두었기 때문에, 변경이 필요할 때 해당 함수만 수정하면 됩니다. 이를 통해 코드 전체에 영향을 미치지 않고 필요한 부분만 유연하게 수정할 수 있습니다.
  3. 시간적 결합 표현:
    실행 순서가 중요한 경우, 도우미 함수로 로직을 추출하면 실행 순서를 더 명확히 표현할 수 있습니다.

Action Item

  1. 코드 블록을 목적에 따라 함수로 추출하자
    코드가 복잡해지거나 반복적으로 사용되는 블록은 도우미 함수로 추출하여 가독성을 높이고 유지보수를 쉽게 하자.
  2. 도우미 함수는 작동 방식이 아닌 목적에 맞는 이름을 짓자
    함수의 이름은 작동 방식이 아니라 무엇을 하는지에 따라 명확히 지어야 합니다. 이를 통해 코드의 의도가 더 명확하게 드러납니다.
  3. 시간적 결합이 필요한 코드에는 도우미 함수로 추출하자
    실행 순서가 중요한 코드 블록은 순서의 의도를 명확히 하기 위해 도우미 함수로 추출하여 코드를 정리하자.

설명하는 주석과 불필요한 주석 지우기

코드를 작성할 때 주석을 적절하게 사용하는 것은 매우 중요합니다. 주석은 명확하지 않은 부분을 설명하거나 특별한 맥락을 기록하기 위해 사용되지만, 불필요한 주석은 오히려 혼란을 초래할 수 있습니다. 주석을 잘못 사용하면, 코드와 주석이 따로 놀게 되어 유지보수에 어려움을 겪을 수 있습니다.

설명하는 주석

설명하는 주석은 코드에서 의도가 명확하지 않은 부분이나 특별한 사정을 설명하는 데 사용됩니다. 코드를 읽는 사람이 해당 주석을 통해 코드의 의도나 맥락을 쉽게 이해할 수 있도록 돕는 것이 목적입니다.

예시

// antd에서는 명시적 `undefined` 할당이 필요하므로 `undefined`를 할당해주는 로직이 존재합니다
const someValue = condition ? undefined : value;

위 주석은 특별한 맥락을 설명하고 있으며, 코드를 이해하는 데 중요한 정보를 제공합니다. 이런 경우 설명하는 주석이 코드의 가독성을 높이는 데 기여할 수 있습니다.

불필요한 주석 지우기

불필요한 주석은 시간이 흐르면서 코드와 주석이 맞지 않게 되어 오히려 혼란을 야기할 수 있습니다. 주석은 코드의 변경에 따라 계속 업데이트되어야 하는데, 이를 놓치면 주석이 잘못된 정보를 전달할 수 있습니다.

주석과 코드 불일치

/**
 * @function calculateTotal
 * @description 세금과 선택적인 할인 금액을 포함한 총 금액을 반환합니다.
 * @param {number} price - 상품의 기본 가격.
 * @param {number} taxRate - 적용할 세율.
 * @param {number} discount - 적용할 할인 금액.
 * @returns {number} 세금과 할인을 적용한 후의 총 금액.
 */
function calculateTotal(
  price: number,
  taxRate: number,
  discount: number | string = 0
): number {
  const discountValue =
    typeof discount === "string" ? parseFloat(discount) : discount;
  return price * (1 + taxRate) - discountValue;
}

문제점:

  • 주석과 코드의 불일치: 함수의 discount 파라미터는 number | string 타입을 수용하도록 변경되었지만, tsdoc 주석에서는 여전히 @param {number} discount로 타입을 number로만 명시하고 있습니다.
  • 타입 중복 및 혼란: TypeScript 코드에 이미 타입 정보가 있는데, tsdoc 주석에 타입을 다시 명시하면 타입 변경 시 주석을 업데이트해야 하는 번거로움이 생기며, 업데이트를 놓치면 혼란을 야기합니다.

불필요한 주석 제거하여 주석과 코드를 일치

주석이 코드와 일치하지 않거나 불필요한 경우, 주석을 삭제하거나 수정하는 것이 좋습니다. 특히, TypeScript의 타입 시스템이 이미 명확한 정보를 제공하는 경우라면 주석에서 타입 정보를 불필요하게 명시할 필요가 없습니다.

/**
 * @function calculateTotal
 * @description 세금과 선택적인 할인 금액을 포함한 총 금액을 반환합니다.
 * @param price - 상품의 기본 가격.
 * @param taxRate - 적용할 세율.
 * @param discount - 선택적인 할인 금액.
 * @returns 세금과 할인을 적용한 후의 총 금액.
 */
function calculateTotal(
  price: number,
  taxRate: number,
  discount: number | string = 0
): number {
  const discountValue =
    typeof discount === "string" ? parseFloat(discount) : discount;
  return price * (1 + taxRate) - discountValue;
}
  • 주석에서 타입 정보 제거: tsdoc 주석에서 {number}와 같은 타입 정보를 제거하여, 코드와 주석 간의 타입 불일치 문제를 해결했습니다.
  • TypeScript 타입 시스템 활용: 코드에 이미 타입 정보가 명시되어 있으므로, 주석에서는 타입을 반복할 필요가 없습니다. 이를 통해 타입 변경 시 주석을 따로 수정할 필요가 없어 유지보수가 용이해집니다.

Action Item

  1. 불필요한 주석은 제거하자
    코드가 명확하게 의도를 드러내는 경우, 불필요한 주석은 피하고 코드 자체로 설명될 수 있도록 하자. 주석이 오래되거나 코드와 맞지 않는 경우 삭제하는 것이 좋습니다.
  2. 주석은 언제나 최신 상태로 유지하자
    코드가 변경되면, 주석도 반드시 수정되어야 한다. 특히 타입스크립트와 같은 언어에서는 타입 정보를 주석에 굳이 명시하지 말고, 코드 자체에 의존하자.
  3. 설명하는 주석을 사용하자
    주석은 코드를 설명하기 위해 존재하므로, 명확하지 않은 부분만 설명하고, 코드 리뷰에서 질문을 받은 부분을 선제적으로 주석으로 남기는 습관을 들이자.
  4. TODO를 남겨 결함을 추적하자
    코드를 수정해야 하는 부분을 발견했을 때, 주석에 TODO를 남겨 나중에 다시 돌아와 수정할 수 있도록 기록하자. 이를 통해 팀원들과도 작업 우선순위를 공유할 수 있다.

글을 마치며

코드 정리에 대한 개념들은 겉으로는 당연해 보일 수 있지만, 실제로 왜 이러한 정리가 필요한지 명확하게 설명하기는 쉽지 않습니다. Tidy First?를 읽으면서 코드 정리의 중요성을 깊이 이해하게 되었고, 이를 통해 동료 개발자들에게도 정리의 필요성을 조금 더 효과적으로 전달할 수 있게 되었습니다.

이 글에서는 보호구문, 대칭으로 맞추기, 읽는 순서, 설명하는 변수와 상수, 도우미 함수 추출, 그리고 설명하는 주석과 불필요한 주석 지우기와 같은 개념들을 살펴보았습니다. 각각의 개념은 코드의 가독성과 유지보수성을 향상시키는 데 큰 도움이 되며, 작은 변화로도 코드 품질에 큰 영향을 미칠 수 있습니다.

하지만 이 글에서 다루지 않은 내용들도 많습니다. 예를 들어, 선언과 초기화를 옮기기와 같은 주제는 프런트엔드 입장에서 공감하기 쉽지 않았습니다. 또한, 응집도 향상과 같은 주제들은 범위가 넓어 이번에는 포함하지 않았습니다. 이러한 내용들은 Tidy First?에서 자세히 다루고 있으니, 직접 책을 구매하여 읽어보시길 강력하게 추천드립니다. 책을 통해 더 깊이 있는 내용을 얻으실 수 있을 것입니다.

마지막으로, 앞으로 새로운 코드를 작성하거나 기존의 레거시 코드를 마주할 때, 이 글에서 소개한 액션 아이템들을 떠올리시는 순간이 있다면 좋겠습니다. 그리고 작은 정리의 습관이 모여 코드베이스 전체의 품질을 향상시키고, 팀의 생산성을 높일 수 있다면 더더욱 좋겠습니다. 코드 정리는 단순히 예쁘게 보이기 위한 것이 아니라, 더 나은 소프트웨어를 만들기 위한 중요한 과정이니까요.

내일은 코드에 새로운 기능을 추가하기 전에 “정리가 먼저인가?”를 떠올려보면 어떨까요?

긴 글 읽어주셔서 감사합니다.

profile
Time waits for no one

3개의 댓글

comment-user-thumbnail
2024년 10월 21일

보호구문은 early return과 같은 내용이네요
책마다 표현하는 방법이 달라서 그럴려나요

2개의 답글