msw

박정훈·2022년 11월 22일
1

기업 과제를 받았고, 바로 index.js를 들어갔다.

if (process.env.NODE_ENV === 'development') {
  worker.start();
}

이게 뭐야? 뭘 start해? 개발 모드일 때만 돌아가는거야?

기업과제 많이 안해본 티를 낸다.

package.json을 살펴보니...

  "devDependencies": {
    "msw": "^0.45.0",
  },

음 확실히 배포 환경에서는 필요가 없나보군.. msw가 뭐하는 애인지 찾아봐야겠다.
mswjs
아...! API mocking 이라고 한다. REST API와 GraphQL을 지원하고... TS도 지원한다고 한다.

아 일단 로컬에서 테스트 용으로 쓰고 있는 곳에 깔아보자.

npm install msw --save-dev

mocks 정의

어떤 요청을 mocked할지 정의하기 위해 request handler를 사용할 것이다. 이들의 method, URL, 또는 기타 다른 기준을 기반으로 모든 요청을 capture하고 어던 응답을 반활할지 특정할 수 있게 해준다.

Mock 정의

Mock Service Worker로 작업시, 요청 핸들러, 브라우저 및 서버별 설정 목록을 mock definition 이라고 한다.
mock definition을 관리하는 데 엄격한 규칙은 없지만, API 모의 관련 모듈을 단일 디렉터리에 유지하는 것이 좋다.

src/mocks 디렉토리를 만든다.

디텍토리가 생겼으면 모든 요청 핸들러를 가질 모듈을 만든다! handler.js를 mocks디렉토리 하위에 만든다.

src/mocks/handlers.js

GraphQL과 REST API 중 REST API로 가보쟈.

Imports

REST API를 mocking 하기 위한 필수 항목을 src/mocks/handlers.js에 import하자.

// src/mocks/handlers.js
import { rest } from 'msw'

Request handler

REST API 요청을 처리하려면 메서드, 경로, 그리고 mocked 응답을 반환하는 함수를 특정해야 한다.

// src/mocks/handlers.js
import { rest } from 'msw'

export const handlers = [
  // Handles a POST /login request
  rest.post('/login', null),

  // Handles a GET /user request
  rest.get('/user', null),
]

Response resolver

인터셉트 된 요청에 응답하기 위해서는 response resolver function을 사용한 mocked 응답을 지정해야 한다.
Response resolver는 다음 인수를 받는 함수다.

  • req
    매칭 요청에 대한 정보
  • res
    mocked 응답을 생성하는 유틸리티 기능
  • ctx
    mocked 응답의 상태 코드, headers, body등을 설정하는데 도움이 되는 함수들의 그룹

이전에 null이었던 post, get의 두번째 인자를 함수로 채워보자.

// src/mocks/handlers.js
import { rest } from "msw";

const postLogin = async (_, res, ctx) => {
  sessionStorage.setItem("is-authenticated", "true");

  return res(ctx.status(200));
};

const fetchUser = async (_, res, ctx) => {
  const isAuthenticated = sessionStorage.getItem("is-authenticated");

  if (!isAuthenticated) {
    return res(
      ctx.status(403),
      ctx.json({
        erroeMessage: "Not authorized",
      })
    );
  }

  return res(
    ctx.status(200),
    ctx.json({
      username: "admin",
    })
  );
};

export const handlers = [
  // Handles a POST /login request
  rest.post("/login", postLogin),

  // Handles a GET /user request
  rest.get("/user", fetchUser),
];

다음 스텝으로 브라우저와 노드가 있는데 나는 브라우저로 간다!

Setup

Mock Service Worker는 요청 interception을 담당하는 서비스 워커를 등록함으로써 클라이언트 측에서 작동한다.
그러나 worker's의 코드를 우리가 직접 작성할 필요는 없고, library에서 배포하는 worker file을 복사한다.
Mock Service Worker는 이를 지원하는 CLI를 제공한다.

다음 명령어를 뙇 치면!
npx msw init public/ --save

public아래에 mockServiceWorker.js가 생겼다.

나중에 살펴보니 package.json에도
"msw": {
"workerDirectory": "public"
}
가 생겼다

Configure worker

mock definition directory(src/mocks)에 서비스 워커를 구성하고 시작할 파일을 만들자!
src/mocks/browser.js
그리고 browser.js에 이전에 만들었던 요청 핸들러인 hanlders로 worker의 instance를 만든다.

// browser.js
// src/mocks/browser.js
import { setupWorker } from 'msw'
import { handlers } from './handlers'
// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers)

Start worker

내가 처음 보고 이거 뭐여? 했던 코드가 드디어 등장!
현재 실행 환경에 따라 조건부로 src/mocks/browser.js을 가져온다.

if (process.env.NODE_ENV === 'development') {
  const { worker } = require('./mocks/browser')
  worker.start()
}

이제 사용해보자

공식 문서대로 예제를 따라쳤지만... 실습은 요즘.. 산책하면서 포켓몬고 하고 있으니까...! 포켓몬 데이터 만들어서 해봐야징!

중간에 service worker가 interception 하는지 확인하기 위해 name을 한글로 바꿔봄!

// src/mocks/handlers.js
import { rest } from "msw";

const fetchPokemon = async (_, res, ctx) => {
  return res(
    ctx.json({
      count: 1154,
      next: `https://pokeapi.co/api/v2/pokemon/?offset=1010&limit=10`,
      previous: `https://pokeapi.co/api/v2/pokemon/?offset=990&limit=10}`,
      results: [
        {
          name: "피카츄 신오 모자",
          url: "https://pokeapi.co/api/v2/pokemon/10096/",
        },
        {
          name: "피카츄 우노바 모자",
          url: "https://pokeapi.co/api/v2/pokemon/10097/",
        },
        {
          name: "피카츄 칼로스 모자",
          url: "https://pokeapi.co/api/v2/pokemon/10098/",
        },
        {
          name: "피카츄 알로아 모자",
          url: "https://pokeapi.co/api/v2/pokemon/10099/",
        },
        {
          name: "라이츄 알로아",
          url: "https://pokeapi.co/api/v2/pokemon/10100/",
        },
        {
          name: "모래두지 알로아",
          url: "https://pokeapi.co/api/v2/pokemon/10101/",
        },
        {
          name: "고지 알로아",
          url: "https://pokeapi.co/api/v2/pokemon/10102/",
        },
        {
          name: "식스테일 알로아",
          url: "https://pokeapi.co/api/v2/pokemon/10103/",
        },
        {
          name: "나인테일 알로아",
          url: "https://pokeapi.co/api/v2/pokemon/10104/",
        },
        {
          name: "디그다 알로아",
          url: "https://pokeapi.co/api/v2/pokemon/10105/",
        },
      ],
    })
  );
};

export const handlers = [
  // Handles a GET /user request
  rest.get("https://pokeapi.co/api/v2/pokemon/", fetchPokemon),
];
// Pokemon.jsx
import React, { useState } from "react";
import PokemonCard from "./PokemonCard";

const Pokemon = () => {
  const [pokemons, setPokemons] = useState([]);

  const handleOnClick = async () => {
    const result = await (
      await fetch("https://pokeapi.co/api/v2/pokemon/")
    ).json();
    setPokemons(result.results);
  };

  return (
    <>
      <button onClick={handleOnClick}>포켓몬 정보 내놔!</button>
      {pokemons.length > 0 &&
        pokemons.map((pokemon) => (
          <PokemonCard name={pokemon.name} url={pokemon.url} />
        ))}
    </>
  );
};

export default Pokemon;
// PokemonCard.jsx
import React, { useState, useEffect } from "react";

const PokemonCard = ({ name, url }) => {
  const [imgURL, setImgURL] = useState({
    front: "",
    back: "",
  });

  useEffect(() => {
    const getPokemonImg = async () => {
      const result = await (await fetch(url)).json();
      setImgURL((cur) => {
        return {
          ...cur,
          front: result.sprites.front_default,
          back: result.sprites.back_default,
        };
      });
    };
    getPokemonImg();
  }, []);

  return (
    <div>
      {imgURL.front && <img src={imgURL.front} alt={`${name} 앞모습`} />}
      {imgURL.back && <img src={imgURL.back} alt={`${name} 뒷모습`} />}
      <h2>{name}</h2>
    </div>
  );
};

export default PokemonCard;

귀여운 피카츄 버전이다!

서비스 워커로부터 200 OK란다. 중간에 잘 가로채서 이름을 한글로 바꿔줬나보다.

하라는 과제 안하고 뭐하냐

profile
그냥 개인적으로 공부한 글들에 불과

0개의 댓글