Vite 프로젝트에서 Vitest로 테스트 환경 시작하기

2026.07.24·수정 2026.09.12·약 21분·작성: 해비·블로그 소개
요약
Vite 프로젝트에서 테스트를 시작한다면 Vitest는 가장 자연스럽게 검토할 수 있는 선택지입니다. 이 글은 설치, 실행 스크립트, 설정 파일, jsdom 환경, 테스트를 늘려 가는 순서를 실무 흐름에 맞춰 정리합니다.

Vite 프로젝트에서 Vitest를 쓰는 이유

Vite 프로젝트에서 Vitest 테스트 흐름을 보여주는 다이어그램

Vite로 만든 프론트엔드 프로젝트는 개발 서버, 모듈 해석, 빠른 변환 속도에 익숙해져 있습니다. 여기에 테스트 도구를 붙일 때도 같은 개발 경험을 유지하고 싶다면 Vitest가 좋은 출발점이 됩니다. VitestVite의 설정과 생태계를 적극적으로 활용하기 때문에, 이미 Vite를 쓰는 프로젝트에서는 테스트 환경을 비교적 자연스럽게 연결할 수 있습니다.

Jest를 써본 개발자라면 describe, it, expect, beforeEach 같은 테스트 작성 방식이 낯설지 않을 것입니다. Vitest도 이런 API를 비슷하게 제공하므로 테스트 문법 자체보다 프로젝트 설정과 실행 흐름에 집중할 수 있습니다. 특히 ESM, TypeScript, 최신 프론트엔드 도구 체인과 맞물릴 때 설정 부담이 줄어드는 편입니다.

Vite 생태계와 맞는 테스트 러너

Vitest의 장점은 단순히 빠르다는 말로 끝나지 않습니다. 이미 프로젝트가 vite.config.ts 또는 vite.config.js에 별칭, 플러그인, 환경 설정을 갖고 있다면 테스트에서도 그 설정을 함께 고려할 수 있습니다. 예를 들어 @ 같은 경로 별칭을 애플리케이션 코드에서 쓰고 있다면, 테스트 코드에서도 같은 방식으로 가져오기를 유지하는 것이 관리하기 쉽습니다.

Jest 대신 Vitest를 고려하는 상황

Jest가 나쁜 선택이라는 뜻은 아닙니다. 이미 큰 프로젝트에서 Jest 설정과 테스트 자산이 안정적으로 운영되고 있다면 그대로 유지하는 편이 나을 수 있습니다. 다만 새 Vite 프로젝트이거나, 기존 Vite 프로젝트에 처음 테스트를 붙이는 상황이라면 도구 체인을 억지로 늘리지 않고 시작할 수 있다는 점에서 Vitest가 실용적입니다.

Vitest 설치와 기본 실행

기존 Vite 프로젝트에 Vitest를 추가하는 흐름은 간단합니다. 먼저 개발 의존성으로 vitest를 설치하고, package.json에 테스트 실행 스크립트를 추가한 뒤, 작은 유틸 함수부터 테스트 파일을 만들어 실행해 보면 됩니다.

필요한 패키지 설치

npm을 사용한다면 다음 명령으로 vitest를 설치할 수 있습니다. pnpm이나 yarn을 쓰는 프로젝트라면 같은 의미의 설치 명령으로 바꾸면 됩니다.

npm install -D vitest

React 컴포넌트 테스트까지 염두에 둔다면 나중에 @testing-library/react, @testing-library/jest-dom, jsdom 같은 패키지가 추가로 필요할 수 있습니다. 하지만 처음부터 모두 설치할 필요는 없습니다. 테스트하려는 코드가 단순 함수인지, 브라우저 DOM을 다루는지에 따라 필요한 도구를 나누는 편이 좋습니다.

package.json 스크립트 구성

package.json에는 보통 test 스크립트와 test:run 스크립트를 나눠 둡니다. 개발 중에는 감시 모드가 편하고, CI에서는 한 번 실행하고 종료되는 명령이 필요하기 때문입니다.

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
  }
}

npm run test는 파일 변경을 감시하면서 테스트를 반복 실행하는 개발용 흐름에 잘 맞습니다. 반면 npm run test:runGitHub Actions 같은 자동화 환경에서 실패 여부만 확인할 때 적합합니다.

첫 테스트 파일 작성

처음 테스트할 코드는 복잡한 화면보다 작은 함수가 좋습니다. 이번 함수의 계약은 0 이상의 안전한 정수 원 단위 금액만 받는 것입니다. 소수·음수·NaN·Infinity는 RangeError로 거절합니다. 반올림이나 환율 계산은 이 함수의 책임이 아닙니다. 정상 금액뿐 아니라 허용하지 않을 입력부터 표로 정하면 테스트 기대값을 구현과 별도로 정할 수 있습니다.

실습 파일

파일 경로를 확인하고 같은 프로젝트 안에 저장하세요. 이미지 등 소스 목록에 없는 파일과 실행 안내는 실습 ZIP에 포함되어 있습니다.

전체 코드

index.html

<!doctype html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>테스트 실습</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

package.json

{
  "name": "testing-practice-7715",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "engines": {
    "node": ">=22.12.0"
  },
  "scripts": {
    "dev": "vite",
    "test": "vitest",
    "test:run": "vitest run",
    "typecheck": "tsc --noEmit",
    "build": "tsc --noEmit && vite build"
  },
  "devDependencies": {
    "vite": "8.3.0",
    "vitest": "4.1.11",
    "typescript": "7.0.2"
  }
}

src/formatPrice.test.ts

import { describe, expect, it } from 'vitest';
import { formatPrice } from './formatPrice';

describe('원 단위 정수 금액 표시', () => {
  it.each([
    [0, '0원'],
    [12000, '12,000원'],
    [1000000, '1,000,000원'],
  ])('%i를 %s로 표시한다', (input, expected) => {
    expect(formatPrice(input)).toBe(expected);
  });
  it.each([-1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1])(
    '유효하지 않은 금액 %s를 거절한다',
    (input) => {
      expect(() => formatPrice(input)).toThrow(RangeError);
    },
  );
});

src/formatPrice.ts

export function formatPrice(value: number): string {
  if (!Number.isSafeInteger(value) || value < 0) {
    throw new RangeError('금액은 0 이상의 안전한 정수여야 합니다.');
  }
  return `${value.toLocaleString('ko-KR')}원`;
}

src/index.css

body {
  margin: 0;
  font-family: sans-serif;
  color: #222;
  background: #fff;
}
main {
  max-width: 36rem;
  margin: 3rem auto;
  padding: 1rem;
}
form {
  display: grid;
  gap: 0.75rem;
}
input,
button {
  font: inherit;
  padding: 0.6rem;
}

src/main.ts

import { formatPrice } from './formatPrice';
import './index.css';

const root = document.getElementById('root')!;
const heading = document.createElement('h1');
heading.textContent = formatPrice(12000);
root.append(heading);

src/vite-env.d.ts

/// <reference types="vite/client" />

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["src", "vite.config.ts"]
}

vite.config.ts

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'node',
    include: ['src/**/*.test.{ts,tsx}'],
  },
});

src/formatPrice.ts 전체 코드 보기

src/formatPrice.test.ts 전체 코드 보기

테스트 파일 이름은 보통 formatPrice.test.ts처럼 대상 파일 이름에 .test를 붙입니다. 프로젝트에 따라 __tests__ 폴더를 따로 둘 수도 있지만, 작은 프로젝트에서는 대상 코드 옆에 테스트 파일을 두는 방식이 읽기 쉽습니다.

it.each는 입력 표의 각 행을 독립된 테스트로 만듭니다. 정상 입력 3개와 잘못된 입력 5개, 총 8개입니다. toThrow에는 함수 호출 결과 대신 호출을 감싼 함수를 전달해야 expect가 예외를 관찰할 수 있습니다. NaN을 문자열로 표시하는 코드가 실수로 추가되어도 거절 테스트가 잡습니다.

Vite 설정과 Vitest 설정 연결하기

Vitest 설정은 별도 파일로 분리할 수도 있고, vite.config.ts 안에 함께 둘 수도 있습니다. Vite 프로젝트에서 테스트를 시작하는 단계라면 기존 설정 파일에 test 옵션을 추가하는 방식이 이해하기 쉽습니다.

별도 vitest.config.ts가 있으면 Vitest는 그 설정을 우선하므로 vite.config.ts가 자동 합쳐진다고 가정하지 마세요. 이 실습은 vite.config.ts 하나에 설정을 모읍니다. 기존 React 프로젝트에서는 plugins와 resolve.alias를 지우지 않고 test만 더합니다.

vite.config에서 test 옵션 다루기

vite.config.ts에서 defineConfig를 사용하고 있다면 test 옵션에 테스트 환경, 전역 API 사용 여부, 설정 파일 등을 지정할 수 있습니다. 단, test 옵션의 타입을 자연스럽게 인식하려면 vitest/config에서 defineConfig를 가져오는 방식을 고려할 수 있습니다.

vite.config.ts 전체 코드 보기

globalsfalse로 두면 테스트 파일마다 describe, it, expect를 명시적으로 가져와야 합니다. 처음에는 조금 번거로워 보이지만, 어떤 API를 쓰는지 파일 안에서 바로 드러나기 때문에 유지보수에는 도움이 됩니다.

TypeScript 프로젝트에서 확인할 부분

TypeScript 프로젝트에서 globalstrue로 설정한다면 tsconfig.jsontypesvitest/globals를 추가해야 할 수 있습니다. 반대로 명시적 import를 쓰는 방식이라면 이 설정 없이도 충분한 경우가 많습니다.

{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

운영 중인 프로젝트라면 tsconfig.app.json, tsconfig.node.json, tsconfig.json이 나뉘어 있을 수 있습니다. 이때는 테스트 파일이 어느 설정의 영향을 받는지 확인해야 합니다. 설정을 한 곳에 급하게 몰아넣기보다, 현재 프로젝트의 TypeScript 구성 방식에 맞춰 최소한으로 추가하는 편이 안전합니다.

Vitest는 TypeScript를 실행 가능한 코드로 변환하지만 기본 실행이 tsc 타입 검사를 대신하지는 않습니다. ZIP의 build 스크립트는 tsc –noEmit 이후 vite build를 실행합니다. 런타임 기대값 통과와 정적 타입 통과를 별도로 확인하는 이유입니다.

jsdom과 테스트 환경 선택

node 테스트 환경과 jsdom 테스트 환경의 차이를 비교한 표

Vitest를 설정하다 보면 environment 값을 무엇으로 둘지 고민하게 됩니다. 기본적으로 단순 함수, 데이터 변환, 날짜 계산, 문자열 처리처럼 브라우저 화면과 무관한 코드는 node 환경이면 충분합니다. 이 경우 실행이 가볍고 테스트 의도도 명확합니다.

node 환경으로 충분한 경우

formatPrice, parseQuery, calculateTotal 같은 함수는 브라우저 windowdocument가 필요하지 않습니다. 이런 테스트에 jsdom을 붙이면 동작은 할 수 있지만, 필요 없는 환경을 켜는 셈입니다. 처음 테스트를 늘릴 때는 가능한 한 단순한 환경에서 검증할 수 있는 코드부터 시작하는 것이 좋습니다.

DOM이나 컴포넌트 테스트에 jsdom이 필요한 경우

document.createElement, localStorage, HTMLElement, React 컴포넌트 렌더링처럼 브라우저와 비슷한 환경이 필요하다면 jsdom을 설치하고 environmentjsdom으로 바꿉니다.

npm install -D jsdom
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "jsdom"
  }
});

모든 테스트에 jsdom이 필요한 것은 아닙니다. 프로젝트 전체 설정을 jsdom으로 두는 방식도 가능하지만, 테스트가 많아질수록 환경을 구분하고 싶어질 수 있습니다. 그때는 파일별 환경 주석이나 별도 설정 분리를 검토할 수 있습니다. 처음 도입 단계에서는 팀이 이해하기 쉬운 하나의 기준을 정하고, 예외가 생길 때 조정하는 편이 현실적입니다.

실무에서 테스트를 늘리는 순서

운영 중인 프로젝트에 테스트를 붙일 때 가장 흔한 실수는 처음부터 모든 것을 테스트하려고 하는 것입니다. 화면, 상태 관리, API 호출, 라우팅, 빌드 설정까지 한 번에 다루려 하면 테스트 자체보다 환경 정리에 더 많은 시간을 쓰게 됩니다. 시작점은 작아야 합니다.

유틸 함수부터 시작하기

가장 먼저 볼 대상은 입력과 출력이 분명한 함수입니다. 가격 포맷, 날짜 포맷, 배열 정렬, 권한 판정, 폼 값 검증처럼 부작용이 적은 함수는 테스트 작성 비용이 낮습니다. 이런 테스트는 실패했을 때 원인을 찾기도 쉽고, 팀 안에서 테스트 문화를 설명하기에도 좋습니다.

  • 입력과 출력이 명확한 function부터 테스트합니다.
  • API 호출이나 브라우저 상태에 강하게 묶인 코드는 뒤로 미룹니다.
  • 버그가 반복해서 발생한 로직은 우선순위를 높입니다.
  • 테스트 이름은 구현 방식보다 기대 동작을 설명하도록 작성합니다.

컴포넌트와 사용자 동작으로 확장하기

유틸 함수 테스트에 익숙해졌다면 컴포넌트 테스트로 확장할 수 있습니다. React라면 @testing-library/react, Vue라면 @vue/test-utils 같은 도구를 함께 사용합니다. 이때 관심사는 컴포넌트 내부의 상태 변수 이름이 아니라 사용자가 보는 텍스트, 클릭 후 바뀌는 결과, 비활성화 상태 같은 외부 동작입니다.

예를 들어 SubmitButton 컴포넌트를 테스트한다면 isLoading이라는 내부 prop 자체보다 로딩 중에 버튼이 비활성화되는지, 화면에 적절한 문구가 보이는지를 확인하는 편이 좋습니다. 테스트가 사용자 관점에 가까울수록 리팩터링에도 덜 흔들립니다.

CI에서 자동 실행하기

테스트가 몇 개라도 생겼다면 로컬에서만 실행하지 말고 CI에 연결하는 것이 좋습니다. 최소한 npm run test:run이 pull request나 main branch push 시점에 실행되도록 구성하면, 깨진 테스트가 코드 리뷰 전에 드러납니다.

CI에서는 저장소에 package-lock.json을 커밋한 다음 npm ci → npm run test:run → npm run build 순서로 실행하세요. 잠금 파일이 없는 첫 다운로드에서는 npm install로 생성합니다. 이 글은 특정 CI 서비스 설정이 아니라 테스트의 종료 코드와 재현 가능한 의존성 설치에 집중합니다.

CI 설정까지 연결하면 테스트는 개인의 습관이 아니라 프로젝트의 기본 안전장치가 됩니다. 다만 처음부터 커버리지 기준을 높게 걸면 팀이 테스트를 부담스럽게 받아들일 수 있습니다. 처음에는 핵심 로직이 계속 통과하는지 확인하는 데 집중하고, 이후 coverage 설정을 단계적으로 추가하는 편이 낫습니다.

정리: Vite 프로젝트의 테스트 출발점으로서

Vite 프로젝트에서 Vitest를 도입하는 일은 거창한 테스트 전략을 세우는 것보다 훨씬 작게 시작할 수 있습니다. vitest를 설치하고, package.json에 실행 스크립트를 추가하고, 작은 함수 하나를 테스트하는 것만으로도 프로젝트는 이미 테스트 가능한 구조로 한 걸음 이동합니다.

node 환경과 jsdom 환경을 구분하고, vite.config.ts에서 필요한 설정만 추가하며, TypeScript 타입 설정을 확인하면 초반에 부딪히는 대부분의 문제를 줄일 수 있습니다. 중요한 것은 모든 코드를 한 번에 테스트하는 것이 아니라, 변경이 잦고 실수가 반복되는 부분부터 검증 가능한 형태로 만드는 것입니다.

VitestVite 프로젝트에서 테스트를 시작하기 위한 부담 낮은 출발점입니다. 작은 유틸 함수 테스트에서 시작해 컴포넌트 테스트와 CI 실행으로 확장하면, 테스트는 별도의 큰 작업이 아니라 개발 흐름 안에 자연스럽게 들어올 수 있습니다.

독립 실습 파일과 실행 순서

아래 ZIP은 이 글만 읽어도 실행할 수 있는 완성 프로젝트입니다. 압축을 풀고 package.json이 있는 폴더에서 Node 22.12 이상을 사용합니다. 처음은 npm install, 이후 생성한 잠금 파일을 보관한 환경은 npm ci를 사용하세요. test는 감시 모드이며 test:run은 한 번 실행하고 종료합니다.

터미널

npm install
npm run test:run
npm run build
npm run dev

검증 버전은 package.json에 고정되어 있습니다. npm run dev는 독자가 화면을 직접 확인하는 명령입니다. 작성 검증은 Node/jsdom 테스트와 TypeScript·Vite 빌드까지 수행했으며 실제 브라우저의 화면·키보드·레이아웃 검증은 수행하지 않았습니다.

src/main.ts

src/main.ts 전체 코드 보기

index.html

index.html 전체 코드 보기

src/vite-env.d.ts의 /// <reference types="vite/client" />는 CSS import 타입을 연결합니다. 이 파일 없이 TypeScript 7에서 CSS 부수 효과 import 오류가 나면 테스트 환경을 바꾸는 대신 타입 선언부터 복구하세요.

검증 결과: 테스트 8개 통과, TypeScript 검사 및 Vite 프로덕션 빌드 통과. README의 오류 재현 연습은 일부러 코드를 바꾸고 실패를 확인한 뒤 복원하도록 구성했습니다.

의도적인 오류 검증도 실행했습니다. 금액 입력 검사를 제거하자 거절 사례가 실패했습니다. 변경을 복원한 완성 코드를 ZIP에 제공합니다.

설정 기준은 Vitest 환경 문서설정 문서에서 확인할 수 있습니다. DOM 실습의 이벤트와 matcher는 user-event 소개, jest-dom의 Vitest 연결을 따릅니다.

실습 프로젝트 ZIP 내려받기

이 글이 도움이 되었나요?

조회 중

테스트 학습 순서

필수 7개 · 전체 9개

읽음 기록 관리

전체 과정 목차 (9개)
  1. 필수 학습 · Vite 프로젝트에서 Vitest로 테스트 환경 시작하기 현재 글
  2. 필수 학습 · React Testing Library로 클릭·입력 테스트 작성하기
  3. 선택 참고 · Vitest document is not defined 오류 해결: jsdom 설정 기준
  4. 필수 선수 · Jest console.error 테스트: try catch 에러 검증
  5. 필수 학습 · React 검색 필터 테스트: 교집합·빈 결과·입력 초기화 검증
  6. 필수 학습 · TanStack Query 컴포넌트 테스트: QueryClient 격리와 실패·재시도·목록 갱신 검증
  7. 필수 학습 · 비동기 테스트 고급: 실패·재시도·경쟁 상태를 결정적으로 검증하기
  8. 필수 학습 · Playwright 첫 E2E 테스트: 로컬 앱 실행부터 결과 확인까지
  9. 선택 참고 · Playwright 테스트 실패 해결: locator가 요소를 찾지 못할 때 체크리스트

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기