좋음은 상대적이다.

‘좋다’라는 말은 흔히 쓰이지만, 막상 그 의미를 정의하려 하면 쉽지 않다. 무엇이 좋은지를 묻는 순간, 우리는 왜 그렇게 느끼는지, 그 판단의 이유를 고민하게 된다. 좋음은 절대적인 가치가 아니라, 상황과 맥락, 개인의 관점에 따라 달라지는 상대적 감정이기 때문이다.

예를 들어 커피 한 잔, 음식, 운동 같은 일상적인 선택을 떠올려보자. 어떤 사람에게는 최상의 선택이 될 수 있지만, 다른 사람에게는 불편하거나 부담스러운 선택일 수 있다.

결국 ‘좋다’는 판단은 개인의 경험, 목적, 환경에 따라 달라지며, 모두에게 좋은 것은 존재하지 않는다. 좋음은 특정한 맥락 안에서만 의미를 갖는, 감정적 판단이다.

이런 사고를 코드에 적용해보면 어떨까? 좋은 코드란 무엇인가라는 질문은, 좋은 커피나 음식이 무엇인가를 묻는 것과 다를 바 없다. 상황과 목적에 따라 달라질 수밖에 없다는 것이다.

좋은 코드란 무엇인가?

우리는 흔히 가독성, 일관성, 유지보수 용이성 같은 기준을 좋은 코드의 핵심으로 꼽는다. SOLID, 높은 응집도·낮은 결합도, 추상화 등도 대표적인 기법이다. 물론 유효한 원칙들이다. 하지만 “항상” 좋은가? 맥락을 벗어나면 원칙은 종종 비용이 된다.

예를 들어, 팀의 의사결정을 돕기 위한 게임 MVP를 빠르게 만들어야 한다고 하자. 이때 좋은 코드는 “지금 목적에 가장 빨리 도달하게 하는 코드"다. 동작하는 결과가 최우선이며, 과도한 설계나 레이어 분리는 ROI가 낮다. 일관성·응집도·결합도 같은 품질 기준은 장기 운영을 전제로 가치를 갖는다. 반면 단발성 실험에서는 구현 집중이 더 합리적일 수 있다. 응집도를 챙기는 일조차, 그걸 챙길 만한 맥락인지부터가 판단의 대상이다.

가령 팀 의사결정용 룰렛을 하루 만에 만든다고 하자. 교과서대로면 API·모델·훅·뷰로 레이어를 나누는 게 ‘좋은 코드’다.

// 교과서적 4레이어 분리
api/roulette.ts
model/roulette.ts
hooks/useRoulette.ts
components/RouletteView.tsx

파일 넷에 추상화 비용까지 딸려온다. 하루 쓰고 버릴 실험엔 이 구조가 오히려 손해다. 같은 걸 한 파일에 뭉치면 이렇게 된다.

1
2
3
4
5
6
function Roulette({ members }: Props) {
  const [result, setResult] = useState<string>();
  const pick = () => members[Math.floor(Math.random() * members.length)];

  return <button onClick={() => setResult(pick())}>{result ?? '뽑기'}</button>;
}

한눈에 들어온다. 지금 목적엔 이쪽이 더 좋은 코드다. 레이어 분리라는 ‘좋은 형태’도 맥락을 벗어나면 비용이 된다.

반대 상황을 보자. 하루 쓰고 버리는 룰렛과 달리, 오래 유지보수해야 하고, API 호출·응답 변환 유틸·파싱 함수·UI 마크업·A/B 테스트 분기까지 뒤섞인 복잡한 화면이라면 어떨까? 이럴 땐 API 호출·응답 변환·UI를 레이어로 나누는 편이 장기적으로 좋은 코드가 된다.

한 컴포넌트에 다 넣으면 이렇게 뒤섞인다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function EventPage({ id }: Props) {
  const [banner, setBanner] = useState<Banner>();

  useEffect(() => {
    fetch(`/api/events/${id}`)
      .then(res => res.json())
      .then(raw => {
        // 응답 변환·파싱까지 여기서
        setBanner({
          title: raw.title.trim(),
          endsAt: new Date(raw.end_at),
          discount: Number(raw.discount_rate) / 100,
        });
      });
  }, [id]);

  const variant = getABVariant(id); // A/B 분기도 뒤섞임
  return variant === 'A' ? <BigCta b={banner} /> : <SmallCta b={banner} />;
}

useEffect 안에 fetch와 변환을 몰아넣은 것부터 눈에 띈다(선언과 호출은 나누는 게 낫다). 레이어로 나누면 각 조각이 제 역할만 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// api/event.ts - 호출
export const fetchEvent = (id: string) => http.get<RawEvent>(`/api/events/${id}`);

// model/event.ts - 응답 변환·파싱
export const toBanner = (raw: RawEvent): Banner => ({
  title: raw.title.trim(),
  endsAt: new Date(raw.end_at),
  discount: Number(raw.discount_rate) / 100,
});

// EventPage.tsx - UI·분기만
function EventPage({ id }: Props) {
  const banner = useEvent(id);      // 호출+변환은 훅 뒤로
  const variant = useABVariant(id);
  return variant === 'A' ? <BigCta b={banner} /> : <SmallCta b={banner} />;
}

역할을 분리하면 각 파일의 변경 이유가 하나로 좁혀진다. 엔드포인트가 바뀌면 api만, 응답 형태가 바뀌면 model만, 화면이 바뀌면 컴포넌트만 건드린다. 변경 이유가 하나로 모이니 결합도는 내려가고 변경 영향 범위도 좁아진다. 이렇게 변경 이유로 코드를 묶는 기준이 곧 응집도다. 하지만 항상 장점만 있는 것은 아니다.

  1. 비용: 추상화는 공짜가 아니다. 구현이 숨겨지면 추적 난이도가 올라가고, 구조를 모르는 사람에게는 학습 비용이 크다.

  2. 오버 엔지니어링: 네 단계로 쪼갰지만, 실제로는 하나로 합쳐도 충분히 단순할 수 있다. 작은 기능에 과도한 레이어링은 유지보수 시간을 오히려 늘린다.

  3. 경계 모호성: 경계가 모호한 요소에서 어디에 두어야 할지 결정 비용이 생긴다.

이런 선택은 사고 자원을 소모한다. 로직 종류가 몇 가지 안 되는 기능이라면 한 파일에 모으는 편이 개발·파악 모두 더 빠를 수 있다. 결합도가 높아지고 길이가 늘어날 위험은 있지만, 복잡도가 낮은 맥락에서는 “한눈에 파악되는 코드"가 오히려 가독성 이점이 된다.

좋은 코드는 ‘형태’가 아니다.

그렇다면 좋은 코드란 무엇일까? 좋은 코드는 고정된 ‘형태’가 아니다. 멘탈 모델이다. 애초에 ‘좋다’라는 개념이 들어간 이상, 그것을 구체적인 형태로 규정할 수는 없다. ‘좋음’은 고정된 사실이 아니라 상대적인 영역이기 때문이다.

그래서 특징 몇 개를 갖췄다고 좋은 코드로 합격·불합격을 매길 수는 없다. 좋은 코드는 지금의 목적과 맥락 안에서 자연스럽게 나온다. 어떤 때는 가독성이 최선이다. 성능이 최선인 때도, 낮은 결합도가 답인 때도 있다. 때로는 명시적 의존성이 의도를 더 또렷하게 드러낸다.

결국 좋은 코드는 손에 잡히는 형태가 아니라, 매번 다시 내리는 판단이다. 어제 좋았던 코드가 오늘은 아닐 수 있다. 그래서 정답을 외우는 대신, 매번 되물을 수밖에 없다. 이 상황에 적합한가.