Mock Service Worker

7 분 소요


RECOMMEND POSTS BEFORE THIS

1. Mock Service Worker

목 서비스 워커(MSW, Mock Service Worker)는 이름처럼 서비스 워커(Service Worker)테스트 더블(test double)을 의미합니다. API 모킹(mocking)을 지원하는 라이브러리이며 서버로 보내는 네트워크 요청을 가로채어 모의 응답을 내려줍니다. 서비스 워커가 무엇이고 어떤 동작을 하는지 안다면 목 서비스 워커를 이해하는데 도움이 됩니다. 먼저 서비스 워커에 대해 간단하게 알아보겠습니다.

1.1. Service Worker

브라우저에서 실행되는 백그라운드 스크립트이며 브라우저와 서버 사이에서 요청과 응답을 처리할 수 있는 네트워크 프록시(proxy)입니다.

서비스 워커는 웹 어플리케이션과 독립적으로 동작하는 스크립트입니다. 서비스 워커 역할을 수행할 수 있는 JavaScript 파일을 브라우저에 등록만 하면 사용할 수 있습니다. 웹 어플리케이션에서 간단한 코드를 통해 서비스 워커를 설치할 수 있습니다.

서비스 워커는 대표적으로 다음과 같은 기능을 제공합니다.

  • 오프라인 브라우징(offline browsing)
  • 백그라운드 동기화
  • 푸시 알림

서비스 워커가 이런 기능들을 제공할 수 있는 이유는 다음과 같습니다.

  • 이벤트 기반으로 동작하며 웹 어플리케이션에서 수행하는 네트워크 요청 이벤트를 가로챌 수 있습니다.
  • 브라우저 캐시 저장소(cache storage)를 사용할 수 있으며 오프라인 상태에서 캐시에 저장된 리소스를 사용할 수 있습니다.
  • 설치를 통해 백그라운드에서 별도로 동작할 수 있습니다.

1.2. How does Mock Servic Worker work?

목 서비스 워커는 브라우저 환경에서 다음과 같은 방법으로 동작합니다. 플로우 다이어그램(flow diagram)에는 브라우저로 표현되어 있지만, 더 정확한 표현은 웹 어플리케이션이라고 생각되어 명칭을 바꿔 설명했습니다.

  1. 웹 어플리케이션에서 서버로 요청을 보냅니다.
    • 서비스 워커가 웹 어플리케이션의 요청을 가로챕니다.
    • 서비스 워커는 네트워크 요청을 fetch 이벤트 콜백 함수를 통해 가로챌 수 있습니다.
  2. 요청 정보를 목 서비스 워커에게 복사하여 전달합니다.
  3. 목 서비스 워커는 해당 요청에 매칭되는 사전에 정의한 핸들러(handler)를 실행합니다.
  4. 서비스 워커는 목 서비스 워커로부터 모의 응답을 전달받습니다.
  5. 서비스 워커는 모의 응답을 웹 어플리케이션에 전달합니다.

https://mswjs.io/docs/#request-flow-diagram

2. Practice

간단한 TODO 리스트 어플리케이션 예시를 통해 목 서비스 워커의 사용 방법을 알아보겠습니다.

  • CRA(Create React App)을 통해 프로젝트를 생성하였으며 전체적인 구조는 다음과 같습니다.
./
├── README.md
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   ├── mockServiceWorker.js
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── mocks
│   │   ├── browser.ts
│   │   ├── handlers.ts
│   │   └── server.ts
│   ├── react-app-env.d.ts
│   ├── reportWebVitals.ts
│   ├── setupTests.ts
│   └── types
│       └── Todo.ts
├── tsconfig.json
└── yarn.lock
  • 프로젝트 경로에서 아래 명령어를 통해 목 서비스 워커 API 의존성을 설치합니다.
$ yarn add msw --dev

yarn add v1.22.17
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
warning " > @testing-library/user-event@13.5.0" has unmet peer dependency "@testing-library/dom@>=7.21.4".
warning "react-scripts > eslint-config-react-app > eslint-plugin-flowtype@8.0.3" has unmet peer dependency "@babel/plugin-syntax-flow@^7.14.5".
warning "react-scripts > eslint-config-react-app > eslint-plugin-flowtype@8.0.3" has unmet peer dependency "@babel/plugin-transform-react-jsx@^7.14.9".
[4/4] 🔨  Building fresh packages...
success Saved 1 new dependency.
info Direct dependencies
└─ msw@1.2.1
info All dependencies
└─ msw@1.2.1
✨  Done in 17.52s.

2.1. Mock Service Worker for Test

리액트 어플리케이션의 테스팅 프레임워크인 jest는 노드(nodejs) 환경에서 동작합니다. 런타임 시 브라우저에 목 서비스 워커를 설치하는 방법과 다르므로 이를 주의하시기 바랍니다.

2.1.1. handlers.ts

모의 응답을 만드는 핸들러를 정의합니다.

  • 프로젝트 src/mocks 경로에 생성합니다.
  • /todos GET 요청
    • 기존에 저장된 TODO 리스트를 모의 응답합니다.
  • /todos POST 요청
    • 요청을 통해 전달받은 내용으로 새로운 TODO를 생성합니다.
    • 신규 TODO가 생성되었다고 가정하고 임의의 아이디와 함께 신규 TODO를 모의 응답합니다.
import { rest } from "msw";
import { Todo } from "../types/Todo";

export const handlers = [
  rest.get("/todos", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        { id: 1, content: "Frontend Study" },
        { id: 2, content: "Backend Study" },
      ])
    );
  }),
  rest.post("/todos", async (req, res, ctx) => {
    const { content } = (await req.json()) as Todo;
    return res(
      ctx.status(200),
      ctx.json({
        id: Math.floor(Math.random() * 1000000 + 1),
        content: content,
      })
    );
  }),
];

2.1.2. server.ts

  • 프로젝트 src/mocks 경로에 생성합니다.
  • 생성한 핸들러를 사용하여 서버 객체를 생성합니다.
    • 개발 서버 환경에서 사용하는 함수인 setupWorker()와 다르므로 주의합니다.
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

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

2.1.3. setupTests.ts

  • 이전 단계에서 생성한 서버 객체를 테스트 코드 실행 시 사용할 수 있도록 테스트 환경 셋업(setup) 스크립트에 아래 코드를 추가합니다.
  • beforeAll() 함수를 통해 테스트 시작 전에 서버를 실행합니다.
  • afterEach() 함수를 통해 각 테스트 완료 후에 핸들러를 초기화합니다.
    • 각 테스트 사이의 상태 커플링(state couping)을 방지합니다.
  • afterAll() 함수를 통해 모든 테스트 완료 후에 서버를 종료합니다.
import "@testing-library/jest-dom";
import {server} from "./mocks/server";

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

2.1.4. App.tsx

간단한 TODO 리스트 어플리케이션입니다.

  • 이전에 작성한 TODO 항목들을 화면에 표시합니다.
  • 신규 TODO를 추가하면 서버로 저장 요청 후 정상적인 응답을 받는 경우 화면에 추가합니다.
import React, { useEffect, useRef, useState } from "react";
import "./App.css";
import axios from "axios";
import { Todo } from "./types/Todo";

function App() {
  const todoTextInputRef = useRef<HTMLInputElement>(null);
  const [todoList, setTodoList] = useState<Todo[]>([]);

  useEffect(() => {
    axios.get("/todos").then((response) => {
      const { data: todoList } = response;
      setTodoList(todoList);
    });
  }, []);

  const addHandler = async () => {
    const { data: newTodo } = await axios.post("/todos", {
      content: todoTextInputRef.current?.value,
    });
    setTodoList((todoList) => [...todoList, newTodo]);
    todoTextInputRef.current!.value = "";
  };

  return (
    <div className="app">
      <div className="todo-list">
        <div>
          {todoList.map((todo) => (
            <li key={todo.id}>{todo.content}</li>
          ))}
        </div>
      </div>
      <div className="todo-form">
        <input ref={todoTextInputRef} type="text" placeholder="NEW TODO" />
        <button onClick={addHandler}>ADD</button>
      </div>
    </div>
  );
}

export default App;

2.1.5. App.test.tsx

단위 테스트는 정상적으로 통과하며 다음과 같은 기능들을 테스트합니다.

  • 목 서비스 워커가 모의 응답을 주기 때문에 별도로 axios 모듈을 스터빙(stubbing)하지 않습니다.
  • renders todo list test
    • 기존에 작성된 TODO 리스트가 랜더링 시 화면에 보이는지 확인합니다.
  • add new todo test
    • 새로운 TODO를 추가하는 버튼을 클릭하면 리스트에 추가되고 입력창은 정리되는지 확인합니다.
import React from "react";
import App from "./App";

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("renders todo list", async () => {
  render(<App />);

  expect(await screen.findByText("Frontend Study")).toBeInTheDocument();
  expect(screen.getByText("Backend Study")).toBeInTheDocument();
});

test("add new todo", async () => {
  render(<App />);

  await userEvent.type(screen.getByPlaceholderText("NEW TODO"), "DevOps Study");
  await userEvent.click(screen.getByText("ADD"));

  expect(await screen.findByText("DevOps Study")).toBeInTheDocument();
  expect(screen.getByPlaceholderText("NEW TODO")).toHaveValue("");
});

2.2. Mock Service Worker for Runtime

목 서비스 워커는 백엔드 서비스가 아직 구성되어 있지 않은 경우 활용할 수 있습니다. 로컬 개발 서버를 통해 리액트 어플리케이션을 실행할 때 목 서비스 워커는 백엔드 서비스를 임시로 대체할 수 있습니다. 위에서 단위 테스트를 위해 정의한 핸들러와 동일한 코드를 사용하므로 핸들러 코드 설명은 제외하겠습니다.

2.2.1. browser.ts

  • 프로젝트 src/mocks 경로에 생성합니다.
  • 생성한 핸들러를 사용하여 목 서비스 워커 객체를 생성합니다.
    • 테스트 환경에서 사용하는 함수인 setupServer()와 다르므로 주의합니다.
import { handlers } from "./handlers";
import { setupWorker } from "msw";

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

2.2.2. index.tsx

  • 리액트 어플리케이션이 실행될 때 목 서비스 워커도 함께 실행되도록 아래 코드를 추가합니다.
  • 개발 서버 환경에서만 실행되도록 NODE_ENV 환경 변수 값을 확인합니다.
    • npm run start 명령어를 통해 개발 서버를 실행하면 NODE_ENV 환경 변수 값이 development입니다.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

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

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

reportWebVitals();

2.2.3. Install Mock Service Worker

Service Worker 포스트에서 설명했듯 서비스 워커는 별도의 스크립트 파일이 필요합니다. MSW 라이브러리는 npx 명령어를 통해 서비스 워커 스크립트 파일 다운로드를 쉽게 제공합니다.

  • npx msw init public/ --save 명령어
    • public 경로에 mockServiceWorker.js 스크립트 파일을 다운로드 받습니다.
$ npx msw init public/ --save

Initializing the Mock Service Worker at "/Users/junhyunk/Desktop/2023-06-06-mock-service-worker/action-in-blog/public"...

Service Worker successfully created!
/Users/junhyunk/Desktop/2023-06-06-mock-service-worker/action-in-blog/public/mockServiceWorker.js

Continue by creating a mocking definition module in your application:

https://mswjs.io/docs/getting-started/mocks

$ ls -al public/

total 88
drwxr-xr-x   9 junhyunk  staff   288 Jun  7 00:42 .
drwxr-xr-x  12 junhyunk  staff   384 Jun  6 22:31 ..
-rw-r--r--   1 junhyunk  staff  3870 Jun  6 21:22 favicon.ico
-rw-r--r--   1 junhyunk  staff  1721 Jun  6 21:22 index.html
-rw-r--r--   1 junhyunk  staff  5347 Jun  6 21:22 logo192.png
-rw-r--r--   1 junhyunk  staff  9664 Jun  6 21:22 logo512.png
-rw-r--r--   1 junhyunk  staff   492 Jun  6 21:22 manifest.json
-rw-r--r--   1 junhyunk  staff  8133 Jun  7 00:42 mockServiceWorker.js
-rw-r--r--   1 junhyunk  staff    67 Jun  6 21:22 robots.txt
2.2.4. Run Dev Server
  • 개발 서버에 접속합니다.
    • 핸들러에 미리 추가한 모의 응답들이 화면에 출력됩니다.
    • 신규 TODO 추가 시에도 정상적으로 동작합니다.
  • 등록된 서비스 워커 정보는 개발자 도구(F12) > 애플리케이션 > Service Workers에서 확인할 수 있습니다.
  • 콘솔 로그에 목 서비스 워커 동작과 관련된 로그가 출력됩니다.

CLOSING

목 서비스 워커를 사용하면 프론트엔드 개발 시 마주치는 문제들을 해결할 수 있을 것 같습니다.

  • 백엔드 서비스 개발이 되지 않은 상태에서 API 관련된 기능을 개발할 수 있다.
  • 프론트엔드 테스트 코드 작성시 중복되는 스텁들을 핸들러 파일에서 공동으로 관리할 수 있다.

API 응답을 위한 스텁 같은 경우에는 화면의 기능에 따라 커지거나 많아지기 때문에 테스트 코드를 보기 어려워집니다. 비즈니스 단위로 핸들러를 만들고 적절한 모의 응답들을 반환한다면 테스트 코드가 짧고 간결해질 것 같습니다.

TEST CODE REPOSITORY

REFERENCE

댓글남기기