About Mock Service Worker
Make Mock Server for frontend development
1. Mock Server ๐ฉโ๐ป
ํ๋ก ํธ์๋๋ ์ฑ ๊ฐ๋ฐ์ ํ๋ค ๋ณด๋ฉด ํญ์ ๋ฌธ์ ๊ฐ API ๊ฐ๋ฐ์ด ๋์ด์์ด์ผ ์ค์ ๊ฐ๋ฐ์ ์งํํ ์ ์๋ค๋ ๊ฒ์ด๋ค. ํ์ฌ์์ ๊ทผ๋ฌดํ ๋ ํ์คํ์ผ๋ก ์ผํ๊ธฐ ๋๋ฌธ์ ๋ด๊ฐ ์๋ฒ DB ํ ์ด๋ธ๋ ๋ง๋ค๊ณ , ์๋ฒ API ๋ ๋ง๋ค๊ณ , ํ๋ก ํธ์๋ ํ๋ฉด๋ ๋ง๋ค์ด์ ์ฌ์ค DB -> ์๋ฒ -> ํ๋ฉด ์์ผ๋ก ๋ง๋ค๋ฉด ๋์ง๋ง ๋๋ถ๋ถ์ ํ์ฌ๊ฐ ํฌ๊ณ ๋ถ์ ํ๊ฐ ๋๋ฉด ๊ฐ๋ฐ ํฌ์ง์ ์ด ๋๋๊ฒ ๋์ด API ๊ฐ ๋์ค๊ธฐ๋ง ๊ธฐ๋ค๋ฆฌ๋ ์ํฉ์ด ์๊ธฐ๊ฒ ๋๋ค.
์ด๊ฑธ ๊ฒฝํํ ๊ฒ ๋๋ A -> B -> C ์์๋ก ๊ฐ๋ฐํ๋ ค๊ณ ํ๋๋ฐ ์ฑ ๊ฐ๋ฐ์๊ฐ B -> C -> A ์์ผ๋ก ํด๋ฌ๋ผ๊ณ โฆ API ๊ฐ ๋์์ผ ๊ฐ๋ฐํ๋ค๊ณ โฆ ์ด๋ด๋ ์ฌ์ฉํ ์ ์๋ ๊ฒ Mock Server ๋ค.
์ฌ์ค Mocking ์ ์ํ ์๋ฒ๋ Spring ๋์ Express ๋ก ๋์ฐ๋ฉด ์๊ฐ๋ณด๋ค ์ฝ๊ฒ ๋์ธ ์ ์๋ค. ํ์ง๋ง ๋๋ถ๋ถ ์ด๊ฑธ ์ ํ์งโฆ ์๋ฌดํผ, Express ๋ก Mock Server ๋ฅผ ๋์ฐ๋ ๊ฒ์ด ๊ท์ฐฎ๋ค๋ฉด ์ฌ์ค Postman ์ ์ฌ์ฉํด์๋ Mock Server ๋ฅผ ๋์ธ ์ ์๋ค(Postman - Mock servers). ๋๋ถ๋ถ Postman ์ API ํ ์คํธ ์ฉ์ผ๋ก ์ฌ์ฉํ์ง๋ง, ์ฌ์ค Postman ์ ์ด๋ฅผ ์ญ์ผ๋ก API ๋ฅผ ๋ง๋ค์ด ์ํ๋ฅผ ์ ์ฅํ๊ณ , Mocking ํ๋ ๊ฒ์ ์ ๊ณตํ ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๊ณ ์๋ค. JSON ์๋ต ๋ฟ ์๋๋ผ Postman ์ด ์ฌ์ฉ ๊ฐ๋ฅํ ๋ค์ํ ์๋ต์ ๋ชจ๋ ์ง์ํ๋ฉฐ, Binary ๋ ๊ฐ๋ฅํด ํ์ผ ํ ์คํธ ์ญ์ ๊ฐ๋ฅํ๋ค. ๋จ์ ์ด๋ผ๋ฉด Postman ์๋ฒ์ ์ธํฐ๋ท ์ฐ๊ฒฐ์ด ํ์ํ๊ธฐ ๋๋ฌธ์ Offline ์์๋ ์ฌ์ฉ์ด ๋ถ๊ฐ๋ฅํ๋ค๋ ๊ฒ ์ ๋? ๊ฒ๋ค๊ฐ ํ์ฌ์์ ์ฌ์ฉํ ๊ฒฝ์ฐ ์๋ง๋ ์ผ์ ๋ ์ด๊ณผ ์ฌ์ฉ์ ๋น์ฉ ๋ถ๊ณผ(?) ์ ๋์ด์ง ์์๊น?
์๋ฌดํผ Node.js ์๋ ์ง์ Express ์๋ฒ๋ฅผ ๋ง๋ค์ง ์์๋ ์ฝ๊ฒ Mock Server ๋ฅผ ๋ง๋ค ์ ์๋ ๋ค์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์กด์ฌํ๋ ๋ฐ, ๊ทธ ์ค ํ๋ก ํธ์๋์์ ๋ง์ด ์ฌ์ฉํ๋ ๊ฒ์ด Mock Service Worker ๋ค. MSW ๋ Node.js ํตํฉ ๋ฟ ์๋๋ผ ๋ธ๋ผ์ฐ์ ์ Service Worker ์ ํตํฉ์ด ๊ฐ๋ฅํ๋ฉฐ, React Native ์์ ํตํฉ ์ญ์ ์ง์ํ๋ ๊ต์ฅํ ๋๋ผ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
2. Install ๐ฉโ๐ป
1. npm install
npm i -D msw
์ค์น ์ดํ ์ฝ๋ ์์ฑ์ ๋ฒ์ ์ ๋ฐ๋ผ ๋ณ๊ฒฝ๋๋ ๋ถ๋ถ์ด ์์ผ๋ฏ๋ก ๊ฐ๊ธ์ MSW ๋ฅผ ํ ๋ฒ ํ์ธํ๋ ๊ฒ์ด ์ข๋ค.
2. Browser Integration
1 ) Copy the worker script
๋ค์ ๋ช ๋ น์ด๋ฅผ ์ฌ์ฉํ๋ฉด Service Worker ์ฌ์ฉ์ ์ํ ์คํฌ๋ฆฝํธ๋ฅผ ์๋์ผ๋ก ์ค์นํด์ค๋ค.
npx msw init <PUBLIC_DIR>
์ผ๋ฐ์ ์ผ๋ก public
์ด๋ผ๋ ๋๋ ํ ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ๋ฉด ๋๋ค.
npx msw init public
์ด์ public ์ mockServiceWorker.js
๊ฐ ์์ฑ๋๋ค.
2 ) Setup
- src/mocks/browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
์์ง import { handlers } from "./handlers";
๋ถ๋ถ์ ์์ฑํ์ง ์์ ์๋ฌ๊ฐ ๋ฐ ๊ฒ์ด์ง๋ง ๋ฌด์ํ์. ๋ฐ๋ก ๋ค์ ์ฑํฐ์ธ
Make handlers ์์ ์์ฑํ ๊ฒ์ด๋ค.
3 ) Conditionally enable mocking
- src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import { App } from './App'
async function enableMocking() {
if (process.env.NODE_ENV !== 'development') return
const { worker } = await import('./mocks/browser')
// `worker.start()` returns a Promise that resolves
// once the Service Worker is up and ready to intercept requests.
return worker.start()
}
enableMocking().then(() => {
ReactDOM.render(<App />, rootElement)
})
3. Make handlers ๐ฉโ๐ป
1. Group request handlers
๋จ์ผ ํธ๋ค๋ฌ๋ฉด src/mocks/handlers.ts
ํ๋๋ง ๋ง๋ค๋ฉด ๋๋๋ฐ, ๋ณดํต API ๋ฅผ ๋ง์ด ๋ง๋ค๋ค ๋ณด๋ฉด ์ฝ๋๊ฐ ํผ์กํด์ง๊ธฐ ๋๋ฌธ์ ๋ฐฑ์๋ ์๋ฒ๋ฅผ
๋ง๋ ๋ค๋ ์๊ฐ์ผ๋ก Endpoint ๋ฅผ ๊ด์ฌ์ฌ ๊ธฐ์ค์ผ๋ก ๋๋์ด handlers ๋ฅผ ๊ฐ๊ฐ์ ํ์ผ๋ก ๊ตฌ์ฑํด src/mocks/handlers/index.ts
ํ๋๋ก
๋ชจ์ผ๋ ๊ฒ์ด ์ข๋ค.
์ ์ฌ์ง์์ db
๋ผ๊ณ ๋ถ๋ฆฌํด ๋์ ์ฝ๋๋ ๊ณต์ ๋ฌธ์์ ํํ ๋ฆฌ์ผ์๋ ๋์ค์ง ์์ ๋ถ๋ถ์ผ๋ก ํ์ ๊ตฌ์ฑ ์์๋ ์๋๋ค. ๋ค๋ง handlers
์์
๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ ๋ฐ์ดํฐ๋ค์ด ํจ๊ป ์กด์ฌํ๋ฉด ๋ผ์ธ ์๊ฐ ๋๋ฌด ๋ง์์ ธ ๊ฐ๋
์ฑ์ด ์ข์ง ๋ชปํ ๋ฌธ์ ๊ฐ ์์ด ๋ถ๋ฆฌํ๋ค.
- src/mocks/handlers/index.ts
import { HttpResponse } from "msw";
import { handlers as optionsHandlers } from "./options";
import { handlers as orderHandlers } from "./order";
import { handlers as productsHandlers } from "./products";
import { handlers as staticHandlers } from "./static";
export const handlers = [
...optionsHandlers,
...orderHandlers,
...productsHandlers,
...staticHandlers,
];
interface Params {
request: Request;
params: {
[key: string]: unknown;
};
cookies: {
[key: string]: unknown;
};
}
export interface HttpResolver {
(params: Params): HttpResponse | Promise<HttpResponse>;
}
์ด index.ts
๊ฐ ๊ฐ๊ฐ์ handlers
๋ฅผ ํ๋๋ก ๋ชจ์ ๊ฒ์ด๋ค. Params
์ HttpResolver
๋ JavaScript ๋ก ์์ฑ์๋ ๋ถํ์ํ ์ฝ๋๋ค.
2. JSON Response
- src/mocks/handlers/options.ts
import { http, HttpResponse } from "msw";
import { HttpResolver } from "./index";
import options from "../db/options";
const getOptionsResolver: HttpResolver = () => {
return HttpResponse.json(options);
};
export const handlers = [http.get("/options", getOptionsResolver)];
๊ธฐ๋ณธ์ ์ผ๋ก ์์ ๊ฐ์ ํํ๋ก JSON ํต์ ์๋ต์ ๋ณด๋ผ ์ ์๋ค.
3. Request Data & Delay
- src/mocks/handlers/order.ts
import { HttpResolver } from "./index";
import { delay, http, HttpResponse } from "msw";
import { orderHistory, OrderInfo } from "../db/order";
const generateOrderNumber = () => Math.floor(Math.random() * 1000000);
const postOrderResolver: HttpResolver = async ({ request }) => {
const price = await request.json().then((totals) => totals.price);
const orderNumber = generateOrderNumber();
const newOrder: OrderInfo = { orderNumber, price };
orderHistory.push(newOrder);
await delay(2000);
return HttpResponse.json(orderHistory, { status: 201 });
};
export const handlers = [http.post("/order", postOrderResolver)];
src/mocks/handlers/index.ts
์์ ์์ฑํ HttpResolver
๋ฅผ ์ฌ์ฉํด API ์์ฒญ์ ์ค์ด ๋ณด๋ด๋ ๋ฐ์ดํฐ(Query Params, Body-data,
Form-data ๋ฑ)์ ์ฌ์ฉํ ์ ์๋ค. ๋ํ ๊ธฐ๋ณธ์ ์ผ๋ก delay
ํจ์๋ฅผ ์ ๊ณตํ๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ์ ์ํ ํ์ ์์ด ๊ฐ์ ธ๋ค ์ฌ์ฉํ๋ฉด ๋๋ค.
4. RESTful & Dynamic API
- src/mocks/handlers/products.ts
import { HttpResolver } from "./index";
import { http, HttpResponse } from "msw";
import products from "../db/products";
const getProductsResolver: HttpResolver = () => {
return HttpResponse.json(products);
};
const postProductResolver: HttpResolver = async ({ request }) => {
const newPost = await request.json();
// code...
return HttpResponse.json({ id: "abc-123" }, { status: 201 });
};
const putProductResolver: HttpResolver = async ({ request, params }) => {
const { id } = params;
const updatePost = await request.json();
// code...
if (id === "abc-123") {
return HttpResponse.json("success", { status: 204 });
} else {
return HttpResponse.error();
}
};
const deleteProductResolver: HttpResolver = ({ params }) => {
const { id } = params;
if (id === "abc-123") {
return HttpResponse.json("success", { status: 200 });
} else {
return HttpResponse.error();
}
};
export const handlers = [
http.get("/products", getProductsResolver),
http.put("/products/:id", postProductResolver),
http.delete("/products", deleteProductResolver),
];
์์ ๊ฐ์ด ๋ฉ์๋๋ง๋ค ํธ๋ค๋ด๋ฅด ์์ฑํ ์ ์๋ค. ๋ฐ๋ผ์ ์ค์ ์๋ฒ API ๋ฅผ ์์ฑํ ๋์ ๋ง์ฐฌ๊ฐ์ง๋ก RESTful ๋ฐฉ์์ ๊ทธ๋๋ก Mocking ํ ์ ์๋ค.
๋ํ /products/:id
์ ๊ฐ์ด URL Params ๋ฅผ ๊ตฌ๋ถํ ์ ์์ด Dynamic URL API ๋ ์ ์๊ฐ ๊ฐ๋ฅํ๋ค.
5. Static
์ด๊ฒ์ ๊ณต์ ๋ฌธ์์ ๋์ค๋ ๋ฐฉ๋ฒ์ด๋ค.
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/images/:imageId', async ({ params }) => {
const buffer = await fetch(`/static/images/${params.imageId}`).then(
(response) => response.arrayBuffer()
)
return HttpResponse.arrayBuffer(buffer, {
headers: {
'Content-Type': 'image/jpeg',
},
})
}),
]
์ฌ๊ธฐ์ fetch
๊ฐ ์์ฒญํ๋ /static/images/...
์ ๊ฒฝ๋ก๋ฅผ ์๊ฐํด๋ณด์. Browser Integration ์ ํ๊ธฐ
๋๋ฌธ์ Service Workers ๊ฐ fetch ์์ฒญ์ ๋ณด๋ด๋ ๊ฒ์ด๋ฏ๋ก ํ์ผ์ ์ฐพ๋ ์์น๋ public/static/images/...
๊ฐ ๋๋ค.
๋ฌธ์ ๋ public
์ ํ๋ก ํธ์๋ ์ฑ์ด ์์ฑ์ ์ผ๋ก ๊ณต๊ฐ ์ ๊ณตํ๋ ํ์ผ๋ค์ ๋ชจ์๋๋ ํน๋ณํ ๋๋ ํ ๋ฆฌ๋ค. ์ฆ, ๊ตณ์ด Mocking ์ ํ ํ์๊ฐ ์์ ๋ฟ ์๋๋ผ
์ฌ๊ธฐ์ Mocking ์ ์ํ ํ์ผ์ ์์ด๋๋ค๋ฉด ํด๋น ํ์ผ๋ค์ด ํจ๊ป ๋น๋๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค. ์ฐ๋ฆฌ๊ฐ ์ํ๋ ๊ฒ์ ๋ฐฑ์๋ ์๋ฒ์ ์ญํ ์ ๋์ ํ Mock Server
๋ฅผ ๋ง๋๋ ๊ฒ์ด๋ค. ์ด ๋ฌธ์ ๋ฅผ ์ด๋ป๊ฒ ํฐ๊ฒฐํ ์ ์์๊น?
1 ) Make specific dir to only for MSW
์ฒซ ๋ฒ์งธ ๋ฐฉ๋ฒ์ ๊ณต์ ๋ฌธ์์ ๋ฐฉ์์ ๊ทธ๋๋ก ์ฌ์ฉํ๊ธฐ ์ํด public
ํ์ ํน์ ๋๋ ํ ๋ฆฌ๋ฅผ MSW
์ ์ฉ์ผ๋ก ์ฌ์ฉํ๋ ๊ฒ์ด๋ค. ์๋ฅผ ๋ค์ด
public/mocks/*
๋ ์ ๋ถ Mocking ์ ์ํ ํ์ผ์ ๋ชจ์๋๋ ๊ฒฝ๋ก๋ก ์ฌ์ฉํ๊ณ , ์ด ๋๋ ํ ๋ฆฌ๋ ๋น๋ ์ต์
์์ ์ ์ธ์ํจ๋ค. ๊ทธ๋ฌ๋ฉด ์ ์์ ์ธ
API Mocking ์ด ๊ฐ๋ฅํ๋ค. ํ์ง๋ง public
์ ํ๋ก ํธ์๋ ์ฑ์ด ์์ ์ ์๋ฒ์์ ์ ๊ณตํ๊ณ ์ ํ๋ ๊ณต๊ฐ ์ ๊ทผ์ด ํ์ฉ๋ ํ์ผ์ ์์น์ํค๋
๋๋ ํ ๋ฆฌ์ด๊ธฐ ๋๋ฌธ์ ์์นซ ์๋ชปํ๋ฉด ํผ๋์ ์ผ๊ธฐ์ํฌ ์ ์๋ค.
2 ) Include static files into src/mocks
๋ ๋ฒ์งธ ๋ฐฉ๋ฒ์ ๋ค๋ฅธ ๋ชจ๋ ์ฝ๋์ ๋ง์ฐฌ๊ฐ์ง๋ก src/mocks
ํ์์ ํ์ผ์ ์์น์ํค๊ณ ์๋ ๊ฒฝ๋ก๋ก ์ ๊ทผํด ํ์ผ์ ์ ๊ณตํ ์ ์๋๋ก ์ฝ๋๋ฅผ ๊ตฌํํ๋ ๊ฒ์ด๋ค.
์ฌ๊ธฐ์ ์ค์ํ ๊ฒ์ ์ผ๋ฐ์ ์ผ๋ก webpack ์ ์ต์
์ค ๋ฆฌ์์ค URL ์ ๋๋
ํ ์์ผ์ฃผ๋ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋จ์ํ fetch
์ ์์ฒญ URL ์ ์๋
๊ฒฝ๋ก๋ก ์์ฑํ๋ ๊ฒ์ ์๋ฌด๋ฐ ์๋ฏธ๊ฐ ์๋ค.
import appleImage from "../static/images/apple.png"
์ ๊ฐ์ด ์ฝ๋๋ก ๋์ ํ์ผ์ import ์ํค๊ณ ์ด๊ฒ์ ์ด์ฉํด fetch(appleImage)
์ ๊ฐ์ด ์ ๊ณตํด์ผ ํ๋ค๋ ๊ฒ์ด๋ค. ๋ฌธ์ ๋ ์ด๋ฏธ์ง ํ๋๋์ด์ผ
์ด๋ฐ์์ผ๋ก ํ ์ ์์ง๋ง ์ด๋ฏธ์ง๊ฐ ๋ง์ ๊ฒฝ์ฐ ์ผ์ผํ ์์ฑํด์ผ ํ ๋ฟ ์๋๋ผ, ์ด๋ฏธ์ง๊ฐ ์ถ๊ฐ๋ ๋๋ง๋ค import ์ฝ๋ ์ญ์ ๊ณ์ ์ถ๊ฐํด์ผํ๋ค.
์ด๋ป๊ฒ ํ๋ฉด ์ด๊ฒ์ dynamic ํ๊ฒ ๋ง๋ค ์ ์์๊น?
const response = await fetch(require(`../static/images/${imageId}`));
// ๋๋
const response = await fetch((await import(`../static/images/${imageId}`)).default);
์ ๊ฐ์ด ์์ฒญํ๋ ๊ฒ์ด๋ค. ๋จ, ์ด๋ ์ฃผ์ํด์ผ ํ ๊ฒ์ด
// ์๋ชป๋ URL
`../static/images/${imageId}`
// ์ฌ๋ฐ๋ฅธ URL
require(`../static/images/${imageId}`)
// ์ฌ๋ฐ๋ฅธ URL
(await import(`../static/images/${imageId}`)).default
๋ผ๋ ๊ฒ์ด๋ค. ์ด ๊ฒฝ์ฐ ์๋ฌด๋๋ await import
๋ณด๋ค๋ require
๊ฐ ๊ฐ๋
์ฑ์ด ์ข๊ธฐ ๋๋ฌธ์ require
๋ฅผ ์ฌ์ฉํ๋๋ก ํ๊ฒ ๋ค.
- src/mocks/handlers/static.ts
import { http, HttpResponse } from "msw";
import { HttpResolver } from "./index";
const getImageResolver: HttpResolver = async ({ params }) => {
const { imageId } = params;
const response = await fetch(require(`../static/images/${imageId}`));
const buffer = await response.arrayBuffer();
const contentType = response.headers.get("Content-Type");
return HttpResponse.arrayBuffer(buffer, {
headers: {
"Content-Type": contentType || "image/jpeg",
},
});
};
export const handlers = [http.get("/images/:imageId", getImageResolver)];
์ด๋ฐ์์ผ๋ก images, videos, fonts ์ ๊ฐ์ ๋๋ ํ ๋ฆฌ๋ฅผ ์ถ๊ฐํ๊ณ API ๋ฅผ ๋ง๋ค๋ฉด ๋๋ค. ์ฐธ๊ณ ๋ก ํค๋๋ Content-Type
๋ง ์ค์ ํ๋๋ก ํ๋ค.
Content-Length ์ ๊ฐ์ ๋ค๋ฅธ ํญ๋ชฉ๋ค์ arrayBuffer
๋ฉ์๋๊ฐ ์๋์ผ๋ก ์ฑ์ ๋ฃ์ด์ค๋ค.
Reference
- โGetting Started syntax.โ mswjs Docs. accessed Jun. 01, 2024, https://mswjs.io.