개요

프론트엔드 테스트는 단위 테스트, 통합 테스트, E2E 테스트의 조합으로 구성됩니다. 2025년 현재 가장 효과적인 테스트 스택은 Vitest(단위/통합), Testing Library(컴포넌트), Playwright(E2E)의 조합입니다. 각 도구의 역할과 효과적인 테스트 전략을 체계적으로 정리합니다.

핵심 개념

1. Vitest: Vite 기반의 테스트 러너로, Jest와 호환되는 API를 제공하면서도 ESM 네이티브 지원과 빠른 실행 속도가 장점입니다. HMR 기반의 워치 모드, 인라인 워커, 브라우저 모드 등을 지원합니다.

2. Testing Library: 사용자 관점에서 컴포넌트를 테스트하는 라이브러리입니다. 구현 세부사항이 아닌 사용자가 보고 상호작용하는 요소를 기준으로 테스트합니다. React, Vue, Svelte 등 다양한 프레임워크를 지원합니다.

3. Playwright: Microsoft가 개발한 E2E 테스트 프레임워크로, Chromium, Firefox, WebKit을 단일 API로 테스트할 수 있습니다. 자동 대기, 네트워크 인터셉트, 트레이싱 등 강력한 기능을 제공합니다.

4. 테스트 피라미드: 많은 단위 테스트, 적절한 통합 테스트, 소수의 E2E 테스트로 구성하되, 프론트엔드에서는 통합 테스트의 비중을 높이는 것이 효과적입니다.

실전 예제

Vitest + Testing Library로 컴포넌트 테스트를 작성합니다.

// __tests__/TodoList.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TodoList } from '../components/TodoList';

describe('TodoList', () => {
  it('새 할일을 추가하고 완료 처리할 수 있다', async () => {
    const user = userEvent.setup();
    render(<TodoList />);

    // 할일 추가
    const input = screen.getByPlaceholderText('할일을 입력하세요');
    await user.type(input, '테스트 작성하기');
    await user.click(screen.getByRole('button', { name: '추가' }));

    // 추가 확인
    expect(screen.getByText('테스트 작성하기')).toBeInTheDocument();

    // 완료 처리
    const todoItem = screen.getByText('테스트 작성하기')
      .closest('li')!;
    const checkbox = within(todoItem)
      .getByRole('checkbox');
    await user.click(checkbox);

    // 완료 상태 확인
    expect(checkbox).toBeChecked();
    expect(todoItem).toHaveClass('completed');
  });

  it('API에서 할일 목록을 로드한다', async () => {
    // MSW 또는 vi.mock으로 API 모킹
    vi.spyOn(global, 'fetch').mockResolvedValue({
      ok: true,
      json: () => Promise.resolve([
        { id: 1, text: '기존 할일', completed: false },
      ]),
    } as Response);

    render(<TodoList />);

    // 비동기 로딩 대기
    expect(await screen.findByText('기존 할일'))
      .toBeInTheDocument();
  });
});

Playwright E2E 테스트 예시입니다.

// e2e/blog.spec.ts
import { test, expect } from '@playwright/test';

test.describe('블로그', () => {
  test('게시글을 작성하고 목록에서 확인한다', async ({ page }) => {
    // 로그인
    await page.goto('/login');
    await page.getByLabel('이메일').fill('admin@example.com');
    await page.getByLabel('비밀번호').fill('password123');
    await page.getByRole('button', { name: '로그인' }).click();

    // 글쓰기 페이지 이동
    await page.goto('/blog/write');
    await page.getByLabel('제목').fill('E2E 테스트 게시글');
    await page.locator('.ql-editor').fill('테스트 본문 내용');

    // 저장
    await page.getByRole('button', { name: '저장' }).click();

    // 목록에서 확인
    await page.goto('/blog');
    await expect(
      page.getByText('E2E 테스트 게시글')
    ).toBeVisible();
  });

  test('카테고리 필터가 정상 동작한다', async ({ page }) => {
    await page.goto('/blog');

    // 카테고리 클릭
    await page.getByRole('link', { name: 'JavaScript' }).click();

    // URL 확인
    await expect(page).toHaveURL(/category=javascript/);

    // 게시글이 표시되는지 확인
    const posts = page.locator('.post-item');
    await expect(posts.first()).toBeVisible();
  });
});

활용 팁

  • 테스트 우선순위: 사용자 핵심 경로(로그인, 결제, 주요 기능)를 E2E로 먼저 커버하고, 비즈니스 로직은 단위 테스트로, 컴포넌트 상호작용은 통합 테스트로 커버하세요.
  • MSW(Mock Service Worker)를 활용하면 네트워크 요청을 서비스 워커 레벨에서 모킹하여 테스트와 개발 환경 모두에서 일관된 모킹이 가능합니다.
  • Vitest의 인라인 스냅샷: expect(value).toMatchInlineSnapshot()을 사용하면 별도 파일 없이 테스트 코드 안에서 스냅샷을 관리할 수 있습니다.
  • Playwright의 trace viewer: 실패한 테스트의 재현이 어려울 때 trace.zip을 열면 각 단계의 DOM 스냅샷, 네트워크 요청, 콘솔 로그를 시각적으로 확인할 수 있습니다.
  • CI 통합: Vitest는 빠르므로 모든 PR에서 실행하고, Playwright E2E는 주요 브랜치 병합 시에만 실행하여 파이프라인을 최적화하세요.
  • 커버리지 100%를 목표로 하지 마세요. 의미 있는 테스트에 집중하는 것이 더 효과적입니다.

마무리

Vitest, Testing Library, Playwright의 조합은 2025년 프론트엔드 테스트의 황금 표준입니다. 각 도구가 맡은 역할이 명확하고, 함께 사용할 때 시너지가 극대화됩니다. 테스트는 코드의 품질을 보장하는 동시에 리팩토링의 안전망이 됩니다. 프로젝트 초기부터 테스트를 도입하여 장기적인 유지보수 비용을 절감하세요.