React + Vite + TypeScript 프로젝트에 테스트 환경을 붙이는 과정을 빈 폴더에서 첫 통과까지 통째로 따라 하는 글입니다. Vitest·Testing Library·jsdom을 설치하고, 설정 파일을 줄 단위로 이해하고, 첫 테스트를 초록불로 만들고, 세팅에서 자주 막히는 지점까지 — 이 글 하나로 끝나요.
시리즈로 따라오고 계셨다면, 0장에서 ‘왜’와 ‘무엇을’을 챙겼으니 이제 1장 — 직접 만드는 단계입니다. 이 편부터 하나의 데모 앱(사용자를 검색하는 작은 대시보드)에 테스트를 얹어가요. 검색으로 처음 오셨어도 괜찮습니다. 어차피 빈 프로젝트에서 시작하니, 그대로 따라오시면 됩니다. 오늘의 목표는 단순해요. 첫 초록불 하나 켜기.
실습 코드: 이 편의 상태가
step-03태그에 그대로 담겨 있습니다.git clone후git checkout step-03으로 따라오세요. 완성본이 궁금하면main을 보면 됩니다.
이번 편에서 배울 것#
- Vitest·Testing Library·jsdom 설치와 설정
npm test로 첫 테스트 실행- 설정에서 자주 막히는 지점
준비물: Node.js 18 이상, 터미널, 코드 에디터(VS Code 등). 터미널에서
node -v가 버전을 출력하면 준비 끝입니다.

프로젝트와 도구 설치#
Vite의 React + TypeScript 템플릿에서 시작합니다. 첫 명령이 프로젝트 뼈대를 만들어주고, 두 번째 줄에서 그 폴더로 들어가 기본 의존성을 설치해요.
npm create vite@latest frontend-testing-lab -- --template react-ts
cd frontend-testing-lab && npm install여기에 테스트 도구를 더합니다. -D는 “개발할 때만 쓰는 패키지"라는 표시예요(배포되는 앱 코드에는 포함되지 않습니다).
npm i -D vitest @vitest/coverage-v8 jsdom \
@testing-library/react @testing-library/jest-dom @testing-library/user-event한 번에 여섯 개나 설치하니 겁먹기 쉬운데, 각자 맡은 일이 하나씩이라 생각보다 단순합니다.
| 패키지 | 하는 일 |
|---|---|
vitest | 테스트를 찾아서 실행하고 결과를 알려주는 러너 — 오늘의 주인공 |
jsdom | Node 안에서 브라우저 화면(DOM)을 흉내 내는 가짜 브라우저 |
@testing-library/react | 컴포넌트를 그리고(render), 화면에서 요소를 찾는(screen) 도구 |
@testing-library/jest-dom | toBeInTheDocument 같은 화면 검증용 매처 모음 |
@testing-library/user-event | 클릭·타이핑을 재현하는 도구 (나중에 본격 등장) |
@vitest/coverage-v8 | 커버리지 리포트 생성기 (나중에 사용) |
이 중 핵심 네 도구가 겹쳐서 일하는 모습을 그림으로 보면 이렇습니다.

왜 Jest가 아니라 Vitest냐고요? Vite 프로젝트라면 Vitest가 자연스러운 선택이에요. 앱이 쓰는 Vite 설정을 그대로 재사용하고, Jest와 API가 거의 호환되며, 빠릅니다.
설정 파일#
vite.config.ts 하나에 앱과 테스트 설정을 함께 둡니다. (데모 repo의 실제 설정입니다.) 처음엔 옵션이 많아 보이지만, 지금 이해해야 할 것은 주석이 달린 두 줄 — environment와 globals — 뿐이에요.
/// <reference types="vitest/config" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom', // 브라우저 DOM 흉내
globals: true, // it/expect를 import 없이
setupFiles: './src/test/setup.ts',
css: false,
exclude: ['e2e/**', 'node_modules/**', 'dist/**'], // E2E는 Playwright 몫
coverage: { provider: 'v8', reporter: ['text', 'html'] },
},
})setupFiles에 지정한 셋업 파일은 모든 테스트가 시작되기 전에 한 번씩 실행되는 준비 코드예요. 지금은 jest-dom 매처(화면 검증용 비교 도구들)를 등록하는 한 줄이 전부입니다. (나중에 MSW 설정이 여기에 추가돼요.)
// src/test/setup.ts
import '@testing-library/jest-dom/vitest'package.json 스크립트도 정리합니다.
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"typecheck": "tsc -p tsconfig.test.json --noEmit"
}vitest run은 한 번 실행하고 끝나는 모드, vitest(run 없이)는 watch 모드입니다. watch 모드로 켜두면 파일을 저장할 때마다 관련 테스트가 자동으로 다시 돌아요 — 코드 고치고, 저장하고, 초록불 확인하고. 이 리듬이 테스트와 친해지는 지름길이라, 개발 중에는 npm run test:watch를 켜두는 걸 추천합니다. (typecheck에 나오는 tsconfig.test.json은 아직 안 만들었는데, 바로 아래 “자주 막히는 지점"에서 왜 필요한지와 함께 다룹니다.)
첫 테스트#
테스트를 쓰기 전에 잠깐 — Vite 템플릿이 만들어준 기본 App.tsx(로고 돌아가는 그 화면)를 우리 데모 앱의 시작점으로 바꿔둡니다. 이걸 건너뛰면 잠시 뒤의 테스트가 빨간불이 되니 꼭 해주세요.
// src/App.tsx — 오늘은 이 정도면 충분합니다
export default function App() {
return (
<main>
<h1>대시보드</h1>
<p>프론트엔드 테스트 연습용 데모 앱입니다.</p>
</main>
)
}(데모 repo의 App에는 사용자 검색 컴포넌트까지 들어 있지만, 그건 나중에 함께 만듭니다. 오늘은 제목 하나면 돼요.)
이제 가장 단순한 테스트로 파이프라인이 도는지 확인합니다. 파일은 src/App.test.tsx — 테스트할 파일 바로 옆에, 이름 끝에 .test.tsx를 붙여 만듭니다. Vitest는 이 패턴의 파일을 자동으로 찾아서 실행해요.
import { it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import App from './App'
it('앱 제목이 보인다', async () => {
render(<App />)
expect(screen.getByRole('heading', { level: 1, name: '대시보드' })).toBeInTheDocument()
})처음 보는 문법을 하나씩 풀어볼게요.
it('설명', 함수)— 테스트 한 개를 정의합니다. 첫 인자의 문장이 실패 리포트에 그대로 뜨니, “무엇을 확인하는지"를 사람 말로 적어두세요.render(<App />)— 컴포넌트를 가짜 브라우저(jsdom)에 실제로 그립니다.screen.getByRole(...)— 그려진 화면에서 요소를 찾습니다. 여기서는 “레벨 1 제목이면서 이름이 ‘대시보드’인 것"을 찾았어요.expect(값).매처()— “이 값이 이래야 한다"는 선언입니다.toBeInTheDocument처럼 점 뒤에 붙는 비교 방법을 **매처(matcher)**라고 불러요.
it으로 정의하고 expect로 선언한다 — 이 두 문장이 앞으로 시리즈 내내 우리의 기본 문형입니다.
npm test
# ✓ src/App.test.tsx (1 test)
#
# Test Files 1 passed (1)
# Tests 1 passed (1)초록 체크가 떴다면 성공입니다. 만약 빨간불이라면 — 당황하지 말고 에러 메시지 첫 줄을 읽어보세요. 대부분 바로 아래 “자주 막히는 지점” 중 하나입니다.
자주 막히는 지점#
document is not defined→environment: 'jsdom'누락toBeInTheDocument가 없다는 타입 에러 → setup에서@testing-library/jest-dom/vitestimport 누락- E2E 파일까지 Vitest가 집는 문제 →
exclude: ['e2e/**'] - 빌드(tsc)가 테스트 파일까지 검사 →
tsconfig.app.json의exclude에src/**/*.test.tsx,e2e추가 - 그럼 테스트 파일의 타입 오류는 누가 잡지? → 좋은 질문이에요. 빌드에서 뺐으니 그대로 두면 아무도 안 잡습니다. 테스트 전용
tsconfig.test.json을 만들고"typecheck": "tsc -p tsconfig.test.json --noEmit"스크립트를 추가하세요. 데모 repo에서 이 설정을 넣자마자 숨어 있던 타입 이슈 2건이 바로 잡혔습니다. CI에도 한 줄이면 들어가요.
마지막 두 항목(tsconfig 분리)은 처음이라면 어렵게 느껴질 수 있어요. 지금 완전히 이해하지 못해도 괜찮습니다 — 첫 초록불에는 영향이 없고, 실제로 그 문제에 부딪히면 그때 이 목록으로 돌아오면 됩니다. 데모 repo에는 전부 설정돼 있으니 비교해볼 수도 있고요.
벌써 접근성이 등장했어요.
getByRole('heading', ...)은 “제목이 렌더됐나"와 “스크린 리더가 제목으로 인식하나"를 동시에 검증합니다. 이 이야기는 뒤에서 깊게 다룰 예정이에요.
한 장 요약#
- 설치는 두 줄: Vite React-TS 템플릿 생성 +
npm i -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event @vitest/coverage-v8— 여섯 개지만 각자 맡은 일이 하나씩 - 설정은
vite.config.ts하나에: 지금 이해할 건environment: 'jsdom'(가짜 브라우저)과globals: true두 줄 setupFiles의 셋업 파일엔 jest-dom 등록 한 줄 — 화면 검증용 매처가 열립니다- 테스트 파일은 대상 파일 옆에
*.test.tsx—it('설명', ...)으로 정의하고expect(값).매처()로 선언 - 빨간불이면 에러 첫 줄부터 읽고, 위의 “자주 막히는 지점” 목록과 대조
첫 초록불을 켰습니다#
환경이 준비됐습니다. 다음 편부터 본격적으로 단위 테스트의 기초 체력을 길러볼게요.
첫 초록불이 켜지는 순간의 도파민은 진짜입니다. PASS ✓ 한 줄 보려고 우리는 이 고생을… 아니, 이 즐거움을 하는 거죠.
레벨업: 테스트 환경을 맨손으로 세팅하고 첫 초록불을 켤 수 있습니다.
다음 편: 단위 테스트 기초 — 순수 함수·경계값·AAA 패턴
낯선 용어가 있었다면 — 용어집에 전부 한 줄씩 정리돼 있어요.
