본문으로 건너뛰기

Vitest 5.0

홍승아9

Vitest 5.0 출시: 성능·Trace View 및 마이그레이션 가이드

2026년 9월 3일, Vitest 5.0이 출시되었습니다(최초 태그 5.0.1). 이번 메이저의 성격은 Vite 8처럼 엔진을 갈아끼우는 종류가 아니라, 기본값을 "안전한 쪽"으로 옮기고 흩어져 있던 산출물·설정을 한 곳으로 모으는 정리에 가깝습니다.

그래서 체감은 이렇게 갈립니다.

  • 새 기능은 눈에 잘 띕니다 — Trace View, vi.when, 중첩 프로젝트
  • 깨지는 건 눈에 잘 안 띕니다clearMocks 기본값 변경, await 안 한 비동기 단언, -t 구분자, 산출물 경로

이 글은 후자에 무게를 둡니다. 릴리스 노트가 "좋아진 것"을 말한다면, 업그레이드 당일에 필요한 건 "조용히 색이 바뀌는 것" 의 목록이기 때문입니다.

📋 목차

  1. 요구사항과 한눈에 보는 변경
  2. 성능
  3. 새로운 기능
  4. 마이그레이션 가이드
  5. 조용히 깨지는 것들
  6. 이 저장소에 대입해보면
  7. 마무리

요구사항과 한눈에 보는 변경

항목요구 버전
Node.js>= 22.12.0
Vite>= 6.4.0 (peer dependency)
Vitest 태그5.0.1 (2026-09-03)

Vite가 peer dependency 로 바뀐 게 첫 번째 관문입니다. npm·pnpm·Bun·Deno는 자동으로 설치하지만, Yarn 사용자는 직접 설치 해야 합니다.

# Yarn만 해당
yarn add -D vite

한눈에 보는 변경은 이렇습니다.

분류내용
성능VM 풀 최대 53% · 격리 스위트 최대 25% · 브라우저 모드 16~18% 단축
새 기능Trace View · 중첩 프로젝트 · vi.when · 벤치마크 재작성 · Temporal 목킹
기본값 변경clearMocks: true · 로케이터 strict · 인라인 프로젝트 설정 상속 · Vite 서버 공유
엄격해짐await 안 한 resolves/rejects 실패 · expect.poll 타임아웃 시 reject
정리산출물 전부 .vitest/ 아래로 · 구 엔트리포인트 제거 · test.sequential 제거

성능

Vitest 팀은 이번 릴리스를 위해 5개부터 1,280개 모듈까지 규모를 달리한 앱을, 풀(pool)과 환경(environment) 조합별로 측정하는 벤치마크 스위트를 새로 만들었습니다. "빨라졌다"를 감이 아니라 축으로 나눠 말하려는 시도입니다.

개선
VM 풀의존성 많은 앱에서 53% 단축
격리(isolated) 스위트대형에서 최대 25% 단축
브라우저 모드Chrome 기준 16~18% 단축

개선의 출처는 크게 세 가지입니다.

  • 인라인 프로젝트가 Vite 서버를 공유 — 프로젝트마다 같은 파일을 다시 변환하던 중복이 사라집니다
  • fsModuleCache — 변환된 모듈을 파일 시스템에 캐시해 실행 간에 유지 합니다. 로컬 반복 실행과 캐시를 태우는 CI에서 값이 큽니다
  • 워커 통신 라운드트립 감소 + 의존성 번들링으로 설치 용량 축소

리포터 출력의 duration 분해에 퍼센트 표기 가 붙은 것도 작지만 실용적인 변화입니다. transform이 느린 건지 collect가 느린 건지를 눈으로 가릅니다.

주의할 점 하나. 위 수치는 풀·환경·모듈 수에 따라 갈리는 값 입니다. jsdom 환경의 작은 유닛 스위트라면 53%는 남의 이야기고, 체감은 설치 용량과 캐시 쪽에서 먼저 옵니다.

새로운 기능

1. Trace View (브라우저 모드)

이번 릴리스에서 가장 눈에 띄는 기능입니다. 브라우저 모드에서 모든 인터랙션과 단언을 DOM 스냅샷으로 기록 해, 테스트가 끝난 뒤 단계별로 되감아 볼 수 있게 합니다.

export default defineConfig({
  test: {
    browser: {
      traceView: true,
    },
  },
});

값은 "실패했을 때"에 몰려 있습니다. 브라우저 테스트 디버깅이 어려운 이유는 실패한 순간의 화면이 이미 사라졌기 때문인데, Trace View는 그 순간을 되살립니다. CI에서 한 번 깨지고 로컬에서 재현이 안 되는 유형에 특히 직접적입니다.

2. 중첩 프로젝트와 설정 상속

모노레포 쪽 변화입니다. 두 가지가 같이 바뀌었습니다.

  • 인라인 프로젝트가 루트 설정을 기본 상속 (extends의 기본값이 true)
  • 참조된 설정 파일이 자기 projects를 선언 할 수 있어, 프로젝트가 계층을 이룹니다

필터도 짧아졌습니다.

vitest -p app        # --project app 의 단축

3. vi.when — 인자별 조건부 목킹

인자에 따라 다른 값을 돌려주도록 목을 짜는 일은 흔한데, 지금까지는 mockImplementation 안에서 직접 분기해야 했습니다. vi.when이 이걸 선언형으로 만듭니다.

import { vi, expect } from 'vitest';
 
const fetchUser = vi.fn();
 
vi.when(fetchUser)
  .calledWith(1)
  .thenReturn({ id: 1, name: 'Ada' })
  .calledWith(expect.any(Number))
  .thenReturn(null);
  • 인자 비교는 깊은 동등성(deep equality) 을 씁니다
  • expect.any() 같은 비대칭 매처(asymmetric matcher) 를 인자 자리에 쓸 수 있습니다
  • 준비한 응답을 다 소진했는지 확인하는 toHaveBeenExhausted 단언이 함께 들어왔습니다

4. 벤치마크 API 재작성

bench최상위 import에서 테스트 픽스처(fixture)로 내려왔습니다.

// 이전 (Vitest 4)
import { bench } from 'vitest';
 
bench('sort', () => {
  data.slice().sort();
});
// 이후 (Vitest 5)
import { test } from 'vitest';
 
test('sort', async ({ bench }) => {
  await bench(() => {
    data.slice().sort();
  });
});

바꾼 이유가 설계상 일관됩니다. 벤치마크가 테스트가 되면 픽스처·훅·retry·필터링을 전부 그대로 물려받습니다. 그동안 벤치마크만 다른 세상에 있던 걸 본류에 합친 것입니다. 결과도 별도 리포터가 아니라 default·json 리포터에 통합됩니다.

대신 사라진 것이 많습니다.

제거된 것대체
bench.skip · bench.onlytest의 skip/only를 사용
benchmark.reporters · benchmark.comparedefault/json 리포터
benchmark.outputFile · benchmark.outputJson.vitest/ 산출물 경로
--compare · --outputJson CLI 플래그

5. 로케이터와 Temporal

  • 로케이터 strict 매칭이 기본locator.exact가 켜진 상태로 시작합니다
  • 에러 메시지에 ARIA 트리 표시browser.locators.errorFormat으로 'html' | 'aria' | 'all' 중 선택
  • Temporal API 목킹vi.useFakeTimers()/vi.setSystemTime()Date와 함께 Temporal도 고정합니다. Temporal.Now가 가짜 시계를 따릅니다

6. 기타 설정·API 추가

항목내용
--repeats같은 테스트를 반복 실행해 flaky 사냥
coverage.autoAttachSubprocess자식 프로세스까지 커버리지 추적
coverage.thresholds.perFile객체 형태를 받습니다
sharedViteServer인라인 프로젝트의 Vite 서버 공유 제어
TestCase.logs()리포터가 콘솔 출력에 접근 할 수 있습니다
커스텀 매처하부 Chai assertion 객체에 접근 가능
HTML 리포터 singleFile: true자기완결형 단일 파일 — CI 아티팩트로 올리기 좋습니다

마이그레이션 가이드

1단계: 런타임과 Vite

node -v          # >= 22.12.0 이어야 합니다

Node 20 계열에 묶여 있다면 여기서 먼저 막힙니다. Vitest 업그레이드가 아니라 런타임 업그레이드 일정 부터 잡아야 하는 상황입니다.

pnpm add -D vitest@5
# Yarn이라면 vite도 명시적으로
yarn add -D vitest@5 vite

2단계: clearMocks 기본값 켜짐

이번 릴리스에서 가장 조용하게 테스트를 바꾸는 변경 입니다. 이제 매 테스트 전에 목 히스토리가 지워집니다.

// 이전에는 통과하던 패턴
it('a', () => {
  doWork();
});
it('b', () => {
  expect(spy).toHaveBeenCalledTimes(2); // a에서 쌓인 호출까지 세던 코드
});

의도된 방향은 맞습니다 — 테스트 간 목 누수를 막는 것. 다만 누수에 기대고 있던 테스트는 이제 실패 합니다. 당장 통과시키려면 되돌릴 수 있습니다.

export default defineConfig({
  test: { clearMocks: false },
});

권장은 되돌리지 말고 고치는 쪽 입니다. 이 플래그를 끄고 넘어가면, 어떤 테스트가 이웃 테스트의 상태에 기대고 있는지를 영영 모르는 채로 갑니다.

3단계: await 안 한 비동기 단언

// 이전: 경고만 출력되고 통과
expect(promise).resolves.toBe(1);
 
// 이후: 테스트 실패
await expect(promise).resolves.toBe(1);

경고를 실패로 승격한 변경입니다. 그동안 이 경고를 흘려보냈다면, 실제로는 아무것도 검증하지 않던 단언이 있었다는 뜻 입니다. 업그레이드 후 새로 빨간불이 켜진 자리는 대부분 원래 비어 있던 자리 입니다.

expect.poll도 같은 방향으로 엄격해졌습니다 — 타임아웃 안에 정착(settle)하지 않으면 이제 reject 합니다. 콜백은 취소용 AbortSignal을 받습니다.

4단계: 호이스팅 목킹 호출은 최상위로

// 이후: 에러
describe('suite', () => {
  vi.mock('./api'); // ❌ 블록 안
});
 
// 올바른 위치
vi.mock('./api'); // ✅ 파일 최상위
describe('suite', () => {});

vi.mock·vi.unmock·vi.hoisted를 함수·블록·콜백 안에서 호출하면 에러를 던집니다. 원래도 호이스팅돼 최상위에서 실행되던 것이라, 적힌 위치와 실제 실행 순서가 달랐던 것 을 바로잡은 셈입니다.

5단계: -t 구분자 변경

# 테스트 풀네임이 ' > ' 로 조인됩니다
vitest -t 'math > adds'    # ✅
vitest -t 'math adds'      # ❌ 더 이상 매치되지 않음

한 세그먼트 안에서만 찾는 패턴(-t '@smoke' 같은 태그)은 영향이 없습니다. 깨지는 건 describe와 test 이름에 걸쳐 있던 패턴 입니다. CI 스크립트에 -t가 박혀 있다면 이 기준으로 한 번 훑어야 합니다.

6단계: 제거·이전된 API

// test.sequential / describe.sequential 제거
test.sequential('name', fn); // ❌
test('name', { concurrent: false }, fn); // ✅

엔트리포인트도 정리됐습니다.

이전이후
vitest/coveragevitest/node
vitest/reportersvitest/node
vitest/environmentsvitest/runtime
vitest/snapshotvitest/runtime
vitest/runners · vitest/suite · vitest/mocker완전 제거

패키지 레벨에서는 @vitest/runner·@vitest/ws-client가 deprecated, @vitest/expect는 더 이상 Vitest의 expect와 상태를 공유하지 않으며, @vitest/browser-webdriverio는 vitest-community로 이전했습니다.

7단계: 산출물 경로 이전

흩어져 있던 출력이 전부 .vitest/ 아래로 모였습니다. .gitignore와 CI 아티팩트 업로드 경로를 같이 고쳐야 하는 항목입니다.

종류이전이후
첨부.vitest-attachements/.vitest/attachments/
스크린샷__screenshots__/.vitest/attachments/failure-screenshots/
Blob 리포트.vitest-reports/.vitest/blob/
HTMLhtml/index.html.vitest/index.html (옵션은 outputDir)
JSONstdout.vitest/json/output.json
JUnitstdout.vitest/junit/output.xml

JSON·JUnit이 stdout에서 파일로 바뀐 것 을 특히 조심해야 합니다. 출력을 파이프로 받아 파싱하던 CI 스텝은 조용히 빈 입력 을 받게 됩니다.

조용히 깨지는 것들

마이그레이션 가이드의 나머지 항목 중, 에러 메시지가 원인을 알려주지 않는 것들만 따로 모았습니다.

변경어떻게 드러나나
커버리지 include/exclude패턴이 프로젝트 루트 기준 상대 경로 로 매칭되고 "포함(contains)" 동작이 사라졌습니다. 와일드카드 없는 패턴은 디렉토리로 취급. 커버리지 숫자가 이유 없이 출렁이면 여기입니다
부모 디렉토리 설정 탐색 중단더 이상 상위 디렉토리에서 설정 파일을 찾지 않습니다. --config를 명시하고, 테스트 탐색은 --dir
toThrow('')빈 문자열은 모든 문자열의 부분 문자열이라 어떤 에러 메시지든 통과 합니다. 빈 메시지만 노리려면 /^$/
toHaveTextContent완전 일치 로 바뀌었습니다. 부분·정규식 매칭은 새 toMatchTextContent()
워커 ID 1-baseVITEST_POOL_ID·VITEST_WORKER_ID가 0이 아니라 1부터 시작합니다. 이 값으로 DB·포트를 나눠 쓰던 셋업이 어긋납니다
jsdom 전역 할당globalThis 프로퍼티 할당이 하부 DOM 구현까지 전파 됩니다
populateGlobaloriginals 맵이 값이 아니라 프로퍼티 디스크립터 를 담습니다. 복원은 Object.defineProperty()
클래스 목 prototype클래스 목의 인스턴스가 구현의 prototype을 상속 합니다. 메서드 접근과 instanceof가 이제 동작합니다(= 이전 동작에 기대던 분기가 뒤집힙니다)
브라우저 automock브라우저 모드가 automock을 제대로 적용합니다. automock된 export는 기본 undefined. 실제 구현을 돌리며 호출만 추적하려면 { spy: true }
resolveConfig 반환값{ vitestConfig, viteConfig } 쌍을 반환하지 않습니다. 해석된 Vite 설정을 반환하고 테스트 설정은 .test로 접근
Vitest UI 토큰 인증http://localhost:51204/__vitest__/?token=... — 북마크해둔 URL이 안 열립니다
브라우저 세션 바인딩러너 URL에 sessionId가 필요합니다. Vitest가 출력하는 URL을 쓸 것

브라우저 모드를 쓴다면 두 개가 더 있습니다 — 커스텀 커맨드에 넘어오는 로케이터가 selector/locator 필드를 가진 SerializedLocator 객체 로 바뀌었고, vitest-browser-vue·vitest-browser-svelterender()가 async 라 await해야 합니다. browser.apibrowser.isolate는 deprecated이고 최상위 api·isolate로 옮겼습니다.

이 저장소에 대입해보면

읽기만 하면 남는 게 없으니, 이 블로그 저장소를 기준으로 재봤습니다. 현재 상태는 이렇습니다.

// package.json
"engines": { "node": ">=20.9.0" },
"devDependencies": { "vitest": "^2.1.8" }
// vitest.config.ts
test: {
  environment: 'jsdom',
  globals: true,
  setupFiles: ['./vitest.setup.ts'],
}
항목판정이유
Node 22.12+⚠️engines>=20.9.0. Vitest보다 이걸 먼저 올려야 합니다
Vitest 2 → 5⚠️두 메이저를 건너뜁니다. 4의 변경까지 같이 훑어야 합니다
-t '@smoke'test:smoke 스크립트의 태그는 한 세그먼트 안이라 구분자 변경과 무관합니다
clearMocks⚠️설정에 없으니 기본값 변경을 그대로 맞습니다 — 업그레이드 시 실측 필요
jsdom 전역 할당⚠️globals: true + jsdom + vitest.setup.ts 조합이라 전파 변경의 사정권
브라우저 모드 항목 전부안 씁니다. E2E는 Playwright가 따로 담당합니다
산출물 경로JSON·JUnit 리포터를 쓰지 않아 영향 없음

정리하면 이 저장소에서 실제로 해야 하는 일은 세 줄 입니다.

  1. Node 22.12+로 engines를 올리는 것이 선행 조건 — 이게 안 되면 나머지는 논의 대상이 아닙니다
  2. clearMocks 기본값을 끄지 않고 그대로 받아본다 — 깨지는 테스트가 있다면 그게 정보입니다
  3. 브라우저 모드 관련 항목은 전부 건너뛴다 — 목록이 길어 보여도 이 저장소가 실제로 통과해야 하는 관문은 몇 개 안 됩니다

3번을 굳이 적어두는 이유가 있습니다. 마이그레이션 문서의 34개 항목을 위에서부터 읽으면 전부 내 일처럼 보이는데, 실제로 걸리는 건 자기가 켠 기능의 수만큼 입니다. 브라우저 모드·벤치마크·커스텀 리포터를 안 쓴다면 34개 중 절반 이상이 처음부터 남의 이야기입니다.

마무리

Vitest 5는 새 기능보다 기본값이 주인공인 릴리스 입니다. clearMocks가 켜지고, 로케이터가 엄격해지고, 넘어가던 경고가 실패가 되고, 흩어진 산출물이 한 디렉토리로 모였습니다. 방향은 전부 같습니다 — 모호하게 통과하던 것을 통과하지 않게 만드는 것.

그래서 업그레이드 후 빨간불이 늘어나는 건 대체로 나쁜 신호가 아닙니다. await하지 않아 아무것도 검증하지 않던 단언, 이웃 테스트의 목 호출까지 세던 카운트 — 원래 비어 있던 자리가 이제 보이는 것입니다. 여기서 clearMocks: false로 되돌리고 싶은 유혹이 제일 큰데, 그 플래그는 문제를 고치는 게 아니라 다시 안 보이게 만듭니다.

업그레이드 체크리스트:

  • ✅ Node.js 22.12+ · Vite 6.4+ 확인 (Yarn이면 vite 직접 설치)
  • clearMocks 기본 활성화 — 끄지 말고 깨지는 테스트를 고칠 것
  • ✅ await 안 한 resolves/rejects 전수 점검
  • vi.mock/vi.hoisted를 파일 최상위로
  • ✅ CI의 -t 패턴이 세그먼트를 걸치지 않는지 확인
  • test.sequential{ concurrent: false }
  • ✅ 산출물 경로 .vitest/ 이전 — .gitignore·아티팩트 업로드·stdout 파싱 스텝
  • ✅ 커버리지 include/exclude 패턴 재검토 (루트 상대 경로 + 디렉토리 취급)
  • VITEST_POOL_ID/VITEST_WORKER_ID를 쓰는 셋업은 1-base로
  • ✅ 브라우저 모드 사용 시: strict 로케이터 · async render() · SerializedLocator · browser.apiapi

참고 문서