한국어

자바스크립트 테스트 주도 개발(TDD)을 마스터하세요. 본 가이드는 레드-그린-리팩터 사이클, Jest를 이용한 실용적인 구현, 최신 개발 모범 사례를 다룹니다.

자바스크립트 테스트 주도 개발: 글로벌 개발자를 위한 종합 가이드

다음 시나리오를 상상해 보세요. 거대한 레거시 시스템의 핵심 코드를 수정하는 임무를 맡았습니다. 두려움이 밀려옵니다. 내가 수정한 부분이 다른 곳을 망가뜨리지는 않을까? 시스템이 여전히 의도대로 작동한다고 어떻게 확신할 수 있을까? 이러한 변화에 대한 두려움은 소프트웨어 개발에서 흔히 발생하는 질병으로, 종종 개발 속도를 늦추고 애플리케이션을 취약하게 만듭니다. 하지만 오류가 프로덕션에 도달하기 전에 잡아내는 안전망을 만들어 자신감을 갖고 소프트웨어를 개발할 방법이 있다면 어떨까요? 이것이 바로 테스트 주도 개발(TDD)이 약속하는 것입니다.

TDD는 단순히 테스트 기법이 아닙니다. 소프트웨어 설계와 개발에 대한 훈련된 접근 방식입니다. TDD는 전통적인 "코드 작성 후 테스트" 모델을 뒤집습니다. TDD에서는 프로덕션 코드를 작성하여 통과시키기 전에 실패하는 테스트를 먼저 작성합니다. 이 간단한 전환은 코드 품질, 설계, 유지보수성에 지대한 영향을 미칩니다. 이 가이드는 전 세계 전문 개발자들을 위해 자바스크립트에서 TDD를 구현하는 방법에 대한 포괄적이고 실용적인 시각을 제공할 것입니다.

테스트 주도 개발(TDD)이란 무엇인가?

핵심적으로, 테스트 주도 개발은 매우 짧은 개발 사이클의 반복에 의존하는 개발 프로세스입니다. 기능을 작성한 다음 테스트하는 대신, TDD는 테스트를 먼저 작성해야 한다고 주장합니다. 이 테스트는 기능이 아직 존재하지 않기 때문에 필연적으로 실패할 것입니다. 그러면 개발자의 임무는 그 특정 테스트를 통과시키기 위한 가장 간단한 코드를 작성하는 것입니다. 테스트가 통과되면 코드를 정리하고 개선합니다. 이 기본적인 순환 과정을 "레드-그린-리팩터(Red-Green-Refactor)" 사이클이라고 합니다.

TDD의 리듬: 레드-그린-리팩터

이 3단계 사이클은 TDD의 심장 박동과 같습니다. 이 리듬을 이해하고 실천하는 것은 이 기술을 마스터하는 데 필수적입니다.

하나의 작은 기능에 대한 사이클이 완료되면, 다음 기능을 위해 새로운 실패하는 테스트로 다시 시작합니다.

TDD의 세 가지 법칙

애자일 소프트웨어 운동의 핵심 인물인 로버트 C. 마틴(Robert C. Martin, "엉클 밥"으로도 알려짐)은 TDD 원칙을 체계화하는 세 가지 간단한 규칙을 정의했습니다.

  1. 실패하는 단위 테스트를 통과시키기 위한 경우가 아니라면, 프로덕션 코드를 작성하지 않는다.
  2. 실패하기에 충분한 만큼만 단위 테스트를 작성하며, 컴파일 실패도 실패에 포함된다.
  3. 실패하는 단 하나의 단위 테스트를 통과시키기에 충분한 만큼만 프로덕션 코드를 작성한다.

이 법칙들을 따르면 레드-그린-리팩터 사이클을 따르게 되며, 프로덕션 코드의 100%가 특정하고 테스트된 요구사항을 만족시키기 위해 작성되었음을 보장합니다.

왜 TDD를 도입해야 하는가? 글로벌 비즈니스 사례

TDD는 개별 개발자에게 엄청난 이점을 제공하지만, 그 진정한 힘은 팀 및 비즈니스 수준, 특히 전 세계적으로 분산된 환경에서 실현됩니다.

자바스크립트 TDD 환경 설정하기

자바스크립트에서 TDD를 시작하려면 몇 가지 도구가 필요합니다. 최신 자바스크립트 생태계는 훌륭한 선택지를 제공합니다.

테스팅 스택의 핵심 구성 요소

단순성과 올인원(all-in-one) 특성 때문에, 예제에서는 Jest를 사용할 것입니다. Jest는 "제로 설정" 경험을 원하는 팀에게 훌륭한 선택입니다.

Jest를 사용한 단계별 설정

TDD를 위한 새 프로젝트를 설정해 보겠습니다.

1. 프로젝트 초기화: 터미널을 열고 새 프로젝트 디렉터리를 만듭니다.

mkdir js-tdd-project
cd js-tdd-project
npm init -y

2. Jest 설치: Jest를 개발 의존성으로 프로젝트에 추가합니다.

npm install --save-dev jest

3. 테스트 스크립트 구성: `package.json` 파일을 엽니다. `"scripts"` 섹션을 찾아 `"test"` 스크립트를 수정합니다. TDD 워크플로우에 매우 유용한 `"test:watch"` 스크립트를 추가하는 것도 강력히 권장합니다.

"scripts": {
  "test": "jest",
  "test:watch": "jest --watchAll"
}

이 `--watchAll` 플래그는 파일이 저장될 때마다 Jest가 자동으로 테스트를 다시 실행하도록 지시합니다. 이는 즉각적인 피드백을 제공하여 레드-그린-리팩터 사이클에 완벽합니다.

이제 끝입니다! 환경이 준비되었습니다. Jest는 `*.test.js`, `*.spec.js`로 이름이 지정되거나 `__tests__` 디렉터리에 있는 테스트 파일을 자동으로 찾습니다.

TDD 실제 적용: `CurrencyConverter` 모듈 구축하기

TDD 사이클을 실용적이고 세계적으로 이해되는 문제인 통화 간 환전에 적용해 보겠습니다. `CurrencyConverter` 모듈을 단계별로 구축해 보겠습니다.

반복 1: 간단한 고정 환율 변환

🔴 RED: 첫 번째 실패하는 테스트 작성하기

첫 번째 요구사항은 고정 환율을 사용하여 특정 금액을 한 통화에서 다른 통화로 변환하는 것입니다. `CurrencyConverter.test.js`라는 새 파일을 만듭니다.

// CurrencyConverter.test.js
const CurrencyConverter = require('./CurrencyConverter');

describe('CurrencyConverter', () => {
  it('should convert an amount from USD to EUR correctly', () => {
    // 준비 (Arrange)
    const amount = 10; // 10 USD
    const expected = 9.2; // 1 USD = 0.92 EUR의 고정 환율 가정

    // 실행 (Act)
    const result = CurrencyConverter.convert(amount, 'USD', 'EUR');

    // 단언 (Assert)
    expect(result).toBe(expected);
  });
});

이제 터미널에서 테스트 와처를 실행합니다:

npm run test:watch

테스트는 멋지게 실패할 것입니다. Jest는 `TypeError: Cannot read properties of undefined (reading 'convert')`와 같은 메시지를 보고할 것입니다. 이것이 우리의 레드(RED) 상태입니다. `CurrencyConverter`가 존재하지 않기 때문에 테스트가 실패합니다.

🟢 GREEN: 통과를 위한 가장 간단한 코드 작성하기

이제 테스트를 통과시켜 봅시다. `CurrencyConverter.js`를 만듭니다.

// CurrencyConverter.js
const rates = {
  USD: {
    EUR: 0.92
  }
};

const CurrencyConverter = {
  convert(amount, from, to) {
    return amount * rates[from][to];
  }
};

module.exports = CurrencyConverter;

이 파일을 저장하는 즉시 Jest는 테스트를 다시 실행하고, 그린(GREEN)으로 바뀔 것입니다. 우리는 테스트의 요구사항을 만족시키기 위해 절대적으로 최소한의 코드를 작성했습니다.

🔵 REFACTOR: 코드 개선하기

코드는 간단하지만, 벌써 개선점을 생각해 볼 수 있습니다. 중첩된 `rates` 객체는 다소 경직되어 있습니다. 지금으로서는 충분히 깔끔합니다. 가장 중요한 것은 테스트로 보호되는 작동하는 기능이 있다는 것입니다. 다음 요구사항으로 넘어갑시다.

반복 2: 알 수 없는 통화 처리하기

🔴 RED: 유효하지 않은 통화에 대한 테스트 작성하기

모르는 통화로 변환하려고 하면 어떻게 해야 할까요? 아마도 에러를 발생시켜야 할 것입니다. `CurrencyConverter.test.js`에 새 테스트로 이 동작을 정의해 봅시다.

// CurrencyConverter.test.js의 describe 블록 안에

it('should throw an error for unknown currencies', () => {
  // 준비 (Arrange)
  const amount = 10;

  // 실행 (Act) & 단언 (Assert)
  // Jest의 toThrow가 작동하도록 함수 호출을 화살표 함수로 감쌉니다.
  expect(() => {
    CurrencyConverter.convert(amount, 'USD', 'XYZ');
  }).toThrow('Unknown currency: XYZ');
});

파일을 저장합니다. 테스트 러너는 즉시 새로운 실패를 보여줍니다. 코드가 에러를 발생시키지 않고 `rates['USD']['XYZ']`에 접근하려다 `TypeError`를 발생시키기 때문에 레드(RED) 상태입니다. 우리의 새로운 테스트가 이 결함을 정확하게 식별했습니다.

🟢 GREEN: 새 테스트 통과시키기

`CurrencyConverter.js`를 수정하여 유효성 검사를 추가합시다.

// CurrencyConverter.js
const rates = {
  USD: {
    EUR: 0.92,
    GBP: 0.80
  },
  EUR: {
    USD: 1.08
  }
};

const CurrencyConverter = {
  convert(amount, from, to) {
    if (!rates[from] || !rates[from][to]) {
      // 더 나은 에러 메시지를 위해 어떤 통화가 알 수 없는지 확인
      const unknownCurrency = !rates[from] ? from : to;
      throw new Error(`Unknown currency: ${unknownCurrency}`);
    }
    return amount * rates[from][to];
  }
};

module.exports = CurrencyConverter;

파일을 저장합니다. 이제 두 테스트 모두 통과합니다. 그린(GREEN)으로 돌아왔습니다.

🔵 REFACTOR: 정리하기

우리의 `convert` 함수가 점점 커지고 있습니다. 유효성 검사 로직이 계산과 섞여 있습니다. 가독성을 높이기 위해 유효성 검사를 별도의 비공개 함수로 추출할 수 있지만, 지금은 아직 관리할 만합니다. 우리의 테스트가 무언가를 깨뜨리면 알려줄 것이기 때문에 이러한 변경을 자유롭게 할 수 있다는 것이 핵심입니다.

반복 3: 비동기 환율 가져오기

환율을 하드코딩하는 것은 현실적이지 않습니다. (모의) 외부 API에서 환율을 가져오도록 모듈을 리팩토링해 보겠습니다.

🔴 RED: API 호출을 모의하는 비동기 테스트 작성하기

먼저, 변환기를 재구성해야 합니다. 이제는 아마도 API 클라이언트와 함께 인스턴스화할 수 있는 클래스가 되어야 할 것입니다. `fetch` API도 모의해야 합니다. Jest는 이것을 쉽게 만듭니다.

이 새로운 비동기 현실에 맞게 테스트 파일을 다시 작성해 봅시다. 다시 행복 경로를 테스트하는 것으로 시작하겠습니다.

// CurrencyConverter.test.js
const CurrencyConverter = require('./CurrencyConverter');

// 외부 의존성 모의(mock)
global.fetch = jest.fn();

beforeEach(() => {
  // 각 테스트 전에 모의(mock) 기록 지우기
  fetch.mockClear();
});

describe('CurrencyConverter', () => {
  it('should fetch rates and convert correctly', async () => {
    // 준비 (Arrange)
    // 성공적인 API 응답 모의(mock)
    fetch.mockResolvedValueOnce({
      json: () => Promise.resolve({ rates: { EUR: 0.92 } })
    });

    const converter = new CurrencyConverter('https://api.exchangerates.com');
    const amount = 10; // 10 USD

    // 실행 (Act)
    const result = await converter.convert(amount, 'USD', 'EUR');

    // 단언 (Assert)
    expect(result).toBe(9.2);
    expect(fetch).toHaveBeenCalledTimes(1);
    expect(fetch).toHaveBeenCalledWith('https://api.exchangerates.com/latest?base=USD');
  });

  // API 실패 등에 대한 테스트도 추가할 것입니다.
});

이것을 실행하면 레드(RED)의 바다가 펼쳐질 것입니다. 우리의 이전 `CurrencyConverter`는 클래스가 아니며, `async` 메서드를 가지고 있지도 않고, `fetch`를 사용하지 않습니다.

🟢 GREEN: 비동기 로직 구현하기

이제 `CurrencyConverter.js`를 다시 작성하여 테스트의 요구사항을 충족시켜 봅시다.

// CurrencyConverter.js
class CurrencyConverter {
  constructor(apiUrl) {
    this.apiUrl = apiUrl;
  }

  async convert(amount, from, to) {
    const response = await fetch(`${this.apiUrl}/latest?base=${from}`);
    if (!response.ok) {
      throw new Error('Failed to fetch exchange rates.');
    }

    const data = await response.json();
    const rate = data.rates[to];

    if (!rate) {
      throw new Error(`Unknown currency: ${to}`);
    }

    // 테스트에서 부동 소수점 문제를 피하기 위한 간단한 반올림
    const convertedAmount = amount * rate;
    return parseFloat(convertedAmount.toFixed(2));
  }
}

module.exports = CurrencyConverter;

저장하면 테스트가 그린(GREEN)으로 바뀌어야 합니다. 금융 계산에서 흔한 문제인 부동 소수점 부정확성을 처리하기 위해 반올림 로직을 추가한 점에 유의하세요.

🔵 REFACTOR: 비동기 코드 개선하기

`convert` 메서드는 가져오기, 오류 처리, 파싱, 계산 등 많은 일을 하고 있습니다. API 통신만 책임지는 별도의 `RateFetcher` 클래스를 만들어 리팩토링할 수 있습니다. 그러면 우리의 `CurrencyConverter`는 이 fetcher를 사용하게 될 것입니다. 이는 단일 책임 원칙을 따르며 두 클래스 모두 테스트하고 유지보수하기 쉽게 만듭니다. TDD는 우리를 이 더 깨끗한 설계로 이끌어 줍니다.

일반적인 TDD 패턴과 안티-패턴

TDD를 연습하다 보면 잘 작동하는 패턴과 마찰을 일으키는 안티-패턴을 발견하게 될 것입니다.

따라야 할 좋은 패턴

피해야 할 안티-패턴

더 넓은 개발 수명주기에서의 TDD

TDD는 진공 상태에서 존재하지 않습니다. 현대 애자일 및 데브옵스(DevOps) 관행, 특히 글로벌 팀에게 아름답게 통합됩니다.

결론: TDD와 함께하는 여정

테스트 주도 개발은 테스트 전략 그 이상입니다. 소프트웨어 개발에 접근하는 방식의 패러다임 전환입니다. TDD는 품질, 자신감, 협업의 문화를 조성합니다. 레드-그린-리팩터 사이클은 깨끗하고, 견고하며, 유지보수 가능한 코드로 당신을 이끄는 꾸준한 리듬을 제공합니다. 그 결과로 나오는 테스트 스위트는 팀을 회귀로부터 보호하는 안전망이 되고, 새로운 팀원을 온보딩하는 살아있는 문서가 됩니다.

학습 곡선이 가파르게 느껴질 수 있고, 초기 속도는 더 느리게 보일 수 있습니다. 하지만 디버깅 시간 감소, 소프트웨어 설계 개선, 개발자 자신감 증가라는 장기적인 이익은 헤아릴 수 없습니다. TDD를 마스터하는 여정은 규율과 연습의 여정입니다.

오늘 시작하세요. 다음 프로젝트에서 작고 중요하지 않은 기능 하나를 골라 그 과정에 전념해 보세요. 테스트를 먼저 작성하세요. 실패하는 것을 지켜보세요. 통과하게 만드세요. 그리고 가장 중요한 것은, 리팩토링하세요. 녹색 테스트 스위트가 주는 자신감을 경험하면, 곧 다른 방식으로는 어떻게 소프트웨어를 만들었는지 궁금해하게 될 것입니다.