Migrating from Create React App

이 가이드는 기존 Create React App 사이트를 Next.js로 마이그레이션하는 데 도움을 줄 것입니다.

왜 전환해야 할까요?

Create React App에서 Next.js로 전환하려는 이유는 여러 가지가 있습니다:

느린 초기 페이지 로딩 시간

Create React App은 순수한 클라이언트 사이드 React를 사용합니다. 클라이언트 사이드 전용 애플리케이션, 또한 싱글 페이지 애플리케이션(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: './build', // 빌드 출력 디렉터리를 `./dist`로 변경합니다.
}
 
export default nextConfig

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

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

CRA 애플리케이션에서 루트 레이아웃 파일에 가장 가까운 것은 <html>, <head>, <body> 태그를 포함하는 index.html 파일입니다.

이 단계에서는 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 '...'
}

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

index.html 파일의 내용을 이전에 생성한 <RootLayout> 컴포넌트로 복사하고 body.div#rootbody.noscript 태그를 <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" href="%PUBLIC_URL%/favicon.ico" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </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" href="%PUBLIC_URL%/favicon.ico" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

알아두면 좋은 점: Next.js는 CRA의 public/manifest.json 파일, 추가 아이콘(단, favicon, icon, apple-icon은 제외), 및 테스트 구성을 무시하지만, 이러한 요소들이 필요하다면 Next.js도 이러한 옵션을 지원합니다. 자세한 내용은 Metadata APITesting 문서를 참조하세요.

4단계: 메타데이터

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.React
 
  Node
}) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </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" href="%PUBLIC_URL%/favicon.ico" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

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>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
app/layout.js
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

마지막으로 Next.js는 Metadata API를 사용하여 나머지 <head> 태그를 관리할 수 있습니다. 최종 메타데이터 정보를 내보낸 metadata 객체로 이동합니다:

app/layout.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'React App',
  description: 'Web site created with Next.js.',
}
 
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: 'React App',
  description: 'Web site created with Next.js.',
}
 
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

위의 변경 사항을 통해 index.html에 모든 것을 선언하는 것에서 Next.js의 규칙 기반 접근 방식으로 전환했습니다 (Metadata API). 이 접근 방식은 페이지의 SEO 및 웹 공유 가능성을 더 쉽게 개선할 수 있게 합니다.

5단계: 스타일

Create React App과 마찬가지로 Next.js는 CSS Modules에 대한 내장 지원을 제공합니다.

전역 CSS 파일을 사용하는 경우 app/layout.tsx 파일에 이를 가져옵니다:

app/layout.tsx
import '../index.css'
 
// ...

Tailwind를 사용하는 경우 postcssautoprefixer를 설치해야 합니다:

Terminal
npm install postcss autoprefixer

그런 다음 프로젝트 루트에 postcss.config.js 파일을 생성합니다:

postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

단계 6: 진입점 페이지 생성

Next.js에서는 page.tsx 파일을 생성하여 애플리케이션의 진입점을 선언합니다. CRA의 이와 유사한 파일은 src/index.tsx 파일입니다. 이 단계에서는 애플리케이션의 진입점을 설정합니다.

app 디렉터리에 [[...slug]] 디렉터리를 만듭니다.

이 가이드는 Next.js를 SPA(Single Page Application)로 설정하는 것을 목표로 하고 있으므로 애플리케이션의 모든 가능한 경로를 포착할 수 있는 진입점 페이지가 필요합니다. 이를 위해 app 디렉터리에 새로운 [[...slug]] 디렉터리를 만듭니다.

이 디렉터리는 선택적 모든 경로 세그먼트라고 불립니다. Next.js는 파일 시스템 기반 라우터를 사용하여 디렉터리를 사용해 라우트를 정의합니다. 이 특별한 디렉터리는 애플리케이션의 모든 경로가 해당 디렉터리의 page.tsx 파일로 이동하도록 보장합니다.

app/[[...slug]] 디렉터리에 새로운 page.tsx 파일을 다음 내용으로 생성합니다:

app/[[...slug]]/page.tsx
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return '...' // 업데이트 예정
}
app/[[...slug]]/page.js
export function generateStaticParams() {
  return [{ slug: [''] }]
}
 
export default function Page() {
  return '...' // 업데이트 예정
}

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

이 파일은 글로벌 CSS를 가져오고 generateStaticParams에게 오직 하나의 경로, 즉 /에서 인덱스 경로를 생성한다고 알려줍니다.

이제 나머지 CRA 애플리케이션을 클라이언트 전용으로 이동합니다.

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

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

클라이언트 전용 애플리케이션을 시작하려면 App 컴포넌트부터 사전 렌더링을 비활성화하도록 Next.js를 구성할 수 있습니다.

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

이제 진입점 페이지를 업데이트하여 새 컴포넌트를 사용합니다:

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

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

Next.js는 정적 이미지 가져오기를 CRA와 약간 다르게 처리합니다. CRA에서는 이미지 파일을 가져오면 해당 공용 URL이 문자열로 반환됩니다:

App.tsx
import image from './img.png'
 
export default function App() {
  return <img src={image} />
}

Next.js에서는 정적 이미지 가져오기가 객체를 반환합니다. 이 객체는 Next.js <Image> 컴포넌트와 함께 직접 사용할 수 있으며, 기존 <img> 태그와 함께 객체의 src 속성을 사용할 수도 있습니다.

<Image> 컴포넌트는 자동 이미지 최적화의 추가 혜택이 있습니다. <Image> 컴포넌트는 이미지의 크기에 따라 결과 <img>widthheight 속성을 자동으로 설정합니다. 이는 이미지가 로드될 때 레이아웃 이동을 방지합니다. 그러나 이는 스타일이 auto로 설정되지 않은 상태에서 하나의 크기만 스타일링된 이미지를 포함한 애플리케이션에 문제를 일으킬 수 있습니다. 스타일이 auto로 설정되지 않으면 크기는 <img> 속성의 값으로 기본 설정되므로 이미지가 왜곡될 수 있습니다.

<img> 태그를 유지하면 애플리케이션의 변경 사항을 줄이고 위의 문제를 방지할 수 있습니다. 이후 로더를 구성하여 이미지를 최적화하거나 자동 이미지 최적화가 있는 기본 Next.js 서버로 이동하여 <Image> 컴포넌트로 마이그레이션할 수 있습니다.

절대 경로로 /public에서 가져온 이미지를 상대 경로로 변환합니다:

// 이전
import logo from '/logo.png'
 
// 이후
import logo from '../public/logo.png'

이미지 객체 대신 src 속성을 <img> 태그에 전달합니다:

// 이전
<img src={logo} />
 
// 이후
<img src={logo.src} />

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

경고: TypeScript를 사용하는 경우 src 속성에 접근할 때 타입 오류가 발생할 수 있습니다. 이를 해결하려면 next-env.d.tstsconfig.json 파일의 include 배열 (opens in a new tab)에 추가해야 합니다. Next.js는 애플리케이션을 단계 9에서 실행할 때 이 파일을 자동으로 생성합니다.

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

Next.js는 CRA와 유사하게 .env 환경 변수를 지원합니다.

주요 차이점은 클라이언트 측에서 환경 변수를 노출하는 데 사용되는 접두사입니다. 모든 REACT_APP_ 접두사가 붙은 환경 변수를 NEXT_PUBLIC_로 변경합니다.

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

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

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "npx serve@latest ./build"
  }
}
.gitignore
# ...
.next
next-env.d.ts

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

단계 10: 정리 작업

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

  • public/index.html 삭제
  • src/index.tsx 삭제
  • src/react-app-env.d.ts 삭제
  • reportWebVitals 설정 삭제
  • CRA 종속성(react-scripts) 제거

번들러 호환성

Create React App과 Next.js는 모두 번들링에 기본적으로 webpack을 사용합니다.

CRA 애플리케이션을 Next.js로 마이그레이션할 때 마이그레이션하려는 커스텀 webpack 구성이 있을 수 있습니다. Next.js는 커스텀 webpack 구성을 제공합니다.

또한 Next.js는 로컬 개발 성능을 향상시키기 위해 next dev --turbo를 통해 Turbopack을 지원합니다. Turbopack은 호환성과 점진적 도입을 위해 일부 webpack 로더도 지원합니다.

다음 단계

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

참고: useParams 훅을 사용하기 위한 정적 내보내기 사용은 현재 지원하지 않습니다 (opens in a new tab).