axios-mock-adapter
프론트엔드 개발을 하다 보면 백엔드 API가 아직 준비되지 않은 상태에서 UI를 먼저 만들어야 하는 상황이 자주 생긴다. 또는 테스트 코드에서 실제 HTTP 요청을 보내지 않고 API 응답을 시뮬레이션해야 할 때도 있다. 이런 경우에 axios 인스턴스의 어댑터를 교체해서 실제 네트워크 요청 없이 원하는 응답을 돌려주는 것이 axios-mock-adapter의 역할이다.
비슷한 도구로 MSW(Mock Service Worker)가 있는데, MSW는 Service Worker를 이용해서 네트워크 레벨에서 요청을 가로채는 방식이다. 반면 axios-mock-adapter는 axios의 내부 어댑터를 직접 교체하는 방식이라 axios에 의존적이지만, 설정이 훨씬 간단하고 별도의 서버 프로세스나 Service Worker 등록 없이 바로 사용할 수 있다. 빠르게 mock을 만들어서 개발하거나, 단위 테스트에서 API 호출을 격리하고 싶을 때 적합하다.
동작 원리
axios는 내부적으로 어댑터(adapter)라는 개념을 사용한다. 브라우저에서는 XMLHttpRequest, Node.js에서는 http 모듈을 사용하는 어댑터가 기본으로 설정되어 있다. axios-mock-adapter는 이 어댑터를 커스텀 어댑터로 교체해서, 요청이 실제 네트워크로 나가지 않고 mock 어댑터 안에서 처리되도록 한다.
axios.get("/api/users")
→ axios 내부 → adapter 호출
→ (원래) XMLHttpRequest로 실제 요청
→ (mock) MockAdapter가 등록된 핸들러에서 응답 반환
핸들러를 등록할 때 HTTP 메서드, URL, 파라미터 조건을 지정하면, 해당 조건에 맞는 요청이 들어왔을 때 미리 정의한 응답을 반환한다. 조건에 맞는 핸들러가 없으면 기본 설정에 따라 에러를 발생시키거나 실제 요청을 통과시킨다.
설치 및 기본 사용법
axios.get("/api/users")
→ axios 내부 → adapter 호출
→ (원래) XMLHttpRequest로 실제 요청
→ (mock) MockAdapter가 등록된 핸들러에서 응답 반환
npm install axios-mock-adapter --save-dev
new MockAdapter(axios)를 호출하는 순간 해당 axios 인스턴스의 어댑터가 교체된다. 이후 해당 인스턴스를 통한 모든 요청은 mock 어댑터를 거치게 된다.
커스텀 axios 인스턴스와 함께 사용
실제 프로젝트에서는 axios.create()로 만든 커스텀 인스턴스를 사용하는 경우가 대부분이다. 이 경우 전역 axios가 아니라 해당 인스턴스에 mock을 붙여야 한다.
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
// mock 인스턴스 생성
const mock = new MockAdapter(axios);
// GET /api/users 요청에 대한 응답 등록
mock.onGet("/api/users").reply(200, [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
// 이제 실제 네트워크 요청 없이 mock 응답이 반환됨
const response = await axios.get("/api/users");
console.log(response.data); // [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
delayResponse 옵션은 응답을 지연시켜서 실제 네트워크 환경을 시뮬레이션한다. 로딩 스피너나 스켈레톤 UI가 제대로 동작하는지 확인할 때 유용하다.
HTTP 메서드별 핸들러
각 HTTP 메서드에 대응하는 메서드가 제공된다.
// api/instance.ts
import axios from "axios";
export const api = axios.create({
baseURL: "https://example.com",
timeout: 5000,
});
// mock 설정
import MockAdapter from "axios-mock-adapter";
import { api } from "./instance";
const mock = new MockAdapter(api, { delayResponse: 800 });
onAny()를 사용하면 모든 HTTP 메서드에 대해 하나의 핸들러를 등록할 수 있다.
mock.onGet("/api/users").reply(200, { users: [] });
mock.onPost("/api/users").reply(201, { id: 3, name: "Charlie" });
mock.onPut("/api/users/1").reply(200, { id: 1, name: "Updated" });
mock.onDelete("/api/users/1").reply(204);
mock.onPatch("/api/users/1").reply(200, { id: 1, name: "Patched" });
mock.onHead("/api/health").reply(200);
mock.onOptions("/api/users").reply(200);
요청 파라미터로 분기
같은 URL이라도 쿼리 파라미터에 따라 다른 응답을 반환해야 할 수 있다. onGet의 두 번째 인자로 params 조건을 지정한다.
mock.onAny("/api/maintenance").reply(503, { message: "점검 중" });
POST 요청의 경우 request body로 분기할 수도 있다.
mock.onGet("/api/search", { params: { type: "title" } })
.reply(200, { results: ["제목 검색 결과"] });
mock.onGet("/api/search", { params: { type: "author" } })
.reply(200, { results: ["작성자 검색 결과"] });
콜백 함수로 동적 응답
정적 응답 대신 콜백 함수를 사용하면 요청 내용에 따라 동적으로 응답을 생성할 수 있다. 콜백은 config 객체를 인자로 받는데, 여기에 요청의 URL, params, data 등 모든 정보가 담겨 있다.
mock.onPost("/api/login", { username: "admin", password: "1234" })
.reply(200, { token: "abc123" });
mock.onPost("/api/login", { username: "admin", password: "wrong" })
.reply(401, { message: "인증 실패" });
콜백의 반환값은 [status, data, headers] 형태의 배열이다. headers는 생략 가능하다. 이 방식을 사용하면 필터링, 정렬, 페이지네이션 등 실제 API의 로직을 그대로 시뮬레이션할 수 있어서, 백엔드 없이도 프론트엔드의 복잡한 요청 시나리오를 테스트할 수 있다.
에러 시뮬레이션
API 호출이 실패하는 경우를 테스트하려면 에러 응답을 등록한다.
mock.onGet("/api/search").reply((config) => {
const { find, type, limit = 10, page = 1 } = config.params;
// 전체 데이터에서 필터링
const filtered = mockData.filter((item) => {
if (type === "title") return item.title.includes(find);
if (type === "author") return item.author.includes(find);
return item.title.includes(find) || item.author.includes(find);
});
// 페이지네이션 처리
const totalCount = filtered.length;
const totalPages = Math.ceil(totalCount / limit);
const startIndex = (page - 1) * limit;
const paginatedData = filtered.slice(startIndex, startIndex + limit);
return [
200,
{
status: 200,
data: paginatedData,
page,
total_count: totalCount,
total_pages: totalPages,
},
];
});
networkError()와 timeout()은 axios가 던지는 에러의 종류가 다르다. networkError()는 error.message가 "Network Error"이고, timeout()은 error.code가 "ECONNABORTED"이다. 프론트엔드에서 에러 종류에 따라 다른 UI를 보여주는 로직이 있다면 이 차이를 이용해서 각각 테스트할 수 있다.
// HTTP 에러 (서버가 응답은 했지만 에러 상태 코드)
mock.onGet("/api/users/999").reply(404, { message: "사용자를 찾을 수 없습니다" });
mock.onPost("/api/users").reply(422, { errors: { email: "이미 사용 중인 이메일" } });
// 네트워크 에러 (서버에 도달하지 못한 경우)
mock.onGet("/api/health").networkError();
// 타임아웃 (서버 응답이 너무 느린 경우)
mock.onGet("/api/slow-endpoint").timeout();
한 번만 응답 (replyOnce)
reply() 대신 replyOnce()를 사용하면 첫 번째 요청에만 해당 응답을 반환하고, 이후 요청은 다음 핸들러나 기본 동작을 따른다. 재시도 로직을 테스트할 때 유용하다.
try {
await axios.get("/api/health");
} catch (error) {
if (error.code === "ECONNABORTED") {
// 타임아웃 → "서버 응답이 느립니다. 잠시 후 다시 시도해주세요."
} else if (error.message === "Network Error") {
// 네트워크 에러 → "인터넷 연결을 확인해주세요."
} else if (error.response?.status === 500) {
// 서버 에러 → "서버에 문제가 발생했습니다."
}
}
같은 URL에 replyOnce()를 여러 번 체이닝하면 요청 순서대로 다른 응답을 반환한다. 세 번째 이후 요청은 등록된 핸들러가 없으므로 에러가 발생하거나 passthrough 설정에 따라 실제 요청이 나간다.
핸들러 초기화
테스트 간 격리를 위해 핸들러를 초기화하는 메서드가 제공된다.
// 첫 번째 요청: 500 에러
mock.onGet("/api/data").replyOnce(500, { message: "서버 에러" });
// 두 번째 요청: 정상 응답
mock.onGet("/api/data").replyOnce(200, { value: 42 });
await axios.get("/api/data"); // 500 에러
await axios.get("/api/data"); // 200 성공
테스트 프레임워크에서는 보통 beforeEach/afterEach에서 이 메서드들을 호출한다.
// 등록된 핸들러만 초기화 (mock 인스턴스는 유지)
mock.reset();
// 핸들러 초기화 + 요청 히스토리도 초기화
mock.resetHandlers();
// mock 인스턴스 완전 제거 (원래 어댑터 복원)
mock.restore();
각 테스트가 끝날 때 restore()를 호출해서 원래 어댑터를 복원한다. 이렇게 하면 테스트 간 mock 상태가 공유되는 문제를 방지할 수 있다.
passThrough로 부분 모킹
모든 요청을 mock하지 않고, 특정 요청만 mock하고 나머지는 실제 서버로 보내고 싶을 때가 있다. passThrough()를 사용하면 된다.
describe("사용자 API", () => {
let mock: MockAdapter;
beforeEach(() => {
mock = new MockAdapter(api);
});
afterEach(() => {
mock.restore();
});
it("사용자 목록을 가져온다", async () => {
mock.onGet("/api/users").reply(200, [{ id: 1, name: "Alice" }]);
const result = await fetchUsers();
expect(result).toHaveLength(1);
});
it("서버 에러 시 빈 배열을 반환한다", async () => {
mock.onGet("/api/users").reply(500);
const result = await fetchUsers();
expect(result).toEqual([]);
});
});
생성자 옵션에 onNoMatch: "passthrough"를 설정하면, 핸들러가 등록되지 않은 요청은 원래 어댑터를 통해 실제 네트워크 요청으로 나간다. 기본값은 핸들러가 없으면 에러를 발생시키는 것이다.
특정 URL에 대해 명시적으로 passthrough를 설정할 수도 있다.
const mock = new MockAdapter(axios, { onNoMatch: "passthrough" });
// /api/users만 mock하고, 나머지는 실제 요청
mock.onGet("/api/users").reply(200, [{ id: 1, name: "Alice" }]);
이 방식은 외부 API는 실제로 호출하면서 내부 API만 mock하는 통합 테스트에서 유용하다.
요청 히스토리 확인
mock 인스턴스에는 어떤 요청이 들어왔는지 기록하는 history 객체가 있다. 테스트에서 "올바른 파라미터로 API를 호출했는지" 검증할 때 사용한다.
mock.onGet("/api/external").passThrough();
mock.history는 HTTP 메서드별로 배열을 가지고 있다. mock.history.get, mock.history.post, mock.history.put 등으로 접근할 수 있고, 각 항목은 axios의 request config 객체다. data, params, headers 등 요청의 모든 정보를 확인할 수 있다.
주의할 점은 mock.history.post[0].data가 문자열이라는 것이다. axios가 요청 body를 JSON 문자열로 직렬화하기 때문이다. 비교할 때 JSON.parse()로 파싱하거나, toContain() 같은 문자열 매처를 사용해야 한다.
MSW와의 비교
| 기준 | axios-mock-adapter | MSW |
|---|---|---|
| 의존성 | axios 전용 | fetch, axios 등 모든 HTTP 클라이언트 |
| 동작 레벨 | axios 어댑터 교체 | Service Worker / 네트워크 레벨 |
| 설정 복잡도 | 매우 간단 | 초기 설정 필요 (worker 등록 등) |
| 브라우저 개발 도구 | 요청이 안 보임 | Network 탭에서 요청 확인 가능 |
| Node.js 지원 | 기본 지원 | setupServer() 별도 설정 |
| 타입 안전성 | 기본적 | 더 나은 타입 지원 |
axios-mock-adapter는 "axios를 쓰고 있고, 빠르게 mock을 설정하고 싶을 때" 적합하다. 프로젝트가 커지면서 fetch로 마이그레이션하거나, 여러 HTTP 클라이언트를 사용하게 되면 MSW가 더 유연한 선택이 된다.
개발 환경에서의 활용 패턴
테스트뿐 아니라 개발 환경에서 mock API 서버 대신 사용하는 패턴도 있다. 환경 변수로 mock 활성화 여부를 제어한다.
mock.onPost("/api/users").reply(201);
await axios.post("/api/users", { name: "Alice", email: "alice@example.com" });
// POST 요청 히스토리 확인
expect(mock.history.post).toHaveLength(1);
expect(JSON.parse(mock.history.post[0].data)).toEqual({
name: "Alice",
email: "alice@example.com",
});
URL에 정규식을 사용할 수 있다는 점도 주목할 만하다. /api/posts/1, /api/posts/42 같은 동적 경로를 하나의 핸들러로 처리할 수 있다.
이 패턴의 장점은 프론트엔드 개발자가 백엔드 API 완성을 기다리지 않고 독립적으로 개발을 진행할 수 있다는 것이다. API 스펙만 합의되면 mock 데이터로 UI를 완성하고, 나중에 mock을 제거하면 실제 API로 전환된다.
정리
- axios 어댑터를 교체하는 방식이라 설정이 간단하고 별도 서버/Worker 없이 바로 사용할 수 있다.
- replyOnce로 순차 응답, networkError/timeout으로 에러 시뮬레이션, history로 요청 검증까지 단위 테스트에 필요한 기능이 갖춰져 있다.
- axios 전용이라는 제약이 있으므로 HTTP 클라이언트가 다양해지면 MSW로의 전환을 고려해야 한다.