Vitest: Jest를 대체하는 차세대 테스팅 프레임워크
Vite 기반의 빠르고 현대적인 테스팅 프레임워크 Vitest의 핵심 기능, Jest와의 비교, 실전 활용법을 알아본다.
JavaScript 프로젝트에서 테스트를 작성할 때 가장 먼저 떠오르는 도구는 Jest다. 오랫동안 사실상의 표준 역할을 해왔고, 대부분의 개발자가 describe, it, expect에 익숙하다. 하지만 Vite 기반 프로젝트가 늘어나면서, Jest를 함께 쓰려면 Babel이나 ts-jest 같은 별도의 변환 설정이 필요하다는 문제가 드러나기 시작했다.
Vite로 개발하면서 테스트는 Jest로 돌리는 구조에서는, 같은 TypeScript 코드를 두 번 다르게 변환하는 셈이다. ESM 지원도 Jest에서는 아직 실험적이라 --experimental-vm-modules 플래그를 붙여야 하고, import.meta 같은 문법은 별도 설정 없이 쓸 수 없다.
Vitest는 이 문제를 해결하기 위해 등장했다. Vite의 변환 파이프라인을 그대로 재사용하여, 설정 중복 없이 빠르고 일관된 테스트 환경을 제공한다.
Vitest란?
Vitest는 Vite 기반의 테스팅 프레임워크다. vite.config.ts의 플러그인, alias, resolve 설정을 테스트에서도 동일하게 사용한다. Jest 호환 API를 제공하면서도 ESM 네이티브 지원, TypeScript 기본 지원, 브라우저 모드 등 현대적인 기능을 갖추고 있다.
- GitHub 스타 16.1k (2026년 3월 기준)
- 최신 버전 v4.0.18
- npm 주간 다운로드 약 1,800만~3,500만
- 라이선스 MIT
- 요구 사항 Vite >= v6.0.0, Node >= v20.0.0
2026년 현재 Nuxt, SvelteKit, Astro, Angular 등 주요 프레임워크가 Vitest를 공식 추천하거나 기본 테스팅 도구로 채택하고 있다. 모던 JavaScript 테스팅의 사실상 표준으로 자리잡았다고 봐도 무방하다.
설치 및 기본 설정
pnpm으로 설치한다.
pnpm add -D vitest
Vite 프로젝트라면 기존 vite.config.ts에 테스트 설정만 추가하면 된다.
// vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'node',
globals: true,
},
})
별도의 설정 파일이 필요하다면 vitest.config.ts를 만들 수도 있다. 이 경우 vite.config.ts의 설정을 자동으로 상속한다.
첫 번째 테스트를 작성해보자.
// src/utils/math.test.ts
import { describe, it, expect } from 'vitest'
function sum(a: number, b: number) {
return a + b
}
describe('sum', () => {
it('두 숫자를 더한다', () => {
expect(sum(1, 2)).toBe(3)
})
it('음수도 처리한다', () => {
expect(sum(-1, 1)).toBe(0)
})
})
package.json에 스크립트를 추가하고 실행한다.
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
pnpm test
vitest 명령어를 그냥 실행하면 watch mode로 동작한다. CI 환경처럼 한 번만 실행하고 종료하려면 vitest run을 사용한다.
핵심 기능
Jest 호환 API와 모킹
Jest에서 마이그레이션할 때 가장 걱정되는 부분이 API 호환성이다. Vitest는 describe, it, expect는 물론이고 vi.fn(), vi.mock(), vi.spyOn() 등 모킹 API도 Jest와 거의 동일하게 제공한다.
import { describe, it, expect, vi } from 'vitest'
const fetchUser = vi.fn()
fetchUser.mockResolvedValue({ id: 1, name: 'Alice' })
describe('UserService', () => {
it('사용자를 조회한다', async () => {
const user = await fetchUser(1)
expect(fetchUser).toHaveBeenCalledWith(1)
expect(user).toEqual({ id: 1, name: 'Alice' })
})
it('호출 횟수를 추적한다', () => {
const callback = vi.fn()
callback('a')
callback('b')
expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenLastCalledWith('b')
})
})
jest.fn() 대신 vi.fn(), jest.mock() 대신 vi.mock()을 쓰는 것 외에는 거의 차이가 없다. 내부적으로는 Tinyspy 기반의 경량 모킹 시스템을 사용하며, Chai 기반 assertion과 Jest 호환 expect API를 모두 지원한다.
타이머 제어도 동일한 패턴이다.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
describe('Timer', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('1초 후에 콜백을 실행한다', () => {
const callback = vi.fn()
setTimeout(callback, 1000)
vi.advanceTimersByTime(1000)
expect(callback).toHaveBeenCalledOnce()
})
})
TypeScript 기본 지원과 타입 테스팅
Vitest는 별도의 ts-jest나 Babel 설정 없이 TypeScript를 바로 사용할 수 있다. Vitest 4.0부터는 Oxc를 활용한 변환을 지원하여 더 빨라졌다.
TypeScript 프로젝트에서 Jest를 사용하려면 보통 이런 과정을 거친다.
# Jest + TypeScript 설정 (비교용)
pnpm add -D jest ts-jest @types/jest
# jest.config.ts에서 transform 설정
# tsconfig.json에서 types 설정
Vitest에서는 이 과정이 필요 없다. .ts 파일을 바로 인식하고 실행한다.
더 흥미로운 기능은 타입 테스팅 이다. expectTypeOf를 사용하면 런타임이 아닌 타입 레벨에서 검증할 수 있다.
import { describe, it, expectTypeOf } from 'vitest'
interface User {
id: number
name: string
email: string
}
type CreateUserInput = Omit<User, 'id'>
describe('User 타입', () => {
it('필요한 프로퍼티를 가진다', () => {
expectTypeOf<User>().toHaveProperty('id')
expectTypeOf<User>().toHaveProperty('name')
expectTypeOf<User>().toHaveProperty('email')
})
it('CreateUserInput에는 id가 없다', () => {
expectTypeOf<CreateUserInput>().not.toHaveProperty('id')
expectTypeOf<CreateUserInput>().toMatchTypeOf<{ name: string; email: string }>()
})
})
유틸리티 타입이나 제네릭 타입이 의도대로 동작하는지 테스트 코드로 검증할 수 있다. 라이브러리를 만들 때 특히 유용하다.
인라인 테스트 (In-Source Testing)
Vitest만의 고유한 기능 중 하나다. Rust의 #[cfg(test)] 패턴처럼, 소스 파일 안에 테스트를 직접 작성할 수 있다.
// src/utils/math.ts
export function add(...args: number[]) {
return args.reduce((a, b) => a + b, 0)
}
export function multiply(a: number, b: number) {
return a * b
}
// 테스트 코드 — 프로덕션 빌드에서는 자동 제거됨
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('add', () => {
expect(add()).toBe(0)
expect(add(1)).toBe(1)
expect(add(1, 2, 3)).toBe(6)
})
it('multiply', () => {
expect(multiply(2, 3)).toBe(6)
})
}
이 기능을 사용하려면 설정에서 includeSource를 지정해야 한다.
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
includeSource: ['src/**/*.{js,ts}'],
},
define: {
'import.meta.vitest': 'undefined',
},
})
define에서 import.meta.vitest를 'undefined'로 설정하면 프로덕션 빌드 시 테스트 코드가 tree-shaking으로 제거된다. 소스 코드와 테스트가 같은 파일에 있으므로 프라이빗 함수나 내부 상태에 직접 접근할 수 있다는 장점이 있다. 유틸리티 함수처럼 작은 단위의 코드를 테스트할 때 별도의 .test.ts 파일을 만들 필요가 없어 편리하다.
브라우저 모드
Vitest 4.0에서 안정화된 브라우저 모드는 실제 브라우저에서 테스트를 실행한다. jsdom이나 happy-dom은 DOM을 시뮬레이션하는 방식이라, 실제 브라우저와 미묘한 차이가 발생하는 경우가 있다. 브라우저 모드를 사용하면 이런 문제를 근본적으로 해결할 수 있다.
pnpm add -D @vitest/browser-playwright
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
enabled: true,
provider: 'playwright',
instances: [
{ browser: 'chromium' },
],
},
},
})
Playwright를 프로바이더로 사용하면 병렬 실행도 지원된다. Vitest 4.0부터는 비주얼 리그레션 테스트 도 가능하다.
import { expect, it } from 'vitest'
import { page } from '@vitest/browser/context'
it('버튼 스크린샷이 일치한다', async () => {
await expect.element(page.getByRole('button')).toMatchScreenshot('primary-button')
})
it('요소가 뷰포트에 보인다', async () => {
await expect.element(page.getByTestId('hero')).toBeInViewport()
})
스크린샷 기반으로 UI 변경을 감지하고, Playwright Traces를 통해 실패한 테스트를 디버깅할 수 있다. jsdom으로는 불가능했던 CSS 레이아웃이나 실제 이벤트 처리를 검증할 수 있다는 점이 가장 큰 차이다.
코드 커버리지
Vitest는 v8(기본)과 Istanbul 두 가지 커버리지 프로바이더를 지원한다. v8은 사전 변환 단계 없이 V8 엔진의 내장 커버리지 기능을 활용하므로 빠르고 메모리 효율적이다.
pnpm add -D @vitest/coverage-v8
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
exclude: ['**/*.test.ts', '**/*.spec.ts'],
reporter: ['text', 'json', 'html'],
},
},
})
pnpm vitest run --coverage
실행하면 터미널에 커버리지 요약이 출력되고, coverage/ 디렉토리에 HTML 리포트가 생성된다. CI에서는 JSON 리포트를 사용해 커버리지 임계값을 설정할 수도 있다.
스냅샷 테스트
Jest 호환 스냅샷 테스팅에 더해, Vitest는 인라인 스냅샷과 파일 스냅샷을 추가로 지원한다.
import { expect, it } from 'vitest'
// 일반 스냅샷 (.snap 파일 생성)
it('결과가 스냅샷과 일치한다', () => {
const result = generateHTML()
expect(result).toMatchSnapshot()
})
// 인라인 스냅샷 (테스트 파일 내에 직접 기록)
it('대문자 변환', () => {
const result = toUpperCase('hello')
expect(result).toMatchInlineSnapshot('"HELLO"')
})
// 파일 스냅샷 (특정 파일과 비교)
it('HTML 출력이 일치한다', async () => {
const html = renderComponent()
await expect(html).toMatchFileSnapshot('./snapshots/component.html')
})
인라인 스냅샷은 테스트 파일을 열었을 때 기대값을 바로 확인할 수 있어서 가독성이 좋다. 파일 스냅샷은 대용량 출력물을 별도 파일로 관리할 때 유용하다. watch mode에서 u 키를 누르거나 vitest -u로 스냅샷을 업데이트할 수 있다.
Jest vs Vitest
두 프레임워크를 항목별로 비교해보자.
| 항목 | Vitest | Jest |
|---|---|---|
| 기반 | Vite | 자체 (jest-haste-map) |
| ESM 지원 | 네이티브 | 실험적 (--experimental-vm-modules) |
| TypeScript | 기본 지원 (Oxc) | ts-jest 또는 Babel 필요 |
| Cold Start | ~2초 | ~12초 (최대 4배 차이) |
| Watch Mode | 380ms (영향받는 테스트만) | 3.4초 (git diff 기반) |
| 메모리 사용 | 약 30% 적음 | 기준값 |
| 설정 복잡도 | Vite 설정 재사용 | 별도 Jest 설정 필요 |
| 브라우저 모드 | 안정화 (Playwright 통합) | 미지원 |
| 인라인 테스트 | 지원 | 미지원 |
| React Native | 미지원 | 지원 |
| 생태계 | 주요 프레임워크 채택 | 가장 큰 서드파티 생태계 |
성능 수치를 정리하면 다음과 같다.
- Cold Start Vitest가 Jest 대비 최대 4배 빠르다 (66% cold start 시간 감소)
- Watch Mode Vitest 380ms vs Jest 3.4초 — 단일 파일 변경 시 약 9배 차이
- 메모리 Vitest가 약 30% 적은 메모리를 사용한다
벤치마크 결과는 프로젝트 크기, 설정, 테스트 유형에 따라 달라질 수 있다. 실제 성능은 직접 측정하는 것이 가장 정확하다.
언제 Jest를 선택해야 하는가
- React Native 프로젝트 (Vitest는 미지원)
- 이미 Jest 기반의 대규모 테스트 스위트가 있고, 마이그레이션 비용이 큰 경우
- Vite를 사용하지 않는 레거시 프로젝트
언제 Vitest를 선택해야 하는가
- Vite 기반 프로젝트라면 거의 무조건 Vitest가 유리하다
- 새로운 프로젝트를 시작할 때
- ESM, TypeScript 네이티브 지원이 필요한 경우
- 빠른 피드백 루프가 중요한 경우
마무리
Vitest는 Vite의 변환 파이프라인을 테스트에 재사용한다는 단순한 아이디어에서 출발했지만, 이제는 브라우저 모드, 타입 테스팅, 인라인 테스트 등 Jest에 없는 고유한 기능까지 갖춘 독립적인 테스팅 프레임워크로 성장했다.
Jest와 API가 호환되기 때문에 마이그레이션 비용도 낮다. 대부분의 경우 jest.fn()을 vi.fn()으로, jest.mock()을 vi.mock()으로 바꾸는 수준이면 충분하다. Vite 기반 프로젝트라면 설정 파일 하나 없이 바로 시작할 수 있다.
Vite를 이미 사용하고 있거나 새 프로젝트를 시작하려는 개발자, 또는 Jest의 ESM이나 TypeScript 설정에 불편함을 느끼고 있었다면 Vitest를 한번 사용해볼 만하다.