클라이언트에서 API 테스트를, MSW.js 도입기

Shyuuuuni·2022년 12월 11일
1

📚 Tech-Post

목록 보기
5/9
post-thumbnail

도입 배경

관련 포스트 : 부스트캠프 웹・모바일 7기 그룹 프로젝트 2주차 회고
관련 GitHub : https://github.com/boostcampwm-2022/web06-weview

출처: Mocking으로 생산성까지 챙기는 FE 개발

  • 프로젝트 기획상으로는 동시에 FE,BE가 같은 기능 개발에 시작해서 바로 바로 테스트 할 수 있는 상태를 기대했다.
  • 하지만 1주차 환경설정을 지나 2주차에 기능 구현을 시작하고 보니 위 이미지처럼 동시에 개발을 시작했다.
  • 이런 상황에서 클라이언트에서 API를 활용해서 기능 테스트를 해보려고 하면 테스트를 위해 테스트하지 않은 코드를 dev브랜치로 올려서 배포해야 하는 문제가 있었다.

출처

서버에 배포하면 사이드이펙트가 생길수도 있고, 서버가 다운되면 또 그것대로 해결해야 했다.

그래서 서버에 배포하지 않고 로컬에서 테스트를 하려면 어떻게 해야 할까 고민했는데,

  • 로컬과 서버를 직접 연결: 로컬 환경에서 API서버에 직접 연결해서 사용한다.
  • 로컬에서 테스트 데이터 사용: 예를들면 fetchPost라는 API를 테스트하기 위해 fetchPostTest API를 구현해서 반환값을 준다거나 하는 식으로 구현할 수 있다고 생각했다.
  • Mocking용 서버를 배포: 로컬에서 연결할 수 있는 Mock Server를 배포해서 연결하는 방식도 고민할 수 있었다.

MSW (Mock Service Worker)

기존 방식의 문제점

  • 로컬과 서버를 직접 연결: 로컬 환경을 서버 환경과 맞춰줘야 하기 때문에 불편했다. 간단하게 CORS라던가..CORS라던가.. (서버 배포 환경에서는 동일 origin의 NGINX를 사용중이다.)
  • 로컬에서 테스트 데이터 사용: 배포 서버에 올릴 때 마다 코드를 API 서버를 호출하는 로직으로 변경해주어야 한다. 또한 다양한 응답코드나 딜레이 등을 테스트하기에 쉽지 않다고 생각했다.
  • Mocking용 서버를 배포: 이미 개발 서버, 실서버 이렇게 두개의 서버를 운용하기로 결정해서, 하나의 서버를 추가로 배포해달라고 BE에게 요청하기에는 어려웠다.

msw.js

Mock by intercepting requests on the network level. Seamlessly reuse the same mock definition for testing, development, and debugging.
msw.js

해당 라이브러리 홈페이지에 들어가면 바로 나오는 소개다.

네트워크 레벨에서 요청을 가로챈다고 한다. 그게 무슨 의미일까?

래퍼런스의 이미지를 보면 이해가 쉬웠다.

  • (1) 브라우저가 API 요청을 보낸다.
  • (2) 서비스 워커가 요청을 복사한다.
  • (3) MSW가 해당 요청을 매칭하여 처리한다.
  • (4) 모의(Mock) 응답을 반환한다.
  • (5) API 요청에 대한 응답을 수신한다.

MSW를 실행시키면 실제 서버까지 요청이 전달되지 않고, 네트워크 레벨에서 요청이 처리된다는 부분이 이런 의미다.

MSW가 포함된 개발

많은 도움을 받은 문서에 나와있는 내용을 참고하여 우리 프로젝트에 적용했다.

  • (1) 기획을 통해 어떤 기능을 구현할지 결정한다. (주마다 백로그를 만들어놓는 방식을 적용했다.)
  • (2)-(3) 해당 기능 구현을 진행하는 FE와 BE가 API 스펙을 협의한다.
  • (4)-(7) FE에서 Mock API와 함께 기능 개발을 진행한다.
  • (8)-(9) BE에서 API를 개발하고 제공한다. 이때 클라이언트가 다시 한번 확인할 수 있게 swagger로 제공했다.
  • (10) 최종적으로 확인하고 배포하면서 기능 테스트를 진행한다.

MSW 적용하기

공식문서가 잘 나와있어서 따라하면 쉽게 적용할 수 있다.

설치

패키지 설치

npm install msw --save-dev
# or
yarn add msw --dev

Mock 디렉토리 설정

우리 프로젝트에서는 위와 같이 사용했다. (mocks 디렉토리는 src 하위 폴더로 생성했다.)

mkdir src/mocks

MSW 초기화

공식문서의 Integrate 부분을 참고하면 브라우저 방식과 Node.js 방식이 다르다고 한다. 혹시 적용할 때 참고하자.

# browser
npx msw init <PUBLIC_DIR> --save

# VITE public 디렉토리 선택
npx msw init public/ --save

우리는 브라우저에서 필요하므로 브라우저 방식을 선택했고, 위와 같이 public 디렉토리를 선택해서 초기화 할 수 있었다.

VITE를 사용했으므로 root 디렉토리의 public/ 디렉토리를 선택했다.

MSW 적용

// main.tsx
import React from "react";
import ReactDOM from "react-dom/client";

import App from "./App";
import { API_MODE } from "./constants/env";
import { MODE } from "./constants/mode";

if (MODE.MOCK === API_MODE) {
  import("@/mocks/browser")
    .then(async ({ worker }) => {
      await worker.start({
        onUnhandledRequest: "bypass",
      });
    })
    .catch((err) => {
      console.error(err);
    });
}

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

먼저 위와같이 환경변수의 실행 환경이 MOCK 환경일 때 msw.js를 초기화하도록 설정했다.

// server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers);
// browser.ts
import { setupWorker } from "msw";
import { handlers } from "./handlers";

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

mock/server.ts파일과 mock/browser.ts 파일을 위와 같이 설정해주고,

//
export const handlers = [
  ...authHandler, // import 해 온 handler 들
  ...postHandler,
  ...reviewHandler,
  ...rankingHandler,
  ...searchHandler,
  ...bookmarkHandler,
];

mock/handlers.ts 파일도 위와 같이 필요한 핸들러를 import해서 배열로 내보냈다.

// handlers/searchHandler.ts
import { rest } from "msw";

import { API_SERVER_URL } from "@/constants/env";
import { history, setHistory } from "@/mocks/datasource/mockDataSource";

// Backend API Server URL
const baseUrl = API_SERVER_URL;

export const searchHandler = [
  rest.get(`${baseUrl}/search/histories`, (req, res, ctx) => {
    return res(ctx.status(200), ctx.delay(500), ctx.json(history));
  }),
  rest.delete(`${baseUrl}/search/histories/:id`, (req, res, ctx) => {
    setHistory(
      history.filter((searchHistory) => searchHistory.id !== req.params.id)
    );
    return res(ctx.status(204), ctx.delay(1000));
  }),
];

그리고 핸들러는 위와 같이 사용했다.

API_SERVER_URL 등은 환경변수로 분리해서 받아왔고, history, setHistory 등은 Mock DB 역할을 수행하는 오브젝트이다. (검색 기록을 간단하게 보관하기 위해서 배열로 사용했다.)

중요한 부분은 rest.get, rest.delete 이 부분이다.

  • GET API_SERVER/search/histories 요청이 오면 응답코드 200-OK, 500ms 딜레이 후, history 값을 json으로 응답한다.
  • DELETE API_SERVER/search/histories/:id 요청이 오면 history 배열에서 id가 요청의 id와 같은 데이터를 제거한다. 이후 204-No Content, 1000ms 딜레이 후 응답한다.

래퍼런스를 참고하면 더 다양한 기능을 사용할 수 있다.

결론

  • 기존 개발 방식에서 FE-BE 개발 속도 차이와 클라이언트단의 API를 사용한 테스트의 불편함이 있었다.
  • msw.js 라이브러리를 도입해서 로컬 환경에서 클라이언트가 API를 사용한 기능 테스트가 가능하도록 구현할 수 있게 되었다.
  • 또한 환경 변수를 설정해서 개발 환경과 배포 환경에서 소스코드 변경 없는 서버 API와 Mock API를 전환할 수 있었다.

지금 생각하면 2주차에 개발을 거의 시작하자마자 도입하자고 요청했던 것이 굉장히 좋은 선택이라고 생각한다.

지금 우리가 서비스하는 기능들에 대해서 거의 대부분 로컬에서 실행할 수 있을 정도로 설정되어 있어서, 나중에 서버를 닫는다면 정적 배포로 클라이언트만 배포해도 좋을 것 같다는 생각도 들었다.

그리고 더 효율적인 협업을 한다는 느낌이 들었다. 특히 CI/CD 과정에서 자동으로 주입하는 환경변수로 실제 소스코드는 변경 없이 배포하면 자동으로 API서버로 요청이 가는 부분이 굉장히 좋은 개발 경험이였다.

앞으로도 혼자 FE를 개발하거나 BE개발자와 협업한다면 적극적으로 MSW를 도입할 것 같다.

래퍼런스

profile
배짱개미 개발자 김승현입니다 🖐

0개의 댓글