MSW - Next.js에 MSW를 설치해보자
📌시작하며…
테스트 작성에 대해 찾아보며 코딩하던 평화로운 어느날… 우연히 우아한 형제들의 기술 블로그에 통합테스트와 관련된 내용을 발견했다. 자세하게 설명되어 있는 글이라 어떤 내용으로 테스트를 작성 하시는지 궁금해 살펴보던 와중 MSW 를 사용해 API값을 mocking 하는 부분이 있었다.
MSW 어디서 많이 들어본 이름인데…🤔
하고 어디서 봤던 이름인지 생각해보던 중, 내가 들었던 단위테스트 인프런 강의에서 등장했던 내용임을 깨달았다! 사실 그 부분은 간단하게만 훑고 앞에서 부터 적용해봤던 지라 아직 실제로 사용해본 적은 없는 라이브러리였다.😅
안그래도, API를 Fetch하고 컴포넌트를 렌더링 하는 부분을 테스트 할 차례이기도 해서, 연습용 프로젝트에 도입을 시도했다.
✅MSW란?
MSW는 API 요청을 가로채서 가짜 응답을 반환 해주는 라이브러리다.
프론트엔드에서는 이 특성을 아주 요긴하게 쓸 수 있다!
예를 들어 내가 아래와 같은 endpoint로 데이터를 받아와 컴포넌트를 렌더링 하는 상황을 생각해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useEffect, useState } from "react"
export default function Component() {
const [data, setData] = useState(null)
async function fetchData() {
try {
const response = await fetch("https://example.com")
const result = await response.json()
setData(result)
} catch (error) {
console.error("데이터 요청에 실패했습니다.: ", error)
}
}
useEffect(() => {
fetchData()
}, [])
return <Children data={data} />
}
백엔드에서 이 endpoint로 api 만들어 준다고 했으니까 이렇게 하면 되겠지?……. 라고 생각하고 컴포넌트를 제작했다. 이제 fetch만 잘 되면 아~무 문제 없을 것 같지만.. 만약 api 제작이 어쩔 수 없이 늦어지는 경우를 생각해보자.😢
데이터 fetching 후 제대로 테스트가 이루어지는지 확인해야 하기 때문에 api 제작을 기다리고 다시 작업에 들어가게 되면, 마감 기한이 성큼 다가온걸 느끼며 그 다음 작업에 충분한 시간을 할애하기가 어려워진다.😱
하지만 MSW와 같은 라이브러리를 사용해 사용자가 예상하고 있는 endpoint가 가짜 응답을 반환하도록 설정해두면, API 지연이나 백엔드 개발 속도와 관계없이 프론트엔드는 독립적으로 개발을 할 수 있다는 아주 큰 장점이 존재한다.
✅그렇지만..
여기서 의문이 들 수도 있다.
그냥
mock.ts
같은 파일 만들어서 작성해도 되고,next.js api
작성해도 되는거 아냐?🤔
지금까지 나도 이런식으로 작업했기에 MSW에 어떤 장점이 있는지 좀 더 찾아봤는데.. 이 부분은 올리브영 기술 블로그에 쓰여진 MSW를 도입하게 된 과정에서 찾아볼 수 있었다.
나는 그동안 mockData.ts 라는 파일을 만들어서 사용했는데, 이걸 import 해서 사용하다 보니 실제 API가 완성되었을 때 fetch하는 부분을 그 때 추가로 작성해야 했고, 덕분에 나도 힘들고, 시간도 많이 소요되는 불편함이 있었다. (특히 fetch Error 상황은 테스트하기도 어려웠고..🥹)
하지만 MSW로 mocking을 해두면 백엔드가 완성되기 전에도 미리 fetch 함수를 작성할 수 있고, Error 상황에도 대비할 수 있기 때문에 후반부에 백엔드 api가 완성 되었을 때 FE에서 추가로 작업해야 할 양이 줄어들 수 있다.
✅설치를 하자! 그런데…
최근 Vitest와 Storybook을 적용하기 위해 만들고 있는 웹페이지에 MSW를 적용하고자 공식문서를 보고, 블로그 글도 여럿 살펴봤는데, 아무래도 최근 업데이트가 있었던 듯 했다.
블로그 글과 공식문서의 내용이 꽤 차이가 있었는데, 공식문서에서는 ctx와 같은 내용은 사용하지 않지만 올해(24년)초에 작성된 글에서도 ctx가 등장하는걸 보니 말이다.
그래서 최대한 공식문서만 보고 설치를 진행하고자 했고, 중간에는 한 달 전에 업로드 된 유튜브의 한 개발자의 튜토리얼 영상을 통해 MSW를 실행할 수 있었다.
✅공식문서를 따라가자
일단 라이브러리를 설치하자! ‼️ 나는 next.js를 주로 사용하기 때문에 next.js와 src/app 폴더를 사용하는 환경에서 msw를 사용한다.
1
npm install msw@latest --save-dev
➡️ handlers.ts
이제 어떤 endpoint의 요청을 가로채서 어떤 데이터를 전달 할 것인지 작성해주는 handlers.ts를 작성하자.
나는 원하는 데이터 형식의 mockImageData를 객체 형식으로 작성해둔것이 있어서 이것을 가져왔다.
1
2
3
4
5
6
7
8
9
//src/mocks/handlers.ts
import { mockImageData } from "@/test/mockdata"
import { http, HttpResponse } from "msw"
export const handlers = [
http.get("https://example.com/user", () => {
return HttpResponse.json(mockImageData)
}),
]
이제 https://example.com/user
로 요청을 보내면 내가 미리 지정해주는 mockImageData
를 전달하게 된다.
➡️ node.ts와 browser.ts
이때, 공식문서에 적혀있는 설명을 좀 읽어보면, MSW에서는 환경에 따라 사용할 통합 모듈을 node.js(ts)
와 browser.js(ts)
로 나눠서 만드는 것을 권장하고 있다. 이렇게 하면 브라우저 환경(Playwright, Cypress, Storybook 등) 과 Node.js 환경(Vitest, Jest, React Native 등) 에서 각각 올바른 모듈을 가져와 재사용할 수 있기 떄문이다.
그럼 두 가지를 나눠 만들어보자
1
2
3
4
5
// src/mocks/node.ts
import { setupServer } from "msw/node"
import { handlers } from "./handlers"
export const server = setupServer(...handlers)
1
2
3
4
5
// src/mocks/browser.ts
import { setupWorker } from "msw/browser"
import { handlers } from "./handlers"
export const worker = setupWorker(...handlers)
마지막으로, 코드가 실행되는 환경(서버 또는 브라우저)에 따라 MSW server 혹은 worker를 초기화 한다.
➡️ index.ts
위에서 초기화를 실행한 내용을 연결해보자. 노드 상황과 브라우저 상황에서 각각 다른 실행을 연결해줄 수 있도록 type
을 이용해 조건을 나누어준다.
1
2
3
4
5
6
7
8
9
10
// src/mocks/index.ts
export async function mocks() {
if (typeof window === "undefined") {
const { server } = await import("./node")
server.listen()
} else {
const { worker } = await import("./browser")
return worker.start()
}
}
➡️ Provider.tsx
app router 사용시에는 Provider.tsx를 만들어서 mocks를 실행시킨다.
1
2
3
4
5
6
7
8
9
10
11
"use client"
import React, { useEffect } from "react"
import { mocks } from "./mocks"
export const Provider = ({ children }: { children: ReactNode }) => {
useEffect(() => {
mocks()
}, [])
return <>{children}</>
}
이제 이걸 layout.tsx 에 감싸주자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "./globals.css"
import { Noto } from "next/font/google"
import { Provider } from "../mocks/Provider"
const node = Noto({ subsets: ["latin"] })
export const metadata = {
title: "MSW",
description: "MSW 세팅하기",
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ko">
<body className={node.className}>
<Provider>{children}</Provider>
</body>
</html>
)
}
✅Instrumentation.ts
(추가적으로 아래와 같은 영상을 발견해 덧붙인다.) Next.js에 MSW를 사용하는 영상에서 Instrumentation.ts
를 src폴더 아래 app폴더와 같은 위치에 생성하는 것을 보며 무슨 역할을 수행하는 파일인지 궁금해져 서치해보니 Next.js에 속한 파일이었다.
Instrumentation(계측)이란 애플리케이션 코드에 모니터링 및 로깅 도구를 통합하는 것을 의미한다. ‘계측’ 이란 말이 확 와닿지가 않는데 무언가의 양/부피를 읽고 기록하는 ‘측정’ 이라는 단어보다 포괄적인 의미로, 여러 정보를 추적하고, 기록하고, 분석하는 더 많은 과정을 포함하는 의미이다.
즉, Instrumentation.ts 파일을 이용해 애플리케이션의 성능과 동작을 추적하고, 운영 중에 발생하는 문제를 디버깅할 수 있다.
다른 내용이 좀 더 있지만, MSW를 적용하기 위한 핵심적인 내용을 살펴보자.
➡️작성 내용
instrumentation.ts
파일은 프로젝트 루트 디렉토리 (src 폴더를 사용 중이라면 그 내부)에 위치시킨다. 이 파일에서는 register라는 함수를 내보내야 하는데, 이 함수는 Next.js 서버 인스턴스가 시작될 때 한 번 호출된다. Next.js는 모든 환경에서 register 함수를 호출하기 때문에, Edge나 Node.js처럼 특정 런타임에서만 지원하는 코드의 경우엔 조건부로 import해야 한다. 이때 현재 나의 환경을 확인하려면 NEXT_RUNTIME 환경 변수를 사용하면 된다.
이 기능을 이용해 필요한 내용을 작성해보면..
1
2
3
4
5
6
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { server } = await import("./mock/node")
server.listen()
}
}
process.env.NEXT_RUNTIME === "nodejs"
조건문을 통해, 현재 실행 중인 환경이 Node.js인지 확인 후, 이 조건이 참이면 mock/node를 import하여 서버를 설정하고 실행하는 코드로 Node.js 환경에서만 모킹된 서버가 활성화되도록 할 수 있다.