
Next.js의 이중환경
Next.js에서 작성된 코드는 서버(Node.js)와 브라우저라는 두 가지 다른 환경에서 실행된다. SSR(또는 SSG)단계에서는 Node.js 환경에서 코드가 실행되고, CSR단계에서는 브라우저 환경에서 코드가 실행된다. 따라서, alert()나 localStorage같은 브라우저 전용 API는 브라우저 환경에서만, fs같은 Node.js 전용 API는 서버환경에서만 호출 가능하다.
MSW
Mock Service Worker는 자바스크립트 API 모킹 라이브러리이다. 모킹 API를 이용하면 API가 준비되지 않은 상태에서도 프론트 엔드 개발이 가능하기 때문에 실제 개발에서 유용하다. 다른 모킹 라이브러리리들은 주로 코드 레벨에서 요청을 가로채는 반면 MSW는 브라우저 전용 API인 Service Woker API를 사용하여 네트워크 레벨에서 작동을 지원하므로, 더 현실적인 테스트 환경을 제공하는 강점이 있다.
추천 아티클 1 Next.js에서 MSW(Mock Service Worker)로 네트워크 Mocking하기
추천 아티클 2 Mocking으로 프론트엔드 DX를 높여보자
문제 상황
수영장 정보사이트 어푸는 SSR과 CSR을 동시에 사용하는 하이브리드 렌더링방식으로 개발할 예정이었기 때문에, 모킹을 Node.js 환경과 브라우저 환경 모두에서 설정할 필요가 있었습니다.
따라서 다음과 같이 설정해주었다.
// 주의 잘못된 코드입니다. 절대 따라하지마세요.
// node환경에서 모 킹설정 : rsc 최상위 layout에 server.listen()함수 실행
//mocks/node.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
//layout.tsx
import { server } from '@/mocks/node'
...
if (process.env.NODE_ENV === 'development') {
server.listen()
}
...
// 브라우저 환경에서 모킹 설정
//mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
//providers/msw-provider.tsx
// latout 컴포넌트에 프로바이더를 추가해줍니다. msw시작에 시간시 소요되므로
// 시작이 완료된 뒤 페이지가 렌더링 되도록 합니다.
'use client'
import { ReactNode, useEffect, useState } from 'react'
import { worker } from "@/mocks/browser"
export function MSWProvider({ children }: { children: ReactNode }) {
useEffect(() => {
async function enableMocking() {
if (
typeof window !== 'undefined' &&
process.env.NODE_ENV === 'development'
) {
await worker.start()
}
}
enableMocking()
}, [])
return <>{children}</>
}그런데, 빌드에러가 발생하였다.
Build Error
Module not found: Package path ./browser is not exported from package /.../frontend/node_modules/msw ...해결방법
에러 메세지 Module not found: Package path ./browser is not exported from package 와 키워드 msw Next.js 구글링해서 해결방법을 찾을 수 있었다. dynamic import을 통해msw/browser모듈을 브라우저 환경인 경우에서만 불러와 해결했다.
'use client'
import { ReactNode, useEffect, useState } from 'react'
export function MSWProvider({ children }: { children: ReactNode }) {
useEffect(() => {
async function enableMocking() {
if (
typeof window !== 'undefined' &&
process.env.NODE_ENV === 'development'
) {
const { worker } = await import('@/mocks/browser') // 수정된 부분
// useEffect훅안에서 클라이언트에서 동적으로 임포트 합니다.
await worker.start()
}
}
enableMocking()
}, [])
return <>{children}</>
}문제원인
이는 msw/browser에서 setupWorker를 import하는 과정에 있었다. 빌드에러시 발생한 안내 메세지대로msw/package.json을 살펴보면 msw/browser를 node.js 환경에서 import하는 경우 경로가 null임으로 모듈을 불러올 수 없다. 이는 msw/browser 모듈이 브라우저 전용 API를 사용하기 때문에, node.js 환경에서 실행될 경우 런타임 에러를 일으키기 때문에, 이를 사전에 방지한 것이다. 따라서, Next.js의 번들러가 코드를 빌드 시에 이는 node.js 환경이므로 mock/browser 모듈을 불러오지 못하게 되는 것이다.
// msw/package.json (2.7.0)
{
"name": "msw",
"version": "2.7.0",
"description": "Seamless REST/GraphQL API mocking library for browser and Node.js.",
"main": "./lib/core/index.js",
"module": "./lib/core/index.mjs",
"types": "./lib/core/index.d.ts",
"exports": {
".": {
"types": "./lib/core/index.d.ts",
"require": "./lib/core/index.js",
"import": "./lib/core/index.mjs",
"default": "./lib/core/index.js"
},
"./browser": {
"types": "./lib/browser/index.d.ts",
"browser": {
"require": "./lib/browser/index.js",
"import": "./lib/browser/index.mjs"
},
"node": null,
"require": "./lib/browser/index.js",
"import": "./lib/browser/index.mjs",
"default": "./lib/browser/index.js"
},
...'use client' 지시어 로 해결하지 못하는 이유
그런데 'use client' 지시어를 추가해 해당 컴포넌트가 클라이언트 컴포넌트임을 알려주었는데도 왜 문제가 발생했을까? 'use client' 지시어는 "이 코드는 오직 브라우저에서만 돌아간다"는 선언이 아니다.
- 컴포넌트는 어떤 번들에 포함되는가?
Next.js는 빌드 시점에 코드 그래프를 분석하여 어떤 모듈을 서버용 번들에 넣을지, 클라이언트용 번들에 넣을지 결정한다.
- 서버 컴포넌트 (.js, .tsx 등 기본값): 서버 전용 번들에만 포함된다. 브라우저로 전송되는 자바스크립트 번들에는 포함되지 않는다.
- 클라이언트 컴포넌트 ('use client' 선언): 서버 번들과 클라이언트 번들 모두에 포함된다.
- 클라이언트 컴포넌트도 서버에서 실행된다
이것이 까다로운 부분이다. Next.js는 사용자 경험(LCP 개선 등)을 위해 모든 컴포넌트를 서버에서 먼저 실행하여 정적 HTML을 만든다. 서버가 클라이언트 컴포넌트를 렌더링해서 HTML 결과물을 만들면, 브라우저는 서버가 만든 HTML 위에 자바스크립트 이벤트 등을 입힌다. 이를 하이드레이션(Hydration)이라고 한다.
- 왜 msw/browser가 빌드 시점에 문제를 일으키는가?
msw/browser 내부에는 브라우저 환경을 전제로 하는 Service Worker 관련 코드나 브라우저 전역 객체 참조가 포함되어 있다. 따라서, 이를 정적으로 임포트하게 되면 서버 번들에 해당 모듈이 포함된 그래프가 그려지게 되고, 이는 빌드에러로 이어진다. 이를 방지하기 위해서는 동적 임포트로 msw/browser 모듈이 서버 번들에 포함되지 않도록 해야 하는 것이다.
회고
사실은 공식 홈페이지에 문제를 예방할 수 있는 Browser intergration 가이드가 잘 작성되어있었는데. 이를 읽지 않았던것이 가장 큰 원인이었다. MSW를 도입하기전 관련 기술블로그 아티클을 잘 읽은 반면, 공식 홈페이지를 읽지 않는 기초적인 실수를 저질렀다. 하지만, 에러를 해결하면서, dynamic Import, node.js 환경과 브라우저 환경의 차이, Next.js의 빌드과정, package.json에서 export 설정과 같은 개념들을 학습할 수 있어서 나름 알찼던것 같다. 앞으로 새 라이브러리를 도입할 때 공식홈페이지를 꼭 하루이틀동안 정독하는 습관이 필요함을 느꼈다.
트러블 슈팅 : 조건부 렌더링에 따른 서버사이드 렌더링 이슈
'use client'
import { ReactNode, useEffect, useState } from 'react'
export function MSWProvider({ children }: { children: ReactNode }) {
const [isReady, setIsReady] = useState(false)
useEffect(() => {
async function enableMocking() {
if (
typeof window !== 'undefined' &&
process.env.NODE_ENV === 'development'
) {
const { worker } = await import('@/mocks/browser') // 수정된 부분
// useEffect훅안에서 클라이언트에서 동적으로 임포트 합니다.
await worker.start()
}
}
enableMocking().then(() => setIsReady(true))
}, [])
if (!isReady) return null
return <>{children}</>
}MSW 모킹 서버를 시작하는 enableMocking()함수가 완료되기 전까지는 일정 시간이 소요된다. 따라서, 서버 실행이전에 API요청이 있게되면 요청에 실패할 수 있다. 따라서 MSW 모킹 서버 준비 상태에 따라 하위 컴포넌트들을 조건부 렌더링하게 코드를 작성했다. 하지만 이 때문에, 루트레이아웃 하위의 컴포넌트 전체에서, 서버사이드 렌더링이 의도한대로 작동하지 못하는 이슈가 있었다. 따라서, 상위 코드처럼 조건부 렌더링 부분을 없애주어 이를 해결했다. 다행히 우리 프로젝트에서는 리액트 쿼리를 사용하고 있었기 때문에, 혹시 모킹 서버 준비 완료 이전에 요청을 하여 요청에 실패하더라도 자동으로 재시도하기 때문에 큰 문제는 없었다.
마치 SPA처럼 빈 바디와 스크립트들을 전송받은 결과. (이슈 해결 이전)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charSet="utf-8"/>
...
</head>
<body class="__variable_4d318d __variable_ea5f4b __variable_fde3a9 __variable_981c89 font-pretendard antialiased">
<script src="/_next/static/chunks/webpack.js?v=1737521313207" async=""></script>
<script>
(self.__next_f = self.__next_f || []).push([0])
</script>
... 무수히 긴 스크립트들 body태그 아래에 html태그는 없었다...
</body>
</html>