내가 Nuxt에서 Vitest 스위트를 10배 빠르게 실행하도록 만들었다

발행: (2025년 12월 20일 오후 08:21 GMT+9)
14 min read
원문: Dev.to

Source: Dev.to

Vitest 테스트 스위트를 Nuxt에서 10배 빠르게 만든 방법

Nuxt 프로젝트에서 Vitest를 사용해 테스트를 실행할 때, 초기 실행 시간이 꽤 오래 걸리는 경우가 있습니다. 특히 CI 파이프라인이나 로컬 개발 환경에서 테스트를 자주 돌려야 할 때, 이 지연은 생산성을 크게 저하시킵니다.

이번 글에서는 VitestNuxt를 결합한 테스트 환경을 최적화하여 실행 속도를 약 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 s0.9 s≈10배
GitHub Actions (ubuntu‑latest)12.3 s1.2 s≈10배

테스트가 10배 빨라졌을 뿐 아니라, CI 비용도 크게 절감되었습니다.


🛠️ 마무리 팁

  1. 의존성 업데이트 시 캐시를 강제로 비우기
    rm -rf .vitest/cache .vite/cache
  2. 테스트가 오래 걸리는 경우 --run 옵션으로 단일 파일만 실행
    npx vitest run tests/unit/MyComponent.spec.ts
  3. vitest --coverage를 사용할 때는 별도 캐시 디렉터리를 지정해 커버리지 계산 시간을 최소화합니다.

📚 참고 자료

위 방법들을 적용하면, Nuxt와 Vitest를 사용하는 프로젝트에서 테스트 속도를 크게 개선할 수 있습니다. 여러분도 직접 시도해 보고, 더 빠른 피드백 루프를 경험해 보세요!

자동화 테스트가 중요한 이유

  • Safety net: 코드베이스가 커질 때 회귀를 방지합니다.
  • Confidence: 새로운 기능을 빠르게 추가하고 검증할 수 있습니다.
  • Speed vs. reliability: 테스트가 많을수록 실행 시간이 길어져 불편함이 될 수 있습니다.

내가 만든 Nuxt 모듈은 Neon DB 연결을 위해 180 + 초가 걸렸습니다. 이 지연은 개발자 경험(DevEx)을 크게 저하시켰고, 테스트 아키텍처를 재고하게 만들었습니다.

원본 (느린) 설정

  1. 테스트 파일당 하나의 Nuxt 앱 – 각 파일이 자체 격리된 Nuxt 인스턴스를 생성했습니다.

  2. 순차 실행 – 테스트가 차례대로 실행되어 부팅 지연이 모두 누적되었습니다.

  3. 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 인스턴스를 재사용해야 합니다.

해결 방안 개요

  1. 전역 설정 – Vitest가 테스트를 시작하기 전에 Nuxt 앱을 한 번 마운트합니다.
  2. 공유 테스트 컨텍스트 – 동일한 Playwright 브라우저(또는 가상 브라우저)를 모든 테스트 파일에 노출합니다.
  3. 환경 변수@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:

  1. Prepares the Nuxt test app via the dedicated setup function.
  2. 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의 한계를 인식할 필요가 있다고 생각합니다. 이것은 나에게 또 다른 좋은 교훈이었고, 여러분도 흥미롭게 보셨길 바랍니다.

여러분의 피드백과 질문을 기다립니다!

Back to Blog

관련 글

더 보기 »