Next.js 교재 · 22편 · 테스트

Next.js 테스트 — Vitest·RTL·Playwright

3층 피라미드 — 유닛 많이, 컴포넌트 중간, E2E 적게. 그게 표준.

테스트 피라미드 3층 컨셉 일러스트

"테스트는 사치품이지 시간 부족할 때 빼는 거 아닌가?" — 한 번 production 에서 결제 로직 회귀 사고 나면 영원히 그 말이 안 나온다. 한 번 짜면 매 PR 마다 자동으로 회귀 검사. 사람이 매번 클릭할 필요 없음.

Next.js 진영의 표준은 3층 — Vitest(유닛), React Testing Library(컴포넌트), Playwright(E2E). 옛 Jest 자리는 빠르고 ESM 친화적인 Vitest 가 대체. 이번 편은 첫 세팅과 핵심 패턴.

1. 테스트 피라미드 — 비율이 핵심

도구속도비율
유닛Vitest매우 빠름 (ms)70%
컴포넌트RTL + Vitest빠름 (100ms)25%
E2EPlaywright느림 (수초)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편.

© 2026 주나이테크(주) @JUNAITECH