목차
개요
공통 컴포넌트의 테스트 방침을 검토하면서, Vitest를 사용한 VRT(Visual Regression Testing)와 접근성 테스트 환경을 구축했습니다.
이 글은 공통 컴포넌트의 테스트 방침 수립 과정에서 최종 선택된 Vitest 기반 테스트 환경의 구축 과정을 정리한 것입니다. 전체 테스트 방침과 도구 선정 과정은 [React] 모노레포 환경에서의 공통 컴포넌트 테스트 방침을 참고해주세요.
왜 Vitest인가
테스트 방침 검토에서 Jest, Storybook, Vitest 세 가지를 비교한 결과, Vitest를 선택했습니다.
- 네이티브 VRT 지원:
toMatchScreenshot를 기본 제공하여 별도 라이브러리 없이 스크린샷 비교가 가능합니다. - 프로덕트 코드와 동일한 기술 스택: Vite 기반이므로 프로덕트 빌드 환경과 동일한 모듈 해석, CSS 처리를 사용합니다.
- 실제 브라우저에서 테스트: Browser Mode를 통해 Playwright 브라우저에서 컴포넌트를 렌더링하므로, jsdom 기반 테스트보다 실제 환경에 가깝습니다.
다른 도구와의 비교가 궁금하다면, [React] Jest + Puppeteer로 VRT 환경 구축과 [React] Storybook Test Runner로 VRT + 접근성 테스트 구축도 참고해주세요.
전체 구조
Vitest 기반 VRT의 동작 흐름은 다음과 같습니다.
- Docker 컨테이너에서 일관된 환경을 제공 (로컬 실행 시)
- Vitest가 Playwright WebKit 브라우저를 실행
- 각 테스트에서 컴포넌트를 렌더링하고 스크린샷을 캡처
- 기존 베이스라인 이미지와 비교하여 차이가 있으면 테스트 실패
- 접근성 테스트는
axe-core로 WCAG 위반 사항을 검사
Docker Container (일관된 폰트 + 브라우저 환경)
→ Vitest (Browser Mode)
→ Playwright WebKit
→ 컴포넌트 렌더링 + 스크린샷 캡처
→ toMatchScreenshot으로 베이스라인 비교
→ axe-core로 접근성 검사
환경 설정
Vitest Browser Mode와 Playwright를 사용하여 VRT + 접근성 테스트 환경을 구축하기 위해서 다음과 같은 설정이 필요합니다.
패키지 설치
Vitest Browser Mode와 VRT/접근성 테스트에 필요한 패키지를 설치합니다.
yarn add -D vitest @vitest/browser @vitest/browser-playwright @vitest/ui @vitest/coverage-istanbul axe-core
각 패키지의 역할은 다음과 같습니다.
| 패키지 | 역할 |
|---|---|
vitest | 테스트 러너 (VRT의 toMatchScreenshot 기본 제공) |
@vitest/browser | Vitest Browser Mode 플러그인 |
@vitest/browser-playwright | Playwright 브라우저 프로바이더 |
@vitest/ui | 테스트 결과를 브라우저 UI로 확인 |
@vitest/coverage-istanbul | 테스트 커버리지 리포트 |
axe-core | 접근성(a11y) 검사 엔진 |
Vitest 설정
다음은 Vitest의 핵심 설정 파일입니다. Browser Mode를 활성화하고, 스크린샷 저장 경로를 설정하여 VRT 테스트를 구성합니다.
// vitest.config.ts
import react from '@vitejs/plugin-react';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react()],
optimizeDeps: {
// axe-core를 사전 번들링하여 테스트 실행 속도 향상
include: ['axe-core'],
},
test: {
browser: {
enabled: true,
provider: playwright({
launchOptions: {
timeout: 60000,
},
}),
// WebKit을 사용하는 이유: CI(Linux)와 로컬(macOS) 모두에서
// 가장 일관된 렌더링 결과를 제공
instances: [{ browser: 'webkit' }],
viewport: { width: 1280, height: 720 },
headless: true,
screenshotFailures: false,
expect: {
toMatchScreenshot: {
// 스크린샷 파일을 테스트 파일과 같은 디렉토리의
// __screenshots__ 폴더에 저장
resolveScreenshotPath: ({ testFileDirectory, arg, ext }) => {
return `${testFileDirectory}/__screenshots__/${arg}${ext}`;
},
},
},
},
// *.vitest.tsx 파일만 테스트 대상으로 포함
include: ['**/*.vitest.tsx'],
setupFiles: ['./vitest-setup.ts'],
// CSS Modules 등 스타일 파일을 실제로 처리
css: true,
coverage: {
provider: 'istanbul',
reporter: ['text', 'html', 'lcov'],
reportsDirectory: './coverage',
},
},
css: {
modules: {
localsConvention: 'camelCase',
},
},
});
주요 설정 포인트는 다음과 같습니다.
browser.instances: [{ browser: 'webkit' }]: WebKit 브라우저를 사용합니다. Chromium과 달리 OS 간 렌더링 차이가 적어 CI와 로컬 환경에서 일관된 스크린샷을 얻을 수 있습니다.browser.expect.toMatchScreenshot.resolveScreenshotPath: 스크린샷 파일을 테스트 파일과 같은 디렉토리의__screenshots__폴더에 저장합니다. 컴포넌트별로 스크린샷을 관리하기 쉬워집니다.include: ['**/_.vitest.tsx']: 기존 Jest 단위 테스트(_.test.tsx)와 Vitest VRT 테스트(\*.vitest.tsx)를 분리합니다. 두 테스트 러너가 서로의 파일을 실행하지 않도록 합니다.css: true: CSS 파일을 실제로 처리하여 스타일이 적용된 상태에서 스크린샷을 캡처합니다.
Setup 파일
테스트 실행 전 공통 설정을 담당하는 파일입니다.
// vitest-setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
// 공통 스타일(CSS 변수 등)을 import하여
// 프로덕트와 동일한 스타일 환경에서 테스트
import './src/utils/styles/common.scss';
// 각 테스트 후 DOM 정리
afterEach(() => {
cleanup();
});
@testing-library/jest-dom/vitest를 import하면 toBeInTheDocument() 등의 DOM matcher를 사용할 수 있습니다. 여기서 공통 스타일 파일을 import하는 이유는 CSS 변수 등이 정의되지 않으면 컴포넌트가 프로덕트와 다르게 렌더링될 수 있기 때문입니다.
.gitignore 설정
테스트 실행 중 생성되는 임시 파일은 Git에서 제외시킬 필요가 있습니다. 다만, VRT의 비교 기준이 되는 스크린샷(__screenshots__/)은 커밋 대상이 됩니다.
# Vitest 첨부 파일 (테스트 실패 시 생성되는 diff 이미지 등)
.vitest-attachments/
Docker로 일관된 렌더링 환경 구축
VRT는 픽셀 단위로 스크린샷을 비교하기 때문에, 렌더링 환경이 조금이라도 다르면 테스트가 실패합니다. 특히 다음 요인들이 스크린샷 차이를 유발합니다:
- 폰트: macOS와 Linux에서 기본 폰트가 다르고, 같은 폰트라도 렌더링 엔진에 따라 미세한 차이가 있습니다.
- OS별 텍스트 렌더링: 안티앨리어싱, 서브픽셀 렌더링 방식이 OS마다 다릅니다.
Docker를 사용하면 로컬 개발 환경(macOS 또는 Windows)과 CI 환경(Linux) 모두에서 동일한 폰트와 브라우저로 테스트를 실행할 수 있어, 환경 차이로 인한 거짓 실패(false positive)를 방지합니다.
Dockerfile
다음과 같이 Dockerfile을 작성하여 Vitest VRT 테스트를 위한 일관된 환경을 구축합니다.
# Dockerfile
FROM node:24.13.0-bookworm
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
# 타임존 설정 (스크린샷의 날짜/시간 표시를 통일)
ENV TZ=Asia/Tokyo
# yarn 사용을 위해 corepack 활성화
RUN corepack enable
# Playwright WebKit의 시스템 의존 패키지 설치
RUN npx playwright install-deps webkit
# 결정론적 렌더링을 위한 폰트 설치
RUN apt-get update && apt-get install -y --no-install-recommends \
fonts-noto-core \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# 루트 설정 파일
COPY package.json yarn.lock .yarnrc.yml .nvmrc ./
# yarn 플러그인
COPY .yarn/plugins/ .yarn/plugins/
# workspace 의존성만 복사
COPY packages/lib/components/package.json packages/lib/components/
COPY packages/config/typescript/ packages/config/typescript/
COPY packages/config/stylelint/package.json packages/config/stylelint/
COPY packages/config/eslint/package.json packages/config/eslint/
# 의존성 설치 (Docker 레이어 캐싱)
# --mode=skip-build: 라이프사이클 스크립트를 건너뜀 (husky install 등의 실패 방지)
RUN yarn install --mode=skip-build
# Playwright WebKit 브라우저 설치
RUN npx playwright install webkit
여기서 주요 설정 포인트는 다음과 같습니다.
fonts-noto-core,fonts-noto-cjk: Noto 폰트를 설치하여 모든 환경에서 동일한 글꼴로 렌더링합니다. CJK(한중일) 폰트도 포함하여 다국어 텍스트가 포함된 컴포넌트도 일관되게 테스트할 수 있습니다.yarn install --mode=skip-build:husky install등 빌드 스크립트를 건너뛰어 Docker 빌드 실패를 방지합니다.- 레이어 캐싱:
package.json과yarn.lock을 먼저 복사하고 설치한 뒤 소스 코드를 마운트하여, 의존성이 변경되지 않으면 캐시를 재사용합니다.
docker-compose.yml
다음과 같이 docker-compose.yml을 작성하여 Docker 컨테이너에서 Vitest VRT 테스트를 실행할 수 있도록 합니다.
# docker-compose.yml
services:
vrt:
build:
# 모노레포 루트를 빌드 컨텍스트로 지정 (모든 workspace의 package.json을 포함하기 위해)
context: ../../..
# 빌드에 사용할 Dockerfile 경로 (빌드 컨텍스트 기준 상대 경로)
dockerfile: packages/lib/components/Dockerfile
image: vitest-vrt
volumes:
# 소스 코드(테스트 파일, 스크린샷 등)를 마운트
- ./:/app/packages/lib/components
# 호스트 OS용 node_modules를 숨기고, 이미지의 Linux용 바이너리를 사용 (익명 볼륨)
- /app/packages/lib/components/node_modules
environment:
# 파일 변경 감시에 폴링 사용 (Docker 마운트 환경에서의 감지용)
- CHOKIDAR_USEPOLLING=true
working_dir: /app/packages/lib/components
여기서 주요 설정 포인트는 다음과 같습니다.
- 익명 볼륨 (
/app/.../node_modules): 호스트(macOS, Windows)의node_modules에는 해당 OS용 네이티브 바이너리가 포함되어 있어 Linux 컨테이너에서 사용할 수 없습니다. 익명 볼륨으로 호스트의node_modules를 숨기고, Docker 이미지 빌드 시 설치된 Linux용 바이너리를 사용할 수 있도록 합니다. - 소스 코드 마운트: 소스 코드만 마운트하여 코드 변경이 실시간으로 반영됩니다. watch 모드에서 파일을 수정하면 자동으로 테스트가 재실행됩니다.
테스트 코드 작성
테스트 환경이 구축되었으니, 이제 실제 테스트 코드를 작성해 보겠습니다.
파일 컨벤션
VRT 테스트 파일은 *.vitest.tsx 파일명 컨벤션을 사용합니다.
ComponentName/
├── index.tsx # 컴포넌트 구현
├── index.test.tsx # 단위 테스트 (Jest)
├── index.vitest.tsx # VRT + 이벤트 + 접근성 테스트 (Vitest)
└── __screenshots__/ # 베이스라인 스크린샷 (자동 생성)
├── button-children-text.png
├── button-variants.png
└── ...
기존의 Jest로 작성된 단위 테스트(*.test.tsx)를 유지하면서, VRT와 이벤트/접근성 테스트는 *.vitest.tsx 파일로 분리하여 작성했습니다. 추후에 모든 테스트를 Vitest로 통합할 수도 있지만, 현재는 VRT 테스트와 단위 테스트를 명확히 구분하여 관리하기 위해 이렇게 구성했습니다.
렌더링 테스트 (Props 변경에 따른 View 확인)
각 Props 조합별로 스크린샷을 캡처하여 View를 검증합니다.
// src/components/Button/Button/index.vitest.tsx
import { render } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import { page } from 'vitest/browser';
import { Button } from '.';
describe('Button 표시 확인', () => {
test('renders with text content', async () => {
render(<Button>Click me</Button>);
await expect
.element(page.getByRole('button'))
.toMatchScreenshot('button-children-text.png');
});
test('renders all variants', async () => {
render(
<div
data-testid="button-variants"
style={{ display: 'inline-flex', gap: '8px' }}
>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
<Button variant="destructive">Destructive</Button>
</div>
);
await expect
.element(page.getByTestId('button-variants'))
.toMatchScreenshot('button-variants.png');
});
test('disabled', async () => {
render(
<div
data-testid="button-disabled"
style={{ display: 'inline-flex', gap: '8px' }}
>
<Button disabled>Disabled</Button>
<Button variant="primary" disabled>
Primary Disabled
</Button>
<Button variant="secondary" disabled>
Secondary Disabled
</Button>
</div>
);
await expect
.element(page.getByTestId('button-disabled'))
.toMatchScreenshot('button-disabled.png');
});
});
여러 variant를 하나의 스크린샷에 모아 비교하면 테스트 수를 줄이면서도 모든 상태를 검증할 수 있습니다.
자주 사용하는 테스트 케이스
Props 변경 외에도, 실제 사용 환경에서 자주 발생하는 엣지 케이스를 테스트합니다.
// 긴 텍스트: text-overflow 등이 제대로 동작하는지 확인
test('긴 텍스트가 포함된 경우', async () => {
render(
<div
data-testid="button-long-text"
style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
width: '300px',
}}
>
<Button>이것은 매우 긴 텍스트가 포함된 버튼입니다</Button>
<Button widthType="small">
이것은 매우 긴 텍스트가 포함된 버튼입니다
</Button>
</div>
);
await expect
.element(page.getByTestId('button-long-text'))
.toMatchScreenshot('button-long-text.png');
});
// 좁은 부모 요소: 컨테이너가 좁을 때 레이아웃이 깨지지 않는지 확인
test('부모 요소의 폭이 좁은 경우', async () => {
render(
<div
data-testid="button-narrow-parent"
style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
width: '80px',
}}
>
<Button>버튼 라벨</Button>
<Button widthType="full">버튼 라벨</Button>
</div>
);
await expect
.element(page.getByTestId('button-narrow-parent'))
.toMatchScreenshot('button-narrow-parent.png');
});
이벤트 테스트
사용자 인터랙션을 시뮬레이션하여 이벤트 핸들러가 올바르게 동작하는지 검증합니다.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, test, vi } from 'vitest';
describe('Event', () => {
test('calls onClick handler when clicked', async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click me</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
test('does not call onClick when disabled', async () => {
const onClick = vi.fn();
render(
<Button onClick={onClick} disabled>
Click me
</Button>
);
await userEvent.click(screen.getByRole('button'));
expect(onClick).not.toHaveBeenCalled();
});
});
접근성 테스트
axe-core를 사용하여 WCAG 접근성 위반 사항을 자동으로 검사합니다.
import { runAxe } from '../../../test-utils/axe';
describe('Accessibility', () => {
test('should have no accessibility violations', async () => {
const { container } = render(<Button>Accessible Button</Button>);
const results = await runAxe(container);
expect(results.violations).toHaveLength(0);
});
test('should have no accessibility violations when disabled', async () => {
const { container } = render(<Button disabled>Disabled Button</Button>);
const results = await runAxe(container);
expect(results.violations).toHaveLength(0);
});
test('should have no accessibility violations for all variants', async () => {
const { container } = render(
<div>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
<Button variant="destructive">Destructive</Button>
</div>
);
const results = await runAxe(container);
expect(results.violations).toHaveLength(0);
});
});
접근성 테스트 유틸리티
모든 테스트에서 공통적으로 사용하는 axe-core 래퍼 유틸리티를 만들어서, 기본 설정을 공유하여 테스트 코드의 중복을 줄였습니다.
// src/test-utils/axe.ts
import axe from 'axe-core';
/**
* 기본 axe-core 설정
*
* 전체 테스트에서 공통으로 비활성화하는 규칙:
* - color-contrast: 테스트 환경의 배경색과 조합으로 오탐이 발생
* - button-name: 아이콘 전용 버튼은 별도 aria-label로 테스트
*/
export const defaultAxeConfig: axe.RunOptions = {
rules: {
'color-contrast': { enabled: false },
'button-name': { enabled: false },
},
};
/**
* axe-core를 실행하여 접근성 위반 사항을 검사
*/
export async function runAxe(
container: Element,
config?: axe.RunOptions
): Promise<axe.AxeResults> {
return axe.run(container, {
...defaultAxeConfig,
...config,
rules: {
...defaultAxeConfig.rules,
...config?.rules,
},
});
}
스크린샷 클린업 스크립트
컴포넌트를 삭제하거나 테스트를 수정하면 더 이상 사용되지 않는 스크린샷 파일이 남을 수 있습니다. 이런 불필요한 파일을 자동으로 정리하기 위해 다음과 같은 스크립트를 작성했습니다.
// tool/cleanup-vitest-screenshots.mjs
#!/usr/bin/env node
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const SRC_DIR = path.resolve(__dirname, '../src')
// *.vitest.tsx 파일에서 toMatchScreenshot('파일명')으로 참조되는
// 스크린샷 이름을 수집
function findUsedScreenshots() {
const testFiles = findFilesRecursively(SRC_DIR, /\.vitest\.tsx$/)
const usedScreenshots = new Set()
for (const testFile of testFiles) {
const content = fs.readFileSync(testFile, 'utf-8')
const matches = content.matchAll(/toMatchScreenshot\(['"]([^'"]*)['"]\)/g)
for (const match of matches) {
const name = match[1].replace(/\.png$/, '')
usedScreenshots.add(name)
}
}
return usedScreenshots
}
// __screenshots__ 폴더의 모든 PNG 파일을 수집
function findScreenshotFiles() { /* ... */ }
function main() {
const usedScreenshots = findUsedScreenshots()
const screenshotFiles = findScreenshotFiles()
let deletedCount = 0
for (const file of screenshotFiles) {
const basename = extractBasename(file)
if (!usedScreenshots.has(basename)) {
fs.unlinkSync(file)
deletedCount++
console.log(`Deleted: ${file}`)
}
}
console.log(`Used screenshots: ${usedScreenshots.size}`)
console.log(`Deleted files: ${deletedCount}`)
}
main()
이 스크립트의 동작 원리는 다음과 같습니다.
- 모든
*.vitest.tsx파일에서toMatchScreenshot('파일명')패턴으로 참조되는 스크린샷 이름을 수집 __screenshots__/폴더의 모든 PNG 파일을 탐색- 참조되지 않는 파일을 삭제
실행 스크립트
이렇게 작성된 테스트를 실행하기 위해, 다음과 같은 스크립트를 package.json에 추가합니다.
// package.json
{
"scripts": {
"test:view": "yarn test:view:docker:build && docker compose run --rm -it vrt npx vitest",
"test:view:ci": "TZ=Asia/Tokyo vitest run",
"test:view:update": "yarn test:view:docker:build && docker compose run --rm vrt npx vitest run --update",
"test:view:docker:build": "docker compose build vrt",
"test:view:cleanup": "node ./tool/cleanup-vitest-screenshots.mjs",
"test:view:coverage": "yarn test:view:docker:build && docker compose run --rm vrt npx vitest run --coverage"
}
}
각각의 스크립트는 다음과 같은 역할을 합니다.
| 스크립트 | 용도 |
|---|---|
test:view | 로컬에서 Docker로 VRT 실행 (watch 모드) |
test:view:ci | CI 환경에서 VRT 실행 (Docker 없이 직접 실행) |
test:view:update | 베이스라인 스크린샷 갱신 (컴포넌트가 의도적으로 변경된 경우) |
test:view:docker:build | Docker 이미지 빌드 |
test:view:cleanup | 사용하지 않는 스크린샷 파일 삭제 |
test:view:coverage | 테스트 커버리지 리포트 생성 |
로컬에서는 Docker를 통해 일관된 환경에서 테스트하고, CI에서는 컨테이너 내에서 직접 실행합니다. test:view:ci에서 TZ=Asia/Tokyo를 설정하는 이유는 날짜/시간을 표시하는 컴포넌트의 스크린샷이 타임존에 따라 달라지는 것을 방지하기 위해서입니다.
CI 연동 (GitHub Actions)
GitHub Actions에서 VRT를 자동으로 실행하는 워크플로우 설정입니다.
# .github/workflows/check_code_components.yml (일부 발췌)
test-components-vrt:
# workflow_dispatch로 수동 실행하거나, 'vrt' 라벨이 붙은 PR에서 자동 실행
if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'vrt')
name: Test components VRT
runs-on: ubuntu-latest
timeout-minutes: 30
container:
image: node:24.13.0-bookworm
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Enable corepack
run: corepack enable
- name: Install fonts for deterministic rendering
run: |
apt-get update && apt-get install -y --no-install-recommends \
fonts-noto-core \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
- name: Install dependencies
run: yarn install --immutable
- name: Get Playwright version
id: playwright-version
run: echo "version=$(yarn info @vitest/browser-playwright --json | jq -r '.children.Version')" >> $GITHUB_OUTPUT
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-webkit-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install webkit --with-deps
- name: Install Playwright dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: yarn playwright install-deps webkit
- name: Test view
run: yarn test:view:ci:components
이 CI 워크플로우의 핵심 포인트는 다음과 같습니다.
- 조건부 실행: 모든 PR에서 VRT를 실행하면 시간이 오래 걸리므로,
vrt라벨이 붙은 PR에서만 자동 실행되도록 설정합니다. 필요시 Actions 탭에서 수동 실행도 가능합니다. - 동일한 폰트 설치: Dockerfile과 마찬가지로
fonts-noto-core,fonts-noto-cjk를 설치하여 로컬 Docker 환경과 동일한 렌더링을 보장합니다. - Playwright 브라우저 캐싱: Playwright 브라우저 설치에 시간이 걸리므로, 버전별로 캐싱하여 CI 실행 시간을 단축합니다.
디렉토리 구조
이 글에서 생성하거나 수정하는 파일들의 전체 디렉토리 구조는 다음과 같습니다.
프로젝트 루트/
├── .github/
│ └── workflows/
│ └── check_code_components.yml # CI 워크플로우 (수정)
├── .gitignore # .vitest-attachments/ 추가 (수정)
├── packages/
│ └── lib/
│ └── components/
│ ├── Dockerfile # Docker 이미지 설정 (신규)
│ ├── docker-compose.yml # Docker Compose 설정 (신규)
│ ├── package.json # 실행 스크립트 추가 (수정)
│ ├── vitest.config.ts # Vitest 설정 (신규)
│ ├── vitest-setup.ts # 테스트 셋업 (신규)
│ ├── tool/
│ │ └── cleanup-vitest-screenshots.mjs # 클린업 스크립트 (신규)
│ └── src/
│ ├── test-utils/
│ │ └── axe.ts # 접근성 테스트 유틸리티 (신규)
│ └── components/
│ └── Button/
│ └── Button/
│ ├── index.tsx
│ ├── index.test.tsx
│ ├── index.vitest.tsx # VRT 테스트 (신규)
│ └── __screenshots__/ # 베이스라인 스크린샷 (자동 생성)
│ ├── button-children-text.png
│ ├── button-variants.png
│ └── ...
정리
이 글에서는 Vitest를 사용한 VRT + 접근성 테스트 환경 구축 방법을 소개했습니다.
| 항목 | 내용 |
|---|---|
| 렌더링/VRT | Vitest Browser Mode + toMatchScreenshot |
| 이벤트 테스트 | @testing-library/user-event로 인터랙션 시뮬레이션 |
| 접근성 테스트 | axe-core로 WCAG 위반 사항 검사 |
| 일관된 렌더링 | Docker + Noto 폰트로 환경 차이 해소 |
| 테스트 분리 | *.vitest.tsx 파일 컨벤션으로 기존 Jest 테스트와 분리 |
| 스크린샷 관리 | 컴포넌트 디렉토리의 __screenshots__/에 저장, 클린업 스크립트 제공 |
| CI 연동 | GitHub Actions에서 vrt 라벨로 조건부 실행 |
Vitest 기반 접근 방식의 장점은 프로덕트 코드와 동일한 Vite 빌드 파이프라인을 사용하므로 CSS Modules, SCSS 등의 처리가 자연스럽고, toMatchScreenshot API가 간결하여 테스트 코드의 가독성이 높다는 점입니다.
팀 내에서 테스트 방침을 검토하는 과정과 다른 도구와의 비교에 대해서는 [React] 모노레포 환경에서의 공통 컴포넌트 테스트 방침을, 기존 Jest 프로젝트에 Vitest를 점진적으로 도입하는 방법에 대해서는 [React] Jest 프로젝트에 Vitest 도입하기 - View 테스트 분리 전략도 참고해 보시기 바랍니다.
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku가 개발한 앱을 한번 사용해보세요.Deku가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.