Waylog Blog

TDD(테스트 주도 개발)로 견고한 코드 작성하기: 실무 적용 가이드

Development

소프트웨어 개발 방법론의 고전이자 영원한 숙제인 TDD (Test Driven Development). "테스트를 먼저 작성하라"는 이 단순한 원칙이 왜 실무에서는 지켜지기 어려운 걸까요? 그리고 TDD를 제대로 수행했을 때 우리는 어떤 이점을 얻을 수 있을까요? 이 글에서는 켄트 벡의 철학부터 시작해, 실제 프론트엔드 개발 환경(Jest + React Testing Library)에서 TDD를 어떻게 수행하는지 단계별로 심도 있게 다룹니다.

1. TDD의 3단계 사이클: Red, Green, Refactor

TDD는 단순히 테스트를 많이 짜는 것이 아닙니다. 생각의 순서를 바꾸는 훈련입니다. 그 핵심에는 Red-Green-Refactor라는 리듬이 있습니다.

1.1 Red: 실패하는 테스트 작성

많은 개발자가 구현 코드를 머릿속에 그리며 테스트를 작성합니다. 하지만 TDD의 첫 단계는 **"우리가 무엇을 만들고 싶은가?"**를 정의하는 것입니다. 아직 구현체가 없으므로, 우리가 원하는 행위를 코드로 작성하면 당연히 실패(Red)해야 합니다. 컴파일 에러가 나거나, 기대값이 다르다는 메시지를 보는 것이 이 단계의 목표입니다.

"실패하지 않는 테스트는 가치가 없다."

1.2 Green: 테스트 통과를 위한 최소한의 구현

이제 테스트를 통과시키기 위해 코드를 작성합니다. 여기서 중요한 원칙은 **"죄악(Sin)을 저질러서라도 빨리 초록불을 보라"**는 것입니다. 하드코딩을 해도 좋고, 스파게티 코드를 짜도 좋습니다. 오직 현재 실패하고 있는 그 테스트 하나를 통과시키는 데에만 집중합니다. 이 과정은 우리에게 "작동하는 코드"에 대한 심리적 안정감을 줍니다.

1.3 Refactor: 중복 제거와 설계 개선

테스트가 통과했다면 이제 비로소 개발자 모자를 쓰고 코드를 다듬을 시간입니다. 변수명을 명확하게 바꾸고, 함수를 분리하고, 중복을 제거합니다. 이때 가장 강력한 무기는 바로 방금 작성한 테스트사입니다. 리팩터링 과정에서 실수로 기능을 망가뜨리더라도, 테스트가 즉시 알려줄 것이기 때문입니다. 이것이 TDD가 주는 "수정의 용기"입니다.

2. 프론트엔드에서의 TDD (React 예시)

백엔드 로직에 비해 UI는 테스트하기 어렵다는 편견이 있습니다. 하지만 **React Testing Library(RTL)**의 등장으로 사용자 관점의 테스트가 가능해졌습니다.

2.1 시나리오 정의

간단한 "할 일 목록(Todo List)"을 만든다고 가정해 봅시다.

  • 입력창에 텍스트를 넣고 '추가' 버튼을 누르면 목록에 아이템이 추가되어야 한다.
  • 입력창은 비워져야 한다.

2.2 테스트 작성 (Red)

test('새로운 할 일을 추가할 수 있다', () => {
  render(<TodoList />);
  const input = screen.getByPlaceholderText('할 일을 입력하세요');
  const button = screen.getByText('추가');

  fireEvent.change(input, { target: { value: 'TDD 공부하기' } });
  fireEvent.click(button);

  expect(screen.getByText('TDD 공부하기')).toBeInTheDocument();
});

이 시점에서 TodoList 컴포넌트는 비어있거나 존재하지 않으므로 테스트는 실패합니다.

2.3 구현 (Green)

function TodoList() {
  const [todos, setTodos] = useState<string[]>([]);
  const [text, setText] = useState('');

  return (
    <div>
      <input 
        placeholder="할 일을 입력하세요" 
        value={text} 
        onChange={e => setText(e.target.value)} 
      />
      <button onClick={() => setTodos([...todos, text])}>추가</button>
      <ul>
        {todos.map(todo => <li key={todo}>{todo}</li>)}
      </ul>
    </div>
  );
}

최소한의 코드로 테스트를 통과시킵니다.

2.4 리팩터링 (Refactor)

addTodo 핸들러 함수를 분리하거나, UI 구조를 개선합니다. 또는 text 상태를 비우는 로직을 추가하지 않았음을 깨닫고 새로운 테스트 케이스를 추가할 수도 있습니다.

3. TDD가 주는 진정한 가치: 문서화와 신뢰

TDD로 작성된 테스트 코드는 그 자체로 살아있는 문서입니다. 새로운 팀원이 합류했을 때, 기획서를 뒤지는 것보다 테스트 코드를 읽는 것이 컴포넌트의 동작을 이해하는 데 훨씬 빠르고 정확합니다. 또한, 배포 날 금요일 오후에도 두려움 없이 코드를 수정하고 배포할 수 있는 자신감을 줍니다.

결론적으로 TDD는 개발 속도를 늦추는 걸림돌이 아닙니다. 오히려 디버깅 시간을 획기적으로 줄여주어, 전체적인 개발 주기를 단축시키고 코드의 품질을 보장하는, 시니어 개발자로 가는 가장 확실한 지름길입니다.