Migrating from Vite

이 가이드는 기존의 Vite 애플리케이션을 Next.js로 마이그레이션하는 방법을 안내합니다.

왜 전환해야 하나요?

Vite에서 Next.js로 전환하고자 하는 이유는 여러 가지가 있습니다:

느린 초기 페이지 로딩 시간

기본 Vite React 플러그인 (opens in a new tab)으로 애플리케이션을 빌드한 경우, 애플리케이션은 순전히 클라이언트 사이드 애플리케이션입니다. 클라이언트 사이드 애플리케이션, 즉 단일 페이지 애플리케이션(SPA)은 종종 초기 페이지 로딩 시간이 느려지는 경향이 있습니다. 이러한 현상은 다음과 같은 이유로 발생합니다:

  1. 브라우저가 React 코드와 전체 애플리케이션 번들을 다운로드하고 실행할 때까지 기다려야 데이터를 로드하는 요청을 보낼 수 있습니다.
  2. 새로운 기능과 추가 종속성이 추가될 때마다 애플리케이션 코드가 커집니다.

자동 코드 분할 없음

이전 문제인 느린 로딩 시간은 코드 분할로 어느 정도 관리할 수 있습니다. 그러나 수동으로 코드 분할을 시도하면 성능이 악화될 수 있습니다. 수동으로 코드 분할을 하면 네트워크 워터폴이 쉽게 발생할 수 있습니다. Next.js는 라우터에 내장된 자동 코드 분할 기능을 제공합니다.

네트워크 워터폴

성능이 저하되는 일반적인 원인은 애플리케이션이 데이터를 가져오기 위해 순차적으로 클라이언트-서버 요청을 할 때 발생합니다. SPA에서 데이터 가져오기를 위한 일반적인 패턴은 플레이스홀더를 초기 렌더링한 후 컴포넌트가 마운트된 후에 데이터를 가져오는 것입니다. 불행히도, 이는 데이터를 가져오는 자식 컴포넌트가 부모 컴포넌트가 자신의 데이터를 로드할 때까지 가져오기를 시작할 수 없음을 의미합니다.

Next.js에서는 클라이언트에서 데이터를 가져오는 것이 지원되지만, 데이터를 서버로 옮겨서 클라이언트-서버 워터폴을 제거할 수도 있습니다.

빠르고 의도적인 로딩 상태

React Suspense를 통한 스트리밍 지원이 내장되어 있어 네트워크 워터폴을 일으키지 않고 UI의 어느 부분을 먼저 로드할지, 어떤 순서로 로드할지를 보다 의도적으로 설정할 수 있습니다.

이를 통해 로드 속도가 더 빠른 페이지를 만들고 레이아웃 이동 (opens in a new tab)을 제거할 수 있습니다.

데이터 가져오기 전략 선택

필요에 따라 Next.js는 페이지 및 컴포넌트 단위로 데이터 가져오기 전략을 선택할 수 있습니다. 빌드 타임에 가져올지, 서버의 요청 타임에 가져올지, 클라이언트에서 가져올지를 결정할 수 있습니다. 예를 들어, CMS에서 데이터를 가져와 빌드 타임에 블로그 게시물을 렌더링할 수 있으며, 이는 CDN에서 효율적으로 캐시될 수 있습니다.

미들웨어

Next.js 미들웨어는 요청이 완료되기 전에 서버에서 코드를 실행할 수 있게 해줍니다. 이는 인증된 페이지를 방문할 때 인증되지 않은 콘텐츠가 잠깐 보이는 것을 방지하기 위해 사용자를 로그인 페이지로 리디렉션하는 데 특히 유용합니다. 미들웨어는 실험 및 국제화에도 유용합니다.

내장 최적화

이미지, 폰트, 서드파티 스크립트는 애플리케이션 성능에 상당한 영향을 미칩니다. Next.js는 이를 자동으로 최적화하는 내장 컴포넌트를 제공합니다.

마이그레이션 단계

이번 마이그레이션의 목표는 Next.js 애플리케이션을 가능한 한 빨리 작동시키는 것이며, 이후 점진적으로 Next.js 기능을 도입할 수 있도록 합니다. 우선, 기존 라우터를 마이그레이션하지 않고 순전히 클라이언트 사이드 애플리케이션(SPA)으로 유지할 것입니다. 이는 마이그레이션 과정에서 발생할 수 있는 문제를 최소화하고 병합 충돌을 줄이는 데 도움이 됩니다.

1단계: Next.js 종속성 설치

먼저 next를 종속성으로 설치해야 합니다:

Terminal
npm install next@latest

2단계: Next.js 구성 파일 생성

프로젝트의 루트에 next.config.mjs를 생성합니다. 이 파일은 Next.js 구성 옵션을 보관합니다.

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Single-Page Application (SPA) 출력.
  distDir: './dist', // 빌드 출력 디렉터리를 `./dist/`로 변경합니다.
}
 
export default nextConfig

알아두면 좋은 점: Next.js 구성 파일로 .js 또는 .mjs를 사용할 수 있습니다.

3단계: TypeScript 구성 업데이트

TypeScript를 사용 중이라면 Next.js와 호환되도록 tsconfig.json 파일을 다음과 같이 업데이트해야 합니다. TypeScript를 사용하지 않는 경우 이 단계를 건너뛸 수 있습니다.

  1. tsconfig.node.json에 대한 프로젝트 참조 (opens in a new tab) 제거
  2. include 배열 (opens in a new tab)./dist/types/**/*.ts./next-env.d.ts 추가
  3. exclude 배열 (opens in a new tab)./node_modules 추가
  4. compilerOptionsplugins 배열 (opens in a new tab){ "name": "next" } 추가: "plugins": [{ "name": "next" }]
  5. esModuleInterop (opens in a new tab)true로 설정: "esModuleInterop": true
  6. jsx (opens in a new tab)preserve로 설정: "jsx": "preserve"
  7. allowJs (opens in a new tab)true로 설정: "allowJs": true
  8. forceConsistentCasingInFileNames (opens in a new tab)true로 설정: "forceConsistentCasingInFileNames": true
  9. incremental (opens in a new tab)true로 설정: "incremental": true

다음은 이러한 변경 사항을 반영한 tsconfig.json의 예입니다:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "allowJs": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": true,
    "plugins": [{ "name": "next" }]
  },
  "include": ["./src", "./dist/types/**/*.ts", "./next-env.d.ts"],
  "exclude": ["./node_modules"]
}

TypeScript 구성에 대한 자세한 내용은 Next.js 문서에서 확인할 수 있습니다.

4단계: 루트 레이아웃 생성

Next.js App Router 애플리케이션에는 애플리케이션의 모든 페이지를 감싸는 루트 레이아웃 파일이 포함되어야 합니다. 이 파일은 `app

` 디렉터리의 최상위에 정의됩니다.

Vite 애플리케이션에서 루트 레이아웃 파일과 가장 유사한 파일은 <html>, <head>, <body> 태그를 포함한 index.html 파일 (opens in a new tab)입니다.

이 단계에서는 index.html 파일을 루트 레이아웃 파일로 변환합니다:

  1. src 디렉터리에 새로운 app 디렉터리를 생성합니다.
  2. 해당 app 디렉터리에 새로운 layout.tsx 파일을 생성합니다:
app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return '...'
}
app/layout.js
export default function RootLayout({ children }) {
  return '...'
}

알아두면 좋은 점: 레이아웃 파일에는 .js, .jsx, .tsx 확장자를 사용할 수 있습니다.

  1. index.html 파일의 내용을 이전에 생성한 <RootLayout> 컴포넌트에 복사하고 body.div#rootbody.script 태그를 <div id="root">{children}</div>으로 교체합니다:
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>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
app/layout.js
export default function RootLayout({ children }) {
  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>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. Next.js는 기본적으로 meta charset (opens in a new tab)meta viewport (opens in a new tab) 태그를 이미 포함하고 있으므로 이를 <head>에서 안전하게 제거할 수 있습니다:
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>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
app/layout.js
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. favicon.ico, icon.png, robots.txt와 같은 메타데이터 파일app 디렉터리 최상위에 배치되어 있는 한 애플리케이션 <head> 태그에 자동으로 추가됩니다. 모든 지원 파일app 디렉터리로 이동한 후 <link> 태그를 안전하게 삭제할 수 있습니다:
app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
app/layout.js
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. 마지막으로 Next.js는 메타데이터 API를 통해 마지막 <head> 태그를 관리할 수 있습니다. 최종 메타데이터 정보를 내보낸 metadata 객체로 이동합니다:
app/layout.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'My App',
  description: 'My App is a...',
}
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
app/layout.js
export const metadata = {
  title: 'My App',
  description: 'My App is a...',
}
 
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

위의 변경 사항으로 인해 index.html에서 모든 것을 선언하는 것에서 Next.js의 프레임워크에 내장된 규칙 기반 접근 방식(메타데이터 API)으로 전환했습니다. 이러한 접근 방식은 페이지의 SEO 및 웹 공유 가능성을 보다 쉽게 개선할 수 있게 합니다.

5단계: 엔트리포인트 페이지 생성

Next.js에서는 page.tsx 파일을 생성하여 애플리케이션의 엔트리포인트를 선언합니다. Vite에서 이 파일에 해당하는 가장 가까운 파일은 main.tsx 파일입니다. 이 단계에서는 애플리케이션의 엔트리포인트를 설정합니다.

  1. app 디렉토리에 [[...slug]] 디렉토리 생성

이 가이드에서는 Next.js를 SPA(단일 페이지 애플리케이션)로 설정하는 것을 목표로 하고 있으므로, 애플리케이션의 모든 가능한 라우트를 처리할 수 있는 페이지 엔트리포인트가 필요합니다. 이를 위해 app 디렉토리에 새로운 [[...slug]] 디렉토리를 생성합니다.

이 디렉토리는 선택적 catch-all 라우트 세그먼트라고 합니다. Next.js는 파일 시스템 기반 라우터를 사용하며, 디렉토리를 사용하여 라우트를 정의합니다. 이 특별한 디렉토리는 애플리케이션의 모든 라우트가 이 디렉토리에 포함된 page.tsx 파일로 이동하도록 합니다.

  1. app/[[...slug]] 디렉토리에 새로운 page.tsx 파일 생성
app/[[...slug]]/page.tsx
import '../../index.css'
 
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return '...' // 이 부분은 나중에 업데이트합니다.
}
app/[[...slug]]/page.js
import '../../index.css'
 
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return '...' // 이 부분은 나중에 업데이트합니다.
}

알아두면 좋은 점: 페이지 파일에는 .js, .jsx, .tsx 확장자를 사용할 수 있습니다.

이 파일은 서버 컴포넌트입니다. next build를 실행하면 이 파일은 정적 자산으로 미리 렌더링됩니다. 동적 코드는 필요하지 않습니다.

이 파일은 전역 CSS를 가져오고 generateStaticParams 함수를 사용하여 루트 경로인 /만 생성하도록 합니다.

이제 나머지 Vite 애플리케이션을 클라이언트 전용으로 실행하도록 이동합니다.

app/[[...slug]]/client.tsx
'use client'
 
import React from 'react'
import dynamic from 'next/dynamic'
 
const App = dynamic(() => import('../../App'), { ssr: false })
 
export function ClientOnly() {
  return <App />
}
app/[[...slug]]/client.js
'use client'
 
import React from 'react'
import dynamic from 'next/dynamic'
 
const App = dynamic(() => import('../../App'), { ssr: false })
 
export function ClientOnly() {
  return <App />
}

이 파일은 'use client' 지시어로 정의된 클라이언트 컴포넌트입니다. 클라이언트 컴포넌트는 여전히 서버에서 HTML로 미리 렌더링된 후 클라이언트로 전송됩니다.

클라이언트 전용 애플리케이션을 시작하기 위해 Next.js에서 App 컴포넌트 이하의 미리 렌더링을 비활성화하도록 구성할 수 있습니다.

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

이제 엔트리포인트 페이지를 새 컴포넌트를 사용하도록 업데이트합니다:

app/[[...slug]]/page.tsx
import '../../index.css'
import { ClientOnly } from './client'
 
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return <ClientOnly />
}
app/[[...slug]]/page.js
import '../../index.css'
import { ClientOnly } from './client'
 
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return <ClientOnly />
}

6단계: 정적 이미지 가져오기 업데이트

Next.js는 Vite와 약간 다르게 정적 이미지 가져오기를 처리합니다. 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> 컴포넌트는 자동 이미지 최적화의 추가 이점을 제공합니다. <Image> 컴포넌트는 이미지의 크기에 따라 결과 <img>widthheight 속성을 자동으로 설정합니다. 이는 이미지 로드 시 레이아웃 이동을 방지합니다. 그러나 이로 인해 한 차원만 스타일링되고 다른 차원이 auto로 스타일링되지 않은 이미지가 포함된 경우 문제가 발생할 수 있습니다. 스타일링되지 않은 경우, 차원은 <img> 차원 속성의 값으로 기본 설정되며, 이는 이미지가 왜곡되어 보이게 할 수 있습니다.

<img> 태그를 유지하면 애플리케이션에서의 변경 사항을 줄이고 위의 문제를 방지할 수 있습니다. 그런 다음 나중에 이미지 최적화의 이점을 활용하기 위해 [로더를 구성](/docs/app/building-your-application/optimizing/images#loaders)하거나, 자동 이미지 최적화가 있는 기본 Next.js 서버로 이동하기 위해 선택적으로 <Image> 컴포넌트로 마이그레이션할 수 있습니다.

  1. /public에서 가져온 이미지의 절대 경로를 상대 경로로 변환:
// 변환 전
import logo from '/logo.png'
 
// 변환 후
import logo from '../public/logo.png'
  1. 이미지 객체 대신 src 속성을 <img> 태그에 전달:
// 변환 전
<img src={logo} />
 
// 변환 후
<img src={logo.src} />

또한 파일 이름을 기준으로 이미지 자산의 공개 URL을 참조할 수 있습니다. 예를 들어, public/logo.png는 애플리케이션에서 /logo.png로 이미지를 제공하므로 src 값이 됩니다.

경고: TypeScript를 사용하는 경우 src 속성에 접근할 때 타입 오류가 발생할 수 있습니다. 이는 지금 무시해도 괜찮습니다. 이 가이드의 끝에서 수정될 것입니다.

7단계: 환경 변수 마이그레이션

Next.js는 Vite와 유사하게 .env 환경 변수를 지원합니다. 주요 차이점은 클라이언트 사이드에서 환경 변수를 노출하는 데 사용되는 접두사입니다.

  • VITE_ 접두사가 있는 모든 환경 변수를 NEXT_PUBLIC_ 접두사로 변경합니다.

Vite는 Next.js에서 지원되지 않는 특수 import.meta.env 객체에 몇 가지 내장 환경 변수를 노출합니다. 다음과 같이 사용을 업데이트해야 합니다:

  • 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.mjs 파일에서 basePathprocess.env.NEXT_PUBLIC_BASE_PATH로 설정합니다:
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Single-Page Application (SPA) 출력.
  distDir: './dist', // 빌드 출력 디렉터리를 `./dist/`로 변경합니다.
  basePath: process.env.NEXT_PUBLIC_BASE_PATH, // 기본 경로를 `/some-base-path
 
`로 설정합니다.
}
 
export default nextConfig
  1. import.meta.env.BASE_URL 사용을 process.env.NEXT_PUBLIC_BASE_PATH로 업데이트합니다.

8단계: package.json의 스크립트 업데이트

이제 애플리케이션이 Next.js로 성공적으로 마이그레이션되었는지 테스트할 수 있어야 합니다. 그러나 그 전에 package.json의 스크립트를 Next.js 관련 명령으로 업데이트하고 .nextnext-env.d.ts.gitignore에 추가해야 합니다:

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}
.gitignore
# ...
.next
next-env.d.ts
dist

이제 npm run dev를 실행하고 http://localhost:3000에 접속합니다. 이제 Next.js에서 애플리케이션이 실행되고 있는 것을 볼 수 있습니다.

예제: Next.js로 마이그레이션된 Vite 애플리케이션의 작동 예제를 보려면 이 풀 리퀘스트 (opens in a new tab)를 확인하십시오.

9단계: 정리

이제 Vite와 관련된 아티팩트를 코드베이스에서 정리할 수 있습니다:

  • main.tsx 삭제
  • index.html 삭제
  • vite-env.d.ts 삭제
  • tsconfig.node.json 삭제
  • vite.config.ts 삭제
  • Vite 종속성 제거

다음 단계

모든 것이 계획대로 진행되었다면 이제 단일 페이지 애플리케이션으로 실행되는 기능적인 Next.js 애플리케이션이 있습니다. 그러나 아직 Next.js의 대부분의 이점을 활용하지 않았습니다. 이제 점진적으로 변경을 시작하여 모든 이점을 누릴 수 있습니다. 다음으로 할 수 있는 작업은 다음과 같습니다: