프론트엔드 환경에서 API 동작을 모킹하는 것은 여러 가지 이유로 필요해진다. 프론트엔드의 동작은 필연적으로 백엔드 API와 강한 의존성을 가지게 되기 때문이다. 그런데 우리는 생산성을 높이기 위해 백엔드와 독립적인 환경에서 개발을 하고 싶다.
- 만약 서버 요청이 들어가는 페이지의 e2e 테스트를 짜고 싶다면?
- 백엔드 개발이 완료되기 전에 프론트엔드 개발에 착수한다면?
- 개발 서버의 특정 API에서 에러가 나고 있어 프론트엔드 개발이 블로킹된 상황이라면?
위와 같은 케이스는 API단을 모킹하는 수고를 들일 만한 충분한 이유가 될 것이다. 특히 두 번째 이유는 꽤나 자주 일어나는 일인데, 보통 프로젝트를 시작할 때 프론트엔드와 백엔드가 동시에 개발에 착수하곤 하기 때문이다. 서비스 런칭 데드라인은 가까워질 터인데, 중요한 비즈니스 로직이 백엔드를 통해야 한다면 백엔드 개발이 전부 끝날 때까지 기다리다가 급하게 연동을 해서 릴리즈를 하는 것은 꽤나 비효율적이고 위험한 일이다.
따라서 일반적으로 프론트엔드 개발자들은 백엔드와 API라는 인터페이스를 어떻게 정할지 논의하고 스펙을 맞춘 뒤 개발에 들어가게 된다. 이때부터는 임의의 데이터를 만들어서 UI/UX 개발을 할 수 있게 된다.
어떻게 모킹을 해야 할까?
모킹을 할 때 첫 번째로 떠오르는 방법은 어플리케이션 코드에 직접 mock 데이터를 넣는 것이다. 예를 들면 다음과 같이 API 통신을 하는 함수에서 직접 mock 데이터를 리턴하도록 설정할 수 있다.
import { SOME_USERS_MOCK } from './someData.mock'
export const fetchUsers = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(SOME_USERS_MOCK)
})
})
}
혹은 화면단에서 직접 모킹 데이터를 넣어주는 코드를 작성할 수도 있겠다.
return (
<>
{SOME_USERS_MOCK.map((user) => {
return <div>{user.username}</div>
})}
</>
);
그러나 이와 같은 방법은 네트워크 통신 단계에서 발생하는 여러 가지 예외 케이스들을 핸들링할 수 없다는 문제가 있다. 예를 들면 로딩 상태일 때 어떤 처리를 할지, 에러 케이스에서는 어떤 예외처리를 할지 등이 있겠다. 실제 API가 완성되어 연동을 하려고 했을 때, 수정해야 할 코드가 많다는 것을 의미하기도 한다. 또한 목 데이터를 주입한 코드베이스를 커밋에 포함했다가 잘못해서 운영환경에 나가기라도 하면…
따라서 모킹 데이터를 어플리케이션단에 직접 두는 것은 그다지 바람직한 설계는 아닌 것으로 보인다. 그렇다면 어떤 대안이 있을까?
MSW란?
MSW는 Mock Service Worker의 약자로, 서비스워커를 통해 API를 쉽게 모킹할 수 있게 해주는 라이브러리다. (이제는 웹 표준이 된 서비스워커에 대한 설명은 여기를 참조하자) 서비스 워커를 통해 네트워크 요청 이벤트를 가로채서 쉽게 mock response를 내려줄 수 있도록 해준다. 마치 모킹 서버 하나를 구축하는 것과 비슷하다고 보면 된다. 이는 실제 어플리케이션단 코드에 목 데이터를 넣지 않아도 모킹이 가능하게끔 해준다. 바로 실제 사용 방법을 알아보자.
설치
msw 패키지를 원하는 프로젝트에 dev dependency로 설치해준다.
npm install msw --save-dev
# or
yarn add msw --dev
모킹 코드 작성
모킹을 위한 코드들을 모아놓기 위해 mocks 디렉토리를 만들어준다.
mkdir src/mocks
디렉토리가 만들어지면, 요청에 대한 핸들러를 모아두는 handlers.js 파일을 만들어준다.
touch src/mocks/handlers.js
이제 본격적으로 요청(Request)에 대한 handler를 작성해보자. RestAPI의 경우 메서드와 경로, 그리고 모킹 데이터를 리턴해주는 resolver 함수를 다음과 같이 작성할 수 있다.
// src/mocks/handlers.js
import { rest } from 'msw'
export const handlers = [
rest.post('/login', (req, res, ctx) => {
// 유저의 auth 상태를 세션 스토리지에 넣어줄 수 있다.
sessionStorage.setItem('is-authenticated', 'true')
return res(
// 200 상태 코드를 내려준다.
ctx.status(200),
)
}),
rest.get('/user', (req, res, ctx) => {
// 해당 세션에서 유저가 인증받은 상태인지 체크한다.
const isAuthenticated = sessionStorage.getItem('is-authenticated')
if (!isAuthenticated) {
// 만약 인증받지 못한 유저라면 403 에러를 뱉는다.
return res(
ctx.status(403),
ctx.json({
errorMessage: 'Not authorized',
}),
)
}
// 인증된 상태라면 모킹된 유저 데이터를 내려준다.
return res(
ctx.status(200),
ctx.json({
username: 'admin',
}),
)
}),
]
handlers에 들어갈 요청이 많아질 경우, 도메인 별로 별도의 파일로 분리하는 것도 좋은 방법이겠다.
브라우저 환경에 통합하기
우리가 직접 서비스 워커를 실행시키는 코드를 짤 필요는 없다. 대신 라이브러리에서 배포한 worker file을 그대로 사용하면 된다. 이를 위해 msw은 친절하게도 CLI를 제공한다.
프로젝트의 public 디렉토리를 대상으로 다음과 같은 명령어를 실행해준다.
npx msw init <PUBLIC_DIR> --save
참고로 CRA, NextJS, VueJS의 경우 public 디렉토리는 모두 ./public 이다.
실행하면 mockServiceWorker.js라는 파일이 생성되었을 것이다.
서비스워커가 돌아가게 해 주는 이 파일은 내가 굳이 건들 필요가 없다. 물론 프로덕션 환경에 배포하지 않아도 된다. (개발 환경에서만 사용할 것이기 때문에)
touch src/mocks/browser.js
여기서 우리는 worker 인스턴스를 생성하고 이전에 이미 정의해놓은 handlers를 등록해줄 것이다.
// 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)
이제 실제로 worker를 시작해주기만 하면 된다. 여기서 주의할 점은, 우리가 mock service worker를 사용하는 환경은 오직 로컬 개발환경이어야 한다는 것이다. 추가적인 테스트 서버에 배포를 해볼 수도 있겠지만, production에서는 절대로 시작되어선 안 된다. 따라서 다음과 같은 분기 처리를 포함해서 worker를 시작하는 코드를 작성해볼 수 있다.
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
if (process.env.NODE_ENV === 'development') {
const { worker } = require('./mocks/browser')
worker.start()
}
ReactDOM.render(<App />, document.getElementById('root'))
이제 개발 서버를 실행해보면 다음과 같은 로그를 콘솔창에서 확인할 수 있을 것이다.
[MSW] Mocking enabled
이때부터 프론트엔드에서 보내는 요청 중 handlers.js에 정의되어 있는 요청은 서비스워커가 가로챈 뒤 mock 응답을 내려주게 된다.
프로젝트에 적용해보고 나니
좋았던 점
우선 네트워크 통신단을 모킹하는 것이다 보니, 어플리케이션단 코드에 영향이 없다는 것이 가장 좋았다. 목 데이터가 실제 프로덕션 환경에 들어갈 위험이 없었고, 모킹을 걷어내고 실제 API를 연동할 때 바꿔야 할 코드가 거의 없다는 점이 굉장히 편리하게 느껴졌다. 확실히 생산성이 좋아졌고, API가 안 나온다고 하더라도 스펙이 정해졌다면 여전히 프론트엔드 개발을 할 수 있었다.
꽤 편리했던 점 중 또 하나는 각종 에러 상황도 직접 목 리퀘스트를 작성해서 테스트해볼 수 있었던 것이었다. 에러 케이스의 화면을 개발할 때 실제 서버 환경에서 테스트하기란 여간 까다로운 것이 아닌데, 직접 목 리퀘스트를 작성하게 될 경우 401, 500 등 여러 가지 경우의 에러를 직접 만들어 테스트할 수 있어서 편리했다.
아쉬웠던 점
리퀘스트를 구체적으로 작성하다 보니, 내가 서버 개발자인 것도 아닌데 어디까지 세세하게 작성해야 하나...에 대한 의문이 들었다. payload가 a와 같이 들어오면 b 라는 response를 주고, auth token이 들어있지 않다면 401을 주는 등 욕심이 나서 백엔드에서 돌아가야 할 로직까지 직접 작성하고 있었던 것이다. 모킹의 핵심은 결국 개발의 편리함을 위한 것이므로, 뭐든 적당한 수준에서 끝내는 게 좋은 것 같다.
Reference
Introduction
What is Mock Service Worker?
mswjs.io
'Tools' 카테고리의 다른 글
모바일 브라우저 디버깅하기 (0) | 2024.01.07 |
---|---|
커멘드 라인으로 Webstorm 실행하기 (0) | 2023.07.14 |