트레이드오프: 클린 테스트 vs. 코드 간결성 in Modern JS

발행: (2026년 1월 20일 오전 02:57 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

Hey fellow devs! 👋

현대 JavaScript와 TypeScript 개발에서는 두 가지 상반된 목표 사이에서 끊임없이 균형을 맞춥니다:

  • 코드 간결성 – 간결하고 최소한의 코드를 작성하는 것.
  • 깨끗한 테스트 – 격리하고 검증하기 쉬운 코드를 작성하는 것.

대개 가장 빨리 작성할 수 있는 코드는 테스트하기 가장 어려운 경우가 많습니다. 반대로, 테스트 용이성을 위해 설계된 코드는 처음 보면 “보일러플레이트가 많다”는 느낌을 줄 수 있습니다.

아래에서는 실제 사례를 통해 이 트레이드‑오프를 살펴보고, 일반적인 패턴에서 장기적이고 확장 가능한 시스템을 설계하기 위한 사고 방식까지 다룹니다.

Round 1: 환경 변수 딜레마

코드 리뷰에서 자주 등장하는 고전적인 논쟁: Vite, Webpack, 혹은 Node가 제공하는 환경 변수(API 키, 기능 플래그 등)를 어떻게 접근할까?

“간단한” 접근법 (정적 상수)

가장 빠른 방법은 변수를 직접 읽어 상수에 저장하는 것이다. 한 줄의 코드, 간단함.

// config.ts
export const IS_PRODUCTION = import.meta.env.PROD;
export const API_URL = import.meta.env.VITE_API_URL;

// myFeature.ts
import { IS_PRODUCTION } from './config';
if (IS_PRODUCTION) {
  // 무서운 실제 작업 수행
}

숨겨진 비용

  • 코드가 빌드 시스템의 전역 상태에 강하게 결합된다.
  • myFeature.ts를 단위 테스트할 때, IS_PRODUCTION은 테스트 파일이 로드되는 즉시 평가된다.
  • 상수가 true 혹은 false로 설정된 뒤에는 같은 테스트 실행 내에서 이를 변경하기가 매우 어렵다.

두 시나리오를 모두 테스트하려면 보통 “전역 스텁”을 사용하게 된다. 예를 들어 Vitest나 Jest에 런타임 환경을 바꾸도록 지시한다:

// ❌ 지저분한 전역 테스트
vi.stubEnv('PROD', 'true'); // 이제 모든 테스트가 프로덕션이라고 생각한다
// …스텁을 해제하지 않으면 다른 테스트가 이상하게 깨진다

“테스트 가능한” 접근법 (Getter 함수)

접근을 함수로 감싼다. 약간의 보일러플레이트가 추가되지만 깔끔한 시임(seam)을 만든다.

// config.ts
export const getIsProduction = () => import.meta.env.PROD;

// myFeature.ts
import { getIsProduction } from './config';
if (getIsProduction()) {
  // 무서운 실제 작업 수행
}

이점: 시임(Seam) 만들기

시임(Michael Feathers가 대중화한 개념)은 소스 코드를 수정하지 않고도 프로그램 동작을 바꿀 수 있는 지점을 말한다. 테스트에서는 더 이상 전역 환경을 해킹할 필요가 없으며, 일반 함수에 스파이만 걸면 된다.

// ✅ 깔끔하고 격리된 테스트
import * as Config from './config';

test('프로덕션일 때만 무서운 작업 수행', () => {
  const spy = vi.spyOn(Config, 'getIsProduction');

  spy.mockReturnValue(true);
  // 프로덕션에 대한 기대값 실행...

  spy.mockReturnValue(false);
  // 비프로덕션에 대한 기대값 실행...
});

테스트 가능한 접근법은 몇 글자의 추가 비용을 감수하고 격리와 제어를 얻는다.

Source:

Round 2: 시간 다루기

간결함이 테스트에 해가 되는 또 다른 영역은 현재 시간을 처리하는 경우입니다.

“간결한” 접근법 (직접 접근)

// discount.ts
export const isDiscountExpired = (expiryDate: Date): boolean => {
  // 여기서는 간결함이 승리합니다:
  const now = new Date();
  return now > expiryDate;
};

문제점: 이 함수는 비결정적입니다. 오늘은 통과하지만 내일은 실패할 수 있습니다. 이를 테스트하려면 시스템 시계를 멈추는 무거운 “가짜 타이머”가 필요합니다.

“테스트 가능한” 접근법 (의존성 주입)

기본값이 있는 매개변수를 통해 시간 소스를 주입합니다—경량 의존성 주입 형태입니다.

// discount.ts
export const isDiscountExpired = (
  expiryDate: Date,
  now: Date = new Date() // 기본값으로 앱 코드를 간단히 유지
): boolean => {
  return now > expiryDate;
};

이제 테스트는 간단하고 결정적이며, 시스템 시계를 모킹할 필요가 없습니다.

// ✅ 깔끔한 테스트
test('checks expiration', () => {
  const fixedNow = new Date('2024-01-01T10:00:00Z');
  const tomorrow = new Date('2024-01-02T10:00:00Z');

  expect(isDiscountExpired(tomorrow, fixedNow)).toBe(false);
});

시니어 엔지니어의 사고방식

주니어 / 중급 개발자는 종종 성공을 속도—기능이 얼마나 빨리 배포되는가—로 측정합니다. 간결함은 단기 속도를 높입니다.

시니어 / 프린시펄 엔지니어유지보수성, 안정성, 위험 감소에 초점을 옮깁니다.

Shift‑Left

우리는 버그를 “좌측으로 이동”하고 싶습니다: QA나 프로덕션에서가 아니라 개발자의 머신에서 유닛 테스트로 일찍 잡는 것입니다.
코드가 간결하지만 전역 상태(import.meta.env, new Date(), 등)에 의존한다면, 개발자들은 설정이 번거롭기 때문에 본능적으로 테스트 작성을 회피합니다. 시임(seam)을 도입(게터 함수, 주입 가능한 의존성)하면 테스트가 쉬워져 보다 건강한 테스트 문화를 장려합니다.

약간의 보일러플레이트를 도입하고, 게터 함수를 만들며, 의존성을 주입하고, 시임을 생성함으로써 테스트 작성에 필요한 마찰을 줄입니다.

결론

간결성 선택은 일회성 프로토타입, 간단한 스크립트, 혹은 로직이 전혀 없는 매우 제한된 UI 컴포넌트에 적합합니다.

테스트 용이성 선택은 비즈니스 로직, 설정, 헬퍼, 그리고 애플리케이션이 시간이 지나도 올바르게 동작하도록 의존하는 모든 것에 적합합니다.

오늘은 코드가 더 많이 보일 수 있지만, 내일은 마음의 평화를 가져다 줍니다.

도움이 되었다면 하트를 눌러 주세요! ❤️

#Hash

Back to Blog

관련 글

더 보기 »

JSBooks: 최고의 JavaScript 책 선별 목록

소개 오늘날 JavaScript를 배우는 것은 압도적으로 느껴질 수 있습니다. 수천 권의 책, 강좌, 튜토리얼이 있으며, 어떤 것이 실제로 도움이 되는지 알기 어렵습니다.