"테스트는 사치품이지 시간 부족할 때 빼는 거 아닌가?" — 한 번 production 에서 결제 로직 회귀 사고 나면 영원히 그 말이 안 나온다. 한 번 짜면 매 PR 마다 자동으로 회귀 검사. 사람이 매번 클릭할 필요 없음.
Next.js 진영의 표준은 3층 — Vitest(유닛), React Testing Library(컴포넌트), Playwright(E2E). 옛 Jest 자리는 빠르고 ESM 친화적인 Vitest 가 대체. 이번 편은 첫 세팅과 핵심 패턴.
1. 테스트 피라미드 — 비율이 핵심
| 층 | 도구 | 속도 | 비율 |
| 유닛 | Vitest | 매우 빠름 (ms) | 70% |
| 컴포넌트 | RTL + Vitest | 빠름 (100ms) | 25% |
| E2E | Playwright | 느림 (수초) | 5% |
비율이 거꾸로면(E2E 가 가장 많음) 아이스크림 콘 안티패턴 — CI 가 30분 걸리고 flaky 한 사고. 유닛이 많아야 빠르고 단단.
2. Vitest — 유닛 테스트 첫 세팅
$ npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
},
});
// vitest.setup.ts
import '@testing-library/jest-dom/vitest';
// lib/utils.test.ts
import { describe, it, expect } from 'vitest';
import { formatPrice } from './utils';
describe('formatPrice', () => {
it('원화 포맷 적용', () => {
expect(formatPrice(1234567)).toBe('₩1,234,567');
});
it('0 도 정상 처리', () => {
expect(formatPrice(0)).toBe('₩0');
});
});
npm run vitest — watch 모드로 파일 변경 시 자동 재실행. 유닛은 빠르니까 IDE 옆에 띄워두고 개발.
3. React Testing Library — 컴포넌트 테스트
핵심 철학 — 사용자가 보는 대로 테스트. 구현 디테일이 아니라 사용자 경험.
// Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';
import { Counter } from './Counter';
describe('Counter', () => {
it('버튼 클릭 시 카운트 증가', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole('button', { name: /증가/i });
expect(screen.getByText('0')).toBeInTheDocument();
await user.click(button);
expect(screen.getByText('1')).toBeInTheDocument();
});
});
핵심 원칙 — getByRole·getByLabelText 우선, getByTestId 는 최후. role/label 로 찾으면 접근성도 자동 검증되는 효과. data-testid="x" 박는 건 다른 방법 다 안 될 때만.
4. Playwright — E2E 테스트
진짜 브라우저(Chromium·Firefox·WebKit) 띄워 실제 사용자처럼 클릭·입력. 가장 느리지만 가장 진짜.
$ npm init playwright@latest
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test('로그인 후 대시보드 이동', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('이메일').fill('
[email protected]');
await page.getByLabel('비밀번호').fill('password123');
await page.getByRole('button', { name: '로그인' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: /환영합니다/ })).toBeVisible();
});
실행 — npx playwright test. 실패 시 스크린샷·비디오·트레이스까지 자동 저장. --ui 플래그 붙이면 인터랙티브 UI 로 단계별 디버깅.
5. CI 통합·실전 권장
GitHub Actions 표준 패턴.
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run vitest -- --run # 유닛·컴포넌트
- run: npx playwright install --with-deps
- run: npx playwright test # E2E
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
실패 시 Playwright HTML 리포트를 자동 업로드. PR 에서 다운받아 어디서 깨졌는지 확인.
실전 권장 — 처음부터 100% 커버리지 노리지 말 것 — 가장 중요한 비즈니스 로직 5가지·결제·인증 흐름 위주로. 시간 남으면 확장. 100% 커버리지에 갇혀 테스트 짜다 정작 기능 못 만드는 게 더 큰 사고.
요약 — 22편 좌표
여기까지 정리. 3층 — Vitest(유닛 70%) → RTL + Vitest(컴포넌트 25%) → Playwright(E2E 5%). RTL 은 getByRole·getByLabelText 우선, testid 는 최후. Playwright 는 실패 시 스크린샷·비디오·트레이스 자동 저장 — 디버깅 친화적. CI 에서 두 단계로 분리. 결제·인증 등 핵심 흐름부터 우선 커버. 다음 편에서 Pages → App Router 마이그레이션.
다음 편 예고 — Pages → App Router
옛 코드 마이그레이션 단계별 가이드. 23편.