프로젝트를 진행하다보면 백앤드의 api 개발여부에 의존적인 상황을 필연적으로 마주치게 됩니다.
예를 들어, 유저의 프로필 페이지를 그려야 한다고 가정했을 때,유저의 정보를 서버로부터 받아 그 데이터를 기반으로 화면을 그려야합니다.
MSW를 적용하기 전에 제가 개발했던 방법은 서버로부터 받는 필드값과 같은 더미 객체 또는 배열을 만들어 화면을 그려낸 뒤, 실제 api 개발이 완료 되었을 때 더미데이터를 지우고 api를 연동하는 방식으로 진행했었습니다.
이 방법이 익숙해서인지 나쁜 방법이라고는 생각하지 않았고 굳이 mock을 도입해야 하는 생각을 갖고 있었으나 최근 사이드 프로젝트를 진행하다보니 조금씩 불편한 상황을 마주하게 되었습니다.
더미 데이터로 화면을 그릴 때 작성한 로직과 실제 api를 연동하여 데이터를 받아오고, 받아온 데이터로 작성할때의 로직은 전혀 다르기 때문에 매번 고쳐줘야 하는 번거로움이 있습니다.
더미데이터의 수량이 증가할수록 해당 더미데이터가 어떤 화면을 그리기 위한 더미데이터였는지 쉽게 찾지 못하고 주석에 의존하는 문제가 발생하였습니다.
사용자의 인가(Authorization)와 관련된 로직에 일종의 더미 값을 넣어 임시로 처리를 하며 개발을 진행하고 있었습니다. 이후 api가 완성되어 더미값과의 연동을 해제하고 api를 연동해두었는데, 며칠 뒤 백엔드에서 해당 api와 관련한 리팩터링, 업그레이드와 관련한 이슈가 생겨 사용이 불가능하게 되었을 때, 해당 api에 의존적으로 개발한 프론트에서는 수정할 필요가 없는 정상적인 로직도 다시 더미로 수정하여 변환해주어야 했습니다.
이러한 문제점을 MSW를 사용하여 어떻게 해결했는지 적어보겠습니다.
Nextjs13에서는 기존의 페이지 라우터 방식이 아닌 컴포넌트 단위로 서버와 클라이언트가 확실히 구분되어야 하기 때문에 적용하는 방법이 어려웠습니다.
npm install msw --save-dev
npx msw init public/
퍼블릭 폴더에 서비스워커를 이닛해줌으로서 API 요청을 가로챌 수 있습니다.
// mocks/browser.ts
import { setupWorker } from "msw";
import handlers from "./handlers";
export const worker = setupWorker(...handlers);
아직 핸들러는 작성하지 않았으나, setupWorker 메서드를 통해 작성한 핸들러들을 묶음으로 가져가 워커를 설정한다는 개념을 이해하는 것이 더 선행되어야 된다고 생각하여 먼저 적어두었습니다.
그렇다면 핸들러는 무엇일까요?
// mocks/handlers.ts
import { rest } from "msw";
import type { RestRequest, ResponseComposition, DefaultBodyType, RestContext } from "msw";
const mockLogin = (req: RestRequest, res: ResponseComposition<DefaultBodyType>, ctx: RestContext) => {
return res(
ctx.status(200),
ctx.delay(100),
ctx.json({
data: {
userId: 9999,
email: 'mocking@mock.com',
name: '김목성',
picture: 'https://avatars.githubusercontent.com/u/10000000?v=4',
role: 'cake',
authProvider: 'Mock',
providerId: 2444,
accessToken: 'accesstoken',
refreshToken: 'refreshtoken',
isInitProfile: false,
gender: 'Male',
birthday: '1990-11-14',
nickname: '모킹데이터'
}
})
);
}
const handlers = [ rest.get('https://api.stokoin.com/api/v1/users', mockLogin) ]
export default handlers;
MSW가 연결되면 자신이 가로채야 할 api를 설정하는 곳입니다.
저는 mockLogin이라는 함수를 작성하였는데요.
http status codectx.status(), 지연시간 ctx.delay(), 내려주는 데이터ctx.json()을 설정할 수 있습니다.
제가 내려주는 데이터는 기존의 api에서 내려주는 데이터와 같아야 하기 때문에 그 형식을 맞추어 주었습니다.
작성한 함수는 배열의 형태로 변수에 담아 저장해주는데요.
각 요소에는 어떤 url에 접속하면 어떤 함수가 실행되어야 하는지 매핑되어 있습니다.
(https://api.yourdomain.com/api/v1/users, mockLogin)
이런 매핑을 통해 명확하게 요청과 응답을 알 수 있어 문제점 2번을 해결할 수 있었습니다.
이 부분은 제가 개인적으로 이해한 부분으로 정확하지 않을 수 있습니다.
Next는 RSC를 사용하여 서버에서 함수를 실행할 수도 있고, 기존 리액트에서 사용하는 것처럼 RCC에서 함수를 실행할 수도 있습니다.
그렇기 때문에 Browser(window)에서 api를 인터샙터하는 워커와 Node(global)에서 api를 인터샙터하는 워커가 필요합니다.
위에 작성한 browser.ts는 클라이언트에 설치한 워커입니다.
서버에도 워커를 설치해보도록 하겠습니다.
import { setupServer } from 'msw/node'
import handlers from './handlers';
export const serverWorker = setupServer(...handlers);
export async function initMSW() {
if(typeof window === 'undefined') {
const { serverWorker } = await import('./server');
serverWorker.listen({
onUnhandledRequest: 'bypass'
});
} else {
const { worker } = await import('./browser');
worker.start({
onUnhandledRequest: 'bypass'
});
}
}
window객체는 브라우저 환경에서 갖는 특별한 전역객체이기때문에 이를 통해 현재 실행환경을 구분할 수 있습니다.
onUnhandledRequest는 캐치하지 못한 요청에 대한 워닝을 띄워주는데, 처음 적용할 때에는 너무 많은 워닝으로 불편함이 있을 수 있어 꺼놓도록 하겠습니다.
// .env.development
NEXT_PUBLIC_API_MOCKING=enabled
// .env.production
// 아무것도 없으면 falsy!
개발환경에서만 MSW를 사용할 것이기에 분기할 수 있는 환경변수가 필요합니다.
.env.development의 환경변수를 보게하고, enabled의 값에 따라 MSW의 실행여부를 판단합니다.
// app/components/msw.tsx
'use client';
import { useState, type PropsWithChildren, useEffect } from 'react';
const isDev = process.env.NEXT_PUBLIC_API_MOCKING === 'enabled';
interface Props {}
export default function MSW({children}: PropsWithChildren<Props>) {
const [ready, setReady] = useState(false);
const init = async () => {
if (isMockingMode) {
const initMock = await import('@/mocks/index')
await initMock.initMSW();
setReady(() => true);
}
}
useEffect(() => {
if(ready) return;
init();
},[ready]);
if(!isDev) return null;
return (
<>
{children}
</>
)
}
// app/layout.tsx
import Recoil from '@/app/components/common/recoil'
import '../styles/globals.css'
import MSW from './components/common/msw'
export const metadata = {
title: '주식처럼 코인하자! let\'s 스토코인!',
description: '스토코인은 가상화폐에 대한 토론을 나누는 커뮤니티입니다.',
}
interface Props {
children: React.ReactNode;
}
export default function RootLayout({children}:Props) {
return (
<html lang='ko'>
<body>
<Recoil>
<MSW>
{children}
</MSW>
</Recoil>
</body>
</html>
)
}
여기까지 작성하였으면 본인의 페이지에서 MSW가 정상적으로 작동하고 있는지 확인해보도록 하겠습니다.

유저가 접속하면 그리팅을 하는 앨리먼트에 정상적으로 모킹데이터가 들어가 있는 것을 확인할 수 있습니다.
개발자도구에 어떤 문구가 뜨는지 확인해볼까요?

MSW가 어떤 요청을 가로채가는지 알 수 있습니다!
++ 실제 MSW가 가로채가는 함수는 아래와 같습니다.
const getUserProfile = async() => {
try {
const response:AxiosResponse<User> = await get('users');
setUser(response.data); // MSW가 반환하는 데이터가 여기에 들어갑니다!
return response.data;
} catch (error) {
const axiosError = error as CustomError
throw axiosError
}
}
get 함수는 아래와 같습니다.
export const get = async <T,P>(url: string, query?: P):Promise<AxiosResponse<T,CustomError>> => {
try {
const response = await axios.get(url, { params: query });
return response.data;
} catch(e) {
const err = e as CustomError;
err.response ? throw err.response.data : throw err;
}
}
도움이 되시길 바라며 부족한 글 읽어주셔서 감사합니다.
현재 RSC (서버컴포넌트)에서는 정상적으로 MSW가 하이재킹을 하지 못하고 있습니다.
이 부분 참고해주세요