사용자 플로우 중심의 E2E 테스트 개선기

Overview

AllClass 프로젝트에 Cypress E2E 테스트를 도입하면서 겪은 시행착오를 정리했다. 처음에는 UI 컴포넌트 렌더링 여부를 체크하는 테스트를 작성했지만, 이것이 실제 사용자 경험과는 거리가 멀다는 것을 깨닫게 되었다. API 모킹에서 실제 API 사용으로, 기존 테스트 없던 환경에서 4개의 핵심 플로우 중심 E2E 테스트를 새로 구축한 과정과 그 결과를 다룬다.

왜 E2E인가

AllClass는 창업 프로젝트로, 앞으로의 유지보수와 기능 추가를 고려했을 때 테스트 자동화가 필요했다. 핵심 기능이 명확한 만큼 사용자가 로그인하고, 강의를 찾고, 리뷰를 작성하는 것 몇 개의 핵심 시나리오만 자동화해도 서비스 안정성을 크게 높일 수 있다고 판단했다.

초기 접근의 한계

프로젝트 막바지에 Cypress를 사용해 E2E(End-to-End) 테스트를 도입했다. E2E 테스트는 사용자 관점에서 실제 서비스 흐름을 따라가며 기능이 잘 작동하는지를 검증하는 도구다.

E2E는 UI 구성요소가 아니라, 서비스의 시나리오를 테스트해야 한다.
Cypress 공식 문서 - Best Practices

하지만 당시 내가 작성한 코드는 E2E보다는 유닛 테스트에 가까운 관점으로 작성되어 있었다.

문제 1: 과도한 UI 검증

예를 들어, 다음은 사용자가 강의에 리뷰를 작성하는 흐름을 테스트한 초기 코드다:

// ❌ 문제의 코드 - 리뷰 작성에 필요없는 UI까지 체크
it('사용자는 리뷰를 작성할 수 있다', () => {
  // 리뷰 작성에는 필요없는 카드 요소들까지 체크
  cy.get('[data-cy="lecture-title"]').should('be.visible');
  cy.get('[data-cy="professor-name"]').should('be.visible');
  cy.get('[data-cy="lecture-description"]').should('be.visible');
  // 리뷰 작성 버튼만 있으면 되는데...
  
  // 실제 리뷰 작성 행동
  cy.get('[data-cy="write-review-button"]').click();
  cy.get('[data-cy="review-content"]').type('리뷰 내용');
  cy.get('[data-cy="submit"]').click();
});

이 코드의 문제는 리뷰 작성을 위해서는 "리뷰 작성 버튼"만 확인하면 되는데, 굳이 강의 타이틀, 교수명, 강의설명 같은 카드의 다른 요소들까지 모두 렌더링 체크를 하고 있다는 것이다. 테스트 목적과 관련 없는 UI 요소들을 검증하느라 시간을 낭비하고 있다.

이러한 접근은 테스트가 깨졌을 때 사용자가 실제로 문제가 있는지보다, 렌더링 로직에 오류가 있는 것처럼 착각하게 만든다. 즉, E2E 테스트의 본질을 떨어뜨린다.

문제 2: Mock vs 실제 API

처음에는 테스트의 안정성을 위해 API를 모킹하려고 했다.

// ❌ 처음 시도: API 모킹으로 "안전한" 테스트
cy.intercept('GET', '/api/lectures/12345', { fixture: 'mock-data.json' })

그러다 카카오엔터프라이즈 블로그에서 이런 글을 읽게 됐다:

"E2E 테스트가 제대로 된 의미를 갖기 위해서는 모든 과정이 실제 환경과 동일해야 한다고 생각합니다. Mock 데이터를 사용하면 개발자가 원하는 시나리오를 쉽게 테스트할 수 있지만, 이는 다시 말하면 Mock 데이터로 만들어진 시나리오는 항상 개발자가 원하는 대로 실행되는 시나리오라는 말이 됩니다."

실제 사용자는 Mock API를 쓰지 않는다. API가 느리거나 에러가 나는 것도 사용자가 경험할 수 있는 현실이고, 그런 상황에서도 서비스가 제대로 동작하는지 확인하는 게 진짜 E2E 테스트의 목적이 아닐까. 라는 생각에 나도 실제 API를 사용하기로 했다.

테스트 리팩토링: 사용자 흐름으로 돌아가기

그래서 나는 다시 원점으로 돌아가, 핵심 사용자 플로우를 정의하는 것부터 시작했다.

1. 핵심 사용자 플로우 정의

AllClass는 대학생들이 강의 후기를 공유하는 서비스다. 그렇다면 사용자는 실제로 뭘 하고 싶어할까?

이걸 바탕으로 전략적 Tier 분류를 통해 중요도를 나눴다:

🚨 Tier 1 (서비스 핵심 가치): 망가지면 서비스 자체가 무의미해지는 필수 기능들

  • 로그인 → 강의 검색 → 리뷰 작성 → 마이페이지 리뷰 관리
  • 이 플로우가 하나라도 실패하면 AllClass의 본질적 가치가 무너짐

🎯 Tier 2 (사용자 경험 향상): Tier 1이 정상이면 동작 가능한 부가 기능들

  • 리뷰 정렬/필터링, 좋아요, 대학교 전환, 상세 네비게이션
  • 중요하지만 Tier 1 테스트 과정에서 자연스럽게 함께 검증되는 기능들

테스트 전략 수립:

  • Tier 1 우선 구현: 4개 핵심 시나리오를 우선적으로 테스트 작성
  • Tier 2 선택적 포함: Tier 1 테스트에서 함께 검증되는 기능들은 추가 테스트 생략
  • CI 자동화: GitHub Actions로 지속적 품질 관리

2. 실제 사용자 경험에만 집중하기

이제는 모든 UI 요소를 테스트하지 않고, 사용자가 실제로 경험하는 흐름에만 집중했다.

Before: UI 요소 11개 체크 → After: 핵심 사용자 플로우

// ✅ 개선된 코드 - API 기반 동적 테스트
it('사용자는 수강한 강의에 리뷰를 작성할 수 있다', () => {
  cy.uiLogin();
  
  // API에서 실제 수강 강의 가져오기
  cy.getMyLectures().then((response) => {
    const enrolledLecture = response.body.data[0];
    const lectureName = enrolledLecture.lectureName;
    
    cy.visitUniversity();
    cy.clickLectureByName(lectureName);
    
    cy.byTest('write-review').click();
    cy.byTest('review-title-input').type(`${lectureName} 수업 후기`);
    cy.byTest('review-content-input').type('좋은 강의였어요!');
    cy.byTest('star-5').click();
    cy.byTest('write-submit').click();
  });
});

가장 큰 변화는 Mock API 제거였다. cy.intercept()로 가짜 데이터를 만드는 대신, 실제 API(/class/me)를 호출해서 사용자가 수강한 강의를 동적으로 가져와 테스트한다. 그리고 반복되는 코드는 cy.getMyLectures(), cy.visitUniversity() 같은 커스텀 커맨드로 추상화했다.

CI: GitHub Actions 연동

테스트를 수동으로 돌릴 수는 없으니까, CI/CD 파이프라인에 연결했다.

# 🚀 CI에 E2E 테스트 단계 추가
e2e:
  needs: lint_build
  runs-on: ubuntu-latest
  steps:
    - name: Cypress run
      uses: cypress-io/github-action@v6
      with:
        build: yarn build
        start: yarn start
        wait-on: 'http://localhost:3000'
        browser: chrome

테스트 접근법의 변화

E2E 테스트 리팩토링을 통한 접근법 개선:

구분Before (잘못된 E2E)After (개선된 E2E)
테스트 관점UI 요소 11개 개별 검증핵심 사용자 플로우 중심
데이터 소스Mock API 의존실제 API 사용
실패 시 의미"버튼이 안 보여요""사용자가 리뷰 작성 불가"

최종 구축 결과:

  • 4개 핵심 플로우 완전 자동화
  • 전체 기능 77.3% 커버리지 달성
  • 리뷰 작성 후 캐시 revalidate 버그 발견 및 해결
  • GitHub Actions CI/CD 자동 실행

마무리

E2E 테스트를 도입하면서 가장 중요한 깨달음은 "무엇을 테스트할 것인가?" 였다.

테스트가 없던 상황에서 전략적 Tier 분류를 통해 4개의 핵심 테스트를 구축했다. 최소한의 테스트 시나리오로 서비스의 본질적 가치를 완전히 보장할 수 있었고, 사용자가 실제로 하는 핵심 여정인 로그인→강의 검색→리뷰 작성→마이페이지 관리가 안정적으로 동작함을 확인할 수 있었다.

"더 많은 E2E 테스트가 정답은 아니다. 테스트는 사용자 중심의 신뢰성 검증에 집중해야 한다."
Google Testing Blog - Just Say No to More End-to-End Tests