나는 다른 React 프로젝트에서 MSW를 도입해 유용하게 활용한 적이 있다. 백엔드 팀원들과 함께 API 스펙에 대해서 논의가 끝났는데 아직 구현이 덜 되어 API를 프론트 측에서 사용할 수 없는 상황이라면, 마냥 기다리기엔 시간이 아깝다. 바로 이 때 API를 Mocking한다면, 프론트에서도 기능/UI 구현 작업을 이어나갈 수 있다. 추후 API가 개발된다면, 자연스럽게 실제 API로 대체하면 된다.
또현 현재 나의 프로젝트에는 OAuth 로그인을 도입한 상태다. 로그인 후에는 서버로부터 쿠키를 통해 access token이 발급되는데, 추후 API 요청을 할 때 이 access token을 함께 Request Header에 실어서 보내야 한다.
그런데 개발 환경인 localhost:3000 에서는 실제 도메인과 다르기 때문에, access token을 받을 수가 없다. 그렇기 때문에 추후 API 요청에서도 access token을 실어서 보낼 수가 없다.
그렇다면 이러한 API 요청이 포함된 컴포넌트를 개발할 때마다, Next.js 앱을 실제 도메인에 배포해서 구현이 잘 되었는지 확인을 해야 한다는 것인데, 이는 다음과 같은 문제점이 있다.
이 방식은 번거롭기도 하고, 결과물을 확인하기까지 시간이 소요된다. 커밋을 하고, 푸시를 하고, 빌드를 기다려야 하는데 최소 2분에서 길면 3~4분까지 소요가 된다.
막상 결과물을 확인했더니 로컬에서 확인하지 못했던 에러가 발생하는 경우가 있다. 이럴 경우에는 커밋을 reset해서 다시 과거 상태로 돌리고 작업을 하게 되는데 이는 굉장히 비효율적이다.
이러한 불편함과 비효율을 없애 로컬 작업 환경을 쾌적하게 만들기 위해 MSW를 도입하고 싶었다.
그런데 Next.js 13 버전, 그리고 app router를 쓰는 환경에서는 MSW를 도입하는 것이 약간 난관이었는데, 내가 마주한 문제점과 해결책을 정리해 보았다.
이 문제를 해결하는 데 일등 공인은 코드스쿼드 부트캠프의 같은 기수 수료생인 콜라이다. 콜라는 이미 다른 프로젝트에서 이 문제를 해결해 본 경험이 있었고, 나에게 아주 자세하고 친절하게 설명해 주셔서 정말 감사했다.(감사의 의미로 기프티콘을 보내드렸다☕️) 콜라와 이야기를 나누는 과정에서 배운 것이 많아 휘발되지 않도록 그 과정을 정리하고 기록해 보았다.
2. MSW의 동작 방식
본격적으로 MSW를 설정하기 전, MSW가 무엇이고 어떤 방식으로 동작하는지 간략하게만 짚고 넘어가보자.
MSW는 무엇이고, 하는 주된 일은 무엇인가?
MSW는 Mock Service Worker. Mock은 ‘모의’, ‘모조’, ‘가짜’ 라는 뜻이다.
이름 안에 Service Worker가 있는 것처럼, Web에서 제공하는 Service Worker API를 사용해서 HTTP 요청을 가로챈다.
Service Worker: 웹 애플리케이션의 메인 스레드와 분리된 별도의 백그라운드 스레드에서 실행시킬 수 있는 기술. 이를 통해 애플리케이션의 UI Block 없이 연산 처리를 할 수 있다. Service Worker를 통해 네트워크 요청이나 응답을 가로채 조작할 수 있게 된다.
MSW는 실제 API 요청이 발생하면, 이 요청을 가로채서 미리 준비해 뒀던 Mock data로 응답해준다.
MSW를 사용하면 Mock Server를 따로 구축하지 않아도 API를 Mocking할 수 있어서 효율성이 증대된다.
Mock Service Worker의 request 흐름
아래 도식은 Mock Service Worker의 request가 어떤 식으로 흘러가는지 큰 그림으로 표현한 것이다. 공식문서를 참조했다.
브라우저에서 요청을 보낸다.
Service Worker는 요청을 받고, 해당 요청을 복제한다. 복제한 요청을 MSW에게 보낸다.
MSW는 요청과 일치하는 목업을 생성한다.
MSW가 모킹한 응답을 Service Worker에게 보낸다.
Service Worker가 모킹된 응답을 브라우저에게 보내고, 브라우저가 최종적으로 이 응답을 받는다.
3. MSW 세팅 과정
MSW가 어떤 흐름으로 Mocking을 해 주는 것인지 파악했으니, 이제 본격적으로 프로젝트에 세팅을 할 차례다.
/api/checklist/today로 API를 요청하게 되면, { isTodayChecklistCreated: true } 라는 json을 반환하도록 getTodayChecklist 함수를 구현했다.
3. worker를 설정해준다.
// src/mocks/browser.js
import { setupWorker } from 'msw'
import { handlers } from './handlers'
// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers)
4. worker를 시작한다.
// src/app/layout.tsx
if (process.env.NODE_ENV === 'development') {
worker.start()
}
공식문서를 따라, 그리고 내가 이전 React 프로젝트에서 했던 것처럼 여기까지 하면 기본적인 세팅은 끝이고, 잘 될 줄 알았으나.. Next.js 13 환경에서는 그렇지 않았다. 에러의 시작이었다.
4. 마주친 문제 1: 서버에서 worker를 시작할 수 없음
RootLayout이 프로젝트의 진입점이기 때문에, 여기서 worker를 시작하면 될 거라고 생각했다.
NODE_ENV를 활용해 개발 모드일 때에만 worker가 시작될 수 있도록 했다. 하지만 에러가 발생했다.
“서버에서는 .start에 접근할 수 없다. 서버 컴포넌트에서는 클라이언트 모듈에 점(.)을 찍을 수 없다. import한 이름만 전달할 수 있다”
즉 worker는 클라이언트 모듈이고 RootLayout은 서버 컴포넌트이기 때문에 위와 같은 에러가 발생하고 있는 것이다.
4-1. 원인: 서버 컴포넌트에서는 클라이언트 모듈을 사용할 수 없다
Next.js는 CSR만 하는 게 아니라 SSR도 하기 때문에, 서버에서 실행되는 코드들이 있다.
RootLayout은 서버 컴포넌트이기 때문에, 기본적으로 서버에서 렌더링된다.
서버에는 worker가 존재하지 않기 때문에, worker라는 모듈을 찾을 수 없고, 당연히 worker를 시작할 수도 없다.
4-2. 해결 방법: 서버 환경과 브라우저 환경을 분기 처리
서버 환경인지, 브라우저 환경인지에 따라 분기처리를 해서 MSW를 실행해준다.
// mocks/index.ts
export async function initBrowserMocks() {
if (isBrowser) {
const { worker } = await import('./browser');
await worker.start();
}
}
export async function initServerMocks() {
if (!isBrowser) {
const { server } = await import('./server');
server.listen();
}
}
여기서 각 모듈을 import 할 때, 일반적으로 파일의 상단에 import를 하는 방식(static import)가 아닌, import() syntax를 활용하여 Dynamic import를 하고 있다.
이 문법을 통해 조건부로 모듈을 로드할 수 있게 된다.
브라우저 환경에서는 브라우저와 관련된 모듈만, 서버 환경에서는 서버에 관련된 모듈만 로드한다.
불필요한 리소스를 로드할 필요가 없게 되어, 초기 로딩 시간을 줄일 수 있다.
5. 마주친 문제 2: 컴포넌트가 이미 렌더링 된 후 MSW 초기화
이제 아까와 같은 Server Error가 뜨지 않았다! 모킹도 잘 되는 것 같았다.
그런데 콘솔을 보니 빨갛게 에러가 뜨고 있었다.
보니까 Mocking이 enabled가 되기도 전에 API 요청을 하고 있었다.
Mocking enabled 이후에는 정상적으로 응답이 되고 있었다.
5-1. 원인: 컴포넌트가 렌더링 된 후에 MSW가 초기화된다
컴포넌트가 이미 렌더링이 된 후에 MSW가 초기화되기 때문에 위와 같은 상황이 발생한다.
컴포넌트가 렌더링이 되면서 API 요청이 발생하게 되고, 이때는 MSW가 초기화 되어있지 않기 때문에 API Request를 보낸다 하더라도 이것을 가로챌 수가 없다.
따라서 404 에러가 발생한다.
5-2. 해결 방법: 컴포넌트가 렌더링되기 전에 MSW를 초기화
컴포넌트가 렌더링되기 전에 MSW를 초기화시킬 수 있으려면, RootLayout에서 children 컴포넌트들이 렌더링 되기 전에 MSW를 실행할 수 있으면 된다.
그렇다면 MSW를 초기화하는 로직을 담은 컴포넌트를 만들고, 그 컴포넌트로 children을 감싸도록 하면 된다!
// .env.local
NEXT_PUBLIC_API_MOCKING=enabled
환경 변수에 다음과 같이 설정한다. 개발 모드일 때만 enabled로 설정하고, production 모드일 때는 설정해두지 않는다.
카카오에서 MSW를 왜 도입했는지가 자세히 언급되고 있다. MSW를 사용하지 않고 Mocking을 했을 때 어떤 어려움을 겪었는지, MSW는 그 문제를 어떻게 해결해 주었는지가 나와 있다. 특히 MSW를 사용해서 현업에서 개발 과정이 어떻게 달라졌는지 상세히 풀어 쓰여져 있는 부분이 좋았다.