외부 API 없이 통합테스트 속도 올리는 법 2026 — emulate로 GitHub·Vercel·Google 로컬 에뮬레이터 붙이는 체크리스트

CI 파이프라인 돌릴 때마다 GitHub API 호출 제한 걸려본 적 있으시죠?

아니면 Vercel deployment 상태 체크하는 테스트가 네트워크 타임아웃으로 실패하거나, Google OAuth 테스트 때문에 실제 계정 만들어서 토큰 발급받고 만료되면 또 갱신하고.

이런 거 하다 보면 “통합테스트 진짜 필요한가?” 의심 들 때 있습니다. 모킹하자니 실제 API랑 똑같은 응답 만들기 어렵고, 실제 API 쓰자니 느리고 불안정하고 비용도 나가고.

2026년 3월 Vercel Labs에서 공개한 emulate는 이 문제를 정면으로 해결하려는 도구입니다.

단순 모킹이 아니라 “production-fidelity API emulation”, 즉 프로덕션급 재현도를 가진 완전히 상태를 가지는 로컬 API 에뮬레이터를 제공합니다.

이 글에서는 실제 emulate를 붙여서 GitHub 커밋 상태 체크, Vercel 배포 흐름 검증, Google OAuth 로그인 흐름 테스트를 외부 네트워크 없이 돌리는 방법을 체크리스트로 정리합니다.


이 글이 필요한 사람

  • 통합테스트가 느린 이유가 코드보다 외부 API 호출 때문이라고 느끼는 사람
  • mock은 가볍지만 너무 가짜 같고, 실제 API는 너무 느려서 중간 해법을 찾는 사람
  • GitHub, Vercel, Google 연동 테스트를 CI에서 더 빨리 돌리고 싶은 사람
  • 아직 직접 도입 전이라도 이 구조가 우리 팀에 맞는지 먼저 판단하고 싶은 사람

먼저 선을 긋고 시작하자

이 글은 emulate 공식 문서와 README를 바탕으로 정리한 도입 체크리스트다. 즉 “내가 이미 이걸 프로덕션 CI에 널리 붙여봤다”는 후기라기보다, 어떻게 붙이면 실무에 먹히는지 operator 관점으로 재구성한 문서에 가깝다.

이 선을 먼저 긋는 이유는 단순하다. TECHTAEK는 괜히 안 해본 걸 해본 척하면 바로 냄새가 난다. 그래서 이 글은 지금 당장 내가 붙인다면 어디부터 본다 쪽으로 읽는 편이 맞다.

왜 Mock이 아니라 Emulator인가

Mock vs Emulator 구분

대부분의 테스트 가이드는 “Mock API를 만들라”고 합니다.

하지만 모킹의 한계는 명확합니다.

첫째, 상태를 가지지 않습니다. GitHub에 PR을 만들고, merge하고, deployment를 트리거하는 일련의 흐름을 모킹하려면 각 단계마다 새로운 stub을 만들어야 합니다.

둘째, 실제 API와 응답 형식이 다를 수 있습니다. Vercel의 deployment 객체는 id, url, state, createdAt, ready 등 수십 개 필드를 가지고 있는데, 모킹할 때 이 전부를 정확히 재현하는 건 거의 불가능합니다.

셋째, API 스펙이 바뀌면 테스트가 무용지물입니다. GitHub가 webhook payload 구조를 바꿨는데 우리 mock은 옛날 버전이면, 테스트는 통과하지만 프로덕션에서는 터집니다.

Emulator는 이 문제를 반대로 접근합니다.

실제 API 서버처럼 작동하는 로컬 서버를 띄우되, 네트워크가 필요 없고 데이터는 메모리에만 저장합니다.

emulate는 Vercel, GitHub, Google 세 가지 서비스의 핵심 API를 재현하며, 각 API의 spec에 맞춰 응답 형식, 에러 코드, rate limit 헤더까지 그대로 제공합니다.


통합테스트가 느려지는 3가지 지점

지점 1: 외부 API 호출 레이턴시

CI 파이프라인에서 실제 GitHub API를 호출하면 왕복 시간이 추가됩니다.

로컬에서는 10ms 안에 끝날 로직이, GitHub API 한 번 다녀오면 200ms, Vercel deployment 상태 폴링까지 더하면 5초 넘게 걸립니다.

테스트 100개 돌리면 500초. 8분 넘게 기다려야 합니다.

지점 2: Rate Limit

GitHub API는 인증 없이 시간당 60건, 인증해도 5000건까지만 호출 가능합니다.

팀원 5명이 동시에 CI를 돌리면 순식간에 한도를 넘어 403 Rate limit exceeded 에러를 만나게 됩니다.

이 때문에 테스트를 건너뛰거나, rate limit 회복될 때까지 재시도 로직을 추가하게 되는데, 이러면 테스트가 더 느려집니다.

지점 3: OAuth Token 만료와 상태 관리

Google OAuth 로그인 흐름을 테스트하려면 실제 Google 계정이 필요하고, access token과 refresh token을 관리해야 합니다.

토큰 만료 시점에 맞춰 재발급 로직을 테스트하려면 실제로 1시간을 기다리거나, 시스템 시간을 조작해야 합니다.

이 모든 게 테스트를 복잡하고 불안정하게 만듭니다.


emulate가 해결하는 것

emulate는 이 세 가지 문제를 한 번에 해결합니다.

첫째, 로컬호스트 속도입니다. GitHub API 호출이 <https://api.github.com이> 아니라 <http://localhost:4001로> 가므로, 레이턴시가 1ms 이하로 떨어집니다.

둘째, rate limit이 없습니다. 로컬 메모리에서 처리하므로 초당 수천 건을 호출해도 문제없습니다.

셋째, 상태가 완전히 격리됩니다. 테스트마다 새 emulator 인스턴스를 띄우면, 이전 테스트에서 만든 PR이나 deployment 상태가 남지 않습니다.

그리고 가장 중요한 건, 실제 API 스펙을 따릅니다. Vercel deployment 객체의 JSON 구조, GitHub webhook payload 형식, Google OAuth 응답이 실제 서비스와 동일합니다.

내가 먼저 볼 판단표

도입 전에 제일 먼저 정리할 건 “좋아 보인다”가 아니라 “우리 팀에 이게 진짜 맞냐”다.

상황 mock emulate 실제 API
PR마다 빠르게 돌릴 피드백 루프 가장 빠름 충분히 빠름 가장 느림
실제 API 구조와 비슷한 응답 검증 약함 강함 가장 강함
rate limit, 네트워크 요동 회피 강함 강함 약함
최신 스펙 변경 감지 약함 약함 강함

내 기준 추천은 이거다. PR CI는 emulate, nightly나 staging smoke는 실제 API, unit은 mock. 이 셋을 섞는 게 제일 덜 답답하다.


5단계 체크리스트: emulate 붙이기

STEP 1: 설치 및 실행 확인

emulate는 npx로 바로 실행할 수 있습니다.

npx emulate

이 명령어 하나로 세 가지 서비스가 동시에 뜹니다:

  • Vercel → <http://localhost:4000>
  • GitHub → <http://localhost:4001>
  • Google → <http://localhost:4002>

터미널에 다음과 같은 로그가 나오면 성공입니다:

[emulate] Starting Vercel API on <http://localhost:4000>
[emulate] Starting GitHub API on <http://localhost:4001>
[emulate] Starting Google API on <http://localhost:4002>

이제 브라우저에서 <http://localhost:4001/users/testuser에> 접속해보세요. GitHub API처럼 JSON 응답이 나옵니다.

체크포인트:
– [ ] npx emulate 실행 시 세 서비스 모두 정상 시작
– [ ] <http://localhost:4001> 접속 시 JSON 응답 확인


STEP 2: 특정 서비스만 띄우기

전부 필요 없고 GitHub API만 테스트하고 싶다면:

emulate --service github

포트 번호를 바꾸고 싶다면:

emulate --service github --port 5000

이러면 GitHub API가 <http://localhost:5000에서> 실행됩니다.

여러 서비스를 동시에 띄우되 포트만 바꾸고 싶다면:

emulate --service vercel,github --port 3000

이 경우 Vercel은 3000, GitHub는 3001에 자동으로 할당됩니다.

실무 팁: CI 환경에서는 포트 충돌을 막기 위해 랜덤 포트를 쓰는 게 안전합니다. 프로그래매틱 API를 쓰면 자동으로 빈 포트를 찾아줍니다. (STEP 3에서 설명)

체크포인트:
– [ ] --service 옵션으로 필요한 서비스만 띄우기
– [ ] --port 옵션으로 포트 번호 변경 확인


STEP 3: 테스트 코드에 통합하기

emulate를 CLI로만 쓰면 테스트 실행할 때마다 수동으로 서버를 띄워야 합니다.

프로그래매틱 API를 쓰면 테스트 setup 단계에서 자동으로 emulator를 시작하고, teardown에서 종료할 수 있습니다.

Vitest 예시:

// test/setup.ts
import { createEmulator } from 'emulate'
import { beforeAll, afterAll } from 'vitest'

let github: any

beforeAll(async () => {
  github = await createEmulator({ 
    service: 'github', 
    port: 0 // 0을 주면 자동으로 빈 포트 찾음
  })

  // 테스트용 환경변수 설정
  process.env.GITHUB_API_URL = github.url
})

afterAll(async () => {
  await github.stop()
})

Jest 예시:

// jest.setup.js
const { createEmulator } = require('emulate')

let vercel

beforeAll(async () => {
  vercel = await createEmulator({ service: 'vercel' })
  process.env.VERCEL_API_URL = vercel.url
})

afterAll(async () => {
  await vercel.stop()
})

이제 테스트 파일에서 process.env.GITHUB_API_URL을 쓰면 자동으로 emulator로 요청이 갑니다.

실무 팁: 프로덕션에서는 <https://api.github.com,> 테스트에서는 <http://localhost:4001> 이렇게 URL을 환경별로 바꿔야 합니다. 이 때문에 API 클라이언트를 만들 때 base URL을 환경변수로 받도록 설계해야 합니다.

// lib/github.ts
const baseURL = process.env.GITHUB_API_URL || '<https://api.github.com'>

export const githubClient = axios.create({ baseURL })

체크포인트:
– [ ] createEmulator() 로 테스트 setup에서 emulator 시작
– [ ] 환경변수로 API base URL 주입
– [ ] teardown에서 stop() 호출


STEP 4: Seed 데이터로 초기 상태 설정

실제 테스트에서는 “PR이 이미 3개 있는 상태”에서 시작하거나, “Vercel 프로젝트가 2개 있는 상태”를 가정해야 할 때가 많습니다.

emulate는 YAML/JSON 파일로 초기 데이터를 주입할 수 있습니다.

emulate.config.yaml 예시:

github:
  users:
    - login: testuser
      name: Test User
      email: test@example.com

  repositories:
    - name: test-repo
      owner: testuser
      private: false
      default_branch: main

  pull_requests:
    - repo: test-repo
      number: 1
      title: "Fix bug in API"
      state: open
      base: main
      head: fix-api-bug

vercel:
  users:
    - id: user_123
      email: dev@example.com
      name: Developer

  teams:
    - id: team_456
      name: Engineering

  projects:
    - id: prj_abc
      name: my-app
      team: team_456

  deployments:
    - project: prj_abc
      url: my-app-xyz.vercel.app
      state: READY
      created_at: 2026-03-24T10:00:00Z

google:
  oauth_clients:
    - client_id: test-client-id
      client_secret: test-secret
      redirect_uris:
        - <http://localhost:3000/auth/callback>

이 파일을 만들고 emulate를 실행하면:

emulate --seed emulate.config.yaml

이제 GET <http://localhost:4001/repos/testuser/test-repo/pulls를> 호출하면 PR #1이 이미 있는 상태로 응답이 옵니다.

실무 팁: seed 파일은 테스트 시나리오별로 여러 개 만들어두는 게 좋습니다.

test/fixtures/
  empty-state.yaml      # 아무것도 없는 상태
  with-prs.yaml         # PR 3개 있는 상태
  with-deployments.yaml # Vercel 배포 5개 있는 상태

체크포인트:
– [ ] emulate.config.yaml 파일 작성
– [ ] --seed 옵션으로 초기 데이터 로드
– [ ] API 호출 시 seed 데이터가 반환되는지 확인


STEP 5: OAuth 및 Webhook 흐름 테스트

emulate의 진짜 강점은 상태를 가지는 API 에뮬레이션입니다.

단순히 /users/testuser 호출에 JSON 응답 주는 게 아니라, OAuth 로그인 → 토큰 발급 → 토큰으로 API 호출 → 토큰 만료 → 갱신 이런 전체 흐름을 로컬에서 재현할 수 있습니다.

OAuth 흐름 테스트 예시

Google OAuth 로그인 통합테스트:

// test/auth.test.ts
import { describe, it, expect, beforeAll } from 'vitest'
import axios from 'axios'

describe('Google OAuth flow', () => {
  let googleURL: string

  beforeAll(() => {
    googleURL = process.env.GOOGLE_API_URL || '<http://localhost:4002'>
  })

  it('should complete full OAuth flow', async () => {
    // 1. Authorization code 요청
    const authURL = `${googleURL}/o/oauth2/v2/auth?client_id=test-client-id&redirect_uri=<http://localhost:3000/auth/callback&response_type=code&scope=openid%20email`>

    // emulate는 자동으로 mock authorization code를 리턴
    // 실제로는 브라우저가 redirect를 따라가지만, 테스트에서는 code를 직접 추출
    const code = 'mock_auth_code_12345'

    // 2. Authorization code를 token으로 교환
    const tokenRes = await axios.post(`${googleURL}/token`, {
      code,
      client_id: 'test-client-id',
      client_secret: 'test-secret',
      redirect_uri: '<http://localhost:3000/auth/callback',>
      grant_type: 'authorization_code'
    })

    expect(tokenRes.data).toHaveProperty('access_token')
    expect(tokenRes.data).toHaveProperty('refresh_token')
    expect(tokenRes.data.token_type).toBe('Bearer')

    // 3. Access token으로 사용자 정보 조회
    const userRes = await axios.get(`${googleURL}/oauth2/v1/userinfo`, {
      headers: { Authorization: `Bearer ${tokenRes.data.access_token}` }
    })

    expect(userRes.data).toHaveProperty('email')
    expect(userRes.data.verified_email).toBe(true)
  })
})

이 테스트는 외부 네트워크 없이 전체 OAuth 흐름을 검증합니다.

실제 Google API를 쓰면 브라우저를 띄워서 로그인 화면을 거쳐야 하지만, emulate에서는 코드만으로 전체 흐름을 시뮬레이션할 수 있습니다.

Webhook 흐름 테스트 예시

GitHub PR merge 후 Vercel deployment가 트리거되는 워크플로우 테스트:

// test/deploy-on-merge.test.ts
import { describe, it, expect } from 'vitest'
import { githubClient } from '../lib/github'
import { vercelClient } from '../lib/vercel'

describe('PR merge triggers Vercel deployment', () => {
  it('should create deployment when PR is merged', async () => {
    // 1. PR merge
    await githubClient.patch('/repos/testuser/test-repo/pulls/1', {
      state: 'closed',
      merged: true
    })

    // 2. Webhook handler가 동작한다고 가정
    // (실제로는 webhook을 emulate가 POST로 보내줌)

    // 3. Vercel deployment 생성 확인
    const deployments = await vercelClient.get('/v13/deployments', {
      params: { projectId: 'prj_abc' }
    })

    expect(deployments.data.deployments).toHaveLength(1)
    expect(deployments.data.deployments[0].state).toBe('BUILDING')
  })
})

emulate는 GitHub의 webhook payload 형식을 그대로 재현하므로, 실제 프로덕션에서 받을 JSON 구조를 테스트할 수 있습니다.

실무 팁: Webhook은 비동기로 동작하므로 테스트에서 폴링 로직을 추가해야 합니다.

// 최대 5초간 deployment 상태가 READY가 될 때까지 대기
await waitForCondition(async () => {
  const res = await vercelClient.get(`/v13/deployments/${deploymentId}`)
  return res.data.state === 'READY'
}, { timeout: 5000, interval: 500 })

체크포인트:
– [ ] OAuth authorization code → token 교환 흐름 테스트
– [ ] Webhook payload 수신 및 처리 로직 검증
– [ ] 비동기 상태 변화 폴링 테스트


언제 emulate를 쓰고, 언제 실제 API를 써야 하나

emulate가 만능은 아닙니다.

emulate를 쓰면 좋은 경우:

  1. CI 파이프라인에서 빠른 피드백이 필요할 때
    PR 올릴 때마다 돌리는 테스트는 5분 안에 끝나야 합니다. 실제 API를 쓰면 10분 넘게 걸리는 테스트를, emulate로는 1분 안에 끝낼 수 있습니다.

  2. 로컬 개발 환경에서 네트워크 없이 개발할 때
    비행기에서 코딩하거나, 카페 와이파이가 불안정한 상황에서도 emulate가 로컬에 떠 있으면 정상적으로 개발할 수 있습니다.

  3. Rate limit 걱정 없이 대량 테스트를 돌려야 할 때
    부하 테스트나 엣지 케이스 검증을 위해 API를 수천 번 호출해야 한다면, 실제 API는 금방 한도를 넘지만 emulate는 문제없습니다.

  4. OAuth 토큰 만료 같은 시간 기반 시나리오를 테스트할 때
    실제 Google OAuth token은 1시간 후 만료되는데, 이걸 테스트하려고 1시간 기다릴 수는 없습니다. emulate에서는 시스템 시간 조작 없이도 만료 시나리오를 바로 재현할 수 있습니다.

실제 API를 써야 하는 경우:

  1. emulate가 아직 지원하지 않는 API를 써야 할 때
    2026년 3월 기준 emulate는 Vercel, GitHub, Google 세 가지만 지원합니다. AWS, Azure, Stripe 같은 서비스는 아직 없습니다.

  2. API 스펙 변경을 감지하고 싶을 때
    emulate는 특정 시점의 API 스펙을 재현하므로, GitHub가 API를 업데이트해도 emulate는 옛날 버전일 수 있습니다. 정기적으로 실제 API에 대해서도 테스트를 돌려서 스펙 변경을 감지해야 합니다.

  3. 프로덕션 배포 전 최종 검증 단계
    emulate 테스트는 통과했지만 실제 API에서는 미묘하게 다를 수 있습니다. Staging 환경에서 실제 API를 쓰는 통합테스트를 한 번 더 돌리는 게 안전합니다.

추천 전략: 하이브리드 접근

  • 로컬 개발 + PR CI: emulate 사용 → 빠른 피드백
  • Daily nightly build: 실제 API 사용 → 스펙 변경 감지
  • Staging 배포 시: 실제 API 사용 → 최종 검증

이렇게 하면 개발 속도와 신뢰성을 둘 다 얻을 수 있습니다.

내가 이 글에서 제일 중요하다고 보는 함정 3개

  1. base URL 주입 안 되는 코드
    이게 안 되면 emulate를 붙일 수가 없다. 테스트 도구 문제가 아니라 설계 문제다.

  2. 상태 격리 전략 없이 에뮬레이터만 띄우는 것
    상태 있는 테스트는 빨라지는 대신 오염도 빨라진다. 고유 ID나 reset 전략 없이 쓰면 디버깅이 더 어려워질 수 있다.

  3. emulate 통과를 실제 API 통과로 오해하는 것
    이 글에서도 적었지만, API 스냅샷과 실제 서비스 최신 상태는 다를 수 있다. smoke test 한 줄이라도 실제 API로 남겨둬야 한다.


실전 체크리스트: emulate 도입 전에 확인할 것

emulate를 팀에 도입하기 전에 점검해야 할 항목들입니다.

1. API 클라이언트 base URL 주입 가능 여부

emulate를 쓰려면 코드에서 API base URL을 환경변수로 받을 수 있어야 합니다.

나쁜 코드:

const response = await fetch('<https://api.github.com/users/testuser')>

이러면 테스트 환경에서 emulate로 요청을 보낼 수 없습니다.

좋은 코드:

const baseURL = process.env.GITHUB_API_URL || '<https://api.github.com'>
const response = await fetch(`${baseURL}/users/testuser`)

체크포인트:
– [ ] 모든 API 클라이언트가 base URL을 설정 가능한가?
– [ ] 환경변수로 URL을 주입할 수 있는가?


2. 테스트 격리 전략

emulate는 상태를 가지므로, 테스트 A에서 만든 PR이 테스트 B에 영향을 줄 수 있습니다.

해결 방법:

  1. 테스트마다 새 emulator 인스턴스 띄우기 (가장 안전, 하지만 느림)
  2. 테스트 후 상태 리셋 API 호출 (빠르지만 리셋 로직 구현 필요)
  3. 고유 ID 사용 (테스트마다 다른 repo 이름, project ID 쓰기)

추천: 통합테스트 스위트 전체를 하나의 emulator 인스턴스에서 돌리되, 각 테스트가 고유 namespace를 쓰도록 설계.

const repoName = `test-repo-${Date.now()}`
await githubClient.post('/user/repos', { name: repoName })

체크포인트:
– [ ] 테스트 간 상태 오염 방지 전략 수립
– [ ] 고유 ID 생성 로직 구현


3. Seed 데이터 관리 전략

seed 파일이 많아지면 관리가 복잡해집니다.

추천 구조:

test/fixtures/
  common/
    users.yaml           # 모든 테스트에서 쓰는 기본 사용자
    teams.yaml
  scenarios/
    pr-review.yaml       # PR 리뷰 테스트용 seed
    deployment.yaml      # 배포 테스트용 seed
    oauth.yaml           # OAuth 테스트용 seed

YAML 파일의 anchor 기능을 쓰면 중복을 줄일 수 있습니다:

# common/users.yaml
default-user: &default-user
  name: Test User
  email: test@example.com

# scenarios/pr-review.yaml
github:
  users:
    - <<: *default-user
      login: reviewer

체크포인트:
– [ ] seed 파일 구조 정의
– [ ] 공통 데이터 분리
– [ ] YAML anchor로 중복 제거


4. CI 환경 설정

CI에서 emulate를 띄우려면 Node.js 환경이 필요합니다.

GitHub Actions 예시:

name: Integration Tests

on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Start emulate
        run: npx emulate --service github,vercel &

      - name: Wait for emulate
        run: |
          timeout 30 bash -c 'until curl -s <http://localhost:4000> > /dev/null; do sleep 1; done'

      - name: Run tests
        run: npm test
        env:
          GITHUB_API_URL: <http://localhost:4001>
          VERCEL_API_URL: <http://localhost:4000>

GitLab CI 예시:

integration-tests:
  image: node:20
  script:
    - npm ci
    - npx emulate --service github,vercel &
    - sleep 5  # emulate 시작 대기
    - npm test
  variables:
    GITHUB_API_URL: <http://localhost:4001>
    VERCEL_API_URL: <http://localhost:4000>

체크포인트:
– [ ] CI 환경에서 emulate 시작 스크립트 작성
– [ ] Health check 로직 추가 (서버 준비 대기)
– [ ] 환경변수 주입 확인


5. emulate 버전 고정

emulate는 아직 초기 단계(2026년 3월 공개)이므로 API가 자주 바뀔 수 있습니다.

package.json에서 버전을 고정하세요:

{
  "devDependencies": {
    "emulate": "^0.1.0"
  }
}

^ 대신 정확한 버전을 쓰면 더 안전합니다:

{
  "devDependencies": {
    "emulate": "0.1.0"
  }
}

체크포인트:
– [ ] package.json에 emulate 버전 고정
– [ ] 정기적으로 changelog 확인 후 업그레이드


주의사항: emulate로 잡을 수 없는 것들

emulate가 해결하는 건 API 계약 검증입니다.

하지만 다음 문제들은 여전히 남습니다:

1. 네트워크 실패 시나리오

emulate는 로컬호스트이므로 네트워크가 끊기거나, 타임아웃이 발생하거나, DNS 조회가 실패하는 시나리오를 재현할 수 없습니다.

이런 건 별도로 chaos engineering 도구(toxiproxy, WireMock 등)를 써야 합니다.

2. API 버전 불일치

emulate가 재현하는 API 스펙은 특정 시점의 스냅샷입니다.

GitHub가 새 API를 릴리즈했는데 emulate가 아직 반영 안 했으면, 테스트는 통과하지만 프로덕션에서 터질 수 있습니다.

대응책: 실제 API에 대한 smoke test를 주기적으로 돌려서 스펙 변경을 감지해야 합니다.

3. Rate Limit 이외의 제약

GitHub API의 rate limit은 emulate에서 재현 안 되지만, 다른 제약들(예: repo 생성 개수 한도, file size 제한)도 정확히 재현되지 않을 수 있습니다.

프로덕션에서만 발생하는 제약 조건은 별도로 문서화하고 수동 테스트해야 합니다.


실전 예시: Vercel 배포 상태 폴링 테스트

실무에서 자주 쓰이는 패턴: Vercel에 배포하고, 상태가 READY가 될 때까지 폴링하는 로직.

// lib/vercel-deploy.ts
export async function deployAndWait(projectId: string, gitRef: string) {
  const baseURL = process.env.VERCEL_API_URL || '<https://api.vercel.com'>
  const token = process.env.VERCEL_TOKEN

  // 1. 배포 생성
  const createRes = await axios.post(`${baseURL}/v13/deployments`, {
    name: projectId,
    gitSource: { ref: gitRef, type: 'github' }
  }, {
    headers: { Authorization: `Bearer ${token}` }
  })

  const deploymentId = createRes.data.id

  // 2. 상태 폴링 (최대 5분)
  const startTime = Date.now()
  const timeout = 5 * 60 * 1000 // 5분

  while (Date.now() - startTime < timeout) {
    const statusRes = await axios.get(`${baseURL}/v13/deployments/${deploymentId}`, {
      headers: { Authorization: `Bearer ${token}` }
    })

    if (statusRes.data.readyState === 'READY') {
      return statusRes.data
    }

    if (statusRes.data.readyState === 'ERROR') {
      throw new Error(`Deployment failed: ${statusRes.data.errorMessage}`)
    }

    await new Promise(resolve => setTimeout(resolve, 5000)) // 5초 대기
  }

  throw new Error('Deployment timeout')
}

emulate 기반 테스트:

// test/vercel-deploy.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { createEmulator } from 'emulate'
import { deployAndWait } from '../lib/vercel-deploy'

describe('Vercel deployment polling', () => {
  let vercel: any

  beforeAll(async () => {
    vercel = await createEmulator({ service: 'vercel', port: 0 })
    process.env.VERCEL_API_URL = vercel.url
    process.env.VERCEL_TOKEN = 'test-token'
  })

  afterAll(async () => {
    await vercel.stop()
  })

  it('should wait until deployment is READY', async () => {
    const deployment = await deployAndWait('my-project', 'main')

    expect(deployment.readyState).toBe('READY')
    expect(deployment.url).toMatch(/\.vercel\.app$/)
  }, { timeout: 10000 }) // 테스트 타임아웃 10초

  it('should throw error if deployment fails', async () => {
    // emulate에서 ERROR 상태를 시뮬레이션하려면
    // seed 파일에 미리 ERROR 상태 deployment를 만들어두거나
    // mock API를 추가로 구현해야 함
    // (2026년 3월 기준 emulate는 상태 전환 시뮬레이션 미지원)

    await expect(deployAndWait('failing-project', 'main')).rejects.toThrow('Deployment failed')
  })
})

이 테스트는 실제 Vercel API 없이 배포 폴링 로직을 검증합니다.

로컬에서 돌리면 5초 안에 끝나고, CI에서도 빠르게 피드백을 받을 수 있습니다.


팀 도입 시 고려사항

1. 학습 곡선

emulate는 새로운 도구이므로 팀원들이 적응하는 데 시간이 걸립니다.

추천 전략:

  • 먼저 한 명이 파일럿 프로젝트에서 emulate를 붙여보고 가이드 작성
  • 팀 내부 워크샵에서 실습 세션 진행
  • FAQ 문서 작성 (포트 충돌, seed 파일 수정법 등)

2. 기존 모킹 코드와의 충돌

기존에 jest.mock()이나 sinon으로 API를 모킹하고 있었다면, emulate로 전환하면서 코드를 수정해야 합니다.

점진적 마이그레이션:

  1. 새로운 테스트부터 emulate 사용
  2. 기존 테스트는 당분간 mock 유지
  3. 리팩토링 일정 잡아서 한 번에 전환

3. 성능 트레이드오프

emulate도 Node.js 프로세스를 띄우는 거라 완전히 0ms는 아닙니다.

테스트 100개를 돌릴 때:

  • Mock 사용: 3초
  • emulate 사용: 10초
  • 실제 API 사용: 300초

emulate가 mock보다는 느리지만, 실제 API보다는 30배 빠릅니다.

“테스트가 너무 느려진다”는 피드백이 나오면, 테스트를 두 가지로 분리하세요:

  • Unit 테스트: Mock 사용, PR마다 실행
  • Integration 테스트: emulate 사용, merge 전에 실행

마무리: 통합테스트에서 네트워크를 빼면 남는 것

emulate가 해결하려는 핵심 문제는 “통합테스트는 중요한데 너무 느리고 불안정하다”입니다.

Mock은 빠르지만 신뢰도가 낮고, 실제 API는 신뢰도가 높지만 너무 느립니다.

emulate는 그 중간을 노립니다. 프로덕션급 재현도 + 로컬 속도.

2026년 3월 기준 Vercel, GitHub, Google 세 가지만 지원하지만, 이 세 가지만으로도 대부분의 웹 앱 통합테스트를 커버할 수 있습니다.

앞으로 AWS, Stripe, Twilio 같은 서비스도 추가되면, emulate는 통합테스트의 새로운 표준이 될 가능성이 높습니다.

당장 적용하기 어렵더라도, “통합테스트를 이렇게 설계할 수 있구나”라는 관점 자체가 중요합니다.

API base URL을 환경변수로 주입받도록 설계하고, 상태를 격리하고, seed 데이터로 초기 조건을 설정하는 이 패턴은 emulate가 아니더라도 다른 도구에서도 유용하게 쓰입니다.


다음에 읽을 글


참고 자료


작성일: 2026년 3월 24일 키워드: emulate, 통합테스트, CI, 로컬 에뮬레이터, API 모킹, Vercel, GitHub, Google, OAuth, Webhook, 2026 채널: TECHTAEK