create-react-app (이하 CRA)은 한동안 React를 시작하는 표준이었습니다. React 공식 문서에서도 해당 도구를 사용하길 권했죠. 현재는 React 공식 문서에서도 Next.js, Remix 등으로 시작하길 권하고 있습니다.
그럼에도 여전히 많은 분들이 CRA로 React를 시작하고 있습니다. 그만큼 편리하고 쉽기 때문이겠죠. 하지만 이 역시 Next.js로 마이그레이션이 필요한 경우가 있습니다. 이번에는 CRA를 Next.js 앱으로 마이그레이션해보겠습니다.
Vite와 달리 CRA는 Next.js 공식 문서에서 마이그레이션 가이드를 제공하지 않고 있습니다. 앞서 배운 Vite 마이그레이션과 Next.js의 설정을 토대로 CRA를 React 앱으로 바꿔봅시다.
Step 1: Next.js 의존성 설치하기
아래의 명령어를 실행해 Next.js 최신 패키지를 설치합니다.
Copy npm install next@latest
Step 2: Next.js 설정 파일 만들기
next.config.js
파일을 만듭니다. 관련 설정에 대해서는 앞서 언급한 <Next.js 설정 읽어보기 - next.config.js> 에서 확인하실 수 있습니다.
Copy /** @type {import('next').NextConfig} */
const nextConfig = {
output : 'export' , // HTML/CSS/JS만 빌드해 Single-Page Application으로 만듭니다.
distDir : './build' , // 빌드된 결과물이 ./build 폴더에 담깁니다.
}
module . exports = nextConfig
Step 3: TypeScript 설정 수정하기
CRA로 시작한 앱의 TypeScript 설정 중 일부를 제거하거나 변경합니다. 특별한 변경이 없었다면 아래와 같은 기본 설정을 가지고 있습니다.
Copy {
"compilerOptions" : {
"target" : "es5" ,
"lib" : [
"dom" ,
"dom.iterable" ,
"esnext"
] ,
"allowJs" : true ,
"skipLibCheck" : true ,
"esModuleInterop" : true ,
"allowSyntheticDefaultImports" : true ,
"strict" : true ,
"forceConsistentCasingInFileNames" : true ,
"noFallthroughCasesInSwitch" : true ,
"module" : "esnext" ,
"moduleResolution" : "node" ,
"resolveJsonModule" : true ,
"isolatedModules" : true ,
"noEmit" : true ,
"jsx" : "react-jsx"
} ,
"include" : [
"src"
]
}
이 파일에서 6가지를 변경합니다. 한 단계씩 수행해봅시다.
include
배열에 문자열 "./build/types/**/*.ts"
와 "./next-env.d.ts"
를 추가합니다.
exclude
필드를 만들고 ["./node_modules"]
를 추가합니다.
moduleResolution
설정을 "bundler"
로 변경합니다.
jsx
설정을 "preserve"
로 변경합니다.
compilerOptions
에 incremental
을 추가하고 true
로 활성화합니다.
compilerOptions
에 plugins
를 추가하고 값으로 [{ "name": "next" }]
를 추가합니다.
결과적으로 아래와 같은 설정이 완성됩니다. 각 설정에 대한 설명은 <Next.js 설정 읽어보기 - tsconfig.json> 에서 확인하실 수 있습니다.
Copy {
"compilerOptions" : {
"target" : "es5" ,
"lib" : [
"dom" ,
"dom.iterable" ,
"esnext"
] ,
"allowJs" : true ,
"skipLibCheck" : true ,
"esModuleInterop" : true ,
"allowSyntheticDefaultImports" : true ,
"strict" : true ,
"forceConsistentCasingInFileNames" : true ,
"noFallthroughCasesInSwitch" : true ,
"module" : "esnext" ,
"moduleResolution" : "bundler" ,
"resolveJsonModule" : true ,
"isolatedModules" : true ,
"noEmit" : true ,
"jsx" : "preserve" ,
"incremental" : true ,
"plugins" : [
{
"name" : "next"
}
]
} ,
"include" : [
"src" ,
"./build/types/**/*.ts" ,
"./next-env.d.ts" ,
] ,
"exclude" : [
"./node_modules"
]
}
Step 4: Root Layout 만들기
Next.js의 App Router에서는 루트 라우트인 app
폴더에 layout
파일이 반드시 필요합니다.
CRA 앱에서는 index.html
파일이 유사한 역할을 합니다. index.html
파일에는 <html>
, <head>
그리고 <body>
태그가 포함되어 있습니다.
총 6단계를 거쳐 index.html
파일을 layout
파일로 변환해봅니다.
app
폴에 layout.tsx
파일을 만듭니다. RootLayout 컴포넌트를 아래와 같이 정의합니다.
Copy export default function RootLayout ({
children ,
} : {
children : React . ReactNode
}) {
return null
}
index.html
에 있는 태그를 앞서 만든 RootLayout
컴포넌트로 붙여넣습니다. 그리고 <body>
태그 안에 있는 값을 <div id="root">{children}</div>
로 대체합니다.
Copy 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" />
< meta name = "theme-color" content = "#000000" />
< meta
name = "description"
content = "Web site created using create-react-app"
/>
< link rel = "apple-touch-icon" href = "%PUBLIC_URL%/logo192.png" />
< link rel = "manifest" href = "%PUBLIC_URL%/manifest.json" />
< title >React App</ title >
</ head >
< body >
< div id = "root" >{children}</ div >
</ body >
</ html >
)
}
Next.js에서는 meta charset과 meta viewport 태그는 이미 포함되어 있습니다. <head>
태그에 있는 값을 지워도 괜찮습니다.
Copy 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" />
< meta name = "theme-color" content = "#000000" />
< meta
name = "description"
content = "Web site created using create-react-app"
/>
< link rel = "apple-touch-icon" href = "%PUBLIC_URL%/logo192.png" />
< link rel = "manifest" href = "%PUBLIC_URL%/manifest.json" />
< title >React App</ title >
</ head >
< body >
< div id = "root" >{children}</ div >
</ body >
</ html >
)
}
favicon.ico
, icon.png
, robots.txt
와 같은 메타데이터 파일을 app
폴더 바로 아래에 두기만하면 <head>
태그에 자동으로 추가됩니다. 지원하는 모든 파일 을 app
폴더로 옮겼다면 <link>
태그 역시 지워도 괜찮습니다. (PWA(Progressive web apps)을 위해 사용하는 manifest.json
파일은 주제에서 벗어나 우선 제외합니다.)
Copy export default function RootLayout ({
children ,
} : {
children : React . ReactNode
}) {
return (
< html lang = "en" >
< head >
// < link rel = "icon" href = "%PUBLIC_URL%/favicon.ico" />
< meta name = "theme-color" content = "#000000" />
< meta
name = "description"
content = "Web site created using create-react-app"
/>
// < link rel = "apple-touch-icon" href = "%PUBLIC_URL%/logo192.png" />
// < link rel = "manifest" href = "%PUBLIC_URL%/manifest.json" />
< title >React App</ title >
</ head >
< body >
< div id = "root" >{children}</ div >
</ body >
</ html >
)
}
Copy import type { Metadata } from 'next'
export const metadata : Metadata = {
title : 'React App' ,
description : 'Web site created using create-react-app' ,
}
export default function RootLayout ({
children ,
} : {
children : React . ReactNode
}) {
return (
< html lang = "en" >
< head >
< meta name = "theme-color" content = "#000000" />
// < meta
// name="description"
// content="Web site created using create-react-app"
// />
// <title>React App</title>
</head>
<body>
<div id = "root" >{children}</ div >
</ body >
</ html >
)
}
남아있는 theme-color
는 viewport
객체를 만들어 옮겨줍니다.
Copy import { Metadata , Viewport } from 'next'
export const metadata : Metadata = {
title : 'React App' ,
description : 'Web site created using create-react-app' ,
}
export const viewport : Viewport = {
themeColor : '#000000' ,
}
export default function RootLayout ({
children ,
} : {
children : React . ReactNode
}) {
return (
< html lang = "en" >
// < head >
// < meta name = "theme-color" content = "#000000" />
// </ head >
< body >
< div id = "root" >{children}</ div >
</ body >
</ html >
)
}
<link rel="stylesheet"/>
태그의 경우 Next.js의 Metadata API에서 지원하지 않습니다. 현재는 기존처럼 <head>
태그에 둬도 무방합니다. 참고1 , 참고2
Step 5: 엔트리포인트 페이지 만들기
CRA 앱에서는 index.tsx
파일이 엔트리포인트(진입점) 역할을 합니다. Next.js에서는 page.tsx
파일이 동일한 역할을 합니다. 이번에는 엔트리포인트 페이지를 만들어봅니다.
지금은 CRA 앱을 Next.js 앱으로 점진적으로 마이그레이션하고 있습니다. 한 번에 모든 걸 바꿨을 때 발생하는 리스크를 줄이기 위함이죠. 때문에 당장은 Next.js의 파일 시스템 기반 라우팅 방식으로 완전히 전환하지 않습니다.
app
폴더에 page.tsx
파일을 만들고 아래와 같은 내용을 정의합니다.
Copy 'use client'
import dynamic from 'next/dynamic'
import '../index.css'
const App = dynamic (() => import ( '../App' ) , { ssr : false })
export default function Page () {
return < App />
}
Next.js에서 클라이언트 컴포넌트는 브라우저로 보내기 전에 서버에서 HTML로 미리 렌더링됩니다. 점진적인 마이그레이션을 위해 <App>
컴포넌트가 서버에서 미리 렌더링되는 것을 막아둬야 합니다. 서버에서 HTML로 미리 렌더링할 수 없는 컴포넌트가 존재할 수 있기 때문입니다.
아래와 같이 dynamic
함수의 ssr
옵션을 false
로 정의해 <App>
컴포넌트가 서버에서 렌더링되지 않도록 합니다.
Copy const App = dynamic (() => import ( '../App' ) , { ssr : false })
Step 6: 정적 이미지 가져오기 방식 수정하기
CRA에서는 정적 이미지를 가져오면 이미지의 URL이 문자열로 반환됩니다.
Copy 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>
컴포넌트로 변경하면 최적화 등에 이점이 있지만 이 역시 점진적인 마이그레이션을 위해 나중으로 미루겠습니다. <img>
태그를 이용해 이미지 가져오기 방식을 수정하겠습니다.
/public
폴더로부터 가져오는 절대 경로로 지정된 이미지 경로를 상대 경로로 변경합니다.
Copy // Before (CRA)
import logo from '/logo.png'
// After (Next.js)
import logo from '../public/logo.png'
src
프로퍼티에 객체를 전달하는 대신 src
값을 전달합니다.
Copy // Before (CRA)
< img src = {logo} />
// After (Next.js)
< img src = { logo .src} />
Step 7: 환경 변수 마이그레이션하기
Next.js와 CRA는 모두 .env 파일로 환경 변수를 정의합니다. 다른 점은 클라이언트 측에 노출되는 환경 변수의 접두사입니다. 마이그레이션을 위해 REACT_APP_
접두사가 붙은 모든 환경 변수를 NEXT_PUBLIC_
로 변경합니다.
Step 8: package.json 스크립트 수정하기
이제 Next.js 앱을 실행시켜볼 수 있습니다. 그 전에 package.json
파일의 scripts
필드를 수정해볼까요.
Copy {
"scripts" : {
"dev" : "next dev" ,
"build" : "next build" ,
"start" : "next start" ,
"lint" : "next lint" ,
}
}
package.json
파일의 eslintConfig
영역을 지우고 npm run lint
명령어를 실행하면 Next.js의 기본 ESLint 설정이 적용된 .eslintrc
파일이 생성됩니다.
다음으로 Next.js와 연관된 몇가지 폴더(.next
)와 파일(next-env.d.ts
)을 .gitignore
에 추가합니다.
Copy # ...
.next
next-env.d.ts
이제 npm run dev
명령어를 실행하고 http://localhost:3000
페이지를 열어보시면 Next.js로 돌아가는 앱을 확인하실 수 있습니다.
next.config.js
파일을 읽을 때 "ReferenceError: module is not defined in ES module scope" 와 같은 에러가 발생했다면 next.config.js
파일의 마지막 줄을 export default nextConfig
로 변경해주세요.
Step 9: 정리하기
이제 CRA와 관련된 파일과 패키지를 제거하면 끝입니다.
Last updated 10 months ago