
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 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'4.Node.js > Vitest&TypeBox' 카테고리의 다른 글
| [Vitest] 7편. Vitest Mocking 이해하기 : vi.mock()과 vi.fn() (0) | 2026.02.10 |
|---|---|
| [Vitest] 6편. MSW(Mock Service Worker) + Vitest 실무 API 테스트 (0) | 2026.02.06 |
| [Vitest] 4편. Vitest로 Web API 테스트하기: Fastify inject() 활용법 (0) | 2026.01.30 |
| [Vitest] 3편. Vitest의 Lifecycle · Async · Execution Control API (0) | 2026.01.28 |
| [Vitest] 2편. Vitest 테스트 구조 설계와 Assertion, Matcher 이해하기 (0) | 2026.01.27 |