MSW로 백엔드 API 모킹하기
이번 포스팅에서는 MSW(Mock Service Worker) 라이브러리를 이용하여 백엔드 API를 모킹(mocking)하는 방법에 대해서 알아보겠습니다.
Mock Service Worker란?
MSW(Mock Service Worker)는 서비스 워커(Service Worker)를 사용하여 네트워크 호출을 가로채는 API 모킹(mocking) 라이브러리입니다. 쉽게 말해, 브라우저에 기생(?)해서 마치 백엔드 API인 척하면서 프론트엔드의 요청에 가짜 데이터를 응답해주는 녀석이라고 볼 수 있겠네요.
비교적에 최근에 도입된 웹 표준 기술인 서비스 워커(Service Worker)를 사용하면 브라우저로 부터 나가는 요청이나 들어오는 응답을 중간에서 감시하거나 변조, 캐싱과 같은 기존에 웹에서 할 수 없었던 부가적인 작업들을 할 수 있는데요. MSW는 이러한 서비스 워커의 능력을 백분 활용하여 웹 개발에 있어서 API 모킹의 수준을 한 단계 올려놓았다고 평가받으며 출시되자 마자 큰 호응을 얻고 있습니다.
MSW를 여러 가지 용도로 활용할 수 있지만 대표적으로 두가지 사례를 생각해볼 수 있는데요.
첫번째는 백엔드 API 개발과 프론트엔드 UI 개발이 동시에 진행되야하는 경우, 백엔드 API 구현이 완료될 때까지 프론트엔드 팀에서 임시로 사용하기 위한 가짜(mock) API를 서비스 워커로 돌리기 위해서이고, 두번째는 테스트를 실행 시 실제 백엔드 API에 네트워크 호출을 하는 대신에 훨씬 빠르고 안정적인 가짜 API 서버를 구축하기 위해서 입니다.
Mock Service Worker의 특징
기존 API 모킹 라이브러리 대비 MSW의 가장 큰 강점은 모킹이 네트워크 단에서 일어나기 때문에 프론트엔드 코드를 실제 백엔드 API와 네트워크 통신하는 것과 크게 다르지 않게 작성할 수 있다는 것입니다. 이 말은 나중에 가짜 API를 실제 API로 대체하는 것이 쉽다는 뜻이며 그만큼 프론트엔드 프로젝트의 개발 생산성이 올라가는 것을 의미합니다.
뿐만 아니라 라이브러리가 상당히 유연하게 디자인이 되어 있어서 개발용으로 브라우저 환경에서 서비스 워커로 돌리든 테스트용으로 Node.js 환경에서 Jest나 Cypress와 같은 테스트 러너(runner)로 돌리든 동일한 요청 핸들러(handler) 코드를 공유해서 사용할 수 있다는 것입니다. 이 말은 API 모킹을 위해서 작성해야하는 코드를 최소화 할 수 있다는 뜻이며 역시 개발 생산성 뿐만 아니라 유지 보수성 측면에서도 긍정적인 효과를 가져올 것입니다.
REST API 모킹과 GraphQL API 모킹을 모두 지원한다는 점도 MSW의 매력적인 부분입니다.
소소한 단점이라고 한다면 내부적으로 서비스 워커를 사용하다보니 인터넷 익스플로러와 같이 서비스 워커를 지원하지 않는 구식 브라우저에서는 동작하지 않는다는 것을 들 수 있겠네요.
실습 프로젝트 생성
실습으로 할일(todo) 관리를 위한 초간단한 React 앱을 개발하고 테스트해보려고 합니다. 저는 그냥 간단하게 Create React App을 사용하여 React 프로젝트를 하나 만들겠습니다.
Create React App에 대한 자세한 설명은 관련 포스팅을 참고 바랍니다.
$ npx create-react-app our-msw
참고로 MSW는 위에서 설명드린 것처럼 서비스 워커리는 웹 표준 기술을 사용하기 때문에 React 뿐만 아니라 VueJS, Svelte.js 등 어떤 자바스크립트 라이브러리/프레임워크와를 사용해도 무방하겠습니다.
MSW 라이브러리 설치
MSW 라이브러리는 npm 저장소에 msw
라는 이름의 패키지로 올라와 있습니다.
다음과 같이 npm 커맨드로 어떤 자바스크립트 프로젝트에서도 쉽게 설치할 수 있습니다.
$ npm i -D msw
기반 코드 자동 생성
MSW는 브라우저에서 서비스 워커를 통해서 작동하기 때문에 서비스 워커 등록을 위한 기본적인 코드가 필요한데요. 다행히도 서비스 워커에 대해서 잘 몰라도 MSW에서 제공하는 CLI 도구를 사용하면 이 코드를 쉽게 만들어 낼 수 있습니다.
$ npx msw init public/ --save
public/
부분에는 프로젝트에서 정적 리소스를 두는 폴더를 지정해줘야하는데요.
Create React App, NextJS, VueJS, Svelte.js, Vite 등 많은 라이브러리/프레임워크에서 public/
을 그대로 사용할 수 있습니다.
요청 핸들러 작성
가짜 API를 구현하려면 요청이 들어왔을 때 임의의 응답을 해주는 핸들러(handler) 코드를 작성해야합니다.
모킹 관련 코드를 프로젝트의 아무데나 상관해도 상관은 없으나 mocks
이라는 디렉토리에 두는 것이 일반적인 관례인데요.
그래서 저는 프로젝트의 src
디렉토리 아래에 mocks
디렉토리를 만들고, 그 안에 handlers.js
라는 파일을 생성한 후 할일 관리 UI를 구현하는데 필요한 두개의 엔드포인트(endpoint)를 위한 핸들러 코드를 작성하겠습니다.
REST API를 모킹할 때는 msw
모듈의 rest
객체를 사용하고요. Express.js 서버에서 볼 수 있는 코딩 패턴과 상당히 유사한 방식으로 핸들러를 구현할 수 있습니다.
할일 목록을 조회하가 위한 GET /todos
엔드포인트는 배열에 담긴 3개의 할일을 응답해주고, 새로운 할일을 추가하기 위한 POST /todos
엔드포인트는 요청 바디로 넘어온 할일을 배열에 추가해줍니다.
import { rest } from "msw";
const todos = ["먹기", "자기", "놀기"];
export const handlers = [
// 할일 목록
rest.get("/todos", (req, res, ctx) => {
return res(ctx.status(200), ctx.json(todos));
}),
// 할일 추가
rest.post("/todos", (req, res, ctx) => {
todos.push(req.body);
return res(ctx.status(201));
})
];
서비스 워커 생성
다음으로 msw
모듈에서 제공하는 setupWorker()
함수를 사용해서 서비스 워커를 생성하겠습니다.
위에서 작성한 요청 핸들러 코드를 불러와서 그대로 setupWorker()
함수의 인자로 넘겨주면 됩니다.
import { setupWorker } from "msw";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
서비스 워커 삽입
이제 서비스 워커를 구동하는 코드를 애플리케이션의 진입 시점(entrypoint)에 삽입을 해보겠습니다.
예를 들어, Create React App으로 만든 애플리케이션의 경우, src/index.js
파일을 다음과 같이 수정해주면 됩니다.
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { worker } from "./mocks/worker";if (process.env.NODE_ENV === "development") { worker.start();}
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>
);
일반적으로 개발 환경에서만 가짜 API를 사용하므로 환경 변수를 체크하여 선택적으로 서비스 워커가 구동되도록 해주고 있습니다.
서비스 워커 테스트
이제 애플리케이션을 구동 후에 브라우저에서 열면 콘솔에 다음과 같이 모킹이 활성화되었다는 메시지가 출력될 것입니다.
[MSW] Mocking enabled.
이제 fetch()
함수를 사용해서 정말로 GET /todos
에 요청을 보내면 가짜 응답이 오는지 확인해보겠습니다.
fetch("/todos")
.then((response) => response.json())
.then((data) => console.log(data));
[MSW] 22:09:24 GET /todos (200 OK)
['먹기', '자기', '놀기']
여기까지 따라오셨다면 애플리케이션에 서비스 워커 설정이 잘 된 것입니다. 💯
React로 UI 구현
그럼 가짜 API가 준비되었으니 이를 이용해서 할일 관리 UI를 구현해보겠습니다.
최초 랜더링 시에 GET /todos
를 호출하여 할일 목록을 가져와 화면에 그려주고, 입력란에 새로운 할일을 넣고 버튼을 누르면 POST /todos
를 호출해줍니다.
전형적인 React의 함수형 컴포넌트이니 구구절한 설명은 생략하도록 할께요.
자바스크립트 라이브러리인 React에 대한 다양한 포스팅은 관련 태그를 통해서 만나보세요.
import { useEffect, useState } from "react";
function App() {
const [todos, setTodos] = useState([]);
const [todo, setTodo] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch("/todos")
.then((res) => res.json())
.then((data) => {
setTodos(data);
setLoading(false);
});
}, []);
const handleSubmit = (event) => {
event.preventDefault();
setLoading(true);
fetch("todos", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: todo
}).then((res) => {
fetch("/todos")
.then((res) => res.json())
.then((data) => {
setTodo("");
setTodos(data);
setLoading(false);
});
});
};
return (
<div>
<h2>할일 목록</h2>
<ul>
{todos.map((todo, idx) => (
<li key={idx}>{todo}</li>
))}
</ul>
<form onSubmit={handleSubmit}>
<input
type="text"
name="todo"
placeholder="새로운 할일"
disabled={loading}
value={todo}
onChange={({ target: { value } }) => setTodo(value)}
/>
<button disabled={!todo}>추가</button>
</form>
</div>
);
}
export default App;
드디어 가짜 API를 이용한 할일 관리 앱이 완성하였습니다! 🎉
테스트 서버 생성
MSW는 개발에서 뿐만 아니라 테스트에서도 활용할 수 있다고 말씀드렸죠?
msw/node
모듈의 setupServer()
함수를 이용하면 간단하게 테스트용 API 서버를 만들 수 있는데요.
위에서 작성한 가짜 응답을 해주는 요청 핸들러를 그대로 재활용하면 됩니다. 👍
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
테스트 서버 설정
테스트 실행 전에 가짜 API 서버를 올렸다가 테스트 실행 후에 내릴 수 있도록 Jest 설정을 해줍니다.
예를 들어, Create React App으로 만든 프로젝트에서는 setupTests.js
에 다음 코드를 추가해주면 됩니다.
import "@testing-library/jest-dom";
import { server } from "./mocks/server";
beforeAll(() => server.listen());afterEach(() => server.resetHandlers());afterAll(() => server.close());
테스팅 라이브러리인 Jest에 대한 다양한 포스팅은 관련 태그를 통해서 만나보세요.
테스트 코드 작성
이제 위에서 작성한 UI를 테스트하는 간단한 코드를 작성해보겠습니다. 입력란에 “공부하기”를 입력하고 버튼을 클릭하면 화면에 “공부하기”가 표시되는 것을 테스트하겠습니다.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import App from "./App";
test("renders todos", async () => {
render(<App />);
const listitems = await screen.findAllByRole("listitem");
expect(listitems).toHaveLength(3);
userEvent.type(screen.getByRole("textbox"), "공부하기");
userEvent.click(screen.getByRole("button"));
expect(await screen.findByText("공부하기")).toBeInTheDocument();
});
전체 코드
본 포스팅에서 작성한 모든 코드는 아래에서 확인하실 수 있습니다.
마치면서
이상으로 MSW 라이브러리를 사용하여 간단한 React 앱을 개발하고 테스트도 해보았습니다. MSW를 잘 활용하셔서 백엔드 API 구현이 완료되기를 기다리느라 스트레스 받지 않으시고 프론트엔드 UI 개발을 진행하실 수 있으셨으면 좋겠습니다.