프론트엔드 개발을 하다보면 네트워크 통신을 하고 싶을 것 입니다.
이러한 네트워크 통신을 하기 위해서 서버에서 제공하는 API 가 있어야 하는데 백엔드 서버를 만들지 못하거나 백엔드 개발자가 API 를 만들어주기를 기다리고 있던 경험 다들 있을 거에요..?
이럴때, 보통 목업 데이터만을 사용해서 먼저 프론트엔드 개발을 하고 서버 API 가 나오고 난 후, 비동기 네트워크 통신을 하셨을 것입니다.
이럴 때, API 스펙이 어느정도 정해져 있으면 빠르게 목업 서버를 만들고 네트워크 통신을 도와 줄 수 있는 라이브러리가 있습니다.
MSW즉, Mock Service Worker
는 말 그대로 가짜 서버 역할을 해서 네트워크 레벨에서 가짜 데이터를 주고 받을 수 있는 라이브러리 입니다.
최신 브라우저 WEB API 중 하나인 service worker
를 이용 하여 네트워크 통신을 해당 msw
가 가로채 가짜 데이터 통신을 시켜주게 됩니다.
출처: MDN (https://developer.mozilla.org/ko/docs/Web/API/Service_Worker_API)
서비스 워커는 연관된 웹 페이지/사이트를 통제하여 탐색과 리소스 요청을 가로채 수정하고,
리소스를 굉장히 세부적으로 캐싱할 수 있습니다.
이를 통해 웹 앱이 어떤 상황에서 어떻게 동작해야 하는지 완벽하게 바꿀 수 있습니다.
이러한 msw
를 사용하는것에 가장 큰 이점은 백엔드 API와 네트워크 통신하는 것과 동일하게 비동기 통신 코드를 작성할 수 있다는 것
입니다.
- MSW 공식 홈페이지
Seamlessly reuse the same mock definition for testing, development, and debugging.
"MSW를 통해 작성한 비동기 코드를 그대로 사용 할 수 있다" 는 아래와 같은 장점을 가지고 있습니다
MSW 는 서비스 워커를 통해서 작동하기 때문에 라이브러리 및 프레임워크에 종속적 이지 않습니다. 사용하시는 어떠한 방법으로 리엑트를 설치해서 사용해보세요 :)
npm i -D msw
yarn add -D msw
npx msw init public/ --save
해당 명령어를 사용하면 이런식으로 서비스 워커가 생기는 것을 볼 수 있습니다.
# mocks 폴더 생성
mkdir src/mocks
# msw 를 다루는 코드를 작성할 파일 생성
touch src/mocks/handlers.ts
# msw 의 서비스 워커와 handler를 연동 시키는 코드를 작성할 파일 생성
touch src/mocks/browser.ts
import { rest } from 'msw'
export const handlers = []
rest.all()
: 어떠한 RESTAPI Method 든 다 요청을 받을 수 있는 msw 만의 method 입니다.rest.get()
rest.post()
rest.put()
rest.patch()
rest.delete()
rest.options()
msw
의 rest api
들은RestHandler
타입을 가지고 있는데 이러한 핸들러들을 모아둔 것 입니다.
나중에, 브라우저 서비스 워커에 이 핸들러를 연동할 것 입니다.
rest.get(path, resolver)
rest.post(path, resolver)
처럼 method 의 첫번째 인자로 path, 두번 째 인자로 request, response, context
를 인자로 갖는 resolver callback 를 넣어줍니다.
body: { username: string }
}let user = {
name: '',
}
//request: Information about the captured request.
//response: Function to create the mocked response.
//context: Context utilities specific to the current request handler.
type ReqBody = { username: string }
rest.post('/login', async (req, res, ctx) => {
// 클라이언트에서 body 에 username 를 string 형태로 보내면,
// 해당 msw에서 req.json() 으로 받아 클라이언트에서 보낸 body를 받아올 수 있습니다.
// 그외의 request property 는 https://mswjs.io/docs/api/request 에서 확인 가능 합니다
const { username } = await req.json<ReqBody>()
user.name = username
if (username) {
return res(ctx.status(200))
}
return res(
ctx.status(403),
ctx.json({
errorMessage: '유저 이름을 등록 해주세요',
})
)
}
해당 코드는, [POST]
요청으로 /login
의 엔드포인트를 갖는 api 를 작성 한 것 입니다.
클라이언트에서 보낸 body 값을 받아 `username` 을 제대로 받아 오게 되면,
`user.name` 을 변경 시키고 `status=200` 을 응답 합니다.
제대로 받지 못하면.
`status=403` 을 응답하고
`errorMessage` 를 response data 에 담아 보냅니다.
// src/mocks/brower
import { setupWorker, SetupWorkerApi } from 'msw'
import { handlers } from './handlers'
export const worker: SetupWorkerApi = setupWorker(...handlers)
워커에 핸들러들을 담아서 setup
해줍니다.
// src/main.tsx
import { worker } from './mocks/browser'
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line global-require
worker.start({ onUnhandledRequest: 'bypass' })
}
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
콘솔 창을 열면 아래와 같은 이미지를 확인 할 수 있습니다
React-Query
, Tailwind-css
// src/features/auth/UserForm.tsx
import React from 'react'
import useInput from '../hooks/useInput'
import { useLogin } from '../hooks/useLogin'
const UserForm = () => {
const [username, setUserName, handleChangeUsername] = useInput()
const { handleFormSubmit, error: loginError } = useLogin({
onSettled: () => {
setUserName('')
},
})
return (
<>
<form
className="w-2/5 h-40 flex justify-center flex-col mx-auto px-5"
onSubmit={handleFormSubmit({ username })}
>
<input
className="w-3/5 border rounded-lg p-2"
placeholder="유저이름을 등록해주세요"
value={username}
onChange={handleChangeUsername}
/>
<button
type="submit"
className="flex border w-fit p-3 mt-1 rounded-lg bg-blue-500 hover:bg-blue-400"
>
유저 등록
</button>
</form>
<p className="w-2/5 flex justify-center flex-col mx-auto px-5 text-red-400">
{loginError?.response?.data.errorMessage}
</p>
</>
)
}
export default UserForm
import axios, { AxiosError, AxiosResponse } from 'axios'
import { useCallback } from 'react'
import { useMutation, UseMutationOptions } from 'react-query'
type LoginRequestVariables = {
username: string
}
const loginRequest = async ({ username }: LoginRequestVariables) => {
const response = await axios.post('/login', { username })
return response
}
type MutationOptions = Omit<
UseMutationOptions<
AxiosResponse<any, any>,
AxiosError<{ errorMessage: string }, any>,
LoginRequestVariables,
unknown
>,
'mutationFn'
>
export const useLogin = (options?: MutationOptions) => {
const mutation = useMutation<
AxiosResponse,
AxiosError<{ errorMessage: string }>,
LoginRequestVariables
>(loginRequest, { ...options })
const handleFormSubmit = useCallback(
({ username }: LoginRequestVariables) =>
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
mutation.mutate({ username })
},
[]
)
return { ...mutation, handleFormSubmit }
}
msw
를 사용하면 모킹 서버를 위한 데이터 요청 코드
를 따로 작성 할 필요 없이,
모킹 서버를 위한 데이터 요청
, 실제 서버를 위한 데이터 요청
모두 동일한 코드로 할 수 있습니다.
유저 등록을 클릭하게 되면 콘솔 창에서 아래와 같이 확인 할 수 있습니다.
저희가 작성한 모킹서버가 정상적으로 작동 함을 확인 할 수 있었습니다 :)
아무 값 없이 유저 등록을 하면 에러 메세지가 보여집니다. 콘솔에는 어떻게 찍혔는지 확인 해봅시다
의도 한 대로 status 403
을 받았음을 확인 할 수 있습니다.
백엔드 개발자가 아니라면, 백엔드 서버를 따로 만들 시간이 없다면, MSW
라이브러리를 사용하는 것은 너무 좋은 선택인것 같습니다
저 또한, 클라이언트 개발이 빨리 되어 서버가 나오기 전에 데이터 값에 따른 변화를 확인 해야 했고 이를 위해 msw
를 도입 하였습니다.
msw
를 사용해 Suspense
, 에러 핸들링
등을 서버 없이 구현 할 수 있었고, 해당 비동기 통신 코드를 실제 서버에서 큰 코드 수정 없이 쓸 수 있었습니다
프론트엔드 개발을 하면 계속해서 msw 를 사용 하게 될 것 같습니다.