[JS] MSW(Mock Service Worker, v 2.0) - API Mocking ํ๊ธฐ
๐ MSW๋?
MSW๋ Mock Service Worker์ ์ฝ์๋ก API ๋ชจํน์ ์ํ ์๋ฐ์คํฌ๋ฆฝํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ๋๋ค.
๋ค์๊ณผ ๊ฐ์ ์ํฉ์์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- ๋ฐฑ์๋ API๊ฐ ๋ฏธ์์ฑ์ผ ๊ฒฝ์ฐ ๋ฏธ๋ฆฌ ๋ก์ง์ ๊ตฌ์ฑํด์ผํ ๋
- ํน์ ์๋ฌ๋ฅผ ๋ฐ์์์ผ ์๋ฌ ํธ๋ค๋ง ํ ์คํธ ํ ๋
MSW๋ Service Worker API๋ฅผ ์ฌ์ฉํ์ฌ ๋ธ๋ผ์ฐ์ ์์ ๋ฐ์ํ๋ ๋คํธ์ํฌ ์์ฒญ์ ๊ฐ๋ก์ฑ๋๋ค. ์ค์ ์๋ฒ ๋์ ๊ฐ์ง ์๋ต์ ๋ง๋ค์ด ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๊ฐ API ํ ์คํธ๋ฅผ ํ ์ ์๋ ํ๊ฒฝ์ ๋ง๋ค์ด์ค๋๋ค.
์ฌ๋ด ํ๋ก์ ํธ์์ API ์ ๋ฌ์ด ๋ฆ์ด์ง๋ฉฐ ํ๋ก ํธ์๋์์ API ์์ฒญ ๋ก์ง์ ๋ฏธ๋ฆฌ ๊ตฌํํ๊ณ ํ ์คํธํ๊ธฐ ์ํด MSW๋ฅผ ๋์ ํ์ต๋๋ค. ๋ฏธ๋ฆฌ DTO๋ฅผ ์ ๋ฌ๋ฐ์ผ๋ฉด ์ถํ ์ค์ API๋ฅผ ์ ๋ฌ๋ฐ์์ ๋ ํฐ ๊ณต์์์ด ๋ฐ๋ก API๋ฅผ ์ฐ๊ฒฐ ํ ์ ์์์ต๋๋ค. ๋ค๋ง, ๋ฆฌ์กํธ ์ฟผ๋ฆฌ์ฒ๋ผ ์ ์ญ์ผ๋ก ์ค์ ์ ํด ์์์น ๋ชปํ ๋คํธ์ํฌ๋ฅผ ๊ฐ๋ก์ฑ๋ ์ํฉ์ ํผํ๊ธฐ ์ํด ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง ์ฌ์ฉํ๊ณ ์ค์ ๋ฆด๋ฆฌ์ฆ๋๋ ๋ธ๋์น์์๋ ์ฌ์ฉํ์ง ์์์ต๋๋ค.
๐MSW ์ค์น (Next.js App Router ํ๊ฒฝ ๊ธฐ์ค)
Next.js์์ MSW๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ ํด๋ผ์ด์ธํธ/์๋ฒ ํ๊ฒฝ ๋ชจ๋ ์๊ตฌ๋๋๋ฐ, ๊ธ ์์ฑ ์์ ๊ธฐ์ค ์๋ฒ ์ฌ์ด๋์์ msw๊ณผ ๋งค๋๋ฝ๊ฒ ํธํํ๋ ๋ฐฉ๋ฒ์ด ์์ด node ์๋ฒ์ ํจ๊ป ์ฌ์ฉํฉ๋๋ค.
1. ์ค์นํ๊ธฐ
$ npm install msw@latest --save-dev
2. public ํด๋์ ์ด๊ธฐํ
$ npx msw init public/ --save
- public ํด๋์ ์ค์น
- mockServiceWorker.js ๊ฐ ์๋์ผ๋ก ์์ฑ
- http ์์ฒญ์ ๊ฐ๋ก์ฑ ์๋ฒ์์ ํต์ ์ ๋ชจ๋ฐฉํ๋ค.
--save
: package.json ์ ๋ฑ๋ก๋์ด msw๋ฅผ ์ ๋ฐ์ดํธ ํ ๋๋ง๋ค ์๋์ผ๋ก ์ ๋ฐ์ดํธ
๐ MSW ์ธํ
๐พ src/mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
// This configures a Service Worker with the given request handlers.
const worker = setupWorker(...handlers);
export default worker;
- CSR ๋คํธ์ํฌ ์์ฒญ์ฉ
- serviceWorker๊ฐ ๊ฐ๋ก์ฑ ๋ธ๋ผ์ฐ์ ์์ฒญ์ ์ ๋ฌ๋ฐ๋๋ค.
๐พ src/mocks/http.ts
import { createMiddleware } from '@mswjs/http-middleware';
import express from 'express';
import cors from 'cors';
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 running on port: ${port}`));
- SSR ๋คํธ์ํฌ ์์ฒญ์ฉ = ์๋ฒ ์ปดํฌ๋ํธ์์ ์ฌ์ฉ
- CSR๋ง ์ฌ์ฉํ ๊ฒฝ์ฐ ํ์๋ ์๋
๐พ src/mocks/handler.ts
import {http, HttpResponse, StrictResponse} from 'msw'
import {faker} from "@faker-js/faker";
function generateDate() {
const lastWeek = new Date(Date.now());
lastWeek.setDate(lastWeek.getDate() - 7);
return faker.date.between({
from: lastWeek,
to: Date.now(),
});
}
const User = [
{id: 'elonmusk', nickname: 'Elon Musk', image: '/yRsRRjGO.jpg'},
{id: 'zerohch0', nickname: '์ ๋ก์ด', image: '/5Udwvqim.jpg'},
{id: 'leoturtle', nickname: '๋ ์ค', image: faker.image.avatar()},
]
const Posts = [];
export const handlers = [
http.post('/api/login', () => {
console.log('๋ก๊ทธ์ธ');
return HttpResponse.json(User[1], {
headers: {
'Set-Cookie': 'connect.sid=msw-cookie;HttpOnly;Path=/'
}
})
}),
http.post('/api/logout', () => {
console.log('๋ก๊ทธ์์');
return new HttpResponse(null, {
headers: {
'Set-Cookie': 'connect.sid=;HttpOnly;Path=/;Max-Age=0'
}
})
}),
// ...
];
- API ์์ฒญ๊ณผ ์๋ต ์ ์
์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ์ฒ๋ฆฌํ๊ธฐ
http.get('/test/search', async ({ request, params }) => {
const url = new URL(request.url);
const keword = url.searchParams.get('keyword');
const page = Number(url.searchParams.get('page'));
const size = Number(url.searchParams.get('size'));
const totalCount = searchParams.length;
const totalPages = Math.round(totalCount / size);
return HttpResponse.json({
result: 'SUCCESS',
resultCode: 200,
message: '์ฑ๊ณต',
contents: test.slice(page * size, (page + 1) * size),
pageNumber: page,
pageSize: size,
totalPages,
totalCount,
isLastPage: totalPages <= page,
isFirstPage: page === 0,
});
})
- url ๊ฐ์ฒด ์์ฑ ํ searchParams์ ํตํด ์ฒ๋ฆฌํฉ๋๋ค.
๐ MSW ์๋ฒ ์คํ
1. package.json ์คํฌ๋ฆฝํธ ์ถ๊ฐ
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"mock": "npx tsx watch ./src/mocks/http.ts"
},
- watch : ์๋ฒ ์ฝ๋๊ฐ ์์ ๋๋ฉด ์๋์ผ๋ก ์ฌ์์
2. ์คํ
$ npm run mock
๐ MSW ์ฌ์ฉ ๋ถ๊ธฐ์ฒ๋ฆฌ
๐พ src\app_component\MSWCoponent.tsx
"use client";
import { useEffect } from "react";
export const MSWComponent = () => {
useEffect(() => {
if (typeof window !== "undefined") {
if (process.env.NEXT_PUBLIC_API_MOCKING === "enabled") {
worker.start({ onUnhandledRequest: "bypass" }); // msw ํธ๋ค๋ฌ๋ก ์์ฒญํ์ง ์์ request๋ ๋ฌด์. console ๊ฒฝ๊ณ ๋ฅผ ์์จ ์ ์๋ค
}
}
}, []);
return null;
};
- MSW๋ ๊ฐ๋ฐํ๊ฒฝ์์๋ง ์ฌ์ฉํ๋ค
๐พ env.local
NEXT_PUBLIC_API_MOCKING=enabled
- ๋ก์ปฌ ํ๊ฒฝ์์๋ง ๋์๊ฐ๋ ํ๊ฒฝ๋ณ์ ํ์ผ
- ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง ์ฝ์ด์ฌ ์ ์๋ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ ๋ณ์๋ฅผ ์ค์ ํ์ฌ ๋ฐฐํฌํ๊ฒฝ์ผ๋๋ MSW๋ฅผ ์ฌ์ฉํ์ง ์๋๋ค.
๐พ src\app\layout.tsx
export default function RootLayout({ children }: Props) {
return (
<html lang="en">
<body className={inter.className}>
<div className={styles.container}>
<MSWComponent /> // โ
{children}
</div>
</body>
</html>
);
}
์ถ์ฒ
- Next.js + ReactQuery๋ก SNS ์๋น์ค ๋ง๋ค๊ธฐ (์ธํ๋ฐ)
Leave a comment