Next.js로 마이그레이션: Create React App(CRA) 가이드 정리

1. 개요

이 문서는 기존 Create React App(CRA) 프로젝트를 **Next.js(App Router)**로 옮길 때의 공식 절차를, “Draft Mode 정리”와 같은 형식으로 재구성한 것입니다.

가이드의 목표는:

  • 최대한 빨리 “동작하는 Next.js 앱”을 만든 뒤
  • Next.js의 장점(SSR/SSG/ISR/라우팅/최적화 등)은 점진적으로 채택하는 것

초기 단계에서는 기존 라우터를 당장 바꾸지 않고 SPA 형태로 먼저 올리는 전략을 사용합니다.


2. Step-by-step 마이그레이션

Step 1: Next.js 설치

npm install next@latest

Step 2: Next.js 설정 파일 생성

프로젝트 루트(package.json과 같은 레벨)에 next.config.ts를 만듭니다.

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'export', // SPA 정적 export 모드
  distDir: 'build', // CRA의 build 디렉터리와 맞춤
}

export default nextConfig

output: 'export'는 “정적 export”라서 SSR/API 같은 서버 기능을 사용할 수 없습니다.
서버 기능을 쓰려면 이후에 이 설정을 제거하고 Next.js 서버 모드로 전환합니다.


Step 3: Root Layout 생성

App Router는 Root Layout이 필수입니다.

  1. src/app 디렉터리 생성 (또는 루트에 app을 두어도 됨)
  2. app/layout.tsx 생성
  3. CRA의 public/index.html 내용을 Layout 구조로 옮김
    • body > div#root 부분을 <div id="root">{children}</div>로 교체

예시(초기 상태):

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  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>
  )
}

Step 4: Metadata로 <head> 정리

Next.js는 기본 meta charset/viewport 등을 자동 포함하므로, 중복되는 태그는 제거합니다.
또한, 최종적으로는 Metadata API 형태로 선언하는 것이 권장됩니다.

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

favicon.ico, robots.txt 같은 파일은 app/ 최상단에 두면 자동으로 <head>에 반영됩니다.


Step 5: Styles 적용

  • CSS Modules / Global CSS 모두 지원
  • 전역 CSS는 app/layout.tsx에서 import
// app/layout.tsx
import '../index.css'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

Step 6: 엔트리포인트 페이지 만들기(옵셔널 캐치올)

CRA는 src/index.tsx가 엔트리입니다.
App Router는 라우트 폴더 아래 page.tsx가 엔트리입니다.

초기에는 라우터를 유지하고 SPA처럼 “모든 경로를 한 페이지로” 받아야 하므로, **옵셔널 캐치올 [[...slug]]**를 사용합니다.

  1. app/[[...slug]]/page.tsx 생성
  2. 단일 라우트(/)만 프리렌더되도록 generateStaticParams 설정
// app/[[...slug]]/page.tsx
export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return '...' // 다음 Step에서 교체
}

Step 7: 클라이언트 전용 엔트리포인트 추가

CRA의 App 컴포넌트를 Next.js에서 “완전한 클라이언트 전용(SPA)”로 띄우기 위해 dynamic(..., { ssr: false })를 사용합니다.

// app/[[...slug]]/client.tsx
'use client'

import dynamic from 'next/dynamic'

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

export function ClientOnly() {
  return <App />
}

그리고 page.tsx에서 이 컴포넌트를 사용합니다.

// app/[[...slug]]/page.tsx
import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}

Step 8: 정적 이미지 import 변경

CRA: 이미지 import → “URL 문자열”
Next.js: 이미지 import → “객체”

따라서 기존 <img>를 유지한다면 logo.src를 사용합니다.

import logo from '../public/logo.png'

export default function App() {
  return <img src={logo.src} />
}

또는 /public/logo.png/logo.png로 접근 가능하므로, URL로 직접 참조하는 방법도 있습니다.


Step 9: 환경 변수 마이그레이션

  • CRA에서 클라이언트 노출: REACT_APP_
  • Next.js에서 클라이언트 노출: NEXT_PUBLIC_

즉, REACT_APP_로 시작하는 환경 변수를 NEXT_PUBLIC_로 변경합니다.


Step 10: package.json 스크립트 변경

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "npx serve@latest ./build"
  }
}

그리고 .gitignore에 추가:

.next
next-env.d.ts

이제 실행:

npm run dev

Step 11: CRA 잔여물 정리

삭제/정리 대상 예시:

  • public/index.html
  • src/index.tsx
  • src/react-app-env.d.ts
  • reportWebVitals 관련 코드
  • react-scripts 의존성 제거

3. 추가 고려사항(기능 등가 맞추기)

3.1 CRA homepage 필드

CRA에서 서브패스 배포를 위해 homepage를 썼다면, Next.js에서는 basePath로 대응합니다.

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  basePath: '/my-subpath',
}

export default nextConfig

3.2 CRA Service Worker / PWA

CRA 기본 서비스 워커를 그대로 가져오기보다, Next.js 방식의 PWA 구성을 검토합니다.

3.3 CRA Proxy(package.json의 proxy)

Next.js에서는 rewrites()로 유사한 프록시 동작을 구성할 수 있습니다.

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'https://your-backend.com/:path*',
      },
    ]
  },
}

export default nextConfig

3.4 커스텀 Webpack

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  webpack: (config) => {
    // 필요한 변경 적용
    return config
  },
}

export default nextConfig

커스텀 Webpack을 쓰려면 개발 스크립트에서 next dev --webpack이 필요할 수 있습니다.

3.5 TypeScript 설정

Next.js가 생성하는 next-env.d.tstsconfig.json의 include에 있어야 합니다.

{
  "include": ["next-env.d.ts", "app/**/*", "src/**/*"]
}