본문 바로가기

TECH

Next.js 환경에 MSW 도입해보기

오랜만에 블로그 글을 쓰게 되었습니다 ✍️

자주 쓰겠다고 마음 먹었는데 좀처럼 쉽지 않네요 ..!

 

최근(이라기엔 시간이 지났지만)에 Next.js 환경에서 MSW를 도입해보게 되었습니다.

SSR과 CSR을 함께 쓰고 있었기 때문에 각 환경에서 어떻게 API Mocking을 해야할지 많이 헤맸는대요...😅

어떻게 세팅해서 사용하고 있는지 작성해보겠습니다.

 

개발 환경은 Page router 기반의 Next.js, MSW, Typescript입니다.

(App router 기반은 다른 라이브러리와 합이 안맞을 수 있어서 세팅이 다를 수 있습니다.. 🥹 )

 

1. MSW란?

(as-is) api를 요청 시, server로 요청

(to-be) api 요청 시, service worker가 그 요청을 가로챔

 

그래서 프론트엔드쪽에서 해당 요청에 대한 응답을 자유자재로 해줄 수 있어서 다양한 화면 테스트가 가능하게 됩니다.

 

2. MSW를 사용하면 좋은 시점 !

1. 로컬 환경에서 백엔드 Api 테스트가 불가능하다. ➡️ 백엔드 Api 개발 완료를 기다리지 않아도 된다.

2. 다양한 응답 status code(200, 400)를 테스트 해볼 수 있다.

3. 기존 코드 단에서의 Api endpoint 변경이 필요 없음

pages/api 를 이용해서 만드는 경우에는 추후 endpoint 변경이 필요할 수 있습니다.

유저 정보를 fetch해서 가져온다고 했을 때, 백엔드와 합의된 api endpoint는 https://hello.world.com/users 라고 할 때 pages/api를 이용하는 경우는 endpoint를 다음과 같이 변경 해주어야 합니다.

(as-is) axios.get('/api/users');
(to-be) axios.get('https://hello.world.com/users');

MSW를 사용하면 endpoint를 변경해주지 않고도 service worker가 요청을 가로채주기 때문에 서비스 단에서 endpoint 변경이 필요 없습니다.

4. build 할 때 MSW 관련 코드들이 제외 된다.

많은 mock데이터들이 포함되는데 build 시에 포함하게 되면 빌드 타임이 길어지기 때문에 안좋겠죠..? 🥹
pages/api 들은 해당 mock데이터까지 빌드타임에 포함되는 것과 달리, MSW를 이용하면 관련 코드들이 빌드 파일에 포함되지 않습니다.

 

 

실제 응답을 컨트롤 할 수 있으므로 response 에 delay를 주어 Fe 단에서 응답이 느리게 오는 환경에서 스켈레톤을 보여준다거나, 로딩 화면을 보여준다거나 그런 환경들을 보다 유용하게 확인할 수 있습니다.

 

3. MSW를 사용할 때의 단점

사실 MSW 자체의 단점은 아니고, 당연하지만 귀찮은 점.. 😅 으로 느껴지는 내용인대요 !

 

1. Mock 데이터를 만들어야 함

Mock 데이터를 만드는게 꽤나 (x2000) 귀찮은 일이기 때문에, 어떻게 이 귀찮음을 잘 해소할 수 있었어야 했습니다.
user 정보에 대한 mock데이터를 만든다고 할 때, 200개가 필요하다고 해보자구요....

 

interface UserType={
	name:string
    age:number
}

export const MockUserData:UserType[]=[
{
	name: '홍길동',
    age: 30,
},
.... 
]

 

이걸 200개나 만들어야 한다니... 너무 끔찍합니당... (여기서 UserType 필드가 더 늘어난다면..? 더 많아지겠죠 ㅠㅠ)

조금 더 편리하게 Mock데이터를 만들어 보겠습니다

interface UserType={
	name:string
    age:number
}

const USER_COUNT=200

export const MockUserData:UserType[]= Array(USER_COUNT).fill(undefined).map((_,index)=> {
	return {
    	name: index+1,
        age: getRandomInt(8, 30)
    }
})

 

코드가 조금 더 간단해지고, 많은 데이터를 만들기는 쉬워졌지만 유의미한 데이터를 만들기 어려울 것 같습니다 .. ㅠㅠ

그래서 faker.js (https://fakerjs.dev/) 를 이용해주었습니다.

interface UserType={
	name:string
    age:number
}

const USER_COUNT=200

export const MockUserData:UserType[]= Array(USER_COUNT).fill(undefined).map((_,index)=> {
	return {
    	name: faker.person.fullName,
        age: faker.number.int({ min:8, max:30})
    }
})

 

다음과 같이 사용해주면 보다 더 의미 있는 데이터를 만들어주기 쉽고, chatGPT를 이용하면... 보다 더 빨리 만들어줍니다 🫡

 

2. 백엔드와의 명세에 대한 합의가 이뤄져야 함 

단점이 아니고 당연한 내용이지만, response에 대한 합의가 정해지지 않으면 일을 2번 해야하기 때문에 작성해두었습니다 ✍️ 

 

 

3. MSW 적용하기

설명이 너무 길어졌으니까 MSW를 적용해보겠습니다..!

( MSW는 최신 버전을 이용해서 이전 버전과는 세팅이 다를 수 있습니다. )

 

1) Handler 만들어주기

ref: https://mswjs.io/docs/concepts/request-handler/

 

앞에 만들어준 Mock데이터로 핸들러를 만들어줘야합니다. 

다음과 같이 작성해준다면, default status (200) 로, MOCK_USER_DATA를 json으로 내려주게 됩니다.

import { http, HttpResponse } from 'msw'

import MOCK_USER_DATA from "../data"
 
export const handlers = [
  http.get('https://hello.world.com/users', ({ request }) => {
    return HttpResponse.json({users: MOCK_USER_DATA })
  }),
]

 

다음과 같이 400 status code 로 내려줄 수도 있습니다 🫡

import { http, HttpResponse } from 'msw'

import MOCK_USER_DATA from "../data"
 
export const handlers = [
  http.get('https://hello.world.com/users', () => {
    return HttpResponse.json(null,{
    status: 400
    })
  }),
]

 

 

2) server와 browser 초기화 시켜주는 로직 추가

ref : https://mswjs.io/docs/api/setup-worker/start/

 

- mocks/server.ts

import { handlers } from './handlers';

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

 

- mocks/browser.ts

import { handlers } from './handlers';

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

 

- mocks/index.ts

export const initServer = async () => {
  if (typeof window === 'undefined') {
    const { server } = await import('./server');
    return server.listen();
  }
};

export const initBrowser = async () => {
  if (typeof window !== 'undefined') {
    const { worker } = await import('./browser');
    return worker.start();
  }
};

 

 

default는 https://localhost.com:3000 으로 되지만, 프로젝트마다 https://localhost:3000/hello 로 자동 라우팅 처리 되는 경우가 있다.....😅  

 

 

위와 같은 경우는 next.config.js 파일에 basePath를 설정해준 경우인대요 ! 이때 실행하면 serviceWorker를 못찾는 경우가 있습니다. 그래서, 다음과 같이 설정해주어야 합니다. 

 

ref: https://mswjs.io/docs/api/setup-worker/start/#url

export const initBrowser = async () => {
  if (typeof window !== 'undefined') {
    const { worker } = await import('./browser');
    return worker.start({
    	url: '/hello/mockServiceWorker.js'
    });
  }
};

 

url 에 basePath를 붙여서 mockServiceWoker.js 의 위치를 명시적으로 표시해주어야 합니다 🙏

 

3) mockServiceWorker 등록해주기 (초기화)

npx msw init public/

 

이렇게 해주면 public 위치에 mockServiceWorker.js 파일이 생성됩니다 !

 

그런데, 여러 사람들이 프로젝트의 msw를 사용할 수 있으니까 package.json 파일을 수정해줍니다.

    "dev": "npx msw init public/ --save && next dev"

 

`--save` 문구를 추가해주면 자동으로 public 폴더에 업데이트 해주게 됩니다.

 

4) server, browser 등록해주기

CSR은 browser (https://mswjs.io/docs/integrations/browser)로, SSR은 node.js (https://mswjs.io/docs/integrations/node) 로 등록을 해주어야 합니다.

 

const IS_MSW_ENABLE = process.env.NODE_ENV === 'development' && env.IS_ENABLE_MOCKING === 'true';
msw 구동 상태인지 확인을 해주어야 하는데,
env를 통해 mocking을 설정한 상태인지 확인해주고, process.env.NODE === 'development' 인지 확인을 해줍니다.
(process.env.NODE 를 development 인지 확인해주는 이유는 Next.js 가 development 에 사용되는 코드인지 , prod 에 사용하는 코드인지에 따라 build 파일에 코드 포함 유무가 결정되기 때문입니다)

env.IS_ENABLE_MOCKING 을 통해, 로컬 개발 환경에서도 msw 사용인지, 아닌지 체킹해주었습니다 ! 

 

- pages/_app.tsx

const IS_MSW_ENABLE = process.env.NODE_ENV === 'development' && env.IS_ENABLE_MOCKING === 'true';

if (IS_MSW_ENABLE) {
  const { initServer } = require('../mocks');
  initServer();
}

export default function App({ Component, pageProps }: AppProps) {

  useEffect(() => {
    if (IS_MSW_ENABLE) {
      const { initBrowser } = require('../mocks');
      initBrowser()
    }
  }, []);

  return <Component {...pageProps} />;
}

useEffect 구문 CSR일 때만 사용되기 때문에 SSR 환경을 커버해주기 위해 App 컴포넌트 밖에서 선언해주게 됩니다 !

 

이렇게 사용했을 때, 문제가 발생하는데 MSW의 준비가 되지 않은 상태에서 Api 요청을 보내게 되면서 무조건 404 에러가 내려오는 경우가 발생한다. 그래서 이 블로그(https://jaypedia.tistory.com/382)를 참고해서 다음과 같이 수정해주었다.

 

const IS_MSW_ENABLE = process.env.NODE_ENV === 'development' && env.IS_ENABLE_MOCKING === 'true';

if (IS_MSW_ENABLE) {
  const { initServer } = require('../mocks');
  initServer();
}

export default function App({ Component, pageProps }: AppProps) {
  const [isMSWLoading, setMSWLoading] = useState(IS_MSW_ENABLE);

  useEffect(() => {
    if (IS_MSW_ENABLE) {
      const { initBrowser } = require('../mocks');
      initBrowser().then(() => {
        setMSWLoading(false);
      });
    }
  }, []);

  if (isMSWLoading) return <div>msw 로딩중</div>;
  return <Component {...pageProps} />;
}

 

 

이렇게 하면 기본적은 MSW 설정은 끝나게 되고, 추가적인 핸들러들만 추가해주면 됩니다.

기초 설정이 조금 길었습니다...! 

공식문서와 여러가지 블로그들 찾아보며 해온 세팅인데 틀린 정보가 있다면 알려주세요 ✨