- 공유 링크 만들기
- X
- 이메일
- 기타 앱
Featured Post
작성자:
Iros
- 공유 링크 만들기
- X
- 이메일
- 기타 앱

요즘 개발하면서 Vitest 이야기를 정말 자주 듣게 되더라고요. 저도 처음부터 Vitest를 좋아했던 건 아니에요. 원래는 Jest를 꽤 오래 썼고, 익숙한 도구를 굳이 바꾸는 걸 별로 좋아하지 않는 편이거든요. 20년 넘게 개발하다 보면 새 도구가 나올 때마다 따라가는 게 능사는 아니라는 걸 몸으로 알게 됩니다. 그런데 어느 날 팀 프로젝트에서 테스트 실행 시간이 슬슬 거슬리기 시작했어요. 저장하고, 기다리고, 다시 고치고, 또 기다리고. 별거 아닌 것 같아도 하루에 수십 번 반복되면 은근히 사람을 지치게 하잖아요.
그때 동료가 “형, 이거 Vitest로 한 번만 바꿔보시죠” 하더라고요. 처음엔 속으로 또 새로운 거냐 싶었는데, 막상 적용해보니 생각보다 느낌이 좋았습니다. 빠른 건 기본이고, 설정이 가볍고, Vite랑 궁합이 좋아서 손이 덜 갔어요. 오늘은 제가 실제 프로젝트에서 Vitest 단위 테스트를 쓰면서 겪었던 시행착오랑, LLM으로 테스트 코드를 만들 때 토큰 아끼는 방식까지 편하게 정리해보려고 해요. 뭐랄까, 문서에 적힌 멋진 설명보다는 “내가 이거 때문에 반나절 날렸다” 싶은 것들 위주입니다.
Vitest가 좋았던 이유는 속도보다도 덜 귀찮다는 점이었어요
처음 Vitest를 켰을 때 가장 먼저 느낀 건 당연히 속도였어요. Jest도 좋은 도구죠. 오래 검증됐고, 자료도 많고, 웬만한 문제는 검색하면 다 나옵니다. 그런데 파일이 수백 개 넘어가는 프론트엔드 프로젝트에서는 테스트 한 번 도는 시간이 슬슬 부담이 되더라고요. 특히 watch 모드에서 저장할 때마다 5초, 8초, 심하면 10초 가까이 기다리는 순간이 있었어요. 개발 흐름이 끊깁니다. 이게 진짜 별거 아닌 듯한데 집중력에는 꽤 치명적이에요.
Vitest는 Vite 기반이라 그런지 변경된 파일 중심으로 반응하는 속도가 확실히 좋았습니다. 테스트가 거의 실시간으로 따라오는 느낌이 있었고요. TDD처럼 테스트를 깨고, 구현하고, 다시 확인하는 식으로 작업할 때 차이가 더 크게 느껴졌어요. 기다리는 시간이 줄어드니까 테스트를 덜 귀찮아하게 됩니다. 이게 저는 꽤 중요하다고 봐요. 좋은 도구는 개발자를 부지런하게 만드는 게 아니라, 귀찮음을 줄여서 자연스럽게 좋은 습관을 만들게 해주거든요.
그리고 제가 더 마음에 들었던 건 설정이 단순하다는 점이었어요. Jest 쓸 때는 Babel, ts-jest, moduleNameMapper, testEnvironment 같은 설정을 맞추다가 괜히 기운 빠질 때가 있었거든요. 물론 한 번 잡아두면 잘 돌아가긴 해요. 그런데 프로젝트마다 조금씩 다르고, TypeScript랑 alias까지 얽히면 은근히 손이 갑니다.
Vitest는 이미 Vite를 쓰고 있는 프로젝트라면 출발선이 훨씬 가벼워요. Vite 설정을 상당 부분 그대로 가져갈 수 있고, TypeScript 처리도 자연스럽습니다. 저희 팀은 Vite 기반 프로젝트랑 일부 Next.js 프로젝트를 같이 관리했는데, Vite 쪽에서는 정말 플러그인 하나 붙이고 바로 테스트가 도는 느낌이었어요.
| 비교 항목 | Jest를 쓸 때 느낌 | Vitest를 쓸 때 느낌 |
|---|---|---|
| 초기 설정 | Babel, ts-jest, moduleNameMapper 등을 챙겨야 할 때가 많음 | Vite 설정을 활용할 수 있어서 출발이 가벼움 |
| TypeScript 지원 | 프로젝트 구조에 따라 별도 설정이 필요할 수 있음 | 기본 경험이 꽤 자연스러움 |
| Watch 모드 | 안정적이지만 프로젝트가 커지면 답답할 때가 있음 | 변경 반영이 빠르고 개발 흐름이 덜 끊김 |
| Vite 프로젝트와의 궁합 | 따로 맞춰줘야 하는 부분이 생김 | 거의 같은 생태계 안에서 움직이는 느낌 |
제 기준에서는 “빠르다”보다 “덜 피곤하다”가 더 큰 장점이었어요. 나이가 들어서 그런가 싶기도 한데요. 하하. 이제는 도구가 아무리 좋아도 설정하다가 반나절 잡아먹으면 손이 잘 안 갑니다. 그런 면에서 Vitest 설정은 꽤 마음 편한 쪽이었어요.
처음 Vitest 붙일 때 제가 꼭 확인하는 설정들
Vitest를 처음 붙일 때는 잘 돌아가는 것처럼 보여도, 실제 테스트를 몇 개 작성하다 보면 자잘한 문제가 튀어나옵니다. 특히 alias, jsdom, setup 파일 이 세 가지는 초반에 잡아두는 게 좋아요. 안 그러면 테스트 코드가 문제가 아니라 환경 때문에 시간을 씁니다. 경험상 이런 시간은 나중에 돌아보면 제일 아까워요.
alias 설정은 초반에 꼭 맞춰두는 게 좋아요
제가 처음 Vitest를 붙이고 바로 만난 에러가 경로 문제였습니다. React 프로젝트에서 @/components, @/utils 같은 alias를 쓰고 있었는데 테스트에서 못 찾는 거예요. 애플리케이션은 멀쩡히 빌드되는데 테스트만 실패하니까 처음엔 조금 억울하더라고요.
보통 이런 식의 에러가 나옵니다.
Error: Failed to resolve import "@/components/UserProfile" from "src/features/user/UserProfile.test.tsx".
Does the file exist?
해결은 어렵지 않았어요. vitest.config.ts에 resolve.alias를 명확히 넣어주면 됩니다. 저는 Vite 설정과 최대한 같은 모양으로 맞춰둡니다. 나중에 누가 봐도 헷갈리지 않게요.
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
여기서 globals: true는 취향이 좀 갈릴 수 있어요. 저는 테스트 코드에서 describe, it, expect를 매번 import하지 않는 쪽을 좋아합니다. 코드가 조금 더 짧고 읽기 편하거든요. 대신 TypeScript에서 타입 인식을 제대로 하려면 tsconfig 쪽에 아래처럼 types를 넣어두는 것도 같이 챙기는 편이에요.
// tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
이거 하나 빠져서 에디터에서 빨간 줄이 계속 뜨면 은근히 신경 쓰입니다. 테스트는 통과하는데 IDE가 화를 내는 상황, 개발자라면 알죠. 괜히 찝찝한 그 느낌이요.
setup 파일에는 반복되는 테스트 환경을 모아두면 편합니다
테스트 코드에서 가장 귀찮은 것 중 하나가 브라우저 API나 외부 의존성 모킹이에요. localStorage, fetch, matchMedia 같은 것들이 대표적이죠. 테스트 파일마다 똑같이 mock을 만들면 코드도 길어지고, 나중에 수정할 때도 여기저기 찾아다녀야 합니다.
저는 setupFiles를 적극적으로 쓰는 편이에요. 전역으로 필요한 설정은 setup 파일에 몰아둡니다. 예를 들면 이런 식입니다.
// src/test/setup.ts
import '@testing-library/jest-dom';
import { vi } from 'vitest';
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
};
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
});
global.fetch = vi.fn();
이렇게 해두면 테스트 파일마다 localStorage를 새로 만들 필요가 없어요. 그리고 @testing-library/jest-dom도 여기서 한 번 import해두면 toBeInTheDocument 같은 matcher를 자연스럽게 쓸 수 있습니다.
다만 한 가지는 조심해야 해요. setup 파일이 너무 비대해지면 테스트 환경이 오히려 불투명해집니다. 모든 테스트에 필요하지 않은 mock까지 전역에 넣기 시작하면, 나중에 특정 테스트가 왜 통과하는지 감이 안 잡히는 순간이 와요. 저는 전역 setup에는 정말 공통으로 필요한 것만 넣고, 특정 케이스에만 필요한 mock은 테스트 파일 안에서 따로 처리합니다. 이 선을 지키는 게 생각보다 중요하더라고요.
watch 모드는 그냥 켜는 것보다 필터링해서 쓰면 훨씬 좋습니다
Vitest의 watch 모드는 꽤 똑똑합니다. 변경된 파일과 관련 있는 테스트를 다시 돌려주니까 개발 중에는 거의 계속 켜두게 돼요. 그런데 프로젝트가 커지면 가끔 원치 않는 테스트까지 같이 도는 경우가 있습니다. 그럴 땐 무작정 기다리지 말고 필터링을 걸어버리는 게 낫습니다.
저는 보통 이렇게 실행합니다.
vitest --watch --reporter=verbose
그리고 터미널에서 t를 누른 뒤 테스트 파일명이나 패턴을 입력합니다. 예를 들어 user 관련 테스트만 보고 싶으면 user를 입력하는 식이에요. 그러면 user.test.ts, UserProfile.test.tsx 같은 파일만 걸러서 돌릴 수 있습니다.
작은 팁인데, 이걸 아느냐 모르느냐에 따라 하루 피로도가 달라집니다. 테스트가 빠르다고 해도 필요 없는 테스트까지 계속 돌리면 결국 답답해지거든요. 저는 특히 버그 하나 잡을 때는 관련 테스트만 좁혀서 돌립니다. 전체 테스트는 커밋 전이나 PR 올리기 전에 한 번 더 확인하고요.
LLM으로 테스트 코드를 만들 때 토큰을 아끼는 방식
요즘은 LLM한테 테스트 코드 초안을 많이 부탁하잖아요. 저도 씁니다. 안 쓸 이유가 없어요. 다만 그대로 믿고 붙여넣으면 테스트 파일이 금방 비대해집니다. AI가 친절하긴 한데, 너무 친절해서 같은 코드를 반복하는 경우가 많거든요. mock 데이터도 매번 만들고, render도 매번 길게 쓰고, 비슷한 테스트를 여러 개로 쪼개서 장황하게 만들어줍니다.
처음엔 “오, 테스트 코드 빨리 나오네” 하고 좋아했는데, 몇 주 지나니까 테스트 파일이 너무 커졌어요. LLM에 다시 수정 요청할 때도 토큰을 많이 먹고, 리뷰할 때도 눈이 피곤했습니다. 그 뒤로는 제 나름의 규칙을 정해뒀어요. Vitest 테스트 코드를 LLM에게 맡기더라도 구조는 사람이 잡아줘야 한다는 쪽입니다.
describe와 beforeEach를 프롬프트에 직접 넣어줍니다
AI에게 “이 컴포넌트 테스트 만들어줘”라고만 말하면 중복이 꽤 많이 나옵니다. 그래서 저는 처음부터 이렇게 말합니다.
Vitest와 Testing Library를 사용해서 테스트 코드를 작성해줘.
describe 블록으로 묶고, 공통 mock 데이터는 beforeEach에서 초기화해줘.
중복 render 함수는 helper로 빼줘.
이렇게 요청하면 결과물이 확실히 단정해집니다. 예를 들어 UserProfile 컴포넌트라면 이런 구조가 낫습니다.
describe('UserProfile 컴포넌트', () => {
let user: { name: string; age: number };
beforeEach(() => {
user = { name: '홍길동', age: 30 };
});
const renderUserProfile = () => {
return render(<UserProfile user={user} />);
};
it('이름을 보여준다', () => {
renderUserProfile();
expect(screen.getByText('홍길동')).toBeInTheDocument();
});
it('나이를 보여준다', () => {
renderUserProfile();
expect(screen.getByText('30')).toBeInTheDocument();
});
});
별거 아닌 것 같지만 이런 습관이 쌓이면 테스트 파일 길이가 꽤 줄어듭니다. LLM에게 다시 물어볼 때도 전체 파일을 붙여넣지 않고 핵심 helper와 테스트 패턴만 보여주면 되니까 LLM 토큰 절약에도 도움이 되고요.
반복 테스트는 test.each로 밀어 넣는 게 깔끔합니다
입력값만 다르고 기대 결과가 비슷한 테스트가 있잖아요. 예전에는 it을 여러 개 쭉 나열하는 식으로 많이 썼습니다. 나쁘진 않아요. 다만 케이스가 5개, 10개로 늘어나면 코드가 금방 길어집니다.
이럴 때는 test.each가 정말 편합니다.
// 조금 장황한 방식
it('입력값이 1이면 "A"를 반환한다', () => {
expect(myFunction(1)).toBe('A');
});
it('입력값이 2이면 "B"를 반환한다', () => {
expect(myFunction(2)).toBe('B');
});
it('입력값이 3이면 "C"를 반환한다', () => {
expect(myFunction(3)).toBe('C');
});
// test.each를 사용한 방식
test.each([
[1, 'A'],
[2, 'B'],
[3, 'C'],
])('입력값이 %i이면 "%s"를 반환한다', (input, expected) => {
expect(myFunction(input)).toBe(expected);
});
이런 건 AI에게도 명확히 말해주는 게 좋아요. “비슷한 케이스는 test.each로 작성해줘”라고요. 그러면 테스트 코드가 훨씬 짧아지고, 나중에 케이스 추가도 편합니다. 배열에 한 줄만 넣으면 되니까요.
모킹은 욕심내지 않는 쪽이 오래 갑니다
모킹은 참 묘합니다. 잘 쓰면 테스트가 빨라지고 안정적인데, 많이 쓰면 테스트가 현실과 멀어집니다. 제가 세워둔 기준은 간단해요. 테스트하려는 대상이 직접 의존하는 것만 모킹한다. 이 정도 선을 넘지 않으려고 합니다.
예를 들어 API를 호출하는 함수가 있고, 그 함수를 사용하는 컴포넌트를 테스트한다고 해볼게요. 이럴 때는 실제 fetch를 끝까지 흉내 내기보다 API 모듈 자체를 mock 처리하는 편이 더 단순할 때가 많습니다.
// api.ts
export async function getUser(id: number) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// user.test.ts
import { describe, expect, it, vi } from 'vitest';
import { getUser } from '../api';
vi.mock('../api', () => ({
getUser: vi.fn().mockResolvedValue({ name: '테스트 유저' }),
}));
describe('getUser', () => {
it('유저 정보를 반환한다', async () => {
const result = await getUser(1);
expect(result).toEqual({ name: '테스트 유저' });
});
});
이렇게 하면 네트워크 호출 없이 테스트가 빠르게 끝납니다. 물론 API 함수 자체를 테스트하는 상황이라면 fetch를 mock해야겠죠. 중요한 건 지금 내가 검증하려는 게 뭔지 먼저 정하는 거예요. 이 기준 없이 mock을 막 쌓기 시작하면, 테스트는 통과하는데 실제 서비스에서는 깨지는 이상한 상황이 생깁니다. 저도 몇 번 겪었고요. 그때마다 조금 씁쓸합니다.
| 상황 | 제가 주로 쓰는 방식 | 이유 |
|---|---|---|
| 순수 함수 테스트 | mock 최소화 | 입력과 출력만 보면 되어서 불필요한 설정을 줄일 수 있음 |
| 컴포넌트에서 API 결과 사용 | API 모듈을 vi.mock으로 대체 | UI 상태 검증에 집중하기 좋음 |
| API 함수 자체 테스트 | fetch 또는 HTTP client mock | 요청/응답 처리 로직을 직접 확인해야 함 |
제가 Vitest 쓰면서 실제로 삽질했던 것들
Vitest가 편하다고 해서 모든 게 술술 풀리진 않았습니다. 특히 Jest에서 넘어오는 프로젝트에서는 “어? 이게 왜 안 되지?” 싶은 순간이 몇 번 있었어요. 도구를 바꿀 때 제일 무서운 건 큰 문제가 아니라 작은 차이입니다. 작은 차이들이 개발 시간을 야금야금 먹거든요.
toBeInTheDocument 에러는 거의 입문 신고식 같았어요
Jest 쓰던 감각으로 테스트를 작성했는데 갑자기 이런 에러가 나왔습니다.
TypeError: expect(...).toBeInTheDocument is not a function
처음엔 제가 import를 빼먹었나 싶었어요. 그런데 알고 보니 @testing-library/jest-dom 설정이 빠진 거였죠. Jest 프로젝트에서는 이미 설정되어 있던 경우가 많아서 당연히 되는 줄 알았는데, Vitest에서는 setup 파일에 직접 넣어줘야 했습니다.
// src/test/setup.ts
import '@testing-library/jest-dom';
정말 한 줄이면 끝나는 문제였는데, 저는 이걸로 30분 넘게 헤맸습니다. 이런 게 개발이에요. 어려운 알고리즘보다 설정 한 줄이 사람을 더 허탈하게 만들 때가 있습니다.
jsdom 환경을 안 잡아두면 DOM 테스트가 이상하게 깨집니다
React 컴포넌트 테스트를 하는데 document나 window 관련 에러가 난다면 test environment를 확인해보는 게 좋습니다. 컴포넌트 테스트에서는 보통 environment: 'jsdom'을 넣어둡니다.
export default defineConfig({
test: {
environment: 'jsdom',
},
});
이것도 빠지면 에러 메시지가 꽤 불친절하게 느껴질 때가 있어요. “document is not defined” 같은 메시지를 보면 순간 백엔드 테스트처럼 돌고 있구나 감이 오긴 하는데, 처음에는 당황합니다.
canvas나 chart 라이브러리는 별도로 달래줘야 할 때가 있습니다
제가 꽤 오래 붙잡았던 문제 중 하나가 chart 라이브러리 테스트였어요. 내부적으로 canvas를 쓰는 라이브러리였는데 jsdom에서 canvas가 제대로 지원되지 않아서 계속 터졌습니다. 이건 Vitest 문제라기보다 jsdom 환경의 한계에 가깝습니다.
그때는 테스트 목적을 다시 봤어요. 내가 진짜 차트가 픽셀 단위로 그려지는 걸 검증하고 싶은 건 아니었거든요. 데이터가 들어갔을 때 차트 컴포넌트가 렌더링되는지, 빈 데이터일 때 안내 문구가 나오는지 정도를 보고 싶었습니다. 그래서 chart 컴포넌트나 canvas 관련 객체를 mock 처리했습니다.
import { vi } from 'vitest';
vi.stubGlobal('ResizeObserver', class {
observe() {}
unobserve() {}
disconnect() {}
});
이런 식으로 ResizeObserver나 matchMedia를 잡아줘야 하는 경우도 있습니다. 차트, 반응형 UI, 에디터, 지도 라이브러리 쪽 테스트에서 종종 나와요. 이럴 때는 억지로 실제 브라우저처럼 만들려고 하기보다, 테스트 범위를 줄이는 게 낫습니다. 단위 테스트는 단위 테스트답게요. 너무 많은 걸 한 번에 검증하려고 하면 테스트가 쉽게 예민해집니다.
제가 Vitest 프로젝트에서 자주 쓰는 체크리스트
새 프로젝트에 Vitest를 붙이거나, Jest에서 Vitest로 옮길 때 저는 아래 목록을 대충 훑어봅니다. 거창한 건 아닌데, 이 정도만 챙겨도 초반 삽질을 꽤 줄일 수 있어요.
- Vite alias와 Vitest alias가 맞는지 확인합니다.
- environment: 'jsdom'이 필요한 테스트인지 먼저 봅니다.
- @testing-library/jest-dom을 setup 파일에서 import했는지 확인합니다.
- 전역 mock은 setup 파일에 넣되, 너무 많이 넣지 않도록 조심합니다.
- 반복 케이스는 test.each로 줄일 수 있는지 봅니다.
- LLM에게 테스트 생성을 맡길 때는 describe, beforeEach, helper 함수 구조를 프롬프트에 명시합니다.
- mock은 “지금 검증하려는 대상의 직접 의존성”까지만 두는 걸 기본값으로 잡습니다.
- watch 모드에서는 필요한 테스트만 필터링해서 돌립니다.
- 전체 테스트는 커밋 전이나 PR 전에 반드시 한 번 더 돌립니다.
이 체크리스트를 팀 위키에 적어둔 뒤로는 새로 들어온 동료들도 적응을 빨리 하더라고요. 개발 문화라는 게 대단한 구호보다 이런 작은 합의에서 만들어지는 것 같습니다. 똑같은 실수를 덜 반복하게 해주는 장치들 말이에요.
Vitest가 잘 맞는 사람, 굳이 안 바꿔도 되는 사람
저는 Vitest를 꽤 좋아하게 됐지만, 모든 프로젝트가 Vitest로 가야 한다고 생각하진 않습니다. 이건 제 주관이 좀 뚜렷한 편인데요. 도구 교체는 늘 비용이 있습니다. 기존 테스트가 많고 Jest 기반으로 안정적으로 잘 돌아가는 레거시 프로젝트라면, 단지 유행이라는 이유만으로 바꾸는 건 별로 추천하지 않아요. 전환 과정에서 깨지는 테스트를 고치고, 설정을 맞추고, 팀원들이 새 도구에 익숙해지는 시간도 결국 비용이니까요.
반대로 새로 시작하는 Vite 프로젝트라면 저는 거의 고민 없이 Vitest를 먼저 봅니다. 특히 아래에 해당한다면 잘 맞을 가능성이 높아요.
- Vite 기반 프론트엔드 프로젝트를 하고 있는 분
- 테스트 실행 속도 때문에 개발 흐름이 자주 끊기는 분
- TypeScript 테스트 설정을 최대한 단순하게 가져가고 싶은 분
- LLM으로 테스트 코드를 생성하면서 중복과 토큰 낭비를 줄이고 싶은 분
- TDD나 빠른 피드백 루프를 중요하게 생각하는 팀
저는 지금도 Vitest를 메인 테스트 도구로 잘 쓰고 있습니다. 가끔 Jest 자료가 더 많아서 부러운 순간은 있지만, 다시 돌아가고 싶다는 생각은 거의 안 해봤어요. 특히 watch 모드에서 바로바로 반응하는 느낌은 한 번 익숙해지면 꽤 중독성이 있습니다.
이 글은 Vitest를 처음 도입하려는 개발자, Jest에서 넘어갈까 고민하는 분, 그리고 LLM으로 테스트 코드를 만들면서 “왜 이렇게 코드가 길어지지?” 하고 느꼈던 분들이 읽으면 도움이 될 것 같아요. 테스트 도구는 결국 개발자를 편하게 해줘야 합니다. 괜히 멋있어 보이는 설정보다, 내일 아침에도 부담 없이 돌릴 수 있는 테스트 환경이 더 오래 가더라고요. 저는 이제 그런 쪽이 좋습니다. 빠르고, 단순하고, 팀원이 봐도 이해되는 것. 그게 오래 일해보니 제일 세더라고요.
댓글
댓글 쓰기