sehyun.dev
React성능개선

MSW 도입 배경 및 고민

2025년 7월 21일

🛠️ 왜 Mock API/MSW를 도입했는가 – 프론트엔드의 현실에서 출발

🤷‍♂️ "API 언제 나와요?" – 프론트엔드의 현실

프론트엔드 개발자라면 한 번쯤 "API 언제 나와요?"라는 질문을 해봤을 겁니다.

회사든 사이드 프로젝트든, 프론트엔드는 항상 백엔드의 API 개발 일정을 기다리게 되는데 이 상황이 반복되면, 일정에 쫓겨 임시 데이터를 하드코딩해 화면을 먼저 그리고, 막판에 한 번에 API를 붙이면서 예상치 못한 버그와 맞닥뜨리는 게 현실입니다.

이 과정에서 프론트엔드는 항상 을(乙)이다.

**BE: "백엔드는 API 다 만들었는데, 프론트가 연동을 못 했네요."
FE: "스펙이 변경된 것 같은데… 이거 다시 해야 해요?"
PO/PM : “FE님 테스트 언제해요?”
FE: “……”**🤦‍♂️

이런 얘기를 듣게 되는 순간, Boom !!💥

🚦왜 이런 문제가 반복될까? – 현실적인 한계

실제 프로젝트에서 기획 → BE → FE 순으로 매끄럽게 개발이 진행되면 가장 이상적이겠지만, 다들 아시겠지만 현실은 그렇지 않습니다. 특히 신규로 개편된 조직이거나, 선임 개발자가 없는 환경에서는 더욱 그렇습니다.

(1) 신규 조직, 프로세스 미정착

개발 문화와 협업 프로세스가 만들어지지 않아 각자 방식대로 개발하게 되고, API 의존도가 높은 프론트엔드는 백엔드의 일정에 종속될 수밖에 없습니다.

(2) 빠른 결과 우선

커뮤니케이션을 잘 유도 해보려 하나… 우리 지금은 별 수 있나?? "빨리 보여줘야 하니까"라는 이유로 하드코딩, 임시 데이터 남발하게 되어 유지보수성 저하되어 미래의 ‘나’는 고통을 받고 있는 모습을 볼 수 있습니다.

(3) API 스펙 변경

막상 API가 나오면 임시로 만들어둔 코드와 실제 데이터가 달라 두세 번씩 코드를 뜯어고치는 악순환이 반복됩니다.

  • Mock data를 제공하지 못하지 못한 경우의 변천사 History
/*
* 데이터는 이렇게 날라오겠지 ? 두둠칫
*/
const generateFakeAPI = (page: number, size: number): Promise<any> => {
  const startIndex = (page - 1) * size;
  const endIndex = startIndex + size;
  return new Promise(resolve => {
    const data = Array.from({length: size}, (_, index) => ({
      id: (startIndex + index).toString(),
      businessId: `Business ${startIndex + index + 1}`,
      partnerCompanyName: `Partner ${startIndex + index + 1}`,
      customerCompanyName: `Customer ${startIndex + index + 1}`,
      division: `Division ${startIndex + index + 1}`,
      detail: `Detail ${startIndex + index + 1}`,
      useMonth: `Month ${startIndex + index + 1}`,
      business: `Business Name ${startIndex + index + 1}`,
      cspCost: Math.floor(Math.random() * 1000),
      ectCost: Math.floor(Math.random() * 1000),
      credit: Math.floor(Math.random() * 1000),
      total: Math.floor(Math.random() * 1000),
      status: (startIndex + index) % 2 === 0 ? '미청구' : '청구',
    }));
    resolve(data);
  });
};
export const useUnpaidListQuery = ({ currentPage }) => {
  const { data: gridList = {} } = useQuery({
    queryKey: ['unpaid', currentPage.page, currentPage.size],
    queryFn: () => generateFakeAPI(currentPage.page, currentPage.size),
    keepPreviousData: true,
  });
  return { gridList };
};
  • 최종 변경 사항을 살펴 보면 변수들이 많이 바뀐 모습을 확인 할 수 있습니다.
export const useUnpaidListQuery = ({ currentPage }) => {
  const { data: gridList = {} } = useQuery({
    queryKey: ['unpaid', currentPage.page, currentPage.size],
    queryFn: () => getRealAPI(currentPage.page, currentPage.size),
    keepPreviousData: true,
  });
  return { gridList };
};
 
// 정보가 많이 바뀌었네 ..........
[
  {
    "id": "1",
    "purInvoiceHeaderId": "111",
    "purInvoiceDetailId": "222",
    "partnerName": "파트너A",
    "customerName": "고객사A",
    "purCspType": "일반",
    "division": "구매",
    "detail": "상세내역",
    "useMonth": "2024-06",
    "business": "클라우드",
    "cspCost": 150000,
    "ectCost": 20000,
    "credit": 0,
    "total": 170000,
    "status": "미청구"
  },
  // ...생략
]

3. 대안은 MSW다! 그럼 왜 Mock API를 도입했나?

이런 악순환을 끊기 위해, FE 주도 개발 구조를 고민하게 되었고 그 방법 중 하나는 MSW의 도입이고 다음과 같은 이점이 있습니다.

  • BE/FE 개발 속도의 간극 해소
  • 화면/기능의 빠른 프로토타입 구현 → 협업자(기획, 디자인, QA, BE 등)와 즉시 공유
  • 실서비스와 똑같은 사용자 경험 제공
  • Mock API와 Real API를 쉽게 전환해, 실제 API가 나오면 바로 연동

즉, 더 이상 "API 언제 나와요?"가 아니라, "이대로 API 만들어주세요!"라고 백엔드에 명확히 요구하는 구조를 만들기 위함이었습니다.

4. MSW 가이드

(1) MSW 파일 별 역할

  • mockServiceWorker.js → API를 가로채는 역할

  • browser.ts → 실제 worker 서비스에 대한 미들웨어 역할

  • handlers.ts → API Mocking 서비스 담당

    • 초기 설치 시 NPX 명령을 날려 세팅이 필요합니다.
      • 클라이언트 사이드에서는 MSW를 실행하기 위해서 아래와 같은 Promise 로 감싸 React 실행 App의 순서를 보장해야 합니다.
      • 테스트 하고자 하는 Mock API를 늘리기 위해서는 handler.ts 만 늘려주시면 됩니다.
    # service worker.js 생성
    npx msw init public/ --save
     
    # browser.ts 생성
    import {setupWorker} from 'msw';
    import {handlers} from './handlers';
    const worker = setupWorker(...handlers);
    export default worker;
     
    # handler.ts 생성
    import mine from './routes/mine/mine';
    export const handlers = [...mine];
     
    # index.ts
    async function deferRender() {
      if (process.env.REACT_APP_APP_ENV === 'local') {
        const worker = require('./mocks/browser');
        worker.default.start({
          onUnhandledRequest: 'bypass',
          quiet: true,
        });
      }
      return Promise.resolve();
    }
    deferRender().then(() => {
      const root = ReactDOM.createRoot(
        document.getElementById('root') as HTMLElement
      );
      root.render(
        <RecoilRoot>
          <DebugObserver />
          <QueryClientProvider client={queryClient}>
            <ReactQueryDevtools initialIsOpen={false} />
            <App />
          </QueryClientProvider>
        </RecoilRoot>
      );
    });

(2) 실제 Mocking API 모델 및 비지니스 서비스 작성

import {AuthError, AuthResponse} from './model';
/**
 * BE에서 제공 될 Mock API 더미 형식
 * {
    "menus": [
        {
            "url": "/usage/fcc/cost",
            "hasRole": true,
        }
        ...
    ],
    "items": [
        {
            "item": "SLA_MANAGE",
            "hasRole": true,
        },
        ...
    ]
}
 */
export const getAuthRoles = (
  items: string[],
  menuUrls: string[]
): AuthResponse => {
  const itemsRole = items.map(item => ({
    item,
    hasRole: true,
    isMock: true,
  }));
  const menuUrlRole = menuUrls.map(url => ({
    url,
    hasRole: true,
    isMock: true,
  }));
  return {
    menus: menuUrlRole,
    items: itemsRole,
  };
};
export const getAuthError = (): AuthError => ({
  message: '유저의 로그인 권한이 없습니다.',
});
 
 
# handler.ts
import {rest} from 'msw';
import {getAuthError, getAuthRoles} from './service';
export default [
  /**
   * 메뉴 조회 권한 및 기능 권한 Mock API
   */
  rest.post('/api_v1/mock/roles/mine', async (req, res, ctx) => {
    const {items, menuUrls} = await req.json();
    try {
      if ('X-AUTH-TOKEN' in req.cookies) {
        const authResponse = getAuthRoles(items, menuUrls);
        return res(ctx.status(200), ctx.json(authResponse));
      } else {
        const authError = getAuthError();
        return res(ctx.status(400), ctx.json(authError));
      }
    } catch (error) {
      return res(ctx.status(500), ctx.json({message: error}));
    }
  }),
];

5. 로컬 MSW 활성화 통해 로컬 환경과 실제 API 환경 비교

(1) 로컬 환경

image.png

(2) 실제 개발 API 환경

image.png