msw 설정
GET SATRT
- get start
npm install msw@latest --save-devnpx msw init public / --save(브라우저에서 요청을 가로채기위한 코드)
export async function initMocks() { if (typeof window === 'undefined') { const { server } = await import('./server'); server.listen({ // 설정되지 않은 url에대한 경고로그 제거 onUnhandledRequest: 'bypass', }); } else { const { worker } = await import('./browser'); worker.start({ // 설정되지 않은 url에대한 경고로그 제거 onUnhandledRequest: 'bypass', }); } }
환경에 따라 동적으로 import 하는 코드
공식 홈페이지에 있는 코드인데
동적으로 import 하는것이 실제로 사용할 일이 별로 없어서 개념이 확실히 안잡혀있었는데
코드를 보면서 상황에따라 동적으로 가져오는것을 확실히 알게됨.
코드도 명확히 서버와 브라우저에서 실행하는 경우라 더욱 이해가 쉬웠음
import { setupWorker } from 'msw/browser'; import { handlers } from './handlers'; export const worker = setupWorker(...handlers);
import { setupServer } from 'msw/node'; import { handlers } from './handlers'; export const server = setupServer(...handlers);
'use client'; import { useState, type PropsWithChildren, useEffect } from 'react'; const isMockingMode = process.env.NEXT_PUBLIC_API_MOCKING === 'enabled'; export const MSWComponent = ({ children }: PropsWithChildren) => { const [mswReady, setMSWReady] = useState(() => !isMockingMode); useEffect(() => { const init = async () => { if (isMockingMode) { const initMocks = await import('./index').then(res => res.initMocks); await initMocks(); setMSWReady(true); } }; if (!mswReady) { init(); } }, [mswReady]); if (!mswReady) { return null; } return <>{children}</>; };
브라우저 환경과 서버 환경에서 동작하는 코드
이후 rootLayout에 컴포넌트 추가.
//@mock/handler.ts import { HttpResponse, http } from 'msw'; export const handlers = [ // Intercept "GET https://example.com/user" requests... http.get('https://example.com/user', () => { // ...and respond to them using this JSON response. return HttpResponse.json({ id: 'c7b3d8e0-5e0b-4b0f-8b3a-3b9f4b3d3b3d', firstName: 'John', lastName: 'Maverick', }); }), http.get('/api/cbs', () => { // ...and respond to them using this JSON response. return HttpResponse.json({ id: 'c7b3d8e0-5e0b-4b0f-8b3a-3b9f4b3d3b3d', firstName: 'John', lastName: 'Maverick', }); }), ];
handler 에서 가로첼 경로 지정.
useEffect 내에서 호출하면 동작하지 않는 문제
콘솔에서는 모킹서버는 동작한것으로 표시

useEffect 내에서 호출하면 msw 가 초기화 되기전에 api요청을 보내는 문제가 발생
코드를 살펴보자..
내부적으로 msw는 global fetch를 덮어씌우면서 동작하는것으로 확인되고
아래 사진처럼 golobalThis의 fetch를 pureFetch로 덮어씌움.
( 서버환경과 클라이언트 환경을 대응하기위해 golobalThis를 사용한 코드는 처음봐서 신기했다.)

이건 실질적으로 브라우저에서 요청을 intercept하는 js코드인데
위에는 요청 종류에 따라 무시할 요청을 거르고
맨밑에 event.reponWith부분에서 서비스 워커가 요청에 대한 응답을 제공하도록 작동한다.

로직 자체는 크게 문제없어보이는데…
nextjs서버가 동작하는것과 msw가 초기화 되는 간격 사이에 싱크가 맞지않아 생기는 문제같다.
실제로 api요청에 딜레이를 주니 해결되는것을 확인하였다.
찾아본 결과 몇가지 해결방법이 있는데
1. express 서버를 동작시키는 방법
코드
// browser.ts import { setupWorker } from 'msw/browser'; import { handlers } from './handlers'; const worker = setupWorker(...handlers); export default worker;
// http.ts import { createMiddleware } from '@mswjs/http-middleware'; import cors from 'cors'; import express from 'express'; import { handlers } from './handlers'; const app = express(); const port = 9090; app.use( cors({ origin: ['http://localhost:3000'], optionsSuccessStatus: 200, credentials: true, }), ); app.use(express.json()); app.use(createMiddleware(...handlers)); app.listen(port, () => console.log(`Mock server is ruuning on port: ${port}`));
// MSWComponent.tsx 'use client'; import { useEffect } from 'react'; export const MSWComponent = () => { useEffect(() => { if (typeof window !== 'undefined') { require('@/mocks/browser'); } }, []); return null; };
// handler.ts import { http, HttpResponse } from 'msw'; const CalendarData = [ { id: 0, date: '2024-03-01', memo: [ { id: 0, text: '식사', }, { id: 1, text: '운동', }, ], }, { id: 1, date: '2024-03-02', memo: [ { id: 0, text: '연차', }, { id: 1, text: '회의', }, ], }, { id: 2, date: '2024-03-03', reservation: 0, canceled: 0, noShow: 0, memo: [], }, { id: 3, date: '2024-03-04', memo: [ { id: 0, text: '공부', }, { id: 1, text: '휴식', }, ], }, { id: 4, date: '2024-03-05', memo: [], }, { id: 30, date: '2024-03-31', memo: [ { id: 0, text: '파티', }, { id: 1, text: '운동', }, ], }, ]; export const handlers = [ http.get('/api/calendar', ({}) => { return HttpResponse.json(CalendarData); }), http.get('https://example.com/api', ({}) => { return HttpResponse.json(CalendarData); }), ];
프로젝트에서는 해당 방법 채택했음
→ axios에 딜레이를 주는건 클라이언트 코드를 건드는것이라 탐탁치 않았고
훅을 사용해 페이지마다 serveiceWorker의 동작을 검증하는것도 마찬가지로 부담이 있을 것 같다.
간단하게 api서버를 사용하는것도 그런용도보다 BFF 를 구현하는 느낌으로 사용하고 싶었다.
하지만 백엔드가 꺼지는 상황을 고려해 목데이터를 넣긴 할 계획이다.
유사시 아래 환경변수 사용해 api router의 우선순위를 높일 계획

NEXT_PUBLIC_API_BASE_URL='http://localhost:9090/api' NEXT_PUBLIC_API_MOCKING='enabled'
NEXT_PUBLIC_API_ROUTER_MOCK_SERVER=false # 유사시 nextjs api router 모킹 사용
NEXT_PUBLIC_API_BASE_URL='https:/your-production-url.com'
기본적으로 .env.local에서 api router모킹을 동작할껀지 정의
이후 프로덕션이면 프로덕션에있는 api 주소가 base주소가
개발환경이면 msw가 가로챌 수 있는 내부주소
import axios from 'axios'; const useMockServer = process.env.NEXT_PUBLIC_API_ROUTER_MOCK_SERVER === 'true'; const baseURL = useMockServer ? 'http://localhost:3000' : process.env.NEXT_PUBLIC_API_BASE_URL; export const axiosInstance = axios.create({ baseURL, });
React
환경 변수의 우선 순위는 다음과 같습니다.
- 컴퓨터 전역 환경 변수
- .env.local 파일
- .env.${NODE_ENV}.local 파일
- .env.${NODE_ENV} 파일
- .env 파일
- REACT_APP_으로 시작하는 환경 변수

문제 1. localhost만 intercept 하기 때문에 필요한 주소에 대해 프록시를 따로 설정해줘야하는 문제
문제 2. 따로 exporess 서버를 run 해줘야하는 사소한 불편함
이부분은 매크로나 npm script로 해결하면 되는 문제라 큰 문제는 아닌것같음.
- 요청에 딜레이주기
코드
const isMockingMode = process.env.NEXT_PUBLIC_API_MOCKING === 'enabled'; const axiosInstance = axios.create({ baseURL: '/api' }); if (isMockingMode) { axiosInstance.interceptors.request.use(async config => { await new Promise(resolve => setTimeout(resolve, 100)); return config; }); } export default axiosInstance;
좀 무식한 방법인데 intercepter에서 api에 딜레이를 주는방법이 있다
당연히 깡으로 딜레이를 주는 방법이기 때문에 그리 안정적이지 않다
일단 동작하는데 지금까지는 문제가 없긴했다..
깃이슈에서는 1000ms에 동작한다는데..
딜레이 쓰레시홀드를 줄여보니깐 100ms까지는 정상적으로 동작한다
이값은 당연히 환경마다 다르게 동작할 여지가 있으므로
위의 방법중 하나를 채택하거나
msw를 고치거나 업데이트를 기다리는 방법이 있을 것같다.
nextjs 14 app router는 아직 실험적 기능도 많은 상태라 이런 호환성이나 라이브러리가 제대로 지원하지 않는 문제가 많은것같다
- nextjs 서버(api router) 사용해서 간단하게 모킹하기
코드

import { faker } from '@faker-js/faker'; import { NextResponse } from 'next/server'; // export async function GET() { // return NextResponse.json({ helloa: "world" }); // } export async function GET() { return NextResponse.json([ { orderId: 1, accommodation: { accommodationId: 1, area: '강원', image: faker.image.urlPicsumPhotos(), accommodationName: '인천 파크마린호텔', address: '주소', category: '호텔', detail: '상품 상세 소개전체', checkIn: '2024-03-15', checkOut: '2024-03-16', tel: '02-xxx-xxx', peoples: 4, room: { roomId: 2, roomName: '1호실 (싱글or트윈)', price: 100000, }, }, }, { orderId: 2, accommodation: { accommodationId: 1, area: '강원', image: 'url', accommodationName: '상품명', address: '강원도 감자리 감자동 감자시', category: '호텔', detail: '상품 상세 소개전체', checkIn: '2024-03-15', checkOut: '2024-03-26', tel: '02-xxx-xxx', peoples: 4, room: { roomId: 2, roomName: '호실이나 분류(싱글or트윈)', price: 100000, }, }, }, ]); }
결과.
기획이 딜레이 되면서 백엔드 개발이 늦춰졌고
모킹은 이미 엔드포인트가 나온 상태에서 개발할때나 유용한데,
기획과, 백엔드 쪽 상황이 계속해서 바뀌는 상황에서 모킹은 큰 의미가 없다고 판단했고.
디자인과 백엔드 API가 큰 차이없이 만들어지는 상황이었기 때문에 결국 모킹서버는
도입하지 않았습니다.
msw 해당 이슈


