4.Node.js/Vitest&TypeBox

[Vitest] 5편. Vitest로 외부 API 테스트하기: axios, undici

쿼드큐브 2026. 2. 5. 08:35
반응형
반응형

 

 

 

테스트하기 삽화 이미지
테스트하기 삽화 이미지

 

5편. Vitest로 외부 API 테스트하기: axios, undici

 

📚 목차
1. 외부 API 테스트 실습 준비
2. Axios vs Undici
3. Axios를 이용한 테스트 (생산성 중심)
4. Undici를 이용한 테스트 (성능 및 표준 중심)

 

📂 [GitHub 예시 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /vitest

 

 

1. 외부 API 테스트 실습 준비

1. Fasticy 실습용 서버 구현하기

import Fastify from 'fastify';
import { testRoutes } from './routes/test.routes';

const app = Fastify({
  logger: true,
});

app.register(testRoutes, { prefix: '/api' });

app.listen({ port: 3001 }).then(() => {
  console.log('Test API server running on http://localhost:3001');
});

 

테스트용 엔드포인트 요약

 

Method Path Input유형
GET /api/echo?message=hello Query
GET /api/users/:id Path Param
POST /api/users Body(JSON)
GET /api/secure Header

 

서버실행

cd /nodejs-tutorials/vitest
npx tsx .\src\ch05\5-1.실습용서버.ts

🚀 Test API Server running on http://localhost:8080

 

2. axios, undici 설치

cd /nodejs-tutorials/vitest
npm install axios undici

PS D:\NodejsDevelope\workspace\nodejs-tutorials\vitest> npm list
nodejs-tutorials@1.0.0 D:\NodejsDevelope\workspace\nodejs-tutorials
└─┬ vitest-study@1.0.0 -> .\vitest
  ├── @vitest/ui@4.0.16
  ├── axios@1.13.2
  ├── fastify@5.6.2
  ├── undici@7.18.2
  └── vitest@4.0.16

 

2. Axios vs Undici

Node.js 환경에서 HTTP 요청을 보낼 때, Axios는 "편리한 도구 세트"라면 Undici는 "빠르고 가벼운 엔진"과 같습니다.

 

1. Axios vs Undici (Node.js Native) 비교

항목 Axios Undici(Node.js Native)
철학 개발자 경험(DX) 및 생산성 중심 성능 극대화 및 Node.js 표준 준수
에러 처리 HTTP 상태 코드가 2xx가 아니면 Exception 발생 상태 코드와 무관하게 항상 응답 객체 반환
데이터 직렬화 JSON 자동 처리 (response.data) JSON.stringify() 및 await body.json() 수동 호출
연결 관리 기본 http.Agent 사용 (상대적으로 느림) 고성능 Connection Pool 및 HTTP Pipelining 내장
추천 상황 프론트/백엔드 공용 코드, 빠른 기능 구현 대규모 트래픽 처리, 고성능 마이크로서비스

 

 

2. 코드 스타일 비교

🔸 Axios: "개발자는 비즈니스 로직만 신경 쓰세요"

try {
  const response = await axios.get('https://api.example.com/data');
  console.log(response.data); // JSON이 이미 객체로 변환되어 있음
} catch (error) {
  // 404나 500 에러는 자동으로 이곳에서 처리됨
  console.error('에러 발생!', error.response.status);
}

 

🔸 Undici (fetch): "표준을 따르고, 성능을 위해 명시적으로 작성하세요"

const response = await fetch('https://api.example.com/data');

if (!response.ok) { // 404/500 에러를 직접 체크해야 함
  throw new Error(`HTTP Error: ${response.status}`);
}

const data = await response.json(); // 스트림을 읽어 JSON으로 직접 변환
console.log(data);

 

3. 언제 무엇을 써야 할까요? (결정 가이드)

1) Axios를 추천하는 경우
▸ 프론트엔드와 백엔드에서 동일한 코드 베이스를 유지하고 싶을 때.
▸ 인터셉터를 활용해 인증 토큰 주입이나 에러 로깅을 공통화하고 싶을 때.
▸ 빠른 프로토타이핑과 높은 생산성이 중요할 때.

 

2) Undici를 추천하는 경우
▸ 초당 수천 건 이상의 요청을 처리해야 하는 고성능 마이크로서비스(MSA)를 구축할 때.
▸ 메모리 사용량을 최소화해야 하는 서버리스(Lambda 등) 환경일 때.
▸ 외부 라이브러리 의존성을 줄이고 Node.js 네이티브 표준을 따르고 싶을 때.

반응형

 

3. Axios를 이용한 테스트 (생산성 중심)

Axios는 인터셉터와 자동 JSON 파싱 덕분에 비즈니스 로직 검증에 매우 편리합니다.

🔸 axios는 JSON 파싱을 자동 처리
🔸 4xx / 5xx 응답은 catch에서 검증
🔸 Status / Payload / Header를 함께 검증해야 의미 있음

 

1. axios 요청 메서드 구조

▸ 축약 메서드 형태

axios.get(url, config)
axios.post(url, data, config)
axios.put(url, data, config)
axios.delete(url, config)

 

▸ 범용 request 형태

axios.request({
  method: 'GET',
  url: 'http://localhost:8080/api/echo',
  headers: {},
  params: {},
  data: {}
})

 

2. axios 응답 객체 구조

{
  data: any,        // 응답 Body (JSON 자동 파싱)
  status: number,   // HTTP Status Code
  headers: object,  // Response Headers
  config: object,   // 요청 설정
  request: object   // 내부 요청 객체
}

 

3. api 테스트 예제

1) Axios 인스턴스 설정

테스트 코드마다 BASE_URL이나 Authorization 헤더를 반복해서 적는 것은 비효율적입니다. axios.create()를 활용해 공통 설정을 분리하면 코드가 훨씬 깔끔해집니다.

import axios from 'axios';

export const testAxios = axios.create({
  baseURL: 'http://localhost:8080',
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json',
    'X-Test-Client': 'Vitest',
  },
});

// validateStatus를 설정하면 4xx, 5xx에서도 throw를 던지지 않게 할 수 있습니다 (선택사항)
// validateStatus: (status) => status < 500

 

2) 주요 테스트 케이스

import { describe, it, expect } from 'vitest';

describe('Axios API 테스트', () => {
  it('GET: 쿼리 파라미터 전달 및 자동 JSON 검증', async () => {
    const response = await testAxios.get('/api/echo', { params: { message: 'hello' } });
    
    expect(response.status).toBe(200);
    expect(response.data).toEqual({ method: 'GET', message: 'hello' });
  });

  it('POST: 객체를 전달하면 자동으로 JSON으로 변환됨', async () => {
    const newUser = { name: 'kim', age: 30 };
    const response = await testAxios.post('/api/users', newUser);
    
    expect(response.status).toBe(201);
    expect(response.data.user).toMatchObject(newUser);
  });

  it('Error: 401 Unauthorized 발생 시 catch 문 활용', async () => {
    try {
      await testAxios.get('/api/secure');
    } catch (error: any) {
      expect(error.response.status).toBe(401);
      expect(error.response.data.message).toBe('Unauthorized');
    }
  });
});

Axios는 상태 코드가 2xx 범위를 벗어나면 예외를 발생시킵니다. 따라서 실패 테스트는 try/catch 문을 활용하며, 테스트가 허무하게 통과되지 않도록 주의해야 합니다.

 

4. Undici를 이용한 테스트 (성능 및 표준 중심)

Undici는 Node.js 최적화 클라이언트로, fetch와 유사한 인터페이스를 가집니다.

🔸 Node.js 내장 fetch()의 실제 구현체
🔸 고성능, 저수준 HTTP 클라이언트
🔸 스트림 기반 응답 처리 모델
🔸 Fastify 내부 HTTP 스택과 동일 계열

 

1. undici 요청 메서드 구조

undici에서 가장 많이 사용하는 테스트용 API는 request()입니다.

가장 기본이 되는 API입니다. Promise를 반환하며, 비동기 방식으로 데이터를 가져옵니다.

import { request } from 'undici';

// 기본 호출 구조
const { statusCode, headers, body } = await request('http://localhost:8080/api/echo', {
  method: 'GET', // 기본값은 GET
  // 필요 시 headers, body 등을 추가 설정할 수 있습니다.
});

 

2. undici 응답 객체 구조

Undici의 응답은 메모리 효율을 위해 스트림(Stream) 방식을 사용합니다.

{
  statusCode: number,
  headers: Record<string, string>,
  body: ReadableStream
}

 

3. Body 처리: 스트림을 데이터로 바꾸기

ubody는 아직 완성된 데이터가 아니라 "데이터가 들어오고 있는 통로"와 같습니다.

이를 우리가 읽을 수 있는 형태로 변환하려면 본문 처리 메서드를 사용해야 합니다.

await body.json()        // 데이터를 다 모아서 JSON 객체로 변환합니다
await body.text()        // 전체 데이터를 하나의 문자열로 변환합니다.
await body.arrayBuffer() // 이미지나 파일 같은 이진(Binary) 데이터로 변환합니다

💡 Undici의 body는 스트림 방식이기 때문에 "한 번만 읽을 수 있다"는 특징이 있습니다.

▸ 다시 읽으면, "이미 본문이 사용되었습니다(Body has already been consumed)"라는 취지의 에러를 던집니다.

▸ const data = await body.json()처럼 한 번 읽은 값을 변수에 담아두고, 그 변수를 여러 번 사용한다.

 

💡 중요: body.json()이 내부에서 하는 일
▸ 스트림 소비: 통로에 흐르는 모든 데이터 조각(Chunks)을 끝까지 기다려 모읍니다.
▸ 디코딩: 모인 데이터를 문자열로 바꿉니다.
▸ 파싱: JSON.parse()를 실행하여 최종 JavaScript 객체를 반환합니다.

 

 

4. 주요 테스트 케이스

import { request } from 'undici';
import { describe, it, expect } from 'vitest';

const BASE_URL = 'http://localhost:3001';

describe('Undici API 테스트', () => {
  it('GET: 명시적인 body.json() 호출이 필요함', async () => {
    const { statusCode, body } = await request(`${BASE_URL}/api/echo?message=hello`);
    
    expect(statusCode).toBe(200);
    const data = await body.json(); // 스트림을 소모하며 JSON 파싱
    expect(data).toMatchObject({ message: 'hello' });
  });

  it('POST: 헤더와 문자열화된 body를 직접 설정', async () => {
    const payload = JSON.stringify({ name: 'kim', age: 30 });
    const { statusCode, body } = await request(`${BASE_URL}/api/users`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: payload
    });

    expect(statusCode).toBe(201);
    expect(await body.json()).toHaveProperty('user.name', 'kim');
  });

  it('Security: 에러 상태 코드도 예외 없이 반환됨', async () => {
    const { statusCode, body } = await request(`${BASE_URL}/api/secure`);
    
    // Axios와 달리 try-catch 없이 statusCode 확인 가능
    expect(statusCode).toBe(401);
    const data = await body.json();
    expect(data).toEqual({ message: 'Unauthorized' });
  });
});

 


※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.

반응형

 

반응형