seungjun.dev

테스트, TDD에 대한 관심과 현실적인 고민

테스트와 TDD 방법론에 대한 관심과 현실적인 고민, 그리고 앞으로의 학습 계획에 대해

고민의 시작: 안전한 리팩토링

이전에 한 코드 리뷰어님께 '안전한 리팩토링' 방법에 대해 질문드렸을 때, "테스트 코드를 치밀하게 짜야 한다" 는 조언을 받았다. 잘 짜인 테스트 케이스가 있다면, 리팩토링 전후로 모든 테스트를 통과시키는 것만으로도 '내부 구조는 바뀌었지만 기능은 동일하게 유지됨'을 보장할 수 있으며 어디다 어떻게 옮겨도 쓸 수 있는 멱등성을 보장할 수 있다는 말씀이었다.

TDD에 관심을 가지게 되다

이 조언을 계기로 이번 과제에서 테스트에 대해 관심을 가지기 시작했고, 자연스럽게 테스트를 주도로하는 방법론인 TDD에도 눈길이 가게 되었다.

TDD의 철학에는 공감하지만, 막상 내가 진행하는 프로젝트에 도입한다고 생각하니 몇 가지 현실적인 장벽이 느껴졌다.

  1. 개발 시간의 압박
    • 테스트를 먼저 설계하고 코드를 작성하는 방식은 각종 테스트 도구(Jest, Vitest, RTL, Storybook 등)에 아직 익숙하지 않은 나에게는 개발 시간이 훨씬 오래 걸릴 것 같다 (이제 테스트에 입문했기 때문)
  2. 테스트 범위의 모호함
    • “모든 컴포넌트에 대해 단위 테스트를 짜야 하는가?” 하는 의문이 든다.
    • 마감 기한이 있는 상황에서 모든 코드에 대한 테스트를 작성하려다, 정작 핵심 요구사항을 완료하지 못할 수도 있다는 두려움이 있다.

물론 TDD에 익숙해져서 효율성과 견고함을 모두 챙길 수 있다면 가장 이상적일 것이다.

그러나 현재로서는 그 균형점을 찾기 어려울 것 같다는 생각이다.

질문

위 고민을 바탕으로 다른 코드 리뷰어님께도 질문을 드렸다.

  • Q. 현업(프론트엔드)에서는 실제로 TDD를 어느 정도의 비중으로 사용하고 있나요?
    • A. TDD는 ‘버그 최소화’가 목적인 방법론이며, 모든 프로젝트에서 최고의 ROI(투자 대비 효율)를 보장하는 것은 아니다. 대부분의 웹/앱 어플리케이션 보다는 OS나 컴파일러처럼 테스트 비용이 매우 큰 분야에 더 적합할 수 있다.
  • Q. TDD를 적용하지 않더라도, '안전한 리팩토링'을 보장할 수 있는 최소한의 테스트는 어느 수준이라고 생각하시나요? (예: 핵심 비즈니스 로직, 복잡한 커스텀 훅 등)
    • A. 테스트 코드 자체가 안전한 리팩토링을 보장하는 것이 아니다. 오히려 유연한 코드 작성, 모듈 설계 및 분리, 클린 아키텍처와 같은 ‘좋은 설계’가 안전한 리팩토링의 더 근본적인 기반이 된다고 생각한다.
  • Q. 개발 속도와 테스트 커버리지 사이에서 현실적으로 균형을 잡는 노하우가 있다면 조언 부탁드립니다.
    • A. 80%의 커버리지도 매우 높은 수치이다. 모든 것을 테스트하려 하기보다, 장애 발생 시 리스크를 기준으로 우선순위를 정하는 게 현명하다. (e.g. 결제, 정산, 계정 시스템 등 장애 발생 시 큰 법적 문제를 일으키거나 핵심적인 기능)
  • Q. 저와 같은 초보 개발자가 TDD를 학습하려 할 때, 처음부터 완벽하게 적용하려 하기보다 어떤 부분에 초점을 맞춰 점진적으로 연습하는 것이 좋을까요?
    • A. TDD를 ‘완벽한 테스트 코드 작성’으로 접근하기보다, ’개발과 테스트 사이의 균형’, ‘다양한 관점으로 접근하기’ 등 방법론 자체의 철학을 이해하고 여러 코드에 적용해보는 연습이 중요하다.

핵심 인사이트

TDD와 테스트에 대해 가질 수 있는 환상을 걷어내고 다음과 같은 인사이트를 얻을 수 있음을 Gemini와 함께 정리해봤다.

TDD를 해야 한다는 당위성에서 벗어나, 어떻게 하면 더 안전하고 효율적으로 개발할 수 있는가에 대한 실무적인 해답이다.

1. 테스트 코드는 좋은 설계의 ‘결과’이지 ‘원인’이 아니다.

“테스트 코드가 있으니 코드를 마음대로 리팩토링해도 안전하다”는 옳지 않을 수 있다.

  • 잘못된 접근: 엉망으로 설계된 코드를 테스트 코드로 덮으려고 노력한다. (리팩토링 시 테스트 코드까지 전부 수정해야 하는 지옥이 펼쳐짐)
  • 올바른 접근: 코드를 유연하고, 모듈화하고, 단일 책임을 갖도록(클린 아키텍처) 잘 설계한다.
    • 그 결과, 테스트하기 쉬운 코드가 되며, 리팩토링 시에도 변경 범위가 명확해져 테스트가 자신의 역할을 제대로 수행한다.
  • 결론: ‘안전한 리팩토링’을 원한다면, TDD 이전에 **좋은 설계(모듈화, 의존성 분리 등)**에 대한 고민을 먼저 하자.

2. 리스크를 기반으로 테스트하자.

“모든 컴포넌트에 대해 테스트를 짜야 하는가?”라는 질문과 관련해, 개발 속도와 커버리지 사이의 균형점을 ‘리스크’에서 찾는다는 해답이다.

  • 테스트 1순위 (High-Risk): 돈, 법적 문제, 사용자 계정 등 (e.g. 결제 로직, 개인정보 처리, 로그인/인증)
    • 이런 부분은 TDD를 적용해볼 만한 가치가 충분
  • 테스트 2순위 (Medium-Risk): 복잡한 비즈니스 로직, 재사용되는 핵심 로직 (e.g. 복잡한 커스텀 훅, 상태 관리 로직, 핵심 유틸 함수)
  • 테스트 3순위 (Low-Risk): 단순 UI 컴포넌트, 정적 페이지 (e.g. 단순 Button 컴포넌트, About 페이지)
    • 이런 부분은 Vitest/RTL보다 Storybook을 통한 시각적 회귀 테스트가 더 효율적일 수 있다.
  • 결론: 높은 테스트 커버리지 수치에 매몰되지 말고, 우리 서비스에서 가장 치명적인 오류가 날 수 있는 코드를 찾아내는 감각을 기르자.

3. TDD는 ‘목표’가 아니라 ‘도구’이다.

TDD를 맹목적으로 따르는 것이 아니라, 그 철학을 이해하고 ‘적용할 부분’과 ‘하지 않을 부분’을 스스로 판단할 수 있어야 한다.

  • TDD는 ‘개발 방법론’이다. 즉, ‘버그 없는 코드’라는 목표를 달성하기 위한 여러 도구(클린 코드, 좋은 설계, 코드 리뷰, 테스트) 중 하나일 뿐이다.
  • 결론: TDD를 완벽하게 수행하는 것에 압박을 느낄 필요가 없다. 그 대신 ‘테스트가 왜 필요한가?’라는 본질을 생각하며, ‘다양한 관점(정상 케이스, 예외 케이스 등)’으로 코드를 바라보는 훈련을 하는 것이 더 중요하다.

4. 안전한 리팩토링을 위한 순서

테스트 코드는 리팩토링을 막는 족쇄가 되어서는 안 되며, ‘좋은 설계’를 기반으로 리팩토링을 장려하는 **’안전망’**이 되어야 한다.

  • 우선 순위
    1. ‘좋은 설계’를 고민한다.
      • 어떻게하면 로직과 뷰를 분리할까?
      • 어떻게 하면 의존성을 낮추고 모듈화할까?
      • 이것이 ‘클린 아키텍처’의 시작
    2. 그 다음 ‘좋은 테스트’를 작성
      • ‘좋은 설계’ 덕분에 테스트하기 쉬워진 코드를 대상으로, 리스크가 높은 로직부터 테스트를 추가

앞으로의 테스트 학습에 대해

조언 받은 내용을 바탕으로 Gemini에게 점진적인 학습 계획을 추천받아 봤다.

나는 프론트엔드 개발을 주 분야로 삼았으므로 프론트엔드 테스트를 기준으로 추천받았다.

1. ‘왜’ 테스트하는가? (리스크 분석 훈련)

  • 무작정 테스트 코드를 짜기 전에, 테스트 피라미드 개념을 다시 한 번 확립한다.

  • 기능을 구현할 때마다 스스로 질문하는 습관을 들인다.
    • “이 코드가 실패하면 사용자에게 어떤 임팩트가 갈까?”
    • “이것은 단순 UI 문제일까, 아니면 데이터 정합성 문제일까?”
    • “이 로직은 단위 테스트(Vitest)가 적합할까, 컴포넌트 통합 테스트(RTL)가 적합할까, 아니면 시각적 테스트(Storybook)가 적합할까?”

2. ‘좋은 설계’가 선행되어야 한다

  • ‘유연한 코드 작성’과 ‘모듈 설계’에 집중한다.
  • 연습 방법
    • 관심사 분리: 비즈니스 로직을 UI 컴포넌트에서 분리해 커스텀 훅으로 빼내는 연습
      • UI(RTL)와 로직(Vitest)을 따로 테스트하기 매우 용이해짐
    • 순수 함수: 가능하면 순수 함수로 로직을 작성
      • 순수 함수는 테스트하기 가장 쉬운 대상

3. TDD는 ‘작은 성공’부터 경험하기

  • 처음부터 React 컴포넌트에 TDD를 적용하려 하면 비동기, 렌더링, 상태 변경 등 복잡도에 압도당하기 쉽다.
  • TDD 연습 순서
    1. 순수 유틸 함수: Vitest를 사용해 Red-Green-Refactor 사이클을 직접 경험해 본다.
      • Red-Green-Refactor 사이클: TDD 방법론의 핵심으로, **실패하는 테스트(Red)**를 먼저 작성하고, **최소한의 코드로 테스트를 통과(Green)**시킨 후, **코드를 개선(Refactor)**하는 세 단계를 반복
    2. 복잡한 커스텀 훅: 로직이 분리된 커스텀 훅을 TDD로 작성한다.
    3. React 컴포넌트: 마지막으로 RTL을 사용해 컴포넌트의 TDD를 시도해 본다.

4. 완벽함보다 ‘균형’과 ‘지속성’ 추구하기

  • ‘완벽한 테스트 코드’를 목표로 하지 않는다.
  • 처음에는 ‘핵심 기능’ 중 하나만이라도 테스트 코드를 작성해 보는 것을 목표로 삼기
  • TDD를 못하더라도, 기능 구현 직후에라도 테스트 코드를 작성하며 ‘테스트와 개발의 연속성’을 유지하는 연습 해보기