vitest + RTL + happy-dom — Vite + Tailwind v4 프로젝트 첫 도입 패턴¶
맥락¶
Vite + Tailwind v4 + shadcn/ui 스택에 vitest를 처음 도입할 때 발생하는 설정 충돌과 Radix UI 호환 문제를 다룬다. 기존에 테스트 인프라가 전혀 없는 상태에서 시작하는 경우를 전제한다.
관련 스택: React 19 + TypeScript + Vite 7 + Tailwind v4 + shadcn/ui + TanStack Query v5
가이드¶
1. vitest.config.ts — mergeConfig 패턴¶
vite.config.ts를 직접 import하고 mergeConfig로 병합한다. Tailwind v4 플러그인은 vite.config에만 선언되어 있으므로, vitest 전용 config를 별도로 작성하면 플러그인이 누락되어 테스트 환경에서 CSS 처리가 깨진다.
// vitest.config.ts
import { mergeConfig } from 'vite';
import { defineConfig } from 'vitest/config';
import viteConfig from './vite.config';
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'happy-dom',
setupFiles: ['./src/test/setupTests.ts'],
globals: false, // 명시적 import 권장 (IDE 자동완성·타입 안전성)
},
})
);
2. setupTests.ts — happy-dom + Radix UI stub¶
@testing-library/jest-dom/vitest 서브패키지를 사용한다. 표준 경로(@testing-library/jest-dom)는 vitest와 함께 사용 시 v6+에서 ReferenceError: expect is not defined가 발생한다.
Radix UI는 hasPointerCapture, releasePointerCapture, scrollIntoView 3개 API를 사용하는데 happy-dom 15+에서 미구현 상태이므로 stub이 필요하다.
// src/test/setupTests.ts
import '@testing-library/jest-dom/vitest';
// happy-dom 15+ 미구현 Radix UI API stub
if (!Element.prototype.hasPointerCapture) {
Element.prototype.hasPointerCapture = () => false;
}
if (!Element.prototype.releasePointerCapture) {
Element.prototype.releasePointerCapture = () => {};
}
if (!Element.prototype.scrollIntoView) {
Element.prototype.scrollIntoView = () => {};
}
3. TanStack Query 훅 테스트 격리 wrapper¶
훅 테스트는 매 호출마다 새 QueryClient 인스턴스를 만들어야 테스트 간 캐시가 오염되지 않는다. retry: false, staleTime: 0, gcTime: 0 3개 옵션이 없으면 재시도·캐시 동작이 테스트를 불안정하게 만든다.
// src/test/queryClientWrapper.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
export function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: 0, gcTime: 0 },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
사용법:
API 모킹은 MSW 없이 vi.mock('@/services/api-client')로 충분하다.
4. 패키지 설치¶
npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom happy-dom
왜 중요한가¶
mergeConfig없이 vitest 전용 config를 작성하면 Tailwind v4 플러그인이 누락되어 테스트 환경에서@import "tailwindcss"처리가 실패하거나 경고가 쌓인다@testing-library/jest-dom(표준 경로) 사용 시 vitest에서expect.extend가 이미 초기화되지 않아 런타임 에러가 발생한다 — 서브패키지 경로가 필수- Radix UI stub 3개를 누락하면 Dialog, Select, Popover 등 Radix 컴포넌트가 포함된 모든 테스트에서
TypeError가 발생한다 - TanStack Query wrapper에서
retry: false가 없으면 실패한 쿼리가 3회 재시도하며 테스트가 느려지고waitFor타임아웃이 발생한다
5. globals: false + @testing-library/react 자동 cleanup 비활성 함정 (9-D1에서 실증)¶
메커니즘¶
vitest.config.ts에 globals: false(명시적 import 모드)를 설정하면 @testing-library/react의 자동 cleanup이 비활성화된다.
RTL은 afterEach(cleanup)을 자동 등록할 때 globalThis.afterEach가 존재하는지 확인한다. globals: false에서는 afterEach가 전역으로 노출되지 않으므로, RTL이 자동 등록을 건너뛴다. 결과적으로 각 테스트가 끝난 뒤 DOM이 초기화되지 않아 이전 테스트 DOM이 누적된다.
증상¶
globals: false환경에서 같은 파일에 describe 블록이 2개 이상 있을 때 12개 테스트 실패- 에러 메시지:
Found multiple elements with the role "button" and name "이전"(이전 테스트 DOM 잔존) - 단일 describe 파일에서는 증상이 없음 — 우연히 통과
기존 4개 파일이 우연히 통과한 이유¶
기존 18개 테스트 파일(ConfirmDialog.test.tsx, AssignBatchModal.test.tsx 등)은 파일당 단일 describe 블록으로 작성되어 있었다. 단일 describe 안에서는 테스트가 순서대로 실행되며 DOM 누적이 드러나지 않는다. → Pagination.test.tsx가 2개 describe(compact / full)를 가지는 첫 파일로 실패를 실증했다.
해결¶
모든 테스트 파일 최상단에 수동 cleanup을 추가한다:
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
afterEach(cleanup);
기존 4개 파일도 소급 추가 (9-D1 evaluator 2차 리뷰에서 지적 → 전면 추가): - ConfirmDialog.test.tsx - AssignBatchModal.test.tsx - AssignBatchModal.handlers.test.tsx - useBatchLogs.test.ts
적용 커밋¶
e827b9a (Phase 9-D1)
적용 시기¶
- Vite + Tailwind v4 + shadcn/ui 스택에 vitest를 처음 도입할 때 → 이 파일의 설정을 그대로 복사
- TanStack Query 훅 테스트를 작성할 때 →
createWrapper()를src/test/queryClientWrapper.tsx에서 import - happy-dom에서 Radix 컴포넌트 테스트 시 stub이 없어 TypeError가 발생하면 → setupTests.ts 확인
globals: false를 유지하는 이유:describe,it,expect,vi를 명시적으로 import해야 IDE 자동완성과 TypeScript 타입 추론이 정확하게 동작한다
관련 자료¶
- 실제 적용 커밋: AssetManagement/Frontend
4e01d30(Phase 9-C.5) - 관련 기록: 2026-04-09-tailwind-v4-shadcn-theme-inline-도입-패턴 — Tailwind v4 설정 맥락
- 후속 기록: 2026-04-10-radix-dismiss-happydom-한계-dialog-mock-2층-테스트-구조 — Radix dismiss 테스트 한계
- 후속 기록: 2026-04-10-vitest-hoisting-vi-hoisted-referenceError — vi.mock factory hoisting 이슈
- 작업지시서: Planning/작업_지시서_Phase9C5_테스트_기반_구축
- 후속 기록(재발): 9-D1 e827b9a — globals: false cleanup 비활성 12개 실패 실증. 섹션 5 추가. Planning/작업_지시서_Phase9D1_Table_Tabs_Pagination