CRA에서 프로덕션급 Next.js로: 첫 마이그레이션 실전 가이드
2025년 기준 Create React App(CRA)에서 프로덕션 환경에 적합한 Next.js로 마이그레이션하는 단계별 실전 가이드입니다. 왜 옮겨야 하는지, 어떻게 구조를 잡고 진행해야 하는지, 어떤 점을 주의해야 하는지를 다룹니다.
CRA에서 프로덕션급 Next.js로: 첫 마이그레이션 실전 가이드
몇 년 전 Create React App(CRA)으로 애플리케이션을 만들었다면, 당신만 그런 게 아닙니다. 그리고 이제 그 셋업이 점점 답답하게 느껴지는 것도 당신만의 일이 아닙니다.
2025년 현재, **Next.js는 프로덕션급 React 애플리케이션의 사실상 표준(de facto standard)**이 되었습니다. 특히 다음을 신경 쓴다면 더 그렇습니다.
- 성능
- SEO와 공유 가능한 페이지
- 확장성과 유지보수성
- 현대적인 풀스택 기능
좋은 소식은 이겁니다. CRA에서 Next.js로의 마이그레이션은 이제 이미 충분히 검증된, 실용적인 경로라는 점입니다. 많은 팀들이 이미—규모가 크고 복잡한 코드베이스도 포함해서—이 마이그레이션을 진행했고, 그 패턴도 상당히 명확해졌습니다. 이 글에서는 이론보다는 실무 관점에 집중해서, CRA에서 프로덕션 준비가 된 Next.js 앱으로 진행하는 첫 마이그레이션 과정을 단계별로 안내합니다.
2025년에 CRA에서 Next.js로 옮겨야 하는 이유
어떻게(how)를 보기 전에, 왜(why)부터 분명히 하고 가는 편이 좋습니다.
1. Next.js는 이제 ‘React 프레임워크’가 아니라 풀스택 플랫폼이다
Next.js는 서버 사이드 렌더링(SSR) React를 위한 프레임워크로 시작했습니다. 하지만 지금은 다음과 같은 기능을 제공하는 풀스택 플랫폼이 되었습니다.
- 파일 기반 라우팅(react-router를 직접 설정할 필요 없음)
- 서버에서의 데이터 패칭(SSR, SSG, ISR)
- API Routes, Server Actions, Edge Functions
- 이미지 최적화, 폰트, 분석(Analytics) 등 내장 기능
- TypeScript와 최신 번들러에 대한 1급 지원
많은 팀에게 이 말은 곧, CRA + Express + 커스텀 SSR + Webpack 설정을 여기저기 이어 붙이는 대신, 하나의 통합된 플랫폼을 쓴다는 의미입니다.
2. 기본값만으로도 더 나은 성능과 SEO
CRA는 **SPA(Single Page Application)**를 빌드합니다. 즉, 모든 렌더링이 자바스크립트가 로드된 이후 클라이언트에서 이뤄집니다. 이 방식은 다음에 좋지 않은 영향을 줄 수 있습니다.
- First Contentful Paint 및 코어 웹 바이탈(Core Web Vitals)
- SEO, 특히 JS 없이 HTML만 볼 때 크롤러가 거의 아무것도 보지 못하는 문제
반면 Next.js는 다음을 기본으로 제공합니다.
- 동적 콘텐츠를 위한 Server-Side Rendering(SSR)
- 빠르고 캐시 가능한 페이지를 위한 Static Site Generation(SSG)
- 하이브리드 접근을 위한 Incremental Static Regeneration(ISR)
실제 마이그레이션 사례들을 보면 페이지 속도, Lighthouse 점수, 검색 순위가 바로 개선되는 경우가 많습니다.
3. 이미 대규모 서비스에서도 검증됨
다양한 오픈 소스 및 상용 애플리케이션이 기존 React 셋업에서 Next.js로 이미 옮겨 갔습니다. 그 과정에서 다음이 증명되었습니다.
- 대형 코드베이스라도 마이그레이션이 충분히 가능하다는 점
- 시간이 지나도 Next.js의 컨벤션이 유지보수에 유리하다는 점
- 모든 걸 한 번에 갈아엎지 않고 점진적으로 Next.js를 도입할 수 있다는 점
당신은 미지의 영역을 탐험하는 것이 아니라, 이미 많은 팀이 먼저 지나간 검증된 경로를 따라가는 셈입니다.
마이그레이션 전략: 전체 재작성 말고, 점진적 도입
마이그레이션을 망치는 가장 빠른 방법은, 이 일을 ‘완전한 재작성(rewrite)’으로 취급하는 것입니다. 대신, **여러 단계(phase)**로 나눠서 생각해야 합니다.
실용적인 접근은 다음과 같습니다.
- 새 Next.js 프로젝트를 부트스트랩한다
app또는pages에서 기존 CRA 구조를 거울처럼 그대로 잡는다- 공유 코드(components, hooks, utilities)를 먼저 옮긴다
- 라우트/페이지를 점진적으로 포팅한다
- 안정화되면 서버 사이드 기능(SSR/SSG, API Routes)을 도입한다
- 마지막으로 프로덕션 환경을 위한 하드닝(env, CI/CD, 모니터링)을 한다
아래에서 단계별로 살펴보겠습니다.
1단계: Next.js 프로젝트 셋업하기
레포지토리 루트(또는 새 레포지토리)에서 다음을 실행합니다.
npx create-next-app@latest
몇 가지 질문을 받게 됩니다. 2025년 기준 프로덕션 환경에서 일반적으로 많이 쓰는 선택은:
- TypeScript: Yes
- ESLint: Yes
- App Router(기본값): Yes — 최신 Next.js 라우팅 시스템입니다.
- Tailwind(선택): 이미 사용 중이거나 유틸리티 CSS를 쓰고 싶다면 Yes
이렇게 하면 적당한 기본값을 가진 Next.js 앱이 scaffolding되어, 바로 쓸 수 있는 상태가 됩니다.
팁: 기존 CRA 코드를 보면서 옮길 수 있도록, 예를 들어
my-app-next처럼 형제 폴더에 생성해두면 편합니다.
2단계: 핵심 차이점 이해하기
파일을 옮기기 전에, 아래와 같은 주요 개념 변화부터 정리해 두는 것이 좋습니다.
1. 라우팅
- CRA: 보통
react-router를 쓰고, 직접Routes를 구성합니다. - Next.js: 라우팅이 파일 기반입니다.
- App Router에서는 라우트가
app/<route>/page.tsx에 위치합니다. - 중첩 폴더는 중첩 라우트를 의미합니다.
- App Router에서는 라우트가
예시:
app/ page.tsx -> / dashboard/ page.tsx -> /dashboard blog/ [slug]/ page.tsx -> /blog/:slug
2. 엔트리 포인트
- CRA:
src/index.tsx에서<App />을 root div에 렌더링합니다. - Next.js:
ReactDOM.render를 직접 호출하지 않습니다. Next가 알아서 처리하고, 우리는 페이지와 레이아웃만 정의합니다.
3. 데이터 패칭
- CRA: 모든 데이터 패칭이 클라이언트에서 일어납니다(
useEffect등). - Next.js: 서버(
page.tsx,layout.tsx, server actions 등)나 클라이언트 어느 쪽에서도 데이터 패칭이 가능합니다.
이 부분이 가장 큰 변화 중 하나이며, 성능과 SEO가 좋아지는 핵심 이유입니다.
3단계: 공유 코드를 먼저 옮기기
라우팅이나 데이터 패칭 방식에 강하게 의존하지 않는 부분부터 시작하세요.
CRA의 src/에서 Next.js의 app/ 또는 상단 src/로 다음과 같은 것들을 옮깁니다.
- UI 컴포넌트 (버튼, 폼, 모달 등)
- Hooks (
useAuth,useTheme등) - 유틸 함수 (포매터, API 클라이언트 등)
- 스타일 (CSS Modules, Tailwind 설정, 글로벌 스타일)
가능하면 import 경로를 최대한 그대로 유지하세요. CRA에서 절대 경로 import를 사용했다면, Next.js의 jsconfig.json 또는 tsconfig.json에서도 동일하게 설정하면 됩니다.
예시 tsconfig.json 경로 설정:
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/components/*": ["./components/*"], "@/lib/*": ["./lib/*"] } } }
4단계: 라우트를 Next.js 페이지로 포팅하기
이제 실제 뷰(페이지)를 옮길 차례입니다.
CRA 라우트를 Next.js 구조에 매핑하기
만약 CRA의 App.tsx가 다음과 같다고 해봅시다.
<Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/products/:id" element={<ProductDetail />} /> </Routes>
Next.js App Router에서는 다음처럼 구성할 수 있습니다.
app/ page.tsx -> Home about/ page.tsx -> About products/ [id]/ page.tsx -> ProductDetail
각 페이지 컴포넌트를 CRA에서 복사해와 해당 page.tsx로 옮기고, import 경로를 필요에 맞게 수정합니다.
공통 UI는 레이아웃으로 사용하기
CRA에서 App.tsx가 모든 페이지를 레이아웃(네비게이션 바, 푸터 등)으로 감싸고 있었다면, 그 역할을 app/layout.tsx로 옮기면 됩니다.
// app/layout.tsx import "./globals.css"; import { ReactNode } from "react"; export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body> <Header /> <main>{children}</main> <Footer /> </body> </html> ); }
이렇게 하면 반복을 줄이고, 앱 전반에 걸친 공통 관심사를 한 곳에 모을 수 있습니다.
5단계: SSR과 SSG를 점진적으로 도입하기
초기에는 동작 변화를 최소화하기 위해 클라이언트 사이드 데이터 패칭을 그대로 유지해도 됩니다. App Router에서 useState, useEffect 같은 훅을 사용하는 페이지나 컴포넌트는 상단에 "use client" 지시어를 추가해 클라이언트 컴포넌트로 표시합니다.
"use client"; export default function DashboardPage() { // 여기에서 클라이언트 훅과 이펙트를 사용 }
기본 동작이 안정화되고 나면, 의미 있는 곳부터 데이터를 서버에서 패칭하는 방식으로 옮겨가면 됩니다.
예시: 데이터가 많은 페이지를 서버 렌더링으로 전환하기
CRA 버전:
function BlogPost() { const { slug } = useParams(); const [post, setPost] = useState<Post | null>(null); useEffect(() => { fetch(`/api/posts/${slug}`) .then(res => res.json()) .then(setPost); }, [slug]); // render }
Next.js(App Router) 서버 렌더링 페이지:
// app/blog/[slug]/page.tsx async function getPost(slug: string) { const res = await fetch(`${process.env.API_URL}/posts/${slug}`, { cache: "no-store", }); if (!res.ok) throw new Error("Failed to fetch post"); return res.json(); } export default async function BlogPostPage({ params, }: { params: { slug: string }; }) { const post = await getPost(params.slug); return ( <article> <h1>{post.title}</h1> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </article> ); }
이제 블로그 포스트에 대한 HTML이 서버에서 생성되므로, SEO와 초기 로드 성능이 눈에 띄게 좋아집니다.
6단계: Next.js를 백엔드 로직에도 활용하기 (API Routes & Server Actions)
Next.js는 렌더링에만 쓰는 도구가 아니라, 말 그대로 풀스택 플랫폼입니다.
기존에 다음과 같은 구조를 썼다면:
- 별도의 Node/Express 서버
- 또는 CRA 프로젝트 안에 임시로 만든 API
이런 엔드포인트 상당수를 Next.js API Routes나 Server Actions로 옮길 수 있습니다.
API Route 예시:
// app/api/posts/[id]/route.ts import { NextResponse } from "next/server"; export async function GET(_: Request, { params }: { params: { id: string } }) { const post = await db.posts.find(params.id); return NextResponse.json(post); }
이렇게 하면 프론트엔드와 백엔드 로직을 하나의 일관된 코드베이스 안에 둘 수 있습니다.
7단계: 프로덕션 환경을 위한 하드닝
프로덕션급 Next.js 앱을 만들려면, 코드가 “돌아가기만 하는 것”으로는 부족합니다.
환경 변수
.env.local, .env.production 등으로 환경 변수를 관리하고, 코드에서는 process.env.MY_KEY 형태로 참조합니다. Vercel, AWS, Netlify 같은 호스팅 플랫폼에도 동일하게 설정해야 합니다.
빌드 & 배포
- CI 파이프라인에
npm run lint,npm run test,npm run build를 추가합니다. - 사용하는 Next.js 기능(SSR, Edge Functions 등)을 호스팅 환경이 지원하는지 확인합니다.
next build출력과 Lighthouse 등을 보면서 번들 크기와 성능을 모니터링합니다.
관측 가능성(Observability)
- Sentry, LogRocket 등으로 로깅과 에러 트래킹을 추가합니다.
- Next.js 내장 Analytics나 자체 분석 도구를 연결합니다.
실제 마이그레이션 사례를 보면, 이 단계까지 완료했을 때 배포 파이프라인이 단순해지고, 성능이 더 예측 가능해졌다는 피드백이 많습니다.
자주 겪는 함정과 피하는 방법
- 클라이언트와 서버 개념을 뒤섞기: App Router를 쓸 때는, 기본이 서버 컴포넌트라는 점을 기억하고, 훅·브라우저 API·이벤트 핸들러가 필요한 컴포넌트에만
"use client"를 붙이세요. - 클라이언트 컴포넌트 남용: 정말 필요한 경우에만 클라이언트 컴포넌트로 만들고, 나머지는 서버 렌더링 상태로 두는 편이 성능에 훨씬 유리합니다.
- SEO와 메타데이터를 무시하기: 각 페이지마다 제목, 설명, Open Graph 정보를 설정하기 위해 Next.js의 Metadata API 또는
<Head>에 해당하는 기능을 적극적으로 활용하세요. - 빅뱅 식 전체 교체: 마이그레이션은 점진적으로 진행하세요. 작은 단위로 옮기고, 검증하고, 다시 다음으로 넘어가는 식으로 진행하는 것이 안전합니다.
마무리: 첫 마이그레이션은 시작일 뿐이다
Create React App에서 Next.js로의 마이그레이션은 더 이상 위험한 실험이 아닙니다. 2025년 현재 기준으로, React 애플리케이션을 만드는 방식과 배포 방식에 발맞추기 위한 검증된 현대화 경로입니다.
구조화된, 점진적인 접근을 따른다면:
- Next.js 앱을 부트스트랩하고
- 공유 컴포넌트와 유틸리티부터 옮기고
- 라우트를 파일 기반 페이지로 포팅하고
- 서버 사이드 렌더링과 정적 생성을 점진적으로 도입하고
- API Routes 및 서버 기능으로 백엔드 로직을 통합하고
- 실제 트래픽을 견딜 수 있도록 프로덕션 환경을 하드닝하는 과정까지 마칠 수 있습니다.
그 결과, 단순히 “돌아가는 React 앱”을 넘어서, 프로덕션에 최적화된 풀스택 플랫폼을 얻게 됩니다. 더 빠르고, 더 잘 확장되며, SEO와 현대적 사용자 경험에 훨씬 잘 맞춰진 아키텍처입니다.
첫 마이그레이션을 통해 패턴을 익히고 나면, Next.js는 단순히 사용하는 도구를 넘어, 앞으로 웹을 위한 React 애플리케이션을 설계하고 생각하는 기본 방식이 될 가능성이 큽니다.