지난 여름 처음 Kent Beck의 Tidy First?를 접했을 때, 그 단순하면서도 실용적인 내용에 크게 공감되었습니다.
최근 지인들과 이 책을 다시 읽으면서 처음 놓쳤던 부분들도 새롭게 발견하고, 개발자들이 꼭 한 번쯤은 읽어봐야 할 책이라는 것을 다시금 느꼈습니다. 하지만 모든 개발 서적이 그렇듯이, 이 책도 프런트엔드에서 바로 이해하고 적용하기에는 조금 어려운 부분이 있었습니다.
그래서 Tidy First?의 핵심 개념들을 프런트엔드에서 어떻게 적용할 수 있을지 정리한 글과 예시, 그리고 몇 가지 액션 아이템을 준비했습니다.
이번 글은 첫 번째 파트인 코드 정리를 다룹니다.
Kent Beck의 Tidy 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("주문을 처리합니다.");
}
책에서 언급한 것처럼, 몇 가지 전제 조건을 미리 체크하여, 조건이 충족되지 않는 경우에는 바로 반환하는 방식입니다. 이를 통해 코드를 한 눈에 이해하기 쉽고, 로직 흐름도 간결하게 정리됩니다.
보호구문을 사용함으로써 얻을 수 있는 가장 큰 이점은 코드의 가독성과 유지보수성입니다. 복잡한 중첩 조건을 사용하면 코드의 흐름을 파악하기 어렵고, 어디에서 어떤 로직이 실행되는지 혼란스럽기 쉽습니다. 보호구문을 사용하면, 코드를 읽는 사람은 불필요한 로직을 생략하고 중요한 로직에만 집중할 수 있습니다.
또한, 보호구문은 코드의 예측 가능성을 높입니다. 코드의 흐름이 명확해지기 때문에, 특정 조건이 충족되지 않는 경우에는 즉시 반환되어야 함을 쉽게 알 수 있습니다. 이로 인해 코드가 더 단순해지고, 디버깅도 쉬워집니다. 특히, 조건이 복잡하거나 여러 개일 때, 중복된 코드 없이 명확하게 로직을 표현할 수 있습니다.
그러나 보호구문을 남용해서는 안 됩니다. 지나치게 많은 보호구문이 사용된 코드는 가독성을 높이려다 오히려 코드가 너무 잘게 쪼개지고 복잡해질 수 있습니다. 보호구문은 주로 필수적인 조건을 미리 확인하고, 코드의 흐름을 단순화하는 데 사용해야 하며, 너무 많은 조건문이 포함된 코드라면 다시 한 번 코드의 구조를 고민해볼 필요가 있습니다.
참고로, 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("주문을 처리합니다.")
}
코드는 유기체처럼 성장합니다. 시간이 지남에 따라 새로운 기능과 요구 사항에 맞추어 변화하고 확장되며, 이러한 과정에서 대칭성을 유지하는 것은 유지보수성과 가독성을 높이는 중요한 요소가 됩니다. 코드의 대칭성은 코드의 일관성을 유지하고, 코드를 읽는 사람에게 직관적인 구조를 제공하여 효율적인 개발을 가능하게 합니다.
대칭적인 코드란, 동일한 동작을 하는 코드를 여러 가지 방식으로 작성하기보다는, 하나의 일관된 방식으로 통일하여 작성하는 것을 의미합니다. 이로 인해 코드는 읽기 쉬워지고, 확장하기 용이해지며, 팀원 간의 협업 효율성도 높아집니다.
다양한 방식으로 동일한 동작을 구현할 수 있지만, 코드 스타일의 일관성은 코드의 품질에 직결됩니다. 아래 예시는 사용자의 나이에 따라 다른 메시지를 출력하는 코드의 여러 구현 방식을 보여줍니다.
function getUserCategory(age) {
if (age >= 18) {
return "성인입니다.";
} else if (age >= 13) {
return "청소년입니다.";
} else {
return "어린이입니다.";
}
}
console.log(getUserCategory(20)); // 출력: 성인입니다.
function getUserCategory(age) {
return age >= 18
? "성인입니다."
: age >= 13
? "청소년입니다."
: "어린이입니다.";
}
console.log(getUserCategory(20)); // 출력: 성인입니다.
function getUserCategory(age) {
switch (true) {
case age >= 18:
return "성인입니다.";
case age >= 13:
return "청소년입니다.";
default:
return "어린이입니다.";
}
}
console.log(getUserCategory(20)); // 출력: 성인입니다.
위 세 가지 예시는 모두 동일한 결과를 반환하지만, 각각 다른 방식으로 구현되어 있습니다. 대칭적으로 맞춘 코드는 이러한 다양한 구현 방식 중 하나를 선택하여 일관된 코드 스타일을 유지하는 것을 목표로 합니다.
대칭적인 코드 작성은 코드베이스에서 중요한 역할을 합니다. 코드의 대칭성은 코드의 일관성을 높여 유지보수가 쉬워지고, 확장성이 커지며, 협업 시 불필요한 충돌을 줄입니다.
코드를 작성할 때 읽기 좋은 순서로 정렬하는 것은 매우 중요합니다. 코드를 읽는 사람이 논리적인 흐름에 따라 코드를 쉽게 이해할 수 있기 때문입니다. 하지만 주의할 점은 순서를 정렬하면서 함수의 동작이나 세부 구현을 변경하지 않아야 한다는 것입니다. 코드를 정리할 때는 코드의 기능을 그대로 유지하면서 순서만 변경해야 합니다.
차례대로 이름, 이메일, 주소에 대한 정보를 수집하는 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?에서 강조하는 정리의 원칙은, 코드의 동작을 변경하지 않고 일관된 기준에 따라 구조를 정돈하는 것입니다. 따라서 본인의 기준을 명확히 정하고 그 기준에 따라 일관되게 정리하는 것이 중요합니다.
코드를 더 명확하고 가독성 좋게 만들기 위해서는 설명하는 변수와 상징적인 상수를 사용하는 것이 중요합니다. 이는 코드의 의도를 더 명확하게 표현하여 유지보수성을 높이고, 코드 수정 시에도 쉽게 이해할 수 있도록 돕습니다. 이 과정에서 표현식의 의도와 리터럴 값을 설명하는 이름으로 치환하는 것이 핵심입니다.
설명하는 변수란, 복잡하거나 의도가 분명하지 않은 표현식을 변수로 추출하여 이름을 통해 그 목적을 명확히 하는 것을 의미합니다. 코드가 복잡해질수록 설명하는 변수는 가독성을 크게 향상시킵니다.
다음 코드는 query
가 비어있는지를 확인하는 로직입니다. 하지만 코드만으로는 그 의도를 명확히 파악하기 어렵습니다. 이 복잡한 표현을 설명하는 변수로 치환해봅시다.
useEffect(() => {
if (Object.keys(query).length === 0) {
return;
}
foo();
}, [query]);
useEffect(() => {
const isInitialPageLoad = Object.keys(query).length === 0;
if (isInitialPageLoad) {
return;
}
foo();
}, [query]);
isInitialPageLoad
라는 변수명을 사용하여 query
가 비어있는 상태가 페이지의 첫 진입을 의미한다는 의도를 명확히 했습니다. 이를 통해 코드를 읽는 사람은 이 조건이 페이지 첫 로드를 확인하는 것임을 쉽게 파악할 수 있습니다.isInitialPageLoad
변수만 수정하면 되므로 변경이 더 쉬워집니다.설명하는 상수는 숫자나 문자열과 같은 리터럴 값을 의미를 명확하게 드러내는 상수로 변환하는 것을 말합니다. 리터럴 값을 설명하는 상수로 바꾸면 가독성과 유지보수성이 크게 향상되며, 코드 곳곳에서 같은 값을 반복적으로 사용할 때 발생하는 오류도 방지할 수 있습니다.
리터럴 상수인 4_800
과 6_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;
}
TAXI_BASE_FARE
와 TAXI_NIGHT_SURCHARGE_FARE
는 각각 기본 요금과 야간 할증 요금임을 바로 파악할 수 있습니다.모든 값을 상수화하는 것이 좋은 것은 아닙니다. 예를 들어, const ONE = 1;
과 같은 상수는 의미 전달에 전혀 도움이 되지 않으므로 지양해야 합니다. 상수화는 코드에서 의미를 명확하게 전달할 수 있을 때만 적용해야 하며, 단순히 리터럴 값을 치환하는 것이 상수화의 목적이 되어서는 안 됩니다.
또한, 관련된 상수는 한 곳에 모아서 관리하는 것도 고려해보면 좋습니다. 이렇게 정리한다면 같이 변경되거나 이해해야 할 상수들을 쉽게 관리하고 유지보수할 수 있도록 도와줍니다.
코드를 더 깔끔하고 목적에 맞게 작성하려면 도우미 함수를 추출하는 것이 중요합니다. 도우미 함수는 목적이 명확하고, 다른 코드와 상호작용이 적은 코드 블록을 함수로 분리하는 방법입니다. 중요한 점은 도우미 함수의 이름은 작동 방식이 아니라 그 목적을 설명하는 이름으로 짓는 것입니다. 이를 통해 코드의 가독성이 향상되고, 유지보수와 코드 재사용이 용이해집니다.
이 작업은 리팩터링의 기본적인 원칙인 메서드 추출과 유사하며, 코드의 복잡성을 줄이고 변경에 유연한 설계를 가능하게 합니다.
시간적 결합이란, 특정 로직이 특정 순서로 실행되어야 할 때 나타나는 상황을 의미합니다. 이러한 로직은 순서가 매우 중요하기 때문에 코드를 도우미 함수로 추출하여 그 순서 의도를 명확하게 표현하는 것이 좋습니다.
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");
}
도우미 함수는 거대한 함수에서 복잡한 로직을 분리해 함수의 가독성과 유지보수성을 크게 향상시킬 수 있습니다. 거대한 함수에 여러 로직이 혼합되어 있으면 함수의 흐름을 파악하기 어려워지는데, 이를 같은 추상화 레벨로 맞춘 헬퍼 함수로 분리하면 함수가 훨씬 명확해집니다.
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");
}
processOrder
함수는 이제 세부 구현을 신경 쓰지 않고 주문 처리 흐름을 쉽게 이해할 수 있습니다. 각 로직을 헬퍼 함수로 분리해 메인 함수가 간결해졌습니다.validateOrder
, handleOrderProcessing
, completeOrder
와 같은 헬퍼 함수로 분리하여 각 부분을 독립적으로 수정할 수 있습니다. 예를 들어, 유효성 검사 로직만 변경해야 할 경우 validateOrder
함수만 수정하면 됩니다.코드를 작성할 때 주석을 적절하게 사용하는 것은 매우 중요합니다. 주석은 명확하지 않은 부분을 설명하거나 특별한 맥락을 기록하기 위해 사용되지만, 불필요한 주석은 오히려 혼란을 초래할 수 있습니다. 주석을 잘못 사용하면, 코드와 주석이 따로 놀게 되어 유지보수에 어려움을 겪을 수 있습니다.
설명하는 주석은 코드에서 의도가 명확하지 않은 부분이나 특별한 사정을 설명하는 데 사용됩니다. 코드를 읽는 사람이 해당 주석을 통해 코드의 의도나 맥락을 쉽게 이해할 수 있도록 돕는 것이 목적입니다.
// 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의 타입 시스템이 이미 명확한 정보를 제공하는 경우라면 주석에서 타입 정보를 불필요하게 명시할 필요가 없습니다.
/**
* @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;
}
{number}
와 같은 타입 정보를 제거하여, 코드와 주석 간의 타입 불일치 문제를 해결했습니다.코드 정리에 대한 개념들은 겉으로는 당연해 보일 수 있지만, 실제로 왜 이러한 정리가 필요한지 명확하게 설명하기는 쉽지 않습니다. Tidy First?를 읽으면서 코드 정리의 중요성을 깊이 이해하게 되었고, 이를 통해 동료 개발자들에게도 정리의 필요성을 조금 더 효과적으로 전달할 수 있게 되었습니다.
이 글에서는 보호구문, 대칭으로 맞추기, 읽는 순서, 설명하는 변수와 상수, 도우미 함수 추출, 그리고 설명하는 주석과 불필요한 주석 지우기와 같은 개념들을 살펴보았습니다. 각각의 개념은 코드의 가독성과 유지보수성을 향상시키는 데 큰 도움이 되며, 작은 변화로도 코드 품질에 큰 영향을 미칠 수 있습니다.
하지만 이 글에서 다루지 않은 내용들도 많습니다. 예를 들어, 선언과 초기화를 옮기기와 같은 주제는 프런트엔드 입장에서 공감하기 쉽지 않았습니다. 또한, 응집도 향상과 같은 주제들은 범위가 넓어 이번에는 포함하지 않았습니다. 이러한 내용들은 Tidy First?에서 자세히 다루고 있으니, 직접 책을 구매하여 읽어보시길 강력하게 추천드립니다. 책을 통해 더 깊이 있는 내용을 얻으실 수 있을 것입니다.
마지막으로, 앞으로 새로운 코드를 작성하거나 기존의 레거시 코드를 마주할 때, 이 글에서 소개한 액션 아이템들을 떠올리시는 순간이 있다면 좋겠습니다. 그리고 작은 정리의 습관이 모여 코드베이스 전체의 품질을 향상시키고, 팀의 생산성을 높일 수 있다면 더더욱 좋겠습니다. 코드 정리는 단순히 예쁘게 보이기 위한 것이 아니라, 더 나은 소프트웨어를 만들기 위한 중요한 과정이니까요.
내일은 코드에 새로운 기능을 추가하기 전에 “정리가 먼저인가?”를 떠올려보면 어떨까요?
긴 글 읽어주셔서 감사합니다.
보호구문은 early return과 같은 내용이네요
책마다 표현하는 방법이 달라서 그럴려나요