> 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로

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와 연관된 패키지들 제거하기


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://book.hajoeun.dev/friendly-next-js/part-2/next.js-2/vite-next.js.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
