(서버 API가 아직 개발중이라면) MSW로 Mock API 제작하기
서버 API 나왔나요? 네? 아직 없다고요?
아직 기업 규모의 프로젝트를 경험해보진 못했지만, 웹 서비스를 만들게 되면 크게 프론트엔드 개발자와 백엔드 개발자로 나누어져 작업을 했습니다. 서로 처음 기획 단계에서 도란도란 어떻게 통신할지, API는 어떤 것이 필요한지 논의를 한 뒤 각자 할 수 있는 걸 열심히 뚝딱뚝딱 만들어갑니다. 그러다 프론트엔드 개발자는 이제 api로 데이터를 받아와 화면에 출력하는 작업을 하고 싶어집니다. 그래서 백엔드 개발자에게 물어봅니다.
'서버 API 나왔나요?'
'아 아직 완성이 안 돼서 없는데요...!'
없더라도 침착하자
사실 당연한 상황이라고 생각합니다. 서버나 API가 미리 갖춰진 프로젝트에 새로이 프론트가 붙는 것이 아니라 무언가 바닥부터 만들기 시작했다면 어떻게 보면 백엔드의 최종 결과물이 이런 API일 가능성이 높기 때문입니다.
그렇다면 프론트엔드 개발자는 어떤 식으로 개발을 해야 할까요? API가 완성되기까지 기다릴 수도 있겠지만, 핵심 기능이 포함되어있다면 막바지에 가서 작업하는 것이 좋은 선택은 아닙니다. 저 개인적으로도 이런 상황에서 임시 데이터를 static 하게 넣거나, 이런 데이터를 반환하는 함수를 하나 만들어서 나중에 API와 소통하는 함수로 내용을 교체하는 등의 접근을 해보았는데, 썩 훌륭한 방법이라고 느끼지는 못했습니다. 그도 그럴 것이, 실제 API가 완성되면 수정해줘야 하는 부분도 많고, 여럿이 협업하는 프로젝트에서 사용되지 않을 코드를 커밋하여 혼선을 야기하는 것 같았기 때문입니다.
프론트엔드의 홀로서기
이런 상황에서 추천받은 것은 Mock API를 만드는 것입니다. 실제 API처럼 보이지만 개발과 테스트를 위해 임시로 행위나 반환하는 데이터를 규정해두고 실제 API 대신 사용하는 방식입니다. 그럼 이걸 어떻게 만들 수 있을까요? 직접 서버를 만들 수도 있겠지만 이번에 추천을 받아 사용해본 것은 MSW 라이브러리였습니다.
MSW (Mock Service Worker)
MSW는 간단하게 Mock API를 만들 수 있게 도와주는 라이브러리입니다. 노드와 브라우저 환경에서 사용할 수 있고, TypeScript도 지원합니다. 대략적인 원리는 서비스 워커를 이용하여 클라이언트에서 나가는 요청을 서버로 보내는 대신 서비스 워커로 보내 Mock API에 지정해둔 반환 값을 받아오는 것이라고 합니다.
Next JS + TypeScript 프로젝트에 적용해보기
MSW를 이용하여 Mock API를 실행하기 위해선 다음 순서로 진행을 하면 됩니다.
- 라이브러리 설치
- Mock API의 동작을 정의하는 handler 파일 생성
- Mock API의 역할을 해줄 worker 혹은 server 생성
- 환경에 따라 3번에서 만들어준 worker혹은 server를 실행
상세한 가이드라인은 제가 도움을 받았던 이 포스팅을 참고해주세요. 이 포스팅에선 위에서 적은 각각의 과정에 대해 코드만 간략하게 남겨볼까 합니다.
예시
저는 GET 통신을 통해 데이터를 받아올 필요가 있었습니다. 그래서 먼저 handler에서 임시 데이터를 하나 선언하고 이를 바로 반환하는 로직을 작성해주었습니다. 이 handler에 넘겨주는 url은 실제로 사용할 엔드포인트를 사용하시면 됩니다. MSW는 GraphQl 도 지원하지만, 저는 REST API로 작성했습니다.
// mocks/handler.ts
import { rest } from 'msw';
import { BASE_URL } from '@/api/index';
const exampleData = 'data';
export const handlers = [
rest.get(`${BASE_URL}/data`, (req, res, ctx) => {
return res(ctx.status(200), ctx.json(data));
}),
];
이후 worker와 server를 만들어줍니다. 전자는 브라우저, 후자는 노드 환경에서 사용하기 위함으로 알고 있는데, 우선 두 가지 모두를 지원할 수 있도록 해주었습니다. 실행되는 환경이 window객체를 가지고 있지 않다면 worker 대신 server를 사용하는 방식입니다.
// mocks/browser.ts
import { setupWorker } from 'msw';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
이렇게 정의해준 뒤, 둘을 실행하는 함수를 작성합니다. (절대 경로로 import 하고 있는 점 참고 부탁드립니다)
// mocks/index.ts
const initMockAPI = async (): Promise<void> => {
if (typeof window === 'undefined') {
const { server } = await import('@/mocks/server');
server.listen();
} else {
const { worker } = await import('@/mocks/browser');
worker.start();
}
};
export default initMockAPI;
이제 이 함수를 가장 상단에서 실행해주면 됩니다. React 프로젝트라면 App.tsx 같은 파일에서 적용하게 될 테지만, Next 기반 프로젝트이기에 _app.tsx에서 실행해주겠습니다. 직접 정의한 App 컴포넌트 선언부 위에서 실행해줍니다.
if (process.env.NODE_ENV === 'development') {
initMockAPI();
}
저는 개발 환경에서 Mock API를 사용하기 위해 위처럼 정의해주었습니다. 만약 test 환경에서도 Mock API를 써야 한다면 조건을 바꾸어주면 됩니다.
마치며
단순한 API 동작의 경우 생각보다 간단하게 구현할 수 있어 굉장히 편리한 라이브러리라고 생각합니다. API 호출 코드는 실제 API와 통신한다고 생각하고 작성해두면 개발 환경에서는 내가 정의한 API로, production 환경에선 실제 API와 별도 작업 없이도 바로 연결이 됩니다.
사실 이런 라이브러리는 그 사용법이 어렵다기보단, 어느 범위까지 어떤 것을 mock 할 것인가에 대한 설계가 가장 어려운 부분이라고 생각합니다. 실제 API와 동일하게 모든 동작, 예외를 다 구현한다면 오히려 프론트엔드 개발을 하기보단 서버 API를 개발하고 있는 주객전도가 된다고 생각합니다. 어디까지나 프론트엔드 개발을 위한 툴이기 때문에 화면이나 UI와 직결되지 않는 부분은 과감하게 쳐내면서 작업을 했던 것 같습니다. 앞으로도 잘 활용한다면 백엔드 개발자에게 부담을 주지 않으면서 자신의 할일을 해낼 수 있는 프론트엔드 개발자가 될 수 있지 않을까 싶습니다.