항해플러스 7~8주차 - 프론트엔드 테스트코드

프론트엔드 테스트코드 알아보기2025-08-29
#테스트코드#항해플러스

7주차 단위, 통합 테스트 코드 작성해보기

7주차 과제는 단일페이지의 캘린더 앱에 대해 단위, 통합 테스트를 작성하는 과제였다.

easy, medium, hard 3가지 난이도로 나누어 선택할 수 있었으며 당근 BP를 받기 위해 hard 난이도에 도전했는데 MSW의 mocking API에 대한 설정까지 해야 했다. mocking 환경도 이전 회사에서는 잘 사용하지 않았어서 조금(많이) 헤맸다^^..

단위 테스트

describe('getDaysInMonth', () => {
  it('1월은 31일 수를 반환한다', () => {});
 
  it('4월은 30일 일수를 반환한다', () => {});
 
  it('윤년의 2월에 대해 29일을 반환한다', () => {});
 
  it('평년의 2월에 대해 28일을 반환한다', () => {});
 
  it('유효하지 않은 월에 대해 적절히 처리한다', () => {});
});

테스트는 처음이라 막막한 과제일 수 있었지만 기본적인 테스트 케이스들이 적혀 있는 상태에서 불필요하거나 명확하지 않은 테스트는 제거, 필요한 테스트는 추가하는 방식으로 진행할 수 있었다.

잠깐 단위 테스트에 대해 살펴보자면,

단위 테스트는 보통 작은 단위의 함수, 컴포넌트 등을 테스트한다. 이때 다른 컴포넌트나 함수에 의존하지 않는 순수 함수를 테스트하는 것이 좋다. 순수 함수는 입력이 같으면 항상 같은 결과를 반환하는 함수를 말한다.

describe('Input', () => {
  it('placeholder가 정확히 설정된다', () => {});
 
  it('focus가 정확히 설정된다', () => {});
});

예시로 하나를 들어보자면, 위와 같이 공통 input 컴포넌트의 placeholder, focus, className 등을 테스트하는 것이 단위 테스트에 적합하다. 다른 훅이나 컴포넌트에 의존하지 않기 때문이다.

통합 테스트

통합 테스트는 여러 컴포넌트나 모듈이 함께 작동하는 방식을 테스트한다. 이때 일부 혹은 전체 시나리오가 될 수 있다. 함수와 컴포넌트 연동 테스트, 서버 연동 테스트(mock), 유저 인터랙션 테스트 정도로 나눌 수 있다. mocking을 이용하는 이유는 네트워크에 영향을 받지 않아 테스트가 안정적이고, 트래픽을 증가시키지 않으며, 필요한 응답을 자유롭게 구성할 수 있기 때문이다.

describe('일정 CRUD 및 기본 기능', () => {
  it('입력한 새로운 일정 정보에 맞춰 모든 필드가 이벤트 리스트에 정확히 저장된다.', async () => {});
 
  it('기존 일정의 세부 정보를 수정하고 변경사항이 정확히 반영된다', async () => {});
 
  it('일정을 삭제하고 더 이상 조회되지 않는지 확인한다', async () => {});
});

과제에서는 아래와 같이 유저가 일정을 생성하고, 수정하고, 삭제하는 시나리오 등을 테스트하도록 하였다. 위에서부터 쭉 설명 부분에 정확히 같은 텍스트가 있는데 이런 방식은 좋지 않다. 유저의 시나리오를 테스트하는 것이기 때문에 명확한 텍스트를 사용하는 것이 좋다.

프론트엔드의 테스트 코드는 보통 **"기능"**을 검증하는 경우가 많다. 요구사항에 맞게 기능이 잘 작동하는지 테스트하는 것이다.
단위 테스트의 경우엔 앱이 정상적으로 돌아가는지 테스트하는 비기능 테스트인 경우가 많으며, 통합 테스트는 이런 모듈들을 함께 테스트하여 실제 요구사항에 비슷하게 테스트할 수 있다.

하지만 통합 테스트는 mocking을 사용하기에 실제 서버와의 통신과는 차이가 있을 수 있다. 따라서 E2E 테스트도 함께 고려해야 완전한 테스트 환경을 구성할 수 있다.

8주차 TDD 적용해보기, 테스트 전략 수립 후 보강하기

8주차 과제는 반복 일정이라는 기능 추가에 대해 TDD를 적용해보고 테스트 전략을 수립하고 보강하는 과제였다.

TDD

TDD는 Test Driven Development의 약자로 테스트 주도 개발이라고 한다. 테스트 주도 개발은 테스트 코드를 먼저 작성하고 이를 통과하는 코드를 작성하는 방식이다.

보통 Red → Green → Refactor의 사이클을 반복하며 코드를 작성해나간다.

function add(a, b) {}
 
describe('Calculator', () => {
  test('두 숫자를 더한 결과를 반환한다', () => {
    expect(add(2, 3)).toBe(5);
  });
});

Red : 먼저 실패하는 테스트 코드를 작성한다.

function add(a, b) {
  return 5; // 하드코딩으로 테스트만 통과시킴
}

Green : 테스트를 통과하는 코드를 작성한다.

function add(a, b) {
  return a + b;
}

Refactor : 코드를 리팩토링한다.

TDD는 코드 작성과 동시에 테스트를 하기에 버그를 미리 발견할 수 있고, 테스트하기 쉬운 코드를 만들다 보면 자연스럽게 결합도가 낮고 응집도가 높은 코드가 나오게 되며, 이는 나중에 리팩토링할 때 큰 도움이 된다.

물론 단점도 있다. 초기 학습 비용도 들어가고 개발 시간도 증가하며 필요 이상의 오버엔지니어링을 하게 될 수 있다. 항상 나오는 말이지만 프로젝트의 특성을 잘 파악하여 이런 전략을 수립하는 것이 옳다고 본다.

테스트 전략 수립

post image

팀원들과 캘린더 앱 프로젝트에 대해 각각 생각하는 테스트 전략을 공유하고 통합하여 수립했다. 대부분 E2E를 도입하냐 마냐였는데 과제 특성상 E2E 테스트를 경험 삼아 해보는 것을 권장했기에 모두 E2E를 도입하기로 했다.

테스트 전략에 대한 이론적인 부분에서는 대표적으로 테스트 피라미드와 테스트 트로피 형태가 있는데 짧게 설명해보자면

🔺 테스트 피라미드 (Test Pyramid)

post image

상위로 갈수록 비용이 크다 - 실행 시간, 유지보수 비용, 환경 의존성 증가
하위로 갈수록 많은 테스트를 작성해야 한다 - 빠르고 안정적인 피드백

구성 비율

  • 70% 단위 테스트 (Unit Tests) - 개별 함수/컴포넌트 테스트
  • 20% 통합 테스트 (Integration Tests) - 모듈 간 상호작용 테스트
  • 10% E2E 테스트 (End-to-End Tests) - 전체 시스템 테스트

장점

  • 빠른 피드백 루프
  • 낮은 유지보수 비용
  • 높은 코드 커버리지

🏆 테스트 트로피 (Test Trophy)

post image

통합 테스트에 가장 많은 투자를 해야 한다는 관점
실제 사용자 경험에 더 가까운 테스트에 집중

구성 비율

  • Static Analysis (정적 분석) - ESLint, TypeScript, Prettier 등
  • Unit Tests (단위 테스트) - 순수 함수, 유틸리티 테스트
  • Integration Tests (통합 테스트) - 가장 큰 비중 - 컴포넌트와 서비스 통합
  • E2E Tests (E2E 테스트) - 핵심 사용자 여정만 선별적으로

Kent C. Dodds가 제안한 이유

  • 통합 테스트가 가장 높은 ROI(투자 대비 효과)를 제공
  • 실제 사용자 경험에 가장 가까운 테스트
  • 단위 테스트만으로는 놓치는 실제 문제들이 많음

이런 테스트 전략에 대한 선택은 프로젝트에 맞는 테스트 전략을 선택해 프로젝트의 신뢰성을 높이는 것에 집중해야 한다.

Cypress로 사용자 시나리오 기반 E2E 테스트 작성하기

E2E 테스트는 실제 사용자가 앱을 사용하는 방식을 테스트한다. 이때 실제 사용자처럼 테스트 환경을 구성하고 테스트를 진행한다. 물론 mocking 환경과 다르게 브라우저에서 진행하기에 속도가 느리고 복잡하다. 대표적으로 사용하는 툴로는 Playwright, Cypress 등이 있다. 나는 Cypress를 선택했는데 이유는 jQuery 스타일의 직관적인 API와 학습 비용이 낮다고 생각해서 선택했다.

describe('📅 캘린더 앱 - 사용자 시나리오 기반 E2E', () => {
  describe('🌅 아침 출근 전 - 하루 일정 확인 및 계획', () => {
    // 오늘 일정 추가 및 확인
  });
 
  describe('💼 업무 중 - 일정 변경 및 삭제', () => {
    // 일정 수정 및 삭제
  });
});

사실 작성해놓고 보니 통합 테스트와 비슷한 느낌이 들지만 실제 브라우저 환경에서 유저의 시나리오를 테스트하는 것이기에 통합 테스트보다 더 현실적인 테스트를 작성할 수 있다고 생각했다.
그리고 확실히 통합 테스트보다는 느린 것이 느껴졌기에 속도가 중요한 프로젝트에는 적합하지 않다고 생각한다.

마무리

실무에서는 테스트 코드를 경험해본 적이 없었고 항해플러스를 진행하기 전에는 왜 프론트엔드에 테스트 코드가 필요한지 의문이었는데 1주차부터 테스트 코드를 접하며 느낀 것이 있다.

특히 테스트 코드의 필요성을 가장 크게 느꼈던 순간은 클린 코드 챕터였다. 엄청난 더티 코드를 점진적으로 개선해나가며 기존 기능이 누락되지 않았는지 체크하는 것이 너무나 힘들었기 때문이다. 또한 테스트 코드를 작성함으로써 결합도가 낮아지고 응집도가 높아진다는 것도 큰 장점으로 다가왔다.

현재 이직을 준비 중인데 다음 회사에서 기회가 된다면 동료분들을 설득하여 주도적으로 테스트 코드를 적용해보고 싶다는 생각이 들었다.