Vitest Browser Mode 완벽 가이드 — 실제 브라우저 테스트·Playwright·비주얼 회귀
이 글의 핵심
Vitest Browser Mode는 테스트를 Node의 시뮬레이션 환경이 아니라 실제 브라우저 안에서 실행하는 기능입니다. window, document, 실제 이벤트 파이프라인, 레이아웃·렌더링 결과에 의존하는 UI를 검증할 때 JSDOM만으로는 부족한 경우가 많습니다. 이 글에서는 Browser Mode의 개념, Playwright와 WebdriverIO 프로바이더 선택, DOM·사용자 상호작용 API, 스냅샷·비주얼 회귀 테스트, 디버깅, CI/CD 연동, 그리고 Jest 대비 Vitest Browser Mode 관점까지 한 번에 정리합니다.
선행 지식: Vitest 기본 설정(
vitest.config), Vite 플러그인,test/expect문법에 익숙하다고 가정합니다. Vitest 입문은vitest-complete-guide포스트를 참고하십시오.
1. Browser Mode가 해결하는 문제
1.1 Node(jsdom) 테스트의 한계
전통적으로 프런트엔드 단위·컴포넌트 테스트는 jsdom 등으로 DOM을 흉내 냅니다. 논리·상태 전이 위주의 테스트에는 충분하지만, 다음과 같은 영역에서는 차이가 큽니다.
- 브라우저 전용 API:
ResizeObserver,IntersectionObserver, Canvas 측정, 일부 CSS·폰트 렌더링 동작 - 실제 이벤트·포커스·IME: 합성 이벤트와 실제 사용자 입력의 미묘한 차이
- 레이아웃·픽셀 단위 결과: 스타일 변경이 시각적으로만 드러나는 회귀
Browser Mode는 Vite 개발 서버 위에서 테스트 번들을 진짜 브라우저로 로드하므로, “브라우저에서만 재현되는” 문제를 테스트 코드로 끌어올 수 있습니다.
1.2 E2E와의 관계
Browser Mode는 브라우저에서 돌아가는 컴포넌트·단위 수준 테스트에 가깝습니다. 전체 앱을 URL부터 띄우고 여러 페이지를 오가는 엔드투엔드(E2E) 테스트(예: Playwright 단독 시나리오)와 역할이 겹치는 부분도 있지만, Vitest의 빠른 재실행·Vite와의 통합·같은 리포 안에서 단위/브라우저 테스트를 나누는 패턴에 강점이 있습니다. 팀에서는 빠른 피드백 = Vitest(jsdom) + Browser Mode, 사용자 여정 검증 = E2E로 나누는 경우가 많습니다.
2. 핵심 개념 정리
2.1 프로바이더(provider)가 필수인 이유
Browser Mode는 “브라우저 프로세스를 누가 띄우고 제어하느냐”를 프로바이더에게 맡깁니다. Vitest는 다음을 지원합니다.
| 구분 | 용도 |
|---|---|
preview (@vitest/browser-preview) | 로컬에서 화면을 빠르게 확인할 때. 이벤트는 시뮬레이션에 가깝습니다. |
playwright (@vitest/browser-playwright) | CI·헤드리스·병렬 실행에 권장. Chrome DevTools Protocol 기반. |
webdriverio (@vitest/browser-webdriverio) | WebDriver 생태계·기존 WDIO 인프라와 맞출 때. |
공식 문서는 CI와 헤드리스까지 고려하면 Playwright 또는 WebdriverIO로 전환할 것을 권장하며, 새 프로젝트에는 설정 용이성과 병렬 실행을 이유로 Playwright를 우선 제안합니다.
2.2 vitest/browser API
브라우저 전용 테스트에서는 page(페이지/로케이터), userEvent(실제에 가까운 입력), expect.element 등 브라우저 전용 단언을 사용합니다. 컴포넌트 렌더링은 프레임워크별 vitest-browser-react, vitest-browser-vue 등 패키지로 감싼 예시가 문서에 정리되어 있습니다.
2.3 제약 사항(알고 가면 디버깅이 빨라집니다)
alert/confirm등 스레드 블로킹 다이얼로그: 페이지 통신이 멈추므로 기본 목(mock)이 제공됩니다. 필요 시 직접 목 처리 권장.- 네이티브 ESM과
vi.spyOn: 브라우저에서는 모듈 네임스페이스가 봉인되어 있어import * as m후vi.spyOn(m, 'fn')이 실패할 수 있습니다. 이 경우vi.mock('./mod.js', { spy: true })패턴을 문서에서 안내합니다.
3. 설치와 초기 설정
3.1 CLI로 시작하기
공식적으로 npx vitest init browser 로 의존성과 기본 설정을 생성할 수 있습니다. 수동 설치 시에는 반드시 프로바이더 패키지를 함께 추가합니다.
Playwright 프로바이더 예시:
npm install -D vitest @vitest/browser-playwright playwright
npx playwright install
WebdriverIO 프로바이더 예시:
npm install -D vitest @vitest/browser-webdriverio webdriverio
로컬에서만 “미리보기” 수준이면 @vitest/browser-preview 만으로도 동작하지만, 실제 브라우저 자동화·헤드리스를 쓰려면 위와 같이 Playwright 또는 WebdriverIO를 설치하는 흐름이 일반적입니다.
3.2 vitest.config에서 Browser Mode 켜기
test.browser.enabled: true 와 최소 한 개의 instances 가 필요합니다. Playwright는 playwright() 팩토리를 import 해서 provider에 넘깁니다.
import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
});
React·Vue 등을 쓰는 경우 해당 Vite 플러그인(@vitejs/plugin-react 등)을 plugins에 넣어야 합니다. 프레임워크별 요구사항은 각 문서를 따릅니다.
3.3 Playwright vs WebdriverIO 브라우저 이름
- Playwright
instances:chromium,firefox,webkit - WebdriverIO
instances:chrome,edge,firefox,safari등 (문서의 호환 표 참고)
크로스 브라우저가 목표면 WebdriverIO·Selenium 그리드·원격 브라우저와 조합하는 시나리오를 검토할 수 있습니다. 반면 로컬·CI에서 빠르게 돌릴 목적이면 Playwright 단일 인스턴스가 단순합니다.
4. DOM 테스트와 사용자 상호작용(User Interactions)
4.1 page 로케이터와 단언
공식 예시처럼 page.getByText, page.getByLabelText 등으로 요소를 찾고, await expect.element(...).toBeInTheDocument() 형태로 DOM 단언을 합니다. @testing-library/jest-dom 계열을 포크한 단언이 내장되어 있어, 익숙한 스타일을 유지하기 쉽습니다.
import { expect, test } from 'vitest';
import { page } from 'vitest/browser';
test('문구가 화면에 보인다', async () => {
await expect.element(page.getByRole('heading', { name: /소개/i })).toBeInTheDocument();
});
4.2 userEvent와 로케이터의 fill / click
실제 브라우저 입력을 재현하려면 vitest/browser의 userEvent를 쓰는 것이 권장됩니다. 문서에서는 @testing-library/user-event를 브라우저 모드에서 직접 쓰지 말 것을 안내합니다. 이유는 해당 라이브러리가 이벤트를 “시뮬레이션”하는 반면, Vitest의 userEvent는 프로바이더에 따라 Chrome DevTools Protocol 또는 WebDriver를 통해 더 현실적인 경로로 동작하기 때문입니다.
import { page, userEvent } from 'vitest/browser';
// userEvent 사용
await userEvent.fill(page.getByLabelText(/이름/i), 'Alice');
// 로케이터 메서드로 동일 동작
await page.getByLabelText(/이름/i).fill('Alice');
폼·키보드·포커스가 많은 UI는 이 API를 기준으로 테스트를 작성하면 E2E에 가까운 신뢰도를 확보할 수 있습니다.
4.3 프레임워크 렌더 헬퍼
React라면 vitest-browser-react의 render, Vue는 vitest-browser-vue 등을 사용하면 screen 객체로 쿼리할 수 있습니다. 팀 표준에 맞춰 “브라우저 전용 테스트 디렉터리”와 “Node(jsdom) 테스트 디렉터리”를 나누는 것도 흔한 패턴입니다. projects 기능으로 프로젝트별 include·환경을 분리하면 스크립트가 명확해집니다.
5. 스냅샷과 비주얼(Visual) 테스트
5.1 DOM 스냅샷
기존 Vitest와 마찬가지로 toMatchInlineSnapshot 등을 사용할 수 있으며, Marko 등 일부 예시에서는 마크업 문자열 스냅샷이 함께 등장합니다. 다만 브라우저 모드의 시각적 회귀는 아래 toMatchScreenshot이 담당합니다.
5.2 toMatchScreenshot — 비주얼 회귀
요소 또는 페이지의 스크린샷을 기준 이미지와 비교합니다. 첫 실행 시 기준(reference) 이미지가 생성되며, 검토 후 재실행하면 이후부터 회귀 감지에 사용됩니다. 기본적으로 __screenshots__ 폴더에 브라우저·OS 정보가 파일명에 포함되어 환경별 덮어쓰기를 줄입니다.
import { expect, test } from 'vitest';
import { page } from 'vitest/browser';
test('히어로 섹션 비주얼', async () => {
await expect(page.getByTestId('hero')).toMatchScreenshot('hero-section');
});
5.3 안정적인 비주얼 테스트를 위한 실무 팁
- 폰트·GPU·OS 차이로 픽셀이 흔들립니다. CI에서는 동일 OS 이미지(예: Ubuntu) +
document.fonts.ready대기 + 고정 viewport (page.viewport또는instances의viewport)를 권장합니다. - 애니메이션은 플레이wright 프로바이더에서 단언 시 기본적으로 비활성화되는 등 안정화 옵션이 있습니다. 그래도 깜빡임이 남으면 CSS로 애니메이션을 끄는 방법을 고려합니다.
- 동적 콘텐츠(시간, 랜덤 ID)는 목 처리하거나, Playwright의
mask등으로 스크린샷에서 제외합니다. - 비교기(
pixelmatch등)의threshold,allowedMismatchedPixelRatio를 팀 기준에 맞게 조정합니다. 텍스트가 많은 영역은 허용 비율을 높이는 식으로 튜닝합니다.
5.4 실패 시 산출물
비주얼 테스트 실패 시 기준·실제·디프 이미지 경로가 로그에 표시됩니다. 디프 이미지는 빨간 영역이 큰 변화, 노란 점무늬는 안티앨리어싱 차이 등으로 해석할 수 있어, 원인이 스타일인지 환경인지 빠르게 나눕니다.
6. 디버깅 도구와 워크플로
6.1 브라우저 UI와 URL
기본적으로 개발 시 브라우저 UI 안에서 테스트가 iframe 등으로 실행됩니다. 헤드리스로 돌리면 UI가 자동으로 열리지 않을 수 있어, @vitest/ui와 --ui를 병행하는 방법이 문서에 소개됩니다. Vitest는 개발 서버와 충돌을 피하기 위해 기본 포트 63315 를 사용하며, watch 모드에서 b 키로 URL을 출력할 수 있습니다.
6.2 Headless와 CI
// vitest.config.ts 발췌
browser: {
provider: playwright(),
enabled: true,
headless: true,
},
CLI에서는 npx vitest --browser.headless 로도 지정 가능합니다. 헤드리스는 Playwright 또는 WebdriverIO 프로바이더가 있을 때 의미가 있습니다.
6.3 한계를 이용한 디버깅 전략
vi.spyOn으로 모듈 전체를 감시하려다 막히면 앞서 언급한 vi.mock(..., { spy: true }) 로 전환합니다. 브라우저 네이티브 ESM 특성을 이해하면 “왜 Node 테스트와 다르게 실패하는지”를 줄일 수 있습니다.
7. CI/CD 통합
7.1 브라우저 바이너리 설치
CI 에이전트에는 브라우저가 없으므로 의존성 설치 후 Playwright 브라우저를 깔아야 합니다. 예:
- name: Install Playwright Browsers
run: npx playwright install --with-deps
WebdriverIO는 팀에서 쓰는 드라이버·셀레늄 그리드에 맞춰 별도 스텝을 둡니다.
7.2 비주얼 테스트만 분리하기
비주얼 회귀는 실패 로그가 길고 스크린샷 리뷰가 필요하므로, package.json 스크립트로 test:unit 과 test:visual 을 나누거나, Vitest projects 로 unit / visual 프로젝트를 분리하는 방식이 문서에서 권장됩니다. PR에서는 단위·통합만 빠르게 돌리고, 야간 또는 수동 워크플로에서 비주얼 전체를 돌리는 식으로 부하를 조절할 수 있습니다.
7.3 기준 스크린샷 업데이트 플로
의도된 UI 변경 시 vitest --update 로 기준을 갱신합니다. 팀 규모가 크면 수동 트리거 워크플로로만 baseline을 갱신하도록 제한해, 잘못된 스크린샷이 main에 섞이는 것을 막습니다. 대용량 이미지는 Git LFS 도 고려합니다.
8. Jest vs Vitest Browser Mode
| 관점 | Jest (jsdom/happy-dom) | Vitest Browser Mode |
|---|---|---|
| 실행 환경 | 주로 Node + DOM 시뮬레이션 | 실제 브라우저 프로세스 |
| 번들·설정 | Jest 자체 설정 + 변환기 | Vite와 동일 파이프라인 |
| 속도 | 프로젝트에 따라 다름 | 브라우저 기동 비용 있음, 병렬·캐시로 완화 |
| API | expect, 모킹 익숙 | Vitest API + vitest/browser |
| 비주얼 | 보통 외부 도구 조합 | toMatchScreenshot 등 내장 흐름 |
Jest는 생태계와 레거시 테스트가 방대하지만, 이미 Vite + Vitest로 이전한 프로젝트에서는 Browser Mode로 “진짜 브라우저 검증”을 같은 도구 체인에 넣기 쉽습니다. 반대로 순수 로직·서버 유틸은 계속 Node 환경에서 돌려 비용을 줄이는 하이브리드가 현실적입니다.
9. 정리
Vitest Browser Mode는 실제 브라우저에서의 DOM·상호작용·비주얼 회귀를 한 프레임워크 안으로 가져오는 수단입니다. Playwright는 설정과 병렬 실행 측면에서 기본 추천이며, WebdriverIO는 기존 WebDriver 인프라와의 정합이 필요할 때 후보입니다. page·userEvent·expect.element로 행동을 검증하고, toMatchScreenshot으로 스타일 회귀를 잡으며, CI에서는 브라우저 설치·비주얼 분리·동일 OS를 맞추는 것이 실패율을 크게 낮춥니다. Node 테스트와 역할을 나누고, E2E와의 경계를 팀 규칙으로 명확히 하면 유지보수 비용 대비 효과가 가장 큽니다.
자주 묻는 질문
Q. JSDOM 테스트를 전부 Browser Mode로 바꿔야 할까요?
A. 아닙니다. 순수 로직·훅 단위 테스트는 Node 환경이 더 빠르고 안정적인 경우가 많습니다. 브라우저 API·레이아웃·실제 입력이 중요한 테스트만 Browser Mode로 옮기는 것이 좋습니다.
Q. @testing-library/user-event를 쓰면 안 되나요?
A. Browser Mode에서는 vitest/browser의 userEvent 사용이 권장됩니다. 시뮬레이션 이벤트와 실제 브라우저 자동화 경로의 차이로 인한 플레이크를 줄이기 위함입니다.
Q. Vitest 3.2 이후 --browser 만으로는 왜 실패하나요?
A. 설정에 browser 관련 옵션이 없을 때 CLI만 --browser 를 주면, Node 테스트와 브라우저 테스트를 구분할 수 없어 실패하도록 변경되었습니다. vitest.config에 test.browser 를 명시하십시오.
Q. 스냅샷이 로컬과 CI에서 다르면?
A. 폰트·OS·GPU 차이가 원인인 경우가 많습니다. CI OS를 고정하고, 폰트 로딩 대기·viewport 고정·비주얼 전용 잡을 분리하는 방식으로 맞춥니다.