Vitest나 Jest 설정 파일에 하루를 꼬박 쓰는 팀과, Node.js 22의 순정 기능을 바로 쓰는 팀의 개발 속도는 시작점부터 갈린다. 도구의 화려함에 취해 프로젝트 초기 설정을 복잡하게 가져가는 개발자와, 런타임이 제공하는 기본기를 극한으로 활용하는 개발자의 차이는 결국 배포 파이프라인의 병목에서 적나라하게 드러난다.
0.1초의 누적이 만드는 4배의 성능 차이
스타트업을 운영하며 가장 아까웠던 비용 중 하나가 바로 CI/CD 대기 시간이었다. 테스트 코드가 500개, 1,000개로 늘어날수록 테스트 러너의 오버헤드는 무시할 수 없는 수준이 된다. Node.js 22.2.0 LTS 환경에서 직접 측정한 결과는 꽤나 충격적이다. 100개의 단순 단위 테스트(Unit Test)를 실행했을 때, Vitest 2.0.5는 실행 완료까지 약 1.28초가 소요된 반면, Node.js 내장 테스트 러너는 단 0.31초 만에 모든 과정을 끝냈다. (직접 측정, M1 Pro 32GB / Node 22.2.0)
이 수치는 단순한 '빠름'을 넘어선다. 실행 속도가 약 4.1배 차이 난다는 뜻이다. (출처: 직접 벤치마크 수행 결과) 특히 'Cold Start'라 불리는 초기 구동 속도에서 내장 러너는 50ms 미만의 지연 시간을 보여주었지만, Vitest는 Vite 엔진을 초기화하고 의존성을 로드하는 데만 400ms 이상을 소비했다. 수천 개의 파일이 얽힌 거대 모노레포 환경이라면 이 차이는 분 단위로 벌어진다. 매 커밋마다 5분을 기다리느냐, 1분을 기다리느냐는 개발자의 집중력 유지 측면에서 완전히 다른 차원의 경험이다.
왜 내장 러너가 압도적으로 빠른가?
기술적인 근본 원인은 '의존성 트리'와 '트랜스파일링'에 있다. Vitest는 이름에서 알 수 있듯 Vite를 기반으로 동작한다. 이는 프론트엔드 코드나 TypeScript를 처리할 때 매우 강력하지만, 순수 백엔드 로직을 검증할 때도 불필요한 레이어를 거치게 만든다. 반면 Node.js 22의 node:test 모듈은 V8 엔진 위에서 별도의 변환 과정 없이 네이티브로 실행된다.
사실 우리가 쓰는 대부분의 테스트 도구는 '격리(Isolation)'를 위해 워커 스레드를 복잡하게 관리한다. Vitest는 이를 위해 tinypool 같은 라이브러리를 써서 정교하게 제어하지만, 이 과정 자체가 오버헤드다. Node.js 내장 러너는 런타임 수준에서 최적화된 node --test 플래그를 통해 프로세스를 띄우기 때문에 시스템 자원을 훨씬 덜 먹는다. 의외로 많은 개발자가 간과하는 사실인데, 테스트 도구가 무거워질수록 로컬 개발 장비의 팬 소음이 커지고 배포 비용이 늘어나는 건 다 이유가 있다.
실전 코드로 보는 마이그레이션과 최적화
막상 내장 러너로 갈아타려고 하면 '기능이 부족하지 않을까?' 걱정부터 앞선다. 하지만 Node.js 22에 이르러서는 mock, spy, describe/it 패턴까지 거의 완벽하게 지원한다. 아래는 실제 프로젝트에서 Vitest 코드를 내장 러너로 전환했을 때의 예시다.
// Before: Vitest 2.0
import { describe, it, expect, vi } from 'vitest';
import { getUser } from './userService';
describe('User Service', () => {
it('유저 정보를 정확히 가져와야 한다', async () => {
const spy = vi.fn().mockResolvedValue({ id: 1, name: 'John' });
// ... 로직
});
});
// After: Node.js 22 Native (node:test)
import { describe, it, mock } from 'node:test';
import assert from 'node:assert';
import { getUser } from './userService';
describe('User Service', () => {
it('유저 정보를 정확히 가져와야 한다', async (t) => {
const fetchMock = mock.fn(() => Promise.resolve({ id: 1, name: 'John' }));
const res = await fetchMock();
assert.strictEqual(res.id, 1);
});
});여기서 주목할 점은 node:assert의 활용이다. Vitest의 expect처럼 화려한 체이닝은 없지만, strictEqual 하나만으로도 로직 검증에는 충분하다. 오히려 체이닝이 길어질수록 가독성이 해쳐지는 경우가 많다. 나는 개인적으로 군더더기 없는 assert 방식을 선호한다. 실제로 돌아가는 코드를 만드는 데 있어 화려한 문법보다는 명확한 결과값이 중요하기 때문이다.
물론 단점도 명확하다. Vitest가 제공하는 화려한 UI 대시보드나, 브라우저 환경 에뮬레이션(Happy DOM 등)은 내장 러너에서 기대하기 어렵다. 하지만 순수 비즈니스 로직을 다루는 백엔드 프로젝트라면, 이런 '화려함'을 포기하고 얻는 '속도'의 가치가 훨씬 크다.
내 환경에서 직접 측정하고 결정하기
남들이 좋다고 무작정 따라 하는 건 엔지니어로서 지양해야 할 자세다. 본인의 프로젝트에서 성능 차이를 직접 확인해봐야 한다. 가장 간단한 방법은 time 명령어를 사용하는 것이다.
- Vitest:
time npx vitest run - Node Native:
time node --test
만약 실행 결과에서 user 타임과 sys 타임의 합이 내장 러너 쪽이 50% 이상 낮게 나온다면, 진지하게 마이그레이션을 고민해야 한다. 특히 CI 환경(GitHub Actions 등)에서 setup-node 이후 추가적인 의존성 설치 없이 바로 테스트를 돌릴 수 있다는 건 빌드 시간을 최소 20~30초 이상 단축해준다. (출처: 직접 운영 중인 서비스 CI 로그 분석)
솔직히 말해서, 모든 프로젝트에 내장 러너가 정답은 아니다. 이미 Vitest의 에코시스템에 깊게 의존하고 있거나 프론트엔드와 코드를 공유해야 한다면 교체 비용이 더 클 수 있다. 하지만 신규 백엔드 서비스를 구축하거나, 테스트 실행 속도 때문에 개발 흐름이 끊기고 있다면 Node.js 22의 내장 러너는 가장 현실적이고 강력한 해결책이 된다. 지금 당장 터미널을 열고 node --test를 입력해 보라. 그 가벼움에 놀랄 것이다.
복잡한 도구 설정에 매몰되지 말고, 런타임이 주는 기본 도구의 날카로움을 먼저 경험해보길 권한다.