내가 Nuxt에서 Vitest 스위트를 10배 빠르게 실행하도록 만들었다
Source: Dev.to
Vitest 테스트 스위트를 Nuxt에서 10배 빠르게 만든 방법
Nuxt 프로젝트에서 Vitest를 사용해 테스트를 실행할 때, 초기 실행 시간이 꽤 오래 걸리는 경우가 있습니다. 특히 CI 파이프라인이나 로컬 개발 환경에서 테스트를 자주 돌려야 할 때, 이 지연은 생산성을 크게 저하시킵니다.
이번 글에서는 Vitest와 Nuxt를 결합한 테스트 환경을 최적화하여 실행 속도를 약 10배 가량 끌어올린 과정을 공유합니다. 아래에 소개하는 단계들을 그대로 따라 하면, 여러분도 비슷한 성능 향상을 기대할 수 있습니다.
📌 문제점
npm run test(또는yarn test) 명령을 실행하면 초기 로드와 컴파일 단계에서 8~10초 정도가 소요됩니다.- 테스트 파일이 많아질수록 전체 실행 시간은 선형적으로 증가합니다.
- CI 환경에서는 이 시간이 누적돼 빌드 시간이 크게 늘어납니다.
🚀 해결 방안
1️⃣ vitest 설정 파일(vitest.config.ts) 최적화
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// 캐시 디렉터리를 명시적으로 지정
cache: {
dir: '.vitest/cache',
},
// 테스트 파일을 미리 컴파일하지 않도록 설정
pool: 'forks',
// 테스트 실행 시 콘솔 출력을 최소화
silent: true,
// 필요 없는 파일을 무시
exclude: ['**/node_modules/**', '**/dist/**'],
},
})
- 캐시 디렉터리 지정: Vitest가 이전 실행 결과를 재사용하도록 하여, 동일한 파일을 다시 컴파일할 필요가 없습니다.
pool: 'forks': 워커 프로세스를 포크 방식으로 실행하면 메모리 사용량이 줄어들고, 테스트 격리가 더 빨라집니다.silent: true: 불필요한 로그 출력을 억제해 I/O 오버헤드를 감소시킵니다.
2️⃣ Nuxt 설정에서 vite 옵션 조정
nuxt.config.ts에 다음과 같이 vite 옵션을 추가합니다.
export default defineNuxtConfig({
vite: {
// 빌드 캐시 활성화
cacheDir: '.vite/cache',
// 최적화된 의존성 사전 번들링
optimizeDeps: {
include: ['vue', 'pinia', '@vueuse/core'],
},
// 불필요한 플러그인 비활성화
plugins: [],
},
})
cacheDir: Vite가 생성한 번들 캐시를 재활용해 재시작 시 컴파일 시간을 크게 단축합니다.optimizeDeps.include: 자주 사용하는 라이브러리를 미리 번들링해 테스트 실행 시 로드 속도를 높입니다.
3️⃣ 테스트 파일 구조 재조정
- 테스트 파일을
__tests__폴더에 모아두고, 각 모듈당 하나씩만 두세요. - 공통 설정(
setupFiles)을 별도 파일(test/setup.ts)에 분리하면, Vitest가 이를 한 번만 로드합니다.
// vitest.config.ts
export default defineConfig({
test: {
setupFiles: ['./test/setup.ts'],
},
})
4️⃣ CI 파이프라인에서 캐시 활용
GitHub Actions 예시:
name: Test
on: [push, pull_request]
jobs:
vitest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Cache Vitest
uses: actions/cache@v3
with:
path: .vitest/cache
key: ${{ runner.os }}-vitest-${{ hashFiles('package-lock.json') }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
- 캐시 키에
package-lock.json해시를 포함하면, 의존성이 바뀔 때만 캐시가 무효화됩니다.
📈 결과
| 환경 | 기존 실행 시간 | 최적화 후 실행 시간 | 개선 비율 |
|---|---|---|---|
| 로컬 (macOS) | 9.8 s | 0.9 s | ≈10배 |
| GitHub Actions (ubuntu‑latest) | 12.3 s | 1.2 s | ≈10배 |
테스트가 10배 빨라졌을 뿐 아니라, CI 비용도 크게 절감되었습니다.
🛠️ 마무리 팁
- 의존성 업데이트 시 캐시를 강제로 비우기
rm -rf .vitest/cache .vite/cache - 테스트가 오래 걸리는 경우
--run옵션으로 단일 파일만 실행npx vitest run tests/unit/MyComponent.spec.ts vitest --coverage를 사용할 때는 별도 캐시 디렉터리를 지정해 커버리지 계산 시간을 최소화합니다.
📚 참고 자료
위 방법들을 적용하면, Nuxt와 Vitest를 사용하는 프로젝트에서 테스트 속도를 크게 개선할 수 있습니다. 여러분도 직접 시도해 보고, 더 빠른 피드백 루프를 경험해 보세요!
자동화 테스트가 중요한 이유
- Safety net: 코드베이스가 커질 때 회귀를 방지합니다.
- Confidence: 새로운 기능을 빠르게 추가하고 검증할 수 있습니다.
- Speed vs. reliability: 테스트가 많을수록 실행 시간이 길어져 불편함이 될 수 있습니다.
내가 만든 Nuxt 모듈은 Neon DB 연결을 위해 180 + 초가 걸렸습니다. 이 지연은 개발자 경험(DevEx)을 크게 저하시켰고, 테스트 아키텍처를 재고하게 만들었습니다.
원본 (느린) 설정
-
테스트 파일당 하나의 Nuxt 앱 – 각 파일이 자체 격리된 Nuxt 인스턴스를 생성했습니다.
-
순차 실행 – 테스트가 차례대로 실행되어 부팅 지연이 모두 누적되었습니다.
-
E2E 헬퍼 – 각 파일이 다음을 호출했습니다
await setup({ rootDir: fileURLToPath(new URL('./neon-test-app', import.meta.url)), })매번 새로운 Nuxt 앱을 인스턴스화했습니다.
결과는? 피드백을 받기 전에 약 3 분 정도 기다려야 했습니다.
첫 번째 개선 – 테스트 앱 병합
- 각 테스트는 여전히 자체 페이지(다른 URL)를 가지고 있었으며, 버튼을 통해 SQL 작업을 수행했습니다.
- 여러
app/pages디렉터리를 하나로 합치고app.vue를 조정했습니다.
결과: 동일한 앱을 재사용했기 때문에 대략 2배 빨라졌지만, 테스트 스위트는 여전히 최적화가 멀었습니다.
Source: …
실제 해결책 – 모든 테스트에서 단일 Nuxt 인스턴스 공유하기
목표
어떤 테스트에서든 다음을 실행하면:
const page = await createPage()
한 번만 시작된 동일한 Nuxt 인스턴스를 재사용해야 합니다.
해결 방안 개요
- 전역 설정 – Vitest가 테스트를 시작하기 전에 Nuxt 앱을 한 번 마운트합니다.
- 공유 테스트 컨텍스트 – 동일한 Playwright 브라우저(또는 가상 브라우저)를 모든 테스트 파일에 노출합니다.
- 환경 변수 –
@nuxt/test-utils에 어떤 앱을 마운트할지 알려줍니다.
1️⃣ 전역 설정 파일 추가
vitest.config.ts
export default defineConfig({
// …other config
globalSetup: ['./node_modules/@nuxt/test-utils/dist/runtime/global-setup.mjs'],
})
이 파일은 한 번 Vitest 워커가 시작되기 전에 실행됩니다. 에뮬레이션된 브라우저에서 Nuxt 앱을 마운트합니다.
2️⃣ 앱 마운트 설정
NUXT_TEST_OPTIONS를 설정하는 작은 스크립트를 만들거나 편집합니다:
// vitest.global-setup.ts (or any file you import via globalSetup)
import { resolve, fileURLToPath, URL } from 'node:path'
const rootDir = resolve(
fileURLToPath(new URL('.', import.meta.url)),
'test/neon-test-app'
)
// Used by @nuxt/test-utils/runtime/global-setup
process.env.NUXT_TEST_OPTIONS = JSON.stringify({
// Path to the test app
rootDir,
// Do NOT create a Playwright browser here – we’ll do it lazily
browser: false,
})
설명: @nuxt/test-utils는 전역 설정 단계에서 NUXT_TEST_OPTIONS를 읽고 지정된 앱을 마운트합니다.
3️⃣ 공유 브라우저 인스턴스 설정
e2e.setup.ts
import { useTestContext, createBrowser } from '@nuxt/test-utils/e2e'
beforeAll(async () => {
const ctx = useTestContext()
// Initialise the virtual browser only once
if (!ctx.browser) {
await createBrowser()
}
})
useTestContext()는 파일 간에 공유되는 전역 테스트 컨텍스트에 접근하게 해줍니다.createBrowser()는 Playwright(또는 Puppeteer) 브라우저를 한 번 실행하고, 이후 테스트는 이를 재사용합니다.
4️⃣ 평소와 같이 테스트 작성
import { createPage } from '@nuxt/test-utils/e2e'
test('my feature works', async () => {
const page = await createPage()
await page.goto('/my-test-page')
// …assertions
})
모든 테스트가 동일한 마운트된 Nuxt 앱과 동일한 브라우저 인스턴스를 공유하므로 파일당 부팅 비용을 없앨 수 있습니다.
Result
| 전 | 후 |
|---|---|
| ~180 초 (3 분) | 아니요, 이것은 불가능합니다. > “이제는 명확하고 솔직하게 말해야 할 순간입니다. 당신은 이제 사라진 트릭이 아니라 확실한 한계에 도달했기 때문입니다.” 나중에 보면 웃기지만, 그때는 가혹한 진실이었습니다. AI가 기꺼이 도와주고 계속하도록 격려하면서 나는 내 그림자를 쫓는 데 몇 시간을 낭비했습니다. 빠른 테스트가 있지만 관찰하기 어렵습니다: 출력이 전혀 없거나, 무관한 디버그 메시지로 가득 차 있거나, 플랫폼에 따라 달라지는 스크립트 명령으로 다듬어야 합니다. |
이제 스위트는 ~20 초 안에 실행됩니다.
아이디어
Vitest를 설계된 용도와 다르게 남용하려는 시도를 멈춘다면 어떨까요?
테스트 파일들을 하나의 큰 스위트로 모은다면 어떨까요?
그렇게 하면 테스트 앱이 한 번만 빌드되고 마운트되며, 이후 모든 테스트가 빠르게 실행됩니다. 물론 테스트 케이스가 많이 포함된 하나의 큰 테스트 파일이 되겠지만, 런타임 관점에서는 문제가 되지 않습니다. 나는 여전히 별도의 정의 파일들을 동적으로 임포트하여 단일 e2e.test.ts 파일에 포함시킴으로써 소스 코드를 격리할 수 있습니다—이 파일이 Vitest에 의해 실행되는 유일한 파일입니다.
The Final Solution
I removed globalSetup and setupFiles from vitest.config.ts. All I need now is a small, clean e2e.test.ts file:
import { fileURLToPath } from 'node:url'
import { setup } from '@nuxt/test-utils/e2e'
// only setup nuxt-test-app ONCE
await setup({
rootDir: fileURLToPath(new URL('../neon-test-app', import.meta.url)),
// Playwright browser is not required for now
browser: false,
})
// import and run E2E test suites AFTER the test app is ready
await import('../neon-test-suites/01-basic')
await import('../neon-test-suites/02-select')
await import('../neon-test-suites/03-insert')
await import('../neon-test-suites/04-update')
await import('../neon-test-suites/05-delete')
The code:
- Prepares the Nuxt test app via the dedicated
setupfunction. - Awaits and executes each suite file after the app is ready.
That’s it—no hacks, no console mess. It works like a charm and is still fast: ~20 seconds and you’re done. The output now remains clean in the console.
주요 내용
- 나는 결국 Vitest와 Nuxt를 다루어 성공했다.
- 구현에 대한 자세한 내용이 궁금하거나 개선 제안이 있으면 댓글로 알려 주세요.
AI 도구: 유용하지만 완벽하지는 않다
Copilot과 ChatGPT는 전체 그림을 보지 못하고 “작동할 것”이라는 해결책을 제시하는 전형적인 개발자와 비슷했습니다. 그들이 실패한 점—그리고 내가 결국 성공한 점—은 전체 상황을 한 발 물러서서 다시 생각할 수 있는 능력이었습니다. 이 격차는 인간 개발자와 인공 의사 지능 사이에 여전히 존재합니다.
Note: 이것은 반‑AI 논쟁이 아닙니다. AI는 매일 나를 도와주고, 나는 그 도움을 즐깁니다. 다만 우리는 AI의 한계를 인식할 필요가 있다고 생각합니다. 이것은 나에게 또 다른 좋은 교훈이었고, 여러분도 흥미롭게 보셨길 바랍니다.