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>에서 확인하실 수 있습니다.

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 설정 중 일부를 제거하거나 변경합니다. 특별한 변경이 없었다면 아래와 같은 기본 설정을 가지고 있습니다.

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": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

이 파일에서 9가지를 변경합니다. 한 단계씩 수행해봅시다.

  1. references 필드에서 tsconfig.node.json을 제거합니다.

  2. include 배열에 문자열 "./dist/types/**/*.ts""./next-env.d.ts"를 추가합니다.

  3. exclude 필드를 만들고 ["./node_modules"]를 추가합니다.

  4. compilerOptionsplugins를 추가하고 값으로 [{ "name": "next" }]를 추가합니다.

  5. esModuleInterop 설정을 true로 활성화합니다.

  6. jsx 설정을 "preserve"로 변경합니다.

  7. allowJs 설정을 true로 활성화합니다.

  8. forceConsistentCasingInFileNames 설정을 true로 활성화합니다.

  9. incremental 설정을 true로 활성화합니다.

결과적으로 아래와 같은 설정이 완성됩니다. 각 설정에 대한 설명은 <Next.js 설정 읽어보기 - tsconfig.json>에서 확인하실 수 있습니다.

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 파일로 변환해봅니다.

  1. src 폴더에 app 폴더를 만듭니다.

  2. app 폴더에 layout.tsx 파일을 만듭니다. RootLayout 컴포넌트를 아래와 같이 정의합니다.

src/app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return null
}
  1. index.html에 있는 태그를 앞서 만든 RootLayout 컴포넌트로 붙여넣습니다. 그리고 <body> 태그 안에 있는 값을 <div id="root">{children}</div>로 대체합니다.

src/app/layout.tsx
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>
  )
}
  1. Next.js에서는 meta charset과 meta viewport 태그는 이미 포함되어 있습니다. <head> 태그에 있는 값을 지워도 괜찮습니다.

src/app/layout.tsx
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>
  )
}
  1. favicon.ico, icon.png, robots.txt와 같은 메타데이터 파일을 app 폴더 바로 아래에 두기만하면 <head> 태그에 자동으로 추가됩니다. 지원하는 모든 파일app 폴폴 옮겼다면 <link> 태그 역시 지워도 괜찮습니다.

src/app/layout.tsx
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>
  )
}
src/app/layout.tsx
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>
  )
}

<link rel="stylesheet"/> 태그의 경우 Next.js의 Metadata API에서 지원하지 않습니다. 현재는 기존처럼 <head> 태그에 둬도 무방합니다. 참고1, 참고2

Step 5: 엔트리포인트 페이지 만들기

Vite 앱에서는 main.tsx 파일이 엔트리포인트(진입점) 역할을 합니다. Next.js에서는 page.tsx 파일이 동일한 역할을 합니다. 이번에는 엔트리포인트 페이지를 만들어봅니다.

지금은 Vite 앱을 Next.js 앱으로 점진적으로 마이그레이션하고 있습니다. 한 번에 모든 걸 바꿨을 때 발생하는 리스크를 줄이기 위함이죠. 때문에 당장은 Next.js의 파일 시스템 기반 라우팅 방식으로 완전히 전환하지 않습니다.

app 폴더에 page.tsx 파일을 만들고 아래와 같은 내용을 정의합니다.

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 />
}

Next.js에서 클라이언트 컴포넌트는 브라우저로 보내기 전에 서버에서 HTML로 미리 렌더링됩니다. 점진적인 마이그레이션을 위해 <App> 컴포넌트가 서버에서 미리 렌더링되는 것을 막아둬야 합니다. 서버에서 HTML로 미리 렌더링할 수 없는 컴포넌트가 존재할 수 있기 때문입니다.

아래와 같이 dynamic 함수의 ssr 옵션을 false로 정의해 <App> 컴포넌트가 서버에서 렌더링되지 않도록 합니다.

const App = dynamic(() => import('../App'), { ssr: false })

Step 6: 정적 이미지 가져오기 방식 수정하기

Vite에서는 정적 이미지를 가져오면 이미지의 URL이 문자열로 반환됩니다.

App.tsx
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> 태그를 이용해 이미지 가져오기 방식을 수정하겠습니다.

  1. /public 폴더로부터 가져오는 절대 경로로 지정된 이미지 경로를 상대 경로로 변경합니다.

// Before (Vite)
import logo from '/logo.png'
 
// After (Next.js)
import logo from '../public/logo.png'
  1. src 프로퍼티에 객체를 전달하는 대신 src 값을 전달합니다.

// Before (Vite)
<img src={logo} />
 
// After (Next.js)
<img src={logo.src} />

혹시 타입 에러를 마주해도 당황하지 마세요. 이후에 next-env.d.ts 파일이 정의되면 에러가 해결됩니다.

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.MODEprocess.env.NODE_ENV

  • import.meta.env.PRODprocess.env.NODE_ENV === 'production'

  • import.meta.env.DEVprocess.env.NODE_ENV !== 'production'

  • import.meta.env.SSRtypeof window !== 'undefined'

Next.js는 BASE_URL 환경 변수도 제공하지 않습니다. 하지만 필요한 경우 환경 변수를 구성할 수 있습니다.

  1. .env 파일에 다음과 같이 변수를 추가하세요.

.env
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
  1. next.config.js 파일에 basePath 설정을 추가하고 process.env.NEXT_PUBLIC_BASE_PATH를 할당합니다.

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  distDir: './dist',
  basePath: process.env.NEXT_PUBLIC_BASE_PATH, // `/some-base-path`.
}
 
module.exports = nextConfig
  1. import.meta.env.BASE_URL가 사용되고 있던 부분을 모두 process.env.NEXT_PUBLIC_BASE_PATH로 변경합니다.

process 객체가 존재하지 않는다는 에러가 발생해도 당황하지 마세요. 이후에 @types/node가 설치되면 에러가 해결됩니다.

Step 8: package.json 스크립트 수정하기

이제 Next.js 앱을 실행시켜볼 수 있습니다. 그 전에 package.json 파일의 scripts 필드를 수정해볼까요.

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  }
}

다음으로 Next.js와 연관된 몇가지 폴더(.next)와 파일(next-env.d.ts)을 .gitignore에 추가합니다.

.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