[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 ์„œ๋ฒ„์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

โ„น๏ธ Notice: 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