[MSW] 백엔드가 api를 만들어주지 않아요... 그런 당신을 위한 MSW

navyjeongs·2023년 11월 11일
23

리액트

목록 보기
4/8
post-thumbnail

프론트엔드 개발자로 프로젝트를 진행하면서 api가 만들어지지 않아 dummy 데이터를 만들거나 개발을 멈췄던 경험 한번씩 있을 것이다.
나 또한 그랬던 적이 있다.

상황

게시글 목록을 api로 불러와서 화면에 게시글 목록을 그리는 페이지를 만들고 있었다.

기획자 : 정석님 혹시 게시글 목록 페이지 확인할 수 있을까요?
나 : 그게, 아직 API가 안 만들어져서 못했어요. 잠시만요.
나 : 백엔드님 혹시 게시글 목록 불러오는 API 만들어졌나요?
BE : 아니요, 제가 다른 거 하느라 못 만들었어요.
나 : 아.... 네 천천히 하세요 ^^(제발 빨리 해주세요...)

이상적인 프로젝트는 다음과 같다.
기획이 완료된 후, 백엔드 개발을 하고 백엔드에서 API가 개발 되었을 때 프론트엔드 개발을 진행하는 것이다.

하지만 리얼월드의 프로젝트는 다음과 같이 진행된다.
기획을 조금 진행하다가 프론트엔드 개발과 백엔드 개발을 같이 진행하고 같이 개발을 진행한다.
즉, API가 만들어지지 않은 상태에서 프론트엔드 개발을 진행한다.

이럴 때 마다, 어떻게 api 호출을 성공했다고하고 작업을 진행하지? 라는 생각을 했다. 이러한 고민을 해결할 수 있는 Mock Service Worker(MSW)가 있다.

MSW

MSW란 API Mocking을 해주는 라이브러리다. 즉, http://localhost:8080/posts 로 요청 한다면 msw가 중간에서 api 요청을 가로채 실제로 api가 요청이 된 것처럼 해준다.

이것을 사용하면 백엔드분들께 api를 만들어달라고 요청하는일이 조금은 줄어들 것이다.

msw를 사용할 수 있는 환경은 다음과 같다.

  • 로컬 개발 환경
  • 통합 테스트
  • E2E 테스트
  • Storybook

아래에서 msw를 사용해보자.

라이브러리 설치 및 워커 스크립트 생성하기

msw 사용을 위해 우선 라이브러리를 설치하자.

yarn add -D msw

웹(어플리케이션)에서 서비스 워커를 사용하기 위해 msw는 CLI를 제공한다. 아래 명령어를 사용하여 public 폴더에 Service Worker 작동을 위한 자바스크립트 파일이 만들어진다.

npx msw init ./public

해당 파일이 만들어 진 후, package.json에서 worker의 위치를 작성한다.

  "scripts" : {
    // ...
  },
  "msw": {
    "workerDirectory": "public"
  },
    

service worker

service worker는 웹 어플리케이션, 브라우저, 네트워크 사이 프록시 서버 역할을 한다. 특히, 네트워크 요청을 가로챌 수 있다.
service worker는 메인 스레드와 분리된 별도의 스레드에서 동작하며, 백그라운드 스레드에서 동작시킬 수 있는 기술이다.

즉, 브라우저가 API를 요청하면 서비스워커는 해당 응답을 복제(clone) 한다.

그 후, msw에서 요청에 맞는 응답을 생성한 후, mocking된 응답을 서비스 워커에 전달하고 브라우저는 mocking된 응답을 받게 된다.

browser.ts

브라우저 환경에서 사용하기 위해 brower.ts를 만들고 다음과 같이 작성한다.

setupWorker 메서드는 브라우저에서 API mocking을 활성화하기 위해 클라이언트-워커 간 통신 채널을 준비하는 역할을 한다.

import { setupWorker } from 'msw/browser';

export const worker = setupWorker();

index.ts

browser.ts를 불러와서 start할 수 있도록 파일을 하나 더 만들자.

나는 '__mocks__'에 index.ts를 만들었다.

  • index.ts에서는 browser.ts를 동적으로 import한다. 이 때,서비스 워커를 등록하는 과정이 비동기 작업이므로 await으로 기다려야한다!!
const initMockAPI = async (): Promise<void> => {
  const { worker } = await import('@__mocks__/browser');
  worker.start();
};

export default initMockAPI;

main.tsx에서 사용하기

나는 vite + react를 사용하고 있어 최상위 파일이 main.tsx다.
msw를 dev모드에서만 사용하고 배포 모드에서는 사용하면 안된다.
따라서, 최상위에서 현재 dev모드인지 확인하고 msw를 사용한다.

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import initMockAPI from '@__mocks__/index.ts';

async function deferRender() {
  // 개발모드 확인하기
  if (!import.meta.env.DEV) {
    return;
  }
  await initMockAPI();
}

deferRender().then(() => {
  ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
});

vite를 사용하지 않는다면 process.env.NODE_ENV로 dev모드를 확인할 수 있다.

if (process.env.NODE_ENV == 'development') {
 	// ... 동일 로직 작성 
}

위와 같이 작성하고 실행 후 콘솔창을 보면 [MSW] Mocking enabled. 라는 메시지를 확인할 수 있다!

주의할 점

서비스 워커를 등록하는 과정이 비동기 작업이므로 반드시 await을 해야한다.!!
만약 await을 하지 않으면 MSW가 켜지기 전에 App이 렌더링되고 api를 호출하여 아래와 같은 오류를 만날 수 있을 것이다.

if (import.meta.env.DEV) {
  // 만약 await을 하지않는다면
  initMockAPI();
}

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
  • MSW가 켜지기전에 api 요청 보낸것을 확인할 수 있다.

목 api 만들고 등록하기

이제 msw가 정상적으로 실행되니 mock api를 만들어보자,
아주 간단하게 http://localhost:8080/posts로 get 요청이 오면 아래의 posts를 응답하는 api다.

postHandler라는 배열을 만들어 http 요청을 작성하자.

배열의 각 요소에는 api의 method, url, response 등을 설정할 수 있다.

아래 예시는 다음과 같다.
method : get,
url : http://localhost:8080/posts,
res : Array

import { HttpResponse, http } from 'msw';

export interface Post {
  id: number;
  title: string;
  content: string;
}

const posts: Array<Post> = [
  {
    id: 1,
    title: '안녕하세요,',
    content: '만나서 반갑습니다',
  },
  {
    id: 2,
    title: '백엔드 님들,',
    content: 'API좀 빨리 만들어주세요.',
  },
  {
    id: 3,
    title: '프론트 님들',
    content: '일주일만 기다려주세요',
  },
];

const postsHandler = [
  http.get('http://localhost:8080/posts', () => {
    return HttpResponse.json({ posts });
  }),
];

export default postsHandler;

put, delete, post등 더 많은 사용법은 아래를 참고하자
https://mswjs.io/docs/api/http

앞에서 mock api를 만들었으니 msw에게 알려준다.
browser.ts로 가서 spread 연산자를 이용해서 해당 handler를 worker에 알려주자

import { setupWorker } from 'msw/browser';
import postsHandler from './handler/postsHandler';

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

사용하기

이제 브라우저에서 요청해보자.
다음과 같은 간단한 페이지를 만든다.
해당 페이지를 posts 목록을 불러와서 화면에 그린다.

import { Post } from '@__mocks__/handler/postsHandler';
import axios from 'axios';
import { useEffect, useState } from 'react';

function App() {
  const [posts, setPosts] = useState<Array<Post>>([]);

  const fetchPosts = async () => {
    const res = await axios({
      method: 'get',
      url: 'http://localhost:8080/posts',
    });

    setPosts(res.data.posts);
    console.log(res.data);
  };

  useEffect(() => {
    fetchPosts();
  }, []);

  return (
    <>
      {posts.map((post) => {
        return (
          <div key={post.id}>
            <span>제목 : {post.title}</span>
            <span>내용 : {post.content}</span>
          </div>
        );
      })}
    </>
  );
}
export default App;

이렇게 api 요청에 성공하는 것을 확인할 수 있다.

또 이렇게 화면에 정상적으로 렌더링할 수 있다.!!

node 환경에서도 사용하기

앞서 브라우저 환경에서 API를 모킹했다. 하지만 아직 문제가 남아있다. 바로 테스트를 하는 환경에서는 해당 코드가 동작하지 않는데 왜냐햐면 해당 코드는 브라우저 환경에서만 동작하고, jest를 실행하는 node 환경에서는 지원을 하지 않기 때문이다.

server.ts

앞서 브라우저 환경에서 작동하는 browser.ts를 작성했다. 이제 node 환경에서 작동하는 server.ts를 만들자.
마찬가지로 mock API인 postsHandler를 setupServer에 등록하자.

import { setupServer } from 'msw/node';
import postsHandler from './handler/postsHandler';

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

설정

다들 jest 환경을 구축하면서 jest.setup.ts 혹은 setupTest.ts 파일이 있을 것이다.
해당 파일에 다음과 같은 내용을 추가한다.

import '@testing-library/jest-dom';
import { server } from './src/__mocks__/server';
import { beforeAll, afterAll, afterEach } from '@jest/globals';

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

import { setupServer } from 'msw/node'; error

만약 리액트 컴포넌트 테스트(react-testing-library)를 하지 않는다면 아래와 같이 testEnvironment를 단순히 node로 바꾸면 해결된다. 하지만 리액트 컴포넌트 테스트(react-testing-library)를 한다면 추가적으로 설정을 해야한다. 해당 설정을 뒷부분에 작성하겠다.

  testEnvironment: 'node',

참고한 이슈

jest에서 사용해보기

아래와 같이 api 요청을 보내고 테스트를 해보자.
mock API의 응답으로 길이가 3인 배열이므로 배열의 길이로 테스트를 해보자!!

import axios from 'axios';

describe('MSW 테스트', () => {
  it('posts API 테스트', async () => {
    const res = await axios({
      method: 'get',
      url: 'http://localhost:8080/posts',
    });

    expect(res.data.posts.length).toBe(3);
    console.log(res.data.posts);
  });
});

정상적으로 node 환경에서도 msw가 사용가능한 것을 확인할 수 있다!

사용하기전 주의점

msw를 사용하기전 주의해야하는 경우가 있다.
바로 API 명세가 작성된 후 사용할 때, msw의 이점이 커진다.

만약 API 명세가 작성되지 않았고, 마음대로 url, query, response 값을 작성 후 mocking해서 사용한다면?

=> 실제 API 명세나온 후 전부 뜯어 고쳐야할 수도 있다.

따라서 API 명세가 작성된 후 사용하는 것을 권장한다.

v2가 v1에 비해 달라진 점

msw ver 1과 달라진 점은 browser와 node가 분리 되었다.

msw ver 1에서는 setupWorker 메서드가 msw에 있었다.

import { setupWorker } from 'msw';

export const worker = setupWorker();

msw ver 2에서는 setupWorker 메서드가 msw/browser로 분리되었다.

import { setupWorker } from 'msw/browser';

export const worker = setupWorker();

testEnvironment를 jsdom에서 node로 변경 시 발생하는 문제점

앞서 testEnvironment를 jsdom에서 node로 변경했는데 jsdom에서 node로 변경한다면 react testing library등을 사용할 수 없다.

이와 관련해서 깃허브 msw의 이슈에서 msw 개발자와 사용자들 사이에 많은 얘기를 주고받았다. 우선 msw 개발자는 해당 오류는 msw와 관련이 없다는 말을 남겼고, 대신 해결책을 아래에 작성해두었다.

https://mswjs.io/docs/migrations/1.x-to-2.x#frequent-issues

https://github.com/mswjs/msw/issues/1786

testEnviroment : node의 문제점

앞서 설명했지만 testEnviroment를 node로 설정하면 리액트 컴포넌트 테스트(react-testing-library)를 사용할 수 없다. 따라서 jsdom을 사용해야한다.

testEnviroment를 node로 변경한다면 react-testing-library를 사용할 수 없으므로 일단 다시 jsdom으로 변경하자.

jest.config.ts

jest.config.ts에서 testEnvironment를 jsdom으로 변경하자.

  testEnvironment: 'jsdom',

Cannot find module ‘msw/node’ (JSDOM) error

  • 위와 같은 에러가 발생하면 jest.config.ts 혹은 jest.config.js에 testEnvironmentOptions을 다음과 같이 추가한다.
module.exports = {
  // 추가할 내용
  testEnvironmentOptions: {
    customExportConditions: [''],
  },
}

Cannot find module ‘msw/node’ (JSDOM)

Request/Response/TextEncoder is not defined (Jest) 가 발생한다면...

위와 같이 작성하고 다시 테스트코드를 실행하면 위와같은 에러가 발생한다.
해당 에러는 사용자 환경에 전역 Node.js가 없어서 발생하는데 주로 jest에 의해 발생된다고 msw 개발자가 말한다.

Request/Response/TextEncoder is not defined (Jest)

jest.config.js의 같은 depth에 jest.polyfills.js을 생성하고 다음의 내용을 작성한다.

// jest.polyfills.js
/**
 * @note The block below contains polyfills for Node.js globals
 * required for Jest to function when running JSDOM tests.
 * These HAVE to be require's and HAVE to be in this exact
 * order, since "undici" depends on the "TextEncoder" global API.
 *
 * Consider migrating to a more modern test runner if
 * you don't want to deal with this.
 */
 
const { TextDecoder, TextEncoder } = require('node:util')
 
Object.defineProperties(globalThis, {
  TextDecoder: { value: TextDecoder },
  TextEncoder: { value: TextEncoder },
})
 
const { Blob } = require('node:buffer')
const { fetch, Headers, FormData, Request, Response } = require('undici')
 
Object.defineProperties(globalThis, {
  fetch: { value: fetch, writable: true },
  Blob: { value: Blob },
  Headers: { value: Headers },
  FormData: { value: FormData },
  Request: { value: Request },
  Response: { value: Response },
})

그 후, undici 라이브러리를 설치한다.

yarn add undici

undici 까지 설치한 후, jest.config.ts에 다음의 내용을 추가한다.

module.exports = {
  setupFiles: ['./jest.polyfills.js'],
}

결과

아래와 같이 testing-library를 사용한다면 에러가 발생했지만 이제 발생하지 않는다.

import Button from '@components/Button';
import { render, screen } from '@testing-library/react';
import axios from 'axios';

describe('MSW 테스트', () => {
  it('posts API 테스트', async () => {
    const res = await axios({
      method: 'get',
      url: 'http://localhost:8080/posts',
    });

    expect(res.data.posts.length).toBe(3);
    console.log(res.data.posts);
  });

  it('버튼 컴포넌트 테스트', () => {
    render(<Button />);

    const ele = screen.getByText(2);

    expect(ele).toBeInTheDocument();
  });
});

남아있는 문제점

임시방편으로 위와 같이 해결했지만 아래 깃허브 이슈를 확인해보면 추후에 더 많은 에러가 발생한다고 한다.
ver 1을 사용하는 사람과 사용하지 않겠다는 사람들이 있다.
나 또한 ver 2로 사용을 하다가 더 많은 에러가 발생하면 ver 1으로 다운그레이드해야될 것 같다!

msw 관련 이슈는 아래에서 확인할 수 있다.

MSW 이슈

후기

이제 백엔드가 API를 만들어주지 않아도 열심히 코딩할 수 있다!

그래도 백엔드님들 사랑합니다

오류가 있다면 댓글로 알려주세요!!

profile
Front-End Developer

1개의 댓글

comment-user-thumbnail
2023년 11월 29일

백앤드에 대한 분노가 잘 느껴지는 글이었습니다.

답글 달기