# 데모 앱에 Vitest 붙이기 — 첫 초록불까지

> React + Vite + TypeScript 데모 앱에 Vitest와 Testing Library를 설치하고, 첫 테스트를 초록불로 만드는 과정을 따라 합니다.

**Published:** 2026-07-05 | **Updated:** 2026-07-06

---



> 「프론트엔드 테스트 제대로 하기」 시리즈의 **3편**입니다. [전체 목차 보기](/series/프론트엔드-테스트-제대로-하기/) · [용어집](/posts/frontend-testing-glossary/)

React + Vite + TypeScript 프로젝트에 테스트 환경을 붙이는 과정을 **빈 폴더에서 첫 통과까지** 통째로 따라 하는 글입니다. Vitest·Testing Library·jsdom을 설치하고, 설정 파일을 줄 단위로 이해하고, 첫 테스트를 초록불로 만들고, 세팅에서 자주 막히는 지점까지 — 이 글 하나로 끝나요.

시리즈로 따라오고 계셨다면, 0장에서 '왜'와 '무엇을'을 챙겼으니 이제 1장 — 직접 만드는 단계입니다. 이 편부터 **하나의 데모 앱**(사용자를 검색하는 작은 대시보드)에 테스트를 얹어가요. 검색으로 처음 오셨어도 괜찮습니다. 어차피 빈 프로젝트에서 시작하니, 그대로 따라오시면 됩니다. 오늘의 목표는 단순해요. **첫 초록불** 하나 켜기.

> 실습 코드: 이 편의 상태가 [`step-03` 태그](https://github.com/IsaacEryn/frontend-testing-lab/tree/step-03)에 그대로 담겨 있습니다. `git clone` 후 `git checkout step-03`으로 따라오세요. 완성본이 궁금하면 [`main`](https://github.com/IsaacEryn/frontend-testing-lab)을 보면 됩니다.

## 이번 편에서 배울 것

- Vitest·Testing Library·jsdom 설치와 설정
- `npm test`로 첫 테스트 실행
- 설정에서 자주 막히는 지점

> **준비물**: [Node.js](https://nodejs.org) 18 이상, 터미널, 코드 에디터(VS Code 등). 터미널에서 `node -v`가 버전을 출력하면 준비 끝입니다.

{{< img src="images/contents/vitest-first-pass.png" alt="npm test를 실행한 터미널 화면 - Vitest v4가 src/App.test.tsx의 '앱 제목이 보인다' 테스트 1개를 초록색 체크 표시와 함께 통과시키고, Test Files 1 passed와 Duration 746ms를 출력했습니다" >}}

---

## 프로젝트와 도구 설치

Vite의 React + TypeScript 템플릿에서 시작합니다. 첫 명령이 프로젝트 뼈대를 만들어주고, 두 번째 줄에서 그 폴더로 들어가 기본 의존성을 설치해요.

```bash
npm create vite@latest frontend-testing-lab -- --template react-ts
cd frontend-testing-lab && npm install
```

여기에 테스트 도구를 더합니다. `-D`는 "개발할 때만 쓰는 패키지"라는 표시예요(배포되는 앱 코드에는 포함되지 않습니다).

```bash
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` | 커버리지 리포트 생성기 (나중에 사용) |

이 중 핵심 네 도구가 겹쳐서 일하는 모습을 그림으로 보면 이렇습니다.

{{< img src="images/contents/testing-stack.png" alt="테스트 도구들의 중첩 구조 다이어그램 - 가장 바깥에 테스트를 찾아 실행하는 러너 Vitest, 그 안에 브라우저 화면을 흉내 내는 jsdom, 다시 그 안에 컴포넌트를 그리고 요소를 찾는 Testing Library와 화면 검증 매처를 더하는 jest-dom이 나란히 놓여 있고, 아래에서 vite.config.ts 파일 하나가 전체를 설정한다는 화살표가 연결되어 있습니다" >}}

왜 Jest가 아니라 Vitest냐고요? Vite 프로젝트라면 Vitest가 자연스러운 선택이에요. 앱이 쓰는 Vite 설정을 그대로 재사용하고, Jest와 API가 거의 호환되며, 빠릅니다.

---

## 설정 파일

`vite.config.ts` 하나에 앱과 테스트 설정을 함께 둡니다. (데모 repo의 실제 설정입니다.) 처음엔 옵션이 많아 보이지만, 지금 이해해야 할 것은 주석이 달린 두 줄 — `environment`와 `globals` — 뿐이에요.

```ts
/// <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 설정이 여기에 추가돼요.)

```ts
// src/test/setup.ts
import '@testing-library/jest-dom/vitest'
```

`package.json` 스크립트도 정리합니다.

```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`(로고 돌아가는 그 화면)를 우리 데모 앱의 시작점으로 바꿔둡니다. 이걸 건너뛰면 잠시 뒤의 테스트가 빨간불이 되니 꼭 해주세요.

```tsx
// src/App.tsx — 오늘은 이 정도면 충분합니다
export default function App() {
  return (
    <main>
      <h1>대시보드</h1>
      <p>프론트엔드 테스트 연습용 데모 앱입니다.</p>
    </main>
  )
}
```

(데모 repo의 App에는 사용자 검색 컴포넌트까지 들어 있지만, 그건 나중에 함께 만듭니다. 오늘은 제목 하나면 돼요.)

이제 가장 단순한 테스트로 파이프라인이 도는지 확인합니다. 파일은 `src/App.test.tsx` — 테스트할 파일 바로 옆에, 이름 끝에 `.test.tsx`를 붙여 만듭니다. Vitest는 이 패턴의 파일을 자동으로 찾아서 실행해요.

```tsx
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`로 선언한다 — 이 두 문장이 앞으로 시리즈 내내 우리의 기본 문형입니다.

```bash
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/vitest` import 누락
- **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 패턴

> 낯선 용어가 있었다면 — [용어집](/posts/frontend-testing-glossary/)에 전부 한 줄씩 정리돼 있어요.

