MSW로 mock 서버 사용해보기 (Next.js/TypeScript)

API가 준비되기 전에 클라이언트에서 mock 서버를 만들면 개발 명세로도 작업이 가능하기에, MSW라는 라이브러리로 mock 서버를 만들어서 진행해 봅니다.
2022.04.30

API가 준비되기 전에 클라이언트에서 mock 서버를 만들면 개발 명세로도 작업이 가능

MSW라는 라이브러리로 mock 서버를 만들어서 진행해 봄

MSW란?

브라우저의 경우 네트워크 단에서 (서비스 워커) 프로그램에서 외부로 향하는 API를 감지하여 mock 서버를 대신 사용해줌

노드의 경우 아래의 라이브러리를 통해 인터셉팅하고 있음

Since Service Worker API cannot run in a non-browser environment, the NodeJS support of request interception is done via a designated [node-request-interceptor](https://github.com/mswjs/node-request-interceptor)  library.

GitHub - mswjs/interceptors: Low-level HTTP/HTTPS/XHR/fetch request interception library.

REST API와 GraphQL을 대응하며 이번 경우는 REST API를 사용함

설정해보기

yarn add -D msw

라이브러리를 설치함

mocks
  ㄴ browser.ts
  ㄴ handlers.ts
  ㄴ index.ts
  ㄴ server.ts

mocks 폴더를 만들고 하위에 아래와 같은 파일들을 만듬

// browser.ts

import { setupWorker } from 'msw';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);

브라우저 (서비스 워커)에서 핸들러를 사용하기 위해 설정해줌

// handlers.ts

import { rest } from 'msw';

export const handlers = [
  rest.get('https://example.com/products/:productId', (req, res, ctx) => {
    const { productId } = req.params;

    const products = [
      {
        id: '22',
        name: 'banana',
        quantity: 3,
      },
    ];

    const product = products.filter((product) => product.id === productId)[0];

    return res(ctx.json(product));
  }),

  rest.get('https://example.com/reviews', (req, res, ctx) => {
    return res(
      ctx.json([
        {
          id: '31',
          author: '길동쓰',
          content: '맛있는 바나나 👍 🍌',
        },
      ]),
    );
  }),
];

앞으로 테스트 할 핸들러들

// index.ts

if (typeof window === 'undefined') {
  const server = import('./server');
  server.then((s) => s.server.listen());
} else {
  const worker = import('./browser');
  worker.then((w) => w.worker.start());
}

export {};

서버에서 (node 같은) 부르는지 웹 브라우저에서 부르는지 확인하여 임포트 함

강제로 module 로 만들기 위해 비어있는 export {} 를 사용함

// server.ts

import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

노드 같은 환경에서의 설정

사용해보기

// _app.tsx

if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
  import('../mocks');
}

function MyApp({component}) {
  ... // 생략

NEXT_PUBLIC_API_MOCKING 라는 환경변수를 사용하게 되는데 이 환경변수에 따라 mock 서버를 사용 할지를 결정하기 때문에 .env 파일같은 환경변수 파일에 해당 환경변수를 추가해 줌

따라서 필요한 환경에서만 사용할 수 있게 됨

// .env

NEXT_PUBLIC_API_MOCKING=enabled

NEXT_PUBLIC 프리픽스를 붙여주는 이유는 퍼를릭 디렉토리에 서비스워커를 등록하기 때문인데 서비스 워커 등록은 아래와 같이 함

// public/ 디렉토리는 자신이 사용하는 라이브러리 프레임워크에 따라 다름
// https://mswjs.io/docs/getting-started/integrate/browser#where-is-my-public-directory

npx msw init public/ --save

위 커맨드를 실행하면 /public/mockServiceWorker.js 란 파일이 생기고 이 파일이 대신 서비스 워커에 mock 서버를 등록 시켜줌

--save 플래그를 붙이면 package.json 에 디렉토리가 등록되고 msw 를 업데이트 할 때 자동으로 해당 코드가 업데이트 된다 함

Note that we are using the --save option to save the given worker directory ("public") in the package.json. That way any updates to the worker script will be automatically applied when updating the msw package in the future.

이제 실제 api를 콜 해봄

// index.tsx

import type { InferGetServerSidePropsType } from 'next';
import { useState } from 'react';

interface Product {
  id: string;
  name: string;
  quantity: number;
}

interface Review {
  id: string;
  author: string;
  content: string;
}

const Home = ({
  product,
}: InferGetServerSidePropsType<typeof getServerSideProps>) => {
  const [reviews, setReviews] = useState<Review[]>([]);

  const handleGetReview = () => {
    fetch('https://example.com/reviews')
      .then((res) => res.json())
      .then(setReviews);
  };

  return (
    <>
      <div key={product.id}>
        <p>상품이름: {product.name}</p>
        <p>상품수량: {product.quantity} </p>
      </div>
      <br />
      <button onClick={handleGetReview}>리뷰 가져오기</button>
      {reviews &&
        reviews.map((review) => (
          <div key={review.id}>
            <p>{review.author}</p>
            <p>{review.content}</p>
          </div>
        ))}
    </>
  );
};

export async function getServerSideProps() {
  const res = await fetch('https://example.com/products/22');
  const product: Product = await res.json();

  return {
    props: {
      product,
    },
  };
}

export default Home;

서버사이드, 클라이언트 사이드에서 모두 잘 받아오는 걸 확인할 수 있음

더 나아가서

유닛 테스트, E2E 테스트, 스토리북 등 여러 테스팅에 사용도 가능하니 여러모로 범용적으로 사용할 수 있으므로 유용해보여 써보는게 좋을듯 함