> For the complete documentation index, see [llms.txt](https://book.hajoeun.dev/friendly-next-js/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://book.hajoeun.dev/friendly-next-js/part-2/next.js-2/vite-next.js.md).

# Vite에서 Next.js로

https\://nextjs.org/docs/app/building-your-application/upgrading/from-vite

Vite로 시작한 React 앱을 Next.js로 바꾸는 방법은 Next.js 공식 문서에서도 가이드하고 있습니다. 총 9개의 단계로 나눠 설명합니다.&#x20;

#### Step 1: Next.js 의존성 설치하기

아래의 명령어를 실행해 Next.js 최신 패키지를 설치합니다.

```bash
npm install next@latest
```

#### Step 2: Next.js 설정 파일 만들기

`next.config.js` 파일을 만듭니다. 관련 설정에 대해서는 앞서 언급한 [\<Next.js 설정 읽어보기 - next.config.js>](/friendly-next-js/part-2/next.js-1/next.config.js.md)에서 확인하실 수 있습니다.

{% code title="next.config.js" %}

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // HTML/CSS/JS만 빌드해 Single-Page Application으로 만듭니다. 
  distDir: './dist', // 빌드된 결과물이 ./dist 폴더에 담깁니다.
}

export default nextConfig
```

{% endcode %}

#### Step 3: TypeScript 설정 수정하기

Vite로 시작한 앱의 TypeScript 설정 중 일부를 제거하거나 변경합니다. 특별한 변경이 없었다면 아래와 같은 기본 설정을 가지고 있습니다.&#x20;

{% code title="tsconfig.json" lineNumbers="true" %}

```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" }]
}
```

{% endcode %}

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

1. `references` 필드에서 `tsconfig.node.json`을 제거합니다.
2. `include` 배열에 문자열 `"./dist/types/**/*.ts"`와 `"./next-env.d.ts"`를 추가합니다.
3. `exclude` 필드를 만들고 `["./node_modules"]`를 추가합니다.
4. `compilerOptions`에 `plugins`를 추가하고 값으로 `[{ "name": "next" }]`를 추가합니다.
5. `esModuleInterop` 설정을 `true`로 활성화합니다.
6. `jsx` 설정을 `"preserve"`로 변경합니다.
7. `allowJs` 설정을 `true`로 활성화합니다.
8. `forceConsistentCasingInFileNames` 설정을 `true`로 활성화합니다.
9. `incremental` 설정을 `true`로 활성화합니다.

결과적으로 아래와 같은 설정이 완성됩니다. 각 설정에 대한 설명은 [\<Next.js 설정 읽어보기 - tsconfig.json>](/friendly-next-js/part-2/next.js-1/tsconfig-json.md)에서 확인하실 수 있습니다.

{% code title="tsconfig.json" lineNumbers="true" %}

```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"]
}
```

{% endcode %}

#### 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` 폴더를 만듭니다.&#x20;
2. `app` 폴더에 `layout.tsx` 파일을 만듭니다. RootLayout 컴포넌트를 아래와 같이 정의합니다.

{% code title="src/app/layout.tsx" %}

```tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return null
}
```

{% endcode %}

3. `index.html`에 있는 태그를 앞서 만든 `RootLayout` 컴포넌트로 붙여넣습니다. 그리고 `<body>` 태그 안에 있는 값을 `<div id="root">{children}</div>`로 대체합니다.

{% code title="src/app/layout.tsx" %}

```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>
  )
}
```

{% endcode %}

4. Next.js에서는 meta charset과 meta viewport 태그는 이미 포함되어 있습니다. `<head>` 태그에 있는 값을 지워도 괜찮습니다.

{% code title="src/app/layout.tsx" %}

```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>
  )
}
```

{% endcode %}

5. `favicon.ico`, `icon.png`, `robots.txt`와 같은 메타데이터 파일을 `app` 폴더 바로 아래에 두기만하면 `<head>` 태그에 자동으로 추가됩니다. [지원하는 모든 파일](https://nextjs.org/docs/app/building-your-application/optimizing/metadata#file-based-metadata)을 `app` 폴폴 옮겼다면 `<link>` 태그 역시 지워도 괜찮습니다.

{% code title="src/app/layout.tsx" %}

```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>
  )
}
```

{% endcode %}

6. Next.js는 `<head>` 태그를 [Metadata API](/friendly-next-js/next.js-1/undefined-5.md)를 통해 관리할 수 있습니다. 남아있는 메타데이터 정보를 `metadata` 객체로 옮겨줍니다.

{% code title="src/app/layout.tsx" %}

```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>
  )
}
```

{% endcode %}

{% hint style="info" %}
`<link rel="stylesheet"/>` 태그의 경우 Next.js의 Metadata API에서 지원하지 않습니다. 현재는 기존처럼 `<head>` 태그에 둬도 무방합니다. [참고1](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#unsupported-metadata), [참고2](https://nextjs.org/docs/messages/no-stylesheets-in-head-component)
{% endhint %}

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

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

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

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

{% code title="app/page.tsx" lineNumbers="true" %}

```tsx
'use client'
 
import dynamic from 'next/dynamic'
import '../index.css'
 
const App = dynamic(() => import('../App'), { ssr: false })
 
export default function Page() {
  return <App />
}
```

{% endcode %}

`'use client'` 라는 선언을 통해 클라이언트 컴포넌트로 정의되었다는 사실을 알 수 있습니다. 이 선언이 없다면 `<Page>` 컴포넌트는 [서버 컴포넌트](/friendly-next-js/next.js-1/undefined.md)로 동작합니다.

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

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

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

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

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

{% code title="App.tsx" %}

```tsx
import image from './img.png' // `image`는 '/assets/img.2d8efhg.png'와 같은 문자열
 
export default function App() {
  return <img src={image} />
}
```

{% endcode %}

반면, Next.js에서는 정적 이미지를 가져오면 객체가 반환됩니다. 이 객체는 Next.js의 `<Image>` 컴포넌트와 함께 사용하거나 `<img>` 태그의 `src` 프로퍼티에 사용할 수도 있습니다.

`<Image>` 컴포넌트로 변경하면 최적화 등에 이점이 있지만 이 역시 점진적인 마이그레이션을 위해 나중으로 미루겠습니다. `<img>` 태그를 이용해 이미지 가져오기 방식을 수정하겠습니다.

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

<pre class="language-tsx"><code class="lang-tsx"><strong>// Before (Vite)
</strong>import logo from '/logo.png'
 
// After (Next.js)
import logo from '../public/logo.png'
</code></pre>

2. `src` 프로퍼티에 객체를 전달하는 대신 `src` 값을 전달합니다.

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

{% hint style="info" %}
혹시 타입 에러를 마주해도 당황하지 마세요. 이후에 [next-env.d.ts](https://book.hajoeun.dev/friendly-next-js/part-2/next.js-2/pages/FwbNgW3JAgIlR9up9PWE#next-env.d.ts) 파일이 정의되면 에러가 해결됩니다.
{% endhint %}

#### 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` 환경 변수도 제공하지 않습니다. 하지만 필요한 경우 환경 변수를 구성할 수 있습니다.

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

{% code title=".env" %}

```bash
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
```

{% endcode %}

2. `next.config.js` 파일에 `basePath` 설정을 추가하고 `process.env.NEXT_PUBLIC_BASE_PATH`를 할당합니다.

{% code title="next.config.js" %}

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  distDir: './dist',
  basePath: process.env.NEXT_PUBLIC_BASE_PATH, // `/some-base-path`.
}
 
module.exports = nextConfig
```

{% endcode %}

3. `import.meta.env.BASE_URL`가 사용되고 있던 부분을 모두 `process.env.NEXT_PUBLIC_BASE_PATH`로 변경합니다.

{% hint style="info" %}
`process` 객체가 존재하지 않는다는 에러가 발생해도 당황하지 마세요. 이후에 `@types/node`가 설치되면 에러가 해결됩니다.
{% endhint %}

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

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

{% code title="package.json" %}

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

{% endcode %}

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

{% code title=".gitignore" %}

```gitignore
# ...
.next
next-env.d.ts
```

{% endcode %}

이제 `npm run dev` 명령어를 실행하고 [`http://localhost:3000`](http://localhost:3000/) 페이지를 열어보시면 Next.js로 돌아가는 앱을 확인하실 수 있습니다.

#### Step 9: 정리하기

이제 Vite와 관련된 파일과 패키지를 제거하면 끝입니다.

* `main.tsx` 삭제하기
* `index.html` 삭제하기
* `vite-env.d.ts` 삭제하기
* `tsconfig.node.json` 삭제하기
* `vite.config.ts` 삭제하기
* Vite와 연관된 패키지들 제거하기
