Skip to content

MSW를 활용한 API Mocking

YUNHO edited this page Jan 2, 2023 · 3 revisions

Table of content

1. Mocking API를 사용한 이유

2. 여러 가지 대안 중 MSW를 선택한 이유

3. MSW가 동작하는 방식

4. 간단한 사용 방법 설명

5. 우리 프로젝트에 적용한 방법 (CRA 기준)

6. 사용 후기 및 개선점


MSW 및 mocking에 대한 아이디어의 대부분을 [kakao-tech: Mocking으로 생산성까지 챙기는 FE 개발](https://tech.kakao.com/2021/09/29/mocking-fe/) 해당 글을 통해 알게되었습니다. 참고한 글만큼 전문적이지는 못하지만 저희가 고민한 mocking에 대한 내용과 어떤 생각을 가지고 msw를 도입하고 어떤 방법으로 활용했는지 공유하고자 합니다!

1. Mocking API를 사용한 이유

Frontend(FE) 개발자의 입장에서 생긴 개발 병목 현상

“000 API 언제쯤 완성되는지 알 수 있을까요???”

FE개발을 하면서 BE개발에 의존적인 상황이 종종 생겼습니다. BE개발자와 함께 API명세를 상의하고 정의했을 때, mocking 데이터와 mocking API가 만들어지기 전까지 샘플 데이터를 만들어서 개발을 진행했습니다. 샘플 데이터만 있을 때는 api 연결을 확인할 수 없어 UI 위주의 개발 작업을 진행해야 했습니다.

하지만 UI 위주의 개발 또한 디자이너 분의 진행상황에 의존적이었습니다. 화면에 표시될 UI를 디자이너분과 상의한 대로 간단한 배치정도만 구성해도 디자인이 바뀌면 전체적으로 다시 수정해야하는 문제가 발생했습니다.

프로젝트를 진행하면서 기획, FE, BE, 디자인 개발이 동시에 진행되다보니 서비스의 엔드포인트를 담당하는 FE는 의존적인 작업이 많아지고, 그에 따른 개발 진행이 더디게 되었습니다. 그렇다고 마냥 기다릴 수 없는 상황에서 샘플 데이터와 간단한 배치만 가지고 FE개발을 진행했습니다. API가 완성되거나 디자인이 완성되면 FE코드를 수정해야하는 일이 빈번했고, 같은 로직에 대한 작업을 2,3번씩 하는 경우가 많았습니다. 또한, API가 완성되고 나서야 API에 설계에 대한 오류를 찾을 수 있어 피드백 작업이 늦어졌습니다.

FE에서 Mocking해서 병목 현상을 해결하자!

어떻게 하면 다른 팀 작업 현황에 종속적이지 않고, 생산성 있는 개발을 할 수 있을까 고민하던 도중 kakao-tech: Mocking으로 생산성까지 챙기는 FE 개발 이라는 글을 발견했습니다. 해당 글에서는 저희가 했던 고민과 비슷한 고민이 담겨 있었고, 그에 대한 해결방안으로 Mocking을 통한 사전 개발을 제시했습니다.

BE개발의 의존성을 줄이기 위해서는 BE개발자와 함께 정한 API의 명세를 토대로 FE에서 실제 API를 mocking할 필요가 있었습니다. (지금와서 생각해보면 이러기 위해 미리 API명세를 같이 정한 건가 싶습니다😅 ) 정해진 API명세로 FE개발을 진행한 뒤, BE작업이 완료되는 시점에 API 주소만 바꿔서 테스트하면 효율적인 개발이 가능해질 것이라고 기대했습니다.

2. 여러 가지 대안 중 MSW를 선택한 이유

Mock API란 실제 api가 완성되기 전까지 프론트에서 사용할 가짜 api를 의미합니다. mocking api tool에는 json-server, postman-mock-server, msw, express 등등 다양한 방법이 있습니다.

네트워크 수준에서 요청을 가로채서 모의합니다. 테스트, 개발 및 디버깅을 위해 동일한 모의 정의를 원활하게 재사용합니다. - msw.io

프론트 인원의 이전 mocking tool 사용경험과 각각의 장단점을 고려했을 때 저희 프론트팀은 msw를 사용하기로 결정했습니다. 프로젝트에서 필요한 Mocking 수준은 실제 네트워크 상호작용과 비슷한 수준이어야 하기 때문에 cookie, header setting, delay, query parameter, path variable, custom response가 가능한 msw를 선택했습니다.

tool pros cons
json-server js환경에서 쉽게 적용가능하다.
URI 커스텀이 용이하다.
json파일 단위로 관리할 수 있다.
진입 장벽이 낮다.
진입 장벽이 낮다.
Request에 대한 mocking이 제약이 많다.
postman-mock-server API명세를 그대로 활용 가능하다.
GUI로 설치 없이 간단하게 사용가능
한 달 제한: 1000회
커스텀한 URI 설정이 어렵다. (path variable을 특정 id로 지정해야한다.)
express 실제 API와 거의 유사한 완성도으로 실행가능하다.
rest api server 그 자체를 만들 수 있다.
학습 비용이 크다
실제 API와 거의 유사한 만큼 구축시간이 오래걸린다.
msw 실제 API와 거의 유사한 완성도로 실행가능하다.
FE폴더 내에서 쉽게 구축가능하다.
배포환경에서 테스트 API를 사용할 수 없다.
별도의 DB없이 런타임의 메모리 데이터를 사용한다.

3. MSW가 동작하는 방식

  • serivce worker란?
  • msw 동작 방식

프로젝트에서 MSW를 사용한 방법을 설명하기에 앞서 MSW에 대한 설명과 간단한 동작 방법에 대한 설명을 하고자 합니다.

앞서 언급했듯이 MSW는 공식문서에서 “네트워크 수준에서 요청을 가로채서 모의합니다. 테스트, 개발 및 디버깅을 위해 동일한 모의 정의를 원활하게 재사용합니다.” 라고 소개하고 있습니다. MSW는 mock서버를 별도로 구축할 필요 없이 네트워크 수준의 API mocking을 가능하게 합니다. MSW가 이러한 기능을 제공할 수 있는 이유는 Service Worker를 통해 HTTP 요청을 가로채기 때문입니다.

Serivce worker란?

서비스 워커는 웹 응용 프로그램, 브라우저, 그리고 (사용 가능한 경우) 네트워크 사이의 프록시 서버 역할을 합니다. - mdn : service worker API

Service Worker는 웹 애플리케이션의 메인 스레드와 분리된 별도의 백그라운드 스레드에서 실행시킬 수 있는 기술 중 하나입니다 - kakao-tech: Mocking으로 생산성까지 챙기는 FE 개발

위의 내용을 종합해 보면 serive worker란 web serber와 web browser 사이의 프록시처럼 동작하는 browser가 웹페이지와는 별도로 백그라운드에서 실행하는 스크립트라고 할 수 있습니다.

service worker를 통해 백그라운드 동기화, 높은 비용의 계산을 대신 처리, 푸시 이벤트 생성 등의 다양한 기능을 구현할 수 있습니다. MSW는 serivce woker의 기능 중 네트워크의 요청을 가로채는 행위를 활용합니다.

MSW 동작 원리 및 활용 방법

MSW 동작원리

브라우저에서 MSW는 아래 그림처럼 동작합니다.

msw

  1. 설치 후 request를 보냅니다.
  2. serivice worker에서 request를 복사합니다.
  3. 해당 요청과 일치하는 mocks 응답을 생성합니다.
  4. 생성한 mock응답을 제공합니다.
  5. 제공된 mock 응답을 response로 반환합니다.

MSW 활용 방법

프로젝트를 진행하면서 다음과 같은 단계를 준수하고자 노력했습니다.

  1. 요구사항분석
  2. 요구사항에 맞는 API 설계
  3. API 명세 작성 (swagger, notion)
  4. MSW mocking API를 활용한 FE개발(UI최소화)
  5. 실제 API개발이 완료되면 직접 연결하면서 테스트
  6. 피드백 및 수정

4. 간단한 사용 방법 설명

설치방법

자세한 설치 방법은 MSW.io - install (공식문서)를 참고해주세요. 공식문서에 설치 방법, 디렉토리 구조, rest API vs Graphql API 선택, node vs web browser 선택 등 단계별로 자세하게 소개하고 있습니다.

핵심 tool 소개

저희 프로젝트에서 사용한 핵심툴은 다음과 같습니다.

delay() : 의도적인 지연 요청

// 유저가 읽은 항목 조회
rest.get(ROOT_API_URL + API.USER.READS, (req, res, ctx) => {
  return res(
    ctx.status(200), // 응답 상태 결정
    ctx.delay(300), // 응답 시간 결정
    ctx.json(getResonseWithData(userList)), // 응답 결과 생성
  );
}),

cookie(): 쿠키 설정 & set(): 헤더 설정

// src/mocks/authHandler.mock/index.js

rest.post('/login', (req, res, ctx) => {
   return res(
    ctx.status(200),
    ctx.cookie({ // 응답 쿠키 설정
    {
      [TOKEN.ACCESS]:'something',
      [TOKEN.REFRESH]: 'something',
    },
    ctx.set({ // 응답 헤더 설정
      [TOKEN.ACCESS]:'something',
      [TOKEN.REFRESH]: 'something',
    }),
    ctx.json(getResonseWithData(mockLoginData)), // 응답 결과 생성
	),
}),

src/mocks/mockUtils.js

mock api를 위한 유틸함수를 만들어서 반복되는 코드를 줄였습니다.

  • randomResponse(res, ctx, successData): 에러 상황을 위해 램덤으로 상태코드를 반환하는 유틸 함수
  • getResonseWithData(): response 형태를 제공하는 유틸 함수

5. 우리 프로젝트에 적용한 방법 (CRA 기준)

실행방법

공식문서 대로 설치했다면 개발환경에서 MSW를 실행해야합니다. 저희 팀은 아래와 같은 설정을 통해 실행했습니다. mocking API를 쓰는 개발환경은 npm run msw 명령어로, 실제 API를 쓰는 개발환경은 npm start로 구분해서 사용했습니다.

cross-env 라이브러리는 OS에 따른 환경변수 문제를 해결하기 위해 사용했습니다.

package.json

"scripts": {
  "start": "react-scripts start",
  "msw": "cross-env REACT_APP_MOCK_TOOL=msw react-scripts start",
},

src/index.jsx (entry)

import React from 'react';
import ReactDOM from 'react-dom';
import worker from 'mocks/browser';
import Styles from 'styles';
import App from './App';

if (process.env.REACT_APP_MOCK_TOOL === 'msw') {
  worker.start();
}

ReactDOM.render(
  <React.StrictMode>
    <Styles>
      <App />
    </Styles>
  </React.StrictMode>,
  document.getElementById('root'),
);

공식문서에 따라 올바르게 설정한 뒤 설정한 프로젝트 환경에 따라 실행하면 개발자 도구에서 다음과 같은 메세지를 확인할 수 있습니다.

msw-ex

mocks 폴더 구조

. # src/mocks
├── handlers.js
├── browser.js
├── mockUtils.js # mock api용 유틸함수
├── authHandler.mock
├── commentHandler.mock
├── etcHandler.mock
├── teamHandler.mock
└── userHandler.mock # 폴더 하위 파일 구성 예시
    ├── index.js # response 구조 모음
    ├── essentialInfo.js # mock data
    ├── myPosts.js # mock data
    ├── userDetail.js # mock data
    └── usersList.js # mock data

도메인에 따른 폴더를 만들고 폴더 하위에 mock 데이터 및 response를 구성했습니다. API 주소의 경우 src/constant/api.constant.js 파일에서 상수화한 뒤 api 폴더 및 mock 폴더에서 사용하는 주소를 일치시켰습니다. userHandler.mock의 예시 코드는 아래와 같습니다.

import { rest } from 'msw';
import {
  getResonseWithData,
  getRandomStatusErrorCode,
  errorResponse,
  randomResponse,
  successResponseWithEmptyData,
} from 'mocks/mockUtils';
import { API, ROOT_API_URL } from 'constant/api.constant';
import { userList } from './usersList';
import { myPosts } from './myPosts';
import { userDetail } from './userDetail';
import { mockEssentialInfo } from './essentialInfo';

const userHandler = [
  // 유저 필수 정보 조회
  rest.get(ROOT_API_URL + API.USER.ESSENTIAL_INFO, (req, res, ctx) => {
    return res(ctx.status(200), ctx.json(getResonseWithData(mockEssentialInfo)));
  }),
  // 유저 필수 정보 생성
  rest.post(ROOT_API_URL + API.USER.ESSENTIAL_INFO, (req, res, ctx) => {
    return res(ctx.status(200), ctx.json(successResponseWithEmptyData));
  }),
  // 유저 목록 조회
  rest.get(ROOT_API_URL + API.USER.INDEX, (req, res, ctx) => {
    const lastPage = req.url.searchParams.get('lastPage');
    const newUserList = userList.map((user) => ({ ...user, id: Number(user.id + lastPage) }));
    if (Number(lastPage) === 3) {
      return res(ctx.status(500), ctx.json(getResonseWithData(newUserList)));
    }
    // return randomResponse(res, ctx, userList);
    return res(ctx.status(200), ctx.json(getResonseWithData(userList)));
  }),
  // ...생략
];

export default userHandler;

storybook과 연동

스토리북 컴포넌트를 만들다보면 순수한 view를 분리하기 어려울 때가 있었습니다. 혹은 API 요청에 따른 화면 상태(loading, error …) 변화를 체크하기 위해 api와 연동해야할 때가 있었습니다. 이 때, 실제 API가 아닌 만들어둔 MSW mocking api와 mocking data를 활용해 개발과정을 단축할 수 있었습니다.

msw storybook addon 공식문서에 따라 설치 및 세팅 이후 mock api를 사용하고자 하는 story 컴포넌트에서 다음과 같이 parameter에 Handlers전달했습니다.

import React from 'react';
import handlers from 'mocks/handlers';
import { userList } from 'mocks/userHandler.mock/usersList';
// 생략

export default {
  title: 'pages/UserBoard',
  component: UserBoard,
  parameters: {
    layout: 'fullscreen',
    msw: handlers,
  },
};

function Template(args) {
  return <UserBoard {...args} />;
}

function EmptyTemplate(args) {
  return (
    <CardsGrid
      {...args}
      CardComponent={UserCard}
      clickLink={`${ROUTE.USER}/`}
      emptyTrigger={emptyTrigger.user}
    />
  );
}

// 로딩 -> 성공 / 에러 흐름을 한 번에 보여주는 스토리
export const Default = Template.bind({});
Default.args = {};

// 요청 성공 이후 UI
export const Empty = EmptyTemplate.bind({});
Empty.args = {
  cards: [],
  isLoading: false,
};

// 로딩 상태 스토리
export const LoadingWithSkeleton = EmptyTemplate.bind({});
LoadingWithSkeleton.args = {
  cards: userList,
  isLoading: true,
};

6. 사용 후기 및 개선점

MSW를 도입함으로써 더 이상 백엔드 API가 완성될 때까지 마냥 기다리는 상황은 발생하지 않았습니다. 서비스의 엔드포인트라는 특성 때문에 BE 작업과 디자인 작업 현황과 완전히 독립적일 수는 없지만 개발 과정에서 기다리는 시간을 줄일 수 있었습니다. (동기적인 작업이 아닌 비동기적인 작업이 가능해졌다고 할 수 있을까요? 😀 )

더불어 API 명세를 이전보다 효율성있게 활용할 수 있었습니다. 이전에는 단순히 API에 따른 응답 값만 데이터로 만들고 실제 API완성을 기다렸다면, 이제 mocking api를 통해 실제 네트워크 환경에과 유사한 수준에서 개발을 진행할 수 있었습니다.

앞으로 프로젝트를 개선하면서 jest 및 react-test-library와 msw를 함께 사용하며 더 나은 유지보수를 위해 개선할 예정입니다 🫠

참고자료

https://tech.kakao.com/2021/09/29/mocking-fe/

https://blog.mathpresso.com/msw로-api-모킹하기-2d8a803c3d5c

https://mswjs.io

https://developer.mozilla.org/ko/docs/Web/API/Service_Worker_API

https://b.limminho.com/archives/1384

Clone this wiki locally