좋은 코드 품질이란?

좋은 코드는 어떻게 판단하는지 알아봅니다.

좋은 코드 품질이란?

저는 개발자를 코드를 작성하는 작가라고 생각합니다. 읽기 좋은 글을 쓰는 것이 작가에게 중요하듯이, 읽기 좋은 코드를 작성하는 것이 개발자에게 중요합니다. 특히 협업 및 유지보수 관점에서 매우 중요합니다.

사실 저는 좋은 코드라는 것은 정답이라기보다는 약속이 아닐까라는 생각에, 같은 팀 내에서 정한 코드 컨벤션을 따르는 것이 결국 좋은 코드라고 생각합니다. 하지만 그 컨벤션을 정하는 과정에 도움이 될 만한 글을 보게 되어서 저도 제 경험을 기반하여 글을 정리해보았습니다.

좋은 코드란 무엇인가: 네 가지 판단 기준

Toss의 Frontend Fundamentals는 좋은 코드를 "변경하기 쉬운 코드"로 정의하고, 이를 가독성, 예측 가능성, 응집도, 결합도라는 네 가지 기준으로 나눠 설명합니다. 각 개념을 하나씩 짚어보면서, 실무에서 이 개념이 부족해서 겪었던 어려움도 함께 정리해봤습니다.

1. 가독성: 한 번에 고려할 맥락을 줄이는 것

가독성은 코드를 읽는 사람이 한 번에 머릿속에 담아야 하는 맥락의 양으로 결정됩니다. 사람이 한 번에 기억할 수 있는 정보는 6~7개 정도로 제한적이라, 코드가 여러 층위의 로직을 한 함수나 파일에 뒤섞어놓으면 논리적으로 맞는 코드라도 읽기 어려워집니다. 좋은 가독성은 관련 없는 맥락을 분리하고, 이름을 붙여 의도를 드러내고, 위에서 아래로 자연스럽게 읽히도록 만드는 데서 나옵니다.

네이버랩스에서 8만 라인 규모 어노테이션 툴을 다룰 때 이 개념이 부족했던 걸 뼈저리게 느꼈습니다. components, hooks, utils처럼 역할별로 흩어진 구조에서는 기능 하나를 이해하려고 서로 다른 3~4개 디렉토리를 오가야 했거든요. 코드가 틀린 게 아니라, 읽는 사람이 감당해야 하는 맥락이 너무 넓게 퍼져 있던 것 같습니다.

typescript
// 역할별로 흩어진 구조: 라벨링 로직 하나를 이해하려면
// src/hooks/useAnnotationLabel.ts
// src/components/LabelSelector.tsx
// src/utils/labelValidator.ts
// 이 세 파일을 오가며 맥락을 이어붙여야 했다

FSD(Feature-Sliced Design)로 기능 단위 디렉토리를 묶고 나서야, 라벨링을 이해하려면 폴더 하나만 보면 되는 구조가 됐습니다.

💡 이렇게 개선할 수 있어요

  • 동시에 실행되지 않는 분기는 아예 별도 컴포넌트로 분리하기
  • 구현 상세는 Wrapper나 HOC로 감싸서 추상화하기
  • 복잡한 조건식이나 매직 넘버에는 의미가 드러나는 이름 붙이기
  • 시점 이동을 줄이고, 위에서 아래로 한 번에 읽히도록 작성하기

2. 예측 가능성: 이름만 보고 동작을 짐작할 수 있는가

예측 가능성은 함수나 이벤트의 이름, 파라미터, 반환 타입만 보고도 동작을 예상할 수 있는 정도를 말합니다. 같은 종류의 함수인데 반환 타입이나 에러 처리 방식이 제각각이면, 코드를 쓰는 사람은 매번 실제 구현을 열어서 확인해야 합니다. 이 신뢰가 깨지면 동료들은 코드를 짐작이 아니라 검증의 대상으로 다루게 됩니다.

흔히 보이는 사례로, 값 포맷팅을 담당하는 유틸 함수들이 있습니다. 이름과 역할은 비슷한데, 실패했을 때 처리 방식이 함수마다 다른 경우입니다.

typescript
// formatPrice: 값이 이상하면 null을 반환
function formatPrice(value: unknown): string | null {
  if (typeof value !== "number") return null;
  return `${value.toLocaleString()}원`;
}

// formatDate: 값이 이상하면 예외를 던짐
function formatDate(value: unknown): string {
  if (!(value instanceof Date)) throw new Error("Invalid date");
  return value.toISOString();
}

formatPrice를 쓸 때는 null 체크만 하면 됐는데, 같은 습관으로 formatDate를 쓰면 예외를 못 잡아서 화면이 그대로 죽어버립니다. 함수 이름만 보고는 어떤 실패 전략을 쓰는지 알 수 없으니, 매번 구현을 열어봐야 하는 거죠. 같은 종류의 함수는 실패 시 동작까지 통일해서, 예를 들어 { ok: true, value } | { ok: false, reason } 형태로 반환 타입을 맞추면 이런 문제를 없앨 수 있습니다.

💡 이렇게 개선할 수 있어요

  • 라이브러리 함수와 이름이 겹치지 않도록 명확하게 구분해서 짓기
  • 같은 종류의 함수/Hook은 반환 타입을 일관되게 유지하기
  • 이름과 파라미터, 반환 타입만으로 예측할 수 없는 숨은 로직(로깅 등)은 호출부로 분리하기

3. 응집도: 함께 바뀌어야 할 코드가 실제로 함께 바뀌는가

응집도는 코드 한 부분을 수정했을 때, 함께 수정되어야 할 다른 부분이 실제로 함께 수정되는지를 말합니다. 응집도가 낮으면 한쪽만 고치고 다른 쪽을 깜빡해서 조용히 버그가 생기죠. 흥미로운 건, 응집도를 높이려고 코드를 무작정 공통화하면 오히려 서로 무관한 코드가 억지로 엮이면서 응집도가 떨어질 수 있다는 점입니다. 같이 수정될 이유가 있는지가 공통화의 기준이 되어야 합니다.

쇼핑몰 장바구니 기능을 예로 들어볼겠습니다. 장바구니에 담을 수 있는 최대 수량이 99라는 제약이 있는데, 이 숫자가 검증 로직과 UI 두 곳에 따로 박혀 있는 경우가 흔합니다.

typescript
// cartValidator.ts
function canAddToCart(quantity: number) {
  return quantity <= 99;
}

// QuantityInput.tsx
function QuantityInput({ quantity }: { quantity: number }) {
  return <button disabled={quantity >= 99}>+</button>;
}

이후 정책이 바뀌어서 최대 수량이 50으로 줄어들면, 검증 로직만 고치고 버튼 비활성화 조건은 그대로 두는 실수가 생기기 쉽습니다. 사용자는 50개까지만 담을 수 있는데 버튼은 99개까지 눌리는 거죠. 두 코드가 항상 같은 값을 참조해야 한다는 관계가 이름에 드러나지 않았기 때문입니다. MAX_CART_QUANTITY 같은 상수 하나로 묶어서, 두 곳이 같은 값을 참조하고 있다는 걸 코드에서 바로 확인할 수 있게 해야 합니다.

💡 이렇게 개선할 수 있어요

  • 함께 수정되는 파일은 종류별이 아니라 도메인/기능별로 같은 디렉토리에 두기
  • 의미가 불분명한 숫자는 상수로 선언해서 의도를 명확히 하기
  • 폼처럼 필드 단위 응집과 전체 단위 응집 중 상황에 맞는 쪽을 선택하기

4. 결합도: 수정했을 때 영향 범위를 예측할 수 있는가

결합도는 코드를 수정했을 때 영향이 미치는 범위를 말해요. 결합도가 낮은 코드는 한 곳을 고쳐도 다른 곳이 깨지지 않는다는 확신을 줍니다. 다만 여기서 자주 오해하는 지점이 있는데, 공통 컴포넌트로 코드를 재사용하는 것 자체가 결합도를 낮추는 건 아니라는 점입니다. 오히려 여러 곳에서 하나의 공통 코드에 기대는 순간, 그 공통 코드를 수정할 때 모든 사용처를 다시 검증해야 하는 강한 결합이 생깁니다.

여러 페이지에서 재사용하는 <ConfirmModal /> 컴포넌트를 예로 들어볼겠습니다. 처음엔 단순한 확인창이었는데, 페이지마다 조금씩 다른 요구사항이 생기면서 옵션이 계속 늘어난 상황입니다.

typescript
function ConfirmModal({
  title,
  confirmText = "확인",
  cancelText = "취소",
  onConfirm,
  onCancel,
  showCloseButton = true,
  closeOnOverlayClick = true,
  variant = "default"
  // 페이지가 늘어날수록 옵션도 함께 늘어난다
}: ConfirmModalProps) {
  // ...
}

이 컴포넌트를 수정할 때마다, 이 컴포넌트에 기대고 있는 모든 페이지의 동작을 하나하나 확인해야 합니다. 겉으로는 코드를 재사용해서 깔끔해 보이지만, 실제로는 페이지들이 이 컴포넌트 하나에 강하게 묶여 있는 거예요. 페이지마다 요구사항이 계속 달라질 여지가 있다면, 억지로 공통화하기보다 페이지별로 별도 컴포넌트를 두고 중복을 허용하는 편이 오히려 결합도를 낮출 수 있습니다.

💡 이렇게 개선할 수 있어요

  • 하나의 함수나 Hook에 여러 책임을 몰아넣지 않고 책임 단위로 쪼개기
  • 요구사항이 갈릴 여지가 있다면 무리하게 공통화하지 않고 중복 코드를 허용하기
  • Props Drilling은 조합(Composition) 패턴이나, 그래도 안 되면 Context API로 해소하기

마치며

네 가지 기준을 따로 떼어 보면 당연한 이야기처럼 보이지만, 실무에서는 한 가지 개념이 부족한 채로 코드를 짜다가 나중에야 "아, 이게 응집도 문제였구나" 하고 뒤늦게 깨닫는 경우가 많았던 것 같습니다. 좋은 코드는 정답이 아니라 판단의 기준이고, 그 기준을 미리 알고 있느냐가 리팩토링 비용을 크게 가르게 됩니다.