2️Vite에서 Next.js로
https://nextjs.org/docs/app/building-your-application/upgrading/from-vite
Vite로 시작한 React 앱을 Next.js로 바꾸는 방법은 Next.js 공식 문서에서도 가이드하고 있습니다. 총 9개의 단계로 나눠 설명합니다.
Step 1: Next.js 의존성 설치하기
아래의 명령어를 실행해 Next.js 최신 패키지를 설치합니다.
npm install next@latest
Step 2: Next.js 설정 파일 만들기
next.config.js
파일을 만듭니다. 관련 설정에 대해서는 앞서 언급한 <Next.js 설정 읽어보기 - next.config.js>에서 확인하실 수 있습니다.
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // HTML/CSS/JS만 빌드해 Single-Page Application으로 만듭니다.
distDir: './dist', // 빌드된 결과물이 ./dist 폴더에 담깁니다.
}
export default nextConfig
Step 3: TypeScript 설정 수정하기
Vite로 시작한 앱의 TypeScript 설정 중 일부를 제거하거나 변경합니다. 특별한 변경이 없었다면 아래와 같은 기본 설정을 가지고 있습니다.
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
이 파일에서 9가지를 변경합니다. 한 단계씩 수행해봅시다.
references
필드에서tsconfig.node.json
을 제거합니다.include
배열에 문자열"./dist/types/**/*.ts"
와"./next-env.d.ts"
를 추가합니다.exclude
필드를 만들고["./node_modules"]
를 추가합니다.compilerOptions
에plugins
를 추가하고 값으로[{ "name": "next" }]
를 추가합니다.esModuleInterop
설정을true
로 활성화합니다.jsx
설정을"preserve"
로 변경합니다.allowJs
설정을true
로 활성화합니다.forceConsistentCasingInFileNames
설정을true
로 활성화합니다.incremental
설정을true
로 활성화합니다.
결과적으로 아래와 같은 설정이 완성됩니다. 각 설정에 대한 설명은 <Next.js 설정 읽어보기 - tsconfig.json>에서 확인하실 수 있습니다.
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"plugins": [{ "name": "next" }],
"esModuleInterop": true,
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"incremental": true
},
"include": ["src", "./dist/types/**/*.ts", "./next-env.d.ts"],
"exclude": ["./node_modules"]
}
Step 4: Root Layout 만들기
Next.js의 App Router에서는 루트 라우트인 app
폴더에 layout
파일이 반드시 필요합니다.
Vite 앱에서는 index.html
파일이 유사한 역할을 합니다. index.html
파일에는 <html>
, <head>
그리고 <body>
태그가 포함되어 있습니다.
총 6단계를 거쳐 index.html
파일을 layout
파일로 변환해봅니다.
src
폴더에app
폴더를 만듭니다.app
폴더에layout.tsx
파일을 만듭니다. RootLayout 컴포넌트를 아래와 같이 정의합니다.
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return null
}
index.html
에 있는 태그를 앞서 만든RootLayout
컴포넌트로 붙여넣습니다. 그리고<body>
태그 안에 있는 값을<div id="root">{children}</div>
로 대체합니다.
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<meta name="description" content="Vite App is a..." />
</head>
<body>
// <div id="root"></div>
// <script type="module" src="/src/main.tsx"></script>
<div id="root">{children}</div>
</body>
</html>
)
}
Next.js에서는 meta charset과 meta viewport 태그는 이미 포함되어 있습니다.
<head>
태그에 있는 값을 지워도 괜찮습니다.
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
// <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
// <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<meta name="description" content="Vite App is a..." />
</head>
<body>
<div id="root">{children}</div>
</body>
</html>
)
}
favicon.ico
,icon.png
,robots.txt
와 같은 메타데이터 파일을app
폴더 바로 아래에 두기만하면<head>
태그에 자동으로 추가됩니다. 지원하는 모든 파일을app
폴폴 옮겼다면<link>
태그 역시 지워도 괜찮습니다.
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
// <link rel="icon" type="image/svg+xml" href="/icon.svg" />
<title>Vite App</title>
<meta name="description" content="Vite App is a..." />
</head>
<body>
<div id="root">{children}</div>
</body>
</html>
)
}
Next.js는
<head>
태그를 Metadata API를 통해 관리할 수 있습니다. 남아있는 메타데이터 정보를metadata
객체로 옮겨줍니다.
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Vite App',
description: 'Vite App is a...',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
// <head>
// <title>Vite App</title>
// <meta name="description" content="Vite App is a..." />
// </head>
<body>
<div id="root">{children}</div>
</body>
</html>
)
}
Step 5: 엔트리포인트 페이지 만들기
Vite 앱에서는 main.tsx
파일이 엔트리포인트(진입점) 역할을 합니다. Next.js에서는 page.tsx
파일이 동일한 역할을 합니다. 이번에는 엔트리포인트 페이지를 만들어봅니다.
지금은 Vite 앱을 Next.js 앱으로 점진적으로 마이그레이션하고 있습니다. 한 번에 모든 걸 바꿨을 때 발생하는 리스크를 줄이기 위함이죠. 때문에 당장은 Next.js의 파일 시스템 기반 라우팅 방식으로 완전히 전환하지 않습니다.
app
폴더에 page.tsx
파일을 만들고 아래와 같은 내용을 정의합니다.
'use client'
import dynamic from 'next/dynamic'
import '../index.css'
const App = dynamic(() => import('../App'), { ssr: false })
export default function Page() {
return <App />
}
'use client'
라는 선언을 통해 클라이언트 컴포넌트로 정의되었다는 사실을 알 수 있습니다. 이 선언이 없다면 <Page>
컴포넌트는 서버 컴포넌트로 동작합니다.
Next.js에서 클라이언트 컴포넌트는 브라우저로 보내기 전에 서버에서 HTML로 미리 렌더링됩니다. 점진적인 마이그레이션을 위해 <App>
컴포넌트가 서버에서 미리 렌더링되는 것을 막아둬야 합니다. 서버에서 HTML로 미리 렌더링할 수 없는 컴포넌트가 존재할 수 있기 때문입니다.
아래와 같이 dynamic
함수의 ssr
옵션을 false
로 정의해 <App>
컴포넌트가 서버에서 렌더링되지 않도록 합니다.
const App = dynamic(() => import('../App'), { ssr: false })
Step 6: 정적 이미지 가져오기 방식 수정하기
Vite에서는 정적 이미지를 가져오면 이미지의 URL이 문자열로 반환됩니다.
import image from './img.png' // `image`는 '/assets/img.2d8efhg.png'와 같은 문자열
export default function App() {
return <img src={image} />
}
반면, Next.js에서는 정적 이미지를 가져오면 객체가 반환됩니다. 이 객체는 Next.js의 <Image>
컴포넌트와 함께 사용하거나 <img>
태그의 src
프로퍼티에 사용할 수도 있습니다.
<Image>
컴포넌트로 변경하면 최적화 등에 이점이 있지만 이 역시 점진적인 마이그레이션을 위해 나중으로 미루겠습니다. <img>
태그를 이용해 이미지 가져오기 방식을 수정하겠습니다.
/public
폴더로부터 가져오는 절대 경로로 지정된 이미지 경로를 상대 경로로 변경합니다.
// Before (Vite)
import logo from '/logo.png'
// After (Next.js)
import logo from '../public/logo.png'
src
프로퍼티에 객체를 전달하는 대신src
값을 전달합니다.
// Before (Vite)
<img src={logo} />
// After (Next.js)
<img src={logo.src} />
Step 7: 환경 변수 마이그레이션하기
Next.js와 Vite는 모두 .env 파일로 환경 변수를 정의합니다. 다른 점은 클라이언트 측에 노출되는 환경 변수의 접두사입니다. 마이그레이션을 위해 VITE_
접두사가 붙은 모든 환경 변수를 NEXT_PUBLIC_
로 변경합니다.
Vite에서는 import.meta.env
라는 특별한 환경 변수를 지원합니다. 해당 객체는 Next.js에선 지원하지 않기 때문에 아래와 같이 변경해야 합니다.
import.meta.env.VITE_
⇒process.env.NEXT_PUBLIC_
import.meta.env.MODE
⇒process.env.NODE_ENV
import.meta.env.PROD
⇒process.env.NODE_ENV === 'production'
import.meta.env.DEV
⇒process.env.NODE_ENV !== 'production'
import.meta.env.SSR
⇒typeof window !== 'undefined'
Next.js는 BASE_URL
환경 변수도 제공하지 않습니다. 하지만 필요한 경우 환경 변수를 구성할 수 있습니다.
.env
파일에 다음과 같이 변수를 추가하세요.
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
next.config.js
파일에basePath
설정을 추가하고process.env.NEXT_PUBLIC_BASE_PATH
를 할당합니다.
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
distDir: './dist',
basePath: process.env.NEXT_PUBLIC_BASE_PATH, // `/some-base-path`.
}
module.exports = nextConfig
import.meta.env.BASE_URL
가 사용되고 있던 부분을 모두process.env.NEXT_PUBLIC_BASE_PATH
로 변경합니다.
Step 8: package.json 스크립트 수정하기
이제 Next.js 앱을 실행시켜볼 수 있습니다. 그 전에 package.json
파일의 scripts
필드를 수정해볼까요.
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}
다음으로 Next.js와 연관된 몇가지 폴더(.next
)와 파일(next-env.d.ts
)을 .gitignore
에 추가합니다.
# ...
.next
next-env.d.ts
이제 npm run dev
명령어를 실행하고 http://localhost:3000
페이지를 열어보시면 Next.js로 돌아가는 앱을 확인하실 수 있습니다.
Step 9: 정리하기
이제 Vite와 관련된 파일과 패키지를 제거하면 끝입니다.
main.tsx
삭제하기index.html
삭제하기vite-env.d.ts
삭제하기tsconfig.node.json
삭제하기vite.config.ts
삭제하기Vite와 연관된 패키지들 제거하기
Last updated