[TYPESCRIPT] Vitest + TypeScript 통합: 빠르게 돌리되 흔들리지 않게 만드는 구성

 

Jest에 익숙한 팀이 Vitest로 넘어갈 때, 처음 체감하는 건 속도입니다. 실행이 빠르고 watch 반응도 가볍습니다.

 

그런데 막상 프로젝트에 붙이면, 테스트가 “돌긴 도는데” 신뢰하기 애매한 구간이 생깁니다. 환경이 어디까지 Jest와 같은지, TypeScript 타입 체크는 어디에서 책임질지, 경로 별칭은 누가 해석하는지 같은 문제들입니다.

 

Vitest는 Vite 생태계에 얹혀 있기 때문에 구성이 잘 맞으면 단순하고 빠릅니다. 반대로 경계가 어긋나면, 타입은 맞는데 테스트만 실패하거나, 반대로 테스트는 통과하는데 타입이 깨지는 상태가 생깁니다.

 

Vitest를 쓰게 되는 이유는 대부분 “개발 속도”다

Vitest는 기본적으로 ESM과 Vite의 모듈 해석 방식을 전제로 합니다. 그래서 프론트엔드(Vite/React/Vue) 쪽에서는 자연스럽게 붙습니다. Node 백엔드에서도 쓸 수 있지만, 그때는 환경을 명시적으로 잡아줘야 합니다.

 

TypeScript 통합에서 가장 먼저 정해야 하는 건 이겁니다. 테스트 실행 단계에서 타입 체크까지 할 것인지, 아니면 테스트는 빠르게 돌리고 타입 체크는 별도로 강제할 것인지입니다. 팀/환경에 따라 달라질 수 있습니다.

 

설치

기본적으로는 Vitest와 TypeScript가 필요합니다. Vite 프로젝트라면 vite는 이미 들어가 있는 경우가 많습니다.

 


npm i -D vitest typescript

 

Node 환경에서 JSDOM이 필요하면 jsdom을 추가합니다.

 


npm i -D jsdom

 

vitest 설정: vite.config.ts에 붙이는 방식

Vitest는 보통 Vite 설정과 같이 관리합니다. 테스트 설정이 따로 놀지 않아서 유지가 편합니다.

 


// vite.config.ts
import { defineConfig } from 'vite';

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

 

environment는 node 또는 jsdom 중 하나를 명시하는 편이 좋습니다. 명시하지 않으면 프로젝트에 따라 기본값이 기대와 달라질 수 있습니다.

 

TypeScript 타입(전역) 설정

Vitest는 expect, describe 같은 전역을 제공합니다. 테스트 파일에서 타입 오류가 나지 않도록 types를 추가해 둡니다.

 


// tsconfig.test.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "types": ["vitest/globals", "node"]
  },
  "include": ["src", "test"]
}

 

테스트 전용 tsconfig를 따로 두는 이유는 단순합니다. prod 코드 tsconfig에 테스트 전역 타입이 섞이면, 빌드 대상과 테스트 대상의 경계가 흐려집니다.

 

경로 별칭(path alias)과 테스트

Vitest는 Vite의 해석 방식을 따르기 때문에, alias를 Vite에 정의하면 테스트에서도 그대로 적용됩니다. 이게 Jest와 비교했을 때 가장 편한 부분 중 하나입니다.

 


// vite.config.ts
import { defineConfig } from 'vite';
import path from 'node:path';

export default defineConfig({
  resolve: {
    alias: {
      '@src': path.resolve(__dirname, './src'),
    },
  },
  test: {
    environment: 'node',
  },
});

 

여기서 중요한 건, tsconfig의 paths와 Vite alias를 일치시키는 것입니다. 둘이 어긋나면 IDE에서는 잘 되는데 테스트에서 깨지거나, 반대로 테스트는 되는데 타입 체크가 실패하는 상태가 생깁니다.

 

테스트 실행 스크립트

실무에서는 보통 다음처럼 나눕니다.

 


// package.json
{
  "scripts": {
    "test": "vitest",
    "test:watch": "vitest --watch",
    "test:ci": "vitest run",
    "typecheck": "tsc -p tsconfig.test.json --noEmit"
  }
}

 

vitest run은 CI에서 쓰기 좋습니다. watch 모드가 아니라 한 번 실행하고 종료합니다.

 

Mocking과 테스트 격리

Vitest는 vi를 통해 mocking을 제공합니다. Jest 스타일과 비슷하지만, 미묘하게 다릅니다.

 


import { vi, describe, it, expect } from 'vitest';

describe('example', () => {
  it('mock', () => {
    const fn = vi.fn().mockReturnValue(1);
    expect(fn()).toBe(1);
  });
});

 

mock이 테스트 간에 누수되기 쉬운 프로젝트에서는 clearMocks/restoreMocks를 설정으로 켜두는 편이 안정적입니다. 특히 모노레포에서 테스트가 많이 돌면, 한 번의 누수가 전체 결과를 흔들기도 합니다.

 

자주 겪는 문제

Vitest를 붙인 뒤 흔히 마주치는 문제는 다음 쪽입니다.

 

  • Node 환경인데 jsdom으로 돌고 있어서 예상치 못한 전역(window)이 생긴다
  • ESM/CJS 경계에서 import 방식이 깨진다
  • tsconfig paths와 Vite alias가 어긋난다
  • 테스트는 통과하지만 타입 체크를 별도로 안 돌려서 타입 오류가 쌓인다

 

Vitest 자체는 빠르게 돌아가는데, 그 속도 때문에 “타입 체크까지 포함됐다고 착각”하는 경우가 생깁니다. Vitest가 타입을 이해하는 건 맞지만, 타입 오류를 빌드처럼 강제해 주는 구조는 아닙니다. 그래서 typecheck를 CI에 넣는 편이 보통은 안전합니다.

 


 

Vitest + TypeScript 통합은 구성 자체는 단순한 편입니다. Vite 설정을 그대로 공유할 수 있어서 alias나 모듈 해석이 자연스럽게 맞습니다.

 

다만 빠른 테스트 실행과 타입 안정성은 별개의 축입니다. 테스트는 Vitest로 가볍게 돌리고, 타입 체크는 tsc로 별도 강제하는 방식이 프로젝트가 커질수록 현실적인 선택이 되는 경우가 많습니다.

 

구성은 팀/환경에 따라 달라질 수 있습니다. 하지만 alias 일치, 환경 명시, 타입 체크 책임 분리는 Vitest를 오래 쓰기 위해 결국 다시 확인하게 되는 지점들입니다.