- Published on
운영 중인 Next.js를 12에서 14로 단계적으로 마이그레이션하기
- Authors

- Name
- GW
개요
Next.js처럼 빠르게 변화하는 프레임워크를 사용하는 서비스에서는 새로운 기능을 개발하는 것뿐 아니라 기존 프레임워크와 라이브러리를 지속적으로 관리하는 것도 중요합니다.
특히 운영 중인 서비스의 메이저 버전을 올리는 작업은 단순히 package.json의 버전 숫자를 변경하는 것만으로 끝나지 않습니다.
Next.js Version 변경
↓
새로운 Version의 Breaking Change 확인
↓
React / Node.js 등 요구 Version 확인
↓
관련 Package의 호환 Version 확인
↓
기존 Next.js 설정과 API 영향 확인
↓
Build 및 기존 기능 검증
실제로 Next.js 12 기반의 서비스를 운영하면서 13을 거쳐 14까지 단계적으로 마이그레이션한 경험이 있습니다.
이번 글에서는 경험을 바탕으로 운영 중인 Next.js 애플리케이션의 메이저 버전을 올릴 때 무엇을 확인해야 하는지 살펴보겠습니다.
주요 내용은 다음과 같습니다.
- 왜 메이저 버전을 단계적으로 올렸는가?
- Next.js만 업데이트하면 끝나는 문제일까?
- 12 → 13 → 14에서 무엇이 달라졌는가?
- App Router 전환도 함께 해야 할까?
- 기존 서비스의 동작을 어떻게 유지할 것인가?
- 프레임워크 업그레이드로 어떤 개선을 가져올 수 있는가?
단순히 Version만 변경하면 되는 것 아닐까?
가장 단순하게 생각하면 Next.js의 버전 업그레이드는 어렵지 않아 보입니다.
pnpm add next@14
하지만 실제 프로젝트에서는 Next.js 하나만 독립적으로 사용하지 않습니다.
Next.js
├── React
├── React DOM
├── Node.js
├── TypeScript
├── eslint-config-next
└── Next.js와 함께 사용하는 여러 Library
따라서 Next.js의 메이저 버전이 변경되면 프레임워크가 요구하는 Runtime과 관련 Package의 호환성도 함께 확인해야 합니다.
예를 들어 Next.js 공식 Upgrade Guide에서는 12에서 13으로 업그레이드할 때 Next.js뿐 아니라 react, react-dom, eslint-config-next도 함께 업데이트하도록 안내하고 있습니다.
TypeScript를 사용한다면 @types/react, @types/react-dom 역시 함께 확인해야 합니다.
*Next.js - Upgrading: Version 13
Next.js만 Version 변경
↓
끝?
이 아니라,
Next.js Version 변경
↓
Framework 요구사항 확인
↓
React / React DOM Version 확인
↓
Node.js Version 확인
↓
Type Definition 확인
↓
관련 Library의 Peer Dependency 확인
↓
기존 설정과 API의 변경 사항 확인
과정으로 접근할 필요가 있습니다.
즉 프레임워크 마이그레이션은 하나의 Package를 업데이트하는 작업이라기보다 프레임워크를 중심으로 연결된 실행 환경과 의존성을 함께 정비하는 작업에 가깝습니다.
왜 12에서 바로 14로 올리지 않았을까?
Next.js 12에서 14로 이동해야 한다면 처음에는 한 번에 14로 올리는 방법을 생각할 수 있습니다.
Next.js 12
↓
Next.js 14
하지만 메이저 버전 사이에는 각각 별도의 Breaking Change와 요구사항이 존재합니다.
한 번에 여러 메이저 버전을 건너뛰면 문제가 발생했을 때 원인을 추적해야 하는 범위도 넓어집니다.
Next.js 12
↓
Next.js 14
↓
Build Error 발생
↓
12 → 13 변경 때문인가?
13 → 14 변경 때문인가?
React Version 때문인가?
Node.js Version 때문인가?
다른 Library 호환성 때문인가?
↓
확인해야 할 범위 증가
반대로 메이저 버전을 하나씩 올리면 각 단계의 공식 Upgrade Guide를 기준으로 변경 사항을 확인할 수 있습니다.
Next.js 12
↓
12 → 13 Upgrade Guide 확인
↓
Next.js 13
↓
Build / 주요 기능 검증
↓
13 → 14 Upgrade Guide 확인
↓
Next.js 14
↓
Build / 주요 기능 검증
이렇게 하면 문제가 발생했을 때 어느 버전의 변경으로 인해 발생했는지 확인해야 하는 범위를 줄일 수 있습니다.
실제 마이그레이션에서도 한 번에 여러 메이저 버전을 건너뛰기보다 순서대로 버전을 올리고, 각 단계에서 Next.js와 함께 변경해야 하는 관련 Package와 설정을 확인했습니다.
중요한 것은 단순히 버전을 하나씩 올리는 것 자체가 아니라 한 번에 변경되는 범위를 작게 유지하는 것입니다.
12에서 13으로 무엇이 달라졌을까?
Next.js 13은 App Router가 처음 소개된 버전으로 많이 알려져 있습니다.
하지만 기존 Pages Router 기반 애플리케이션을 업그레이드하는 입장에서는 App Router 외에도 확인해야 할 변경 사항이 있었습니다.
공식 Upgrade Guide를 기준으로 대표적인 변경을 간단하게 정리하면 다음과 같습니다.
| 항목 | Next.js 13에서 확인할 변화 |
|---|---|
| React | 최소 Version 18.2.0 |
| Node.js | 최소 Version 16.14.0 |
| Image | 새로운 next/image가 기본 동작 |
| Link | <Link> 내부 <a> 사용 방식 변경 |
| SWC Minify | 기본값 변경 |
target | 기존 Config 제거 |
| Browser | Internet Explorer 기본 지원 대상 제외 |
*Next.js - Upgrading: Version 13
이 중 중요한 점은 각각의 변경이 서로 다른 영역에 영향을 줄 수 있다는 것입니다.
따라서 Upgrade Guide를 읽을 때 단순히 새로운 기능을 확인하기보다 현재 프로젝트에서 사용하고 있는 기능과 설정에 영향을 주는 변경이 있는지 확인하는 것이 중요합니다.
React와 Node.js 요구사항 확인하기
Next.js 13에서는 최소 React 버전이 18.2.0으로 변경되었습니다.
또한 현재 공식 Version 13 Upgrade Guide에서는 최소 Node.js 버전을 16.14.0으로 안내하고 있습니다.
따라서 Next.js만 변경하기 전에 프로젝트의 Runtime과 React 환경이 새로운 요구사항을 만족하는지 먼저 확인해야 합니다.
현재 React Version 확인
↓
현재 Node.js Version 확인
↓
Framework 요구 Version과 비교
↓
필요한 Dependency 함께 Upgrade
특히 여러 개발자와 CI가 동일한 프로젝트를 실행한다면 각 환경에서 사용하는 Node.js Version도 함께 확인해야 합니다.
예를 들어 개발자의 로컬 환경에서는 새로운 Next.js가 정상적으로 실행되더라도, CI에서 더 낮은 Node.js Version을 사용하고 있다면 Build가 실패할 수 있습니다.
개발자 A → Node.js 18 → 정상 실행
개발자 B → Node.js 14 → 실행 문제 발생
CI 서버 → Node.js 14 → Build 실패
이 상태에서 Framework의 최소 Node.js 요구사항이 변경되면 일부 환경에서만 문제가 발생할 수도 있습니다.
따라서 Next.js를 Upgrade할 때는 Framework Version만 변경하는 것이 아니라 로컬 개발 환경과 CI에서 사용하는 Node.js Version도 새로운 요구사항을 만족하는지 함께 확인해야 합니다.
Breaking Change를 모두 수정해야 할까?
공식 문서에 Breaking Change가 있다고 해서 모든 프로젝트에서 해당 변경 작업이 필요한 것은 아닙니다.
예를 들어 Next.js 13에서는 next/image와 next/link의 동작이 변경되었습니다.
하지만 실제 영향 범위는 현재 프로젝트가 해당 API를 어떤 방식으로 사용하고 있는지에 따라 달라집니다.
공식 Upgrade Guide 확인
↓
Breaking Change 발견
↓
현재 프로젝트에서 사용하는 기능인가?
↓
No
→ 영향 없음
Yes
↓
현재 사용 방식이 변경 대상인가?
↓
No
→ 영향 없음
Yes
↓
Migration 필요
이 과정이 중요한 이유는 큰 프로젝트일수록 모든 변경 사항을 무작정 수정하는 것보다 실제 코드에 영향을 주는 변경부터 좁혀가는 것이 안전하기 때문입니다.
예를 들어 공식 문서에서는 next/link의 변경을 자동으로 반영하기 위한 Codemod도 제공합니다.
npx @next/codemod new-link .
Codemod는 프레임워크 API 변경에 맞춰 반복적인 코드 수정을 자동화하기 위한 도구입니다.
다만 Codemod가 있다고 해서 결과를 검증하지 않고 그대로 적용하는 것이 목적은 아닙니다.
Breaking Change 확인
↓
프로젝트 영향 범위 검색
↓
필요하다면 Codemod 활용
↓
변경된 코드 확인
↓
Type Check / Build
↓
기존 기능 검증
결국 Codemod는 변경 작업을 줄여주는 도구일 뿐이며, 적용된 코드가 기존 기능에 문제를 만들지 않는지 직접 확인하고 검증하는 과정이 필요합니다.
13에서 14로 넘어갈 때도 다시 확인해야 한다
Next.js 13에서 정상적으로 동작한다고 해서 바로 14로 버전 숫자만 변경하는 것은 아닙니다.
13 → 14에서도 다시 해당 버전의 Upgrade Guide를 기준으로 변경 사항을 확인해야 합니다.
Next.js 14 공식 Upgrade Guide에서 확인할 수 있는 대표적인 변화는 다음과 같습니다.
| 항목 | Next.js 14에서 확인할 변화 |
|---|---|
| Node.js | 최소 Version 18.17 |
next export | 제거되고 output: 'export' 사용 |
ImageResponse | next/server에서 next/og로 변경 |
@next/font | 제거되고 내장 next/font 사용 |
next-swc | WASM Target 제거 |
*Next.js - Upgrading: Version 14
여기에서도 접근 방식은 같습니다.
Next.js 13에서 정상 동작 확인
↓
14 Upgrade Guide 확인
↓
Runtime 요구사항 확인
↓
Breaking Change 확인
↓
현재 프로젝트의 영향 범위 확인
↓
Next.js 및 관련 Dependency Upgrade
↓
Type Check / Build
↓
주요 기능 검증
즉 12 → 13에서 한 번 검증했다고 끝나는 것이 아니라 각 메이저 버전을 하나의 독립적인 변경 단계로 보고 확인하는 것입니다.
다음으로는 이러한 변경을 운영 중인 서비스에서 어떻게 검증하고 개발 환경의 개선으로 연결할 수 있는지 살펴보겠습니다.
Next.js만 올리면 끝나는 문제가 아니다
앞에서 살펴본 것처럼 프레임워크의 메이저 버전이 변경되면 Next.js 하나만 확인해서는 안 됩니다.
실제 애플리케이션에서는 Next.js를 중심으로 React와 여러 라이브러리가 함께 동작하기 때문입니다.
이때 함께 확인해야 하는 것 중 하나가 Peer Dependency입니다.
Peer Dependency는 해당 라이브러리가 정상적으로 동작하기 위해 프로젝트에 어떤 Package와 Version이 필요한지 알려주는 조건입니다.
예를 들어 어떤 UI 라이브러리가 다음과 같은 Peer Dependency를 가지고 있다고 가정해보겠습니다.
{
"peerDependencies": {
"react": "^17.0.0"
}
}
이는 해당 라이브러리가 프로젝트에 설치된 React 17과 함께 사용하는 것을 전제로 하고 있다는 의미입니다.
기존 프로젝트가 다음과 같은 구성이라면 문제가 없을 수 있습니다.
Next.js 12 + React 17 + UI Library
→ React 17 지원
하지만 Next.js를 Upgrade하면서 React도 18로 변경한다면 상황이 달라집니다.
Next.js Upgrade
↓
React 17 → React 18
↓
기존 UI Library의 Peer Dependency 확인
↓
React ^17.0.0만 지원
↓
새로운 React Version과 호환되는지 확인 필요
즉 Next.js와 React를 Upgrade할 때는 기존에 사용하던 라이브러리들이 변경된 React Version을 지원하는지도 함께 확인해야 합니다.
따라서 package.json에 적힌 Version을 모두 최신으로 만드는 것이 목적이 아니라, 서로 연결된 Dependency가 함께 정상적으로 동작할 수 있는 Version 조합을 만드는 것이 중요합니다.
기존 Pages Router는 유지하고 App Router는 점진적으로 도입하기
Next.js 13에서 App Router가 등장했다고 해서 기존 애플리케이션 전체를 바로 App Router로 전환해야 하는 것은 아닙니다.
Next.js 공식 문서에서도 Next.js 13으로 업그레이드하는 것과 App Router로 마이그레이션하는 것은 별개의 작업으로 설명하고 있습니다.
또한 app 디렉터리는 기존 pages 디렉터리와 동시에 사용할 수 있도록 설계되어 있어, 기존 Pages Router를 유지하면서 새로운 페이지부터 App Router를 점진적으로 도입할 수 있습니다.

위 그림처럼 기존 pages 디렉터리를 유지하면서 일부 Route부터 app 디렉터리로 이동할 수 있습니다.
따라서 프레임워크 업그레이드와 Router 전환을 한 번에 진행하지 않고 서로 다른 작업으로 나누어 검증할 수 있습니다.
*Next.js - Migrating from Pages to App Router
기존 Pages Router 기반 서비스
↓
Next.js Framework Version 먼저 Upgrade
↓
기존 pages 디렉터리는 그대로 유지
↓
기존 기능이 정상적으로 동작하는지 검증
↓
신규 페이지 또는 일부 Route부터 app 디렉터리에 구현
↓
검증하면서 필요한 영역을 점진적으로 App Router로 이동
이 방식은 Framework Upgrade와 Router Migration이라는 서로 다른 변화를 한 번에 진행하지 않도록 도와줍니다.
1. Framework Migration
Next.js 12
↓
Next.js 13
↓
Next.js 14
2. Router Migration
Pages Router
↓
App Router
두 작업을 동시에 진행하면 단순히 Routing 방식만 달라지는 것이 아닙니다.
App Router에서는 기존 Pages Router와 비교해 여러 개념이 함께 변경됩니다.
Pages Router → App Router
→ Route / Layout 구조 변경
→ Data Fetching 방식 변경
→ Server / Client Component 경계 도입
→ Metadata와 Routing API 변경
따라서 운영 중인 서비스에서는 Framework Version을 먼저 안정화한 뒤 Router는 별도의 마이그레이션으로 다루는 방식이 변경 범위를 관리하기 쉽습니다.
App Router로 전환하면 무엇이 달라질까?
공식 마이그레이션 가이드에서는 pages에서 app으로 이동할 때 여러 변경 사항을 함께 안내하고 있습니다.
모든 내용을 다 살펴보기보다, 기존 Pages Router와 비교했을 때 특히 알아둘 만한 차이를 중심으로 정리하면 다음과 같습니다.
Route와 Layout 구조
Pages Router에서는 파일 자체가 Route를 나타냈습니다.
pages/
├── index.tsx
├── about.tsx
└── posts/
└── [id].tsx
App Router에서는 폴더가 Route Segment를 나타내고, 실제 페이지는 page.tsx와 같은 Special File로 정의합니다.

각 폴더는 URL의 Route Segment에 대응하며, 폴더를 중첩하면 URL 경로도 같은 구조로 중첩됩니다.
app/
├── page.tsx
├── about/
│ └── page.tsx
└── posts/
└── [id]/
└── page.tsx

App Router에서는 폴더를 만드는 것만으로 Route가 외부에 공개되지 않습니다.
해당 Segment에 page.tsx가 있어야 사용자가 접근할 수 있는 페이지가 됩니다.
따라서 Route 폴더 안에 컴포넌트나 스타일 같은 관련 파일을 함께 둘 수도 있습니다.
또한 layout.tsx를 이용해 여러 Route에서 공유하는 UI를 Layout으로 구성할 수 있습니다.
app/
├── layout.tsx
├── page.tsx
└── dashboard/
├── layout.tsx
└── page.tsx
기존 pages/_app.tsx와 pages/_document.tsx가 담당하던 전역 구조 역시 App Router에서는 Root Layout으로 이동합니다.
다만 점진적으로 마이그레이션하는 동안에는 기존 pages Route가 계속 동작해야 하므로 _app과 _document를 바로 제거해서는 안 됩니다.
Migration 진행 과정
Pages Route 존재 → _app / _document 유지
↓
App Router Route 사용 → app/layout.tsx 사용
↓
모든 Pages Router Route Migration 완료 → 기존 _app / _document 제거 검토
Data Fetching 방식
Pages Router에서는 서버 데이터 조회를 위해 getServerSideProps, getStaticProps 같은 별도의 함수를 사용했습니다.
export const getServerSideProps = async () => {
const data = await getData();
return {
props: { data },
};
};
const Page = ({ data }) => {
return <div>{data.title}</div>;
};
export default Page;
App Router에서는 Page와 Layout이 기본적으로 Server Component이기 때문에 Server Component 안에서 직접 비동기 데이터 조회를 수행할 수 있습니다.
const getData = async () => {
const response = await fetch('https://example.com/api/data', {
cache: 'no-store',
});
return response.json();
};
const Page = async () => {
const data = await getData();
return <div>{data.title}</div>;
};
export default Page;
공식 마이그레이션 가이드에서는 기존의 대표적인 Data Fetching API도 다음과 같이 달라진다고 설명합니다.
Pages Router
├─ getServerSideProps
├─ getStaticProps
└─ getStaticPaths
↓ App Router로 전환
App Router
├─ Server Component에서 직접 Data Fetching
├─ fetch의 Cache / Revalidation 사용
└─ generateStaticParams로 동적 경로 생성
즉 App Router로의 전환은 단순한 디렉터리 구조 변경이 아니라 Rendering과 Data Fetching 방식을 함께 변경하는 작업입니다.
Server Component와 Client Component
또 하나의 큰 변화는 App Router의 Component가 기본적으로 Server Component라는 점입니다.
Pages Router에서 작성하던 Component와 비슷하게 useState, useEffect, Event Handler와 같은 Client 기능이 필요하다면 Client Component로 구분해야 합니다.
'use client';
import { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((prev) => prev + 1)}>{count}</button>;
};
export default Counter;
따라서 기존 페이지를 App Router로 옮길 때는
기존 Page Component
↓
Server에서 처리할 수 있는 부분과 Client 기능이 필요한 부분을 확인
↓
필요한 영역만 Client Component로 분리
↓
나머지는 Server Component로 유지
처럼 Component의 실행 위치도 함께 생각해야 합니다.
Next.js 공식 가이드 역시 기존 페이지를 점진적으로 옮길 때 먼저 기존 Page를 Client Component로 이동해 기존 동작을 최대한 유지한 뒤, 점차 비인터랙티브한 영역을 Server Component로 옮기는 방식을 제시합니다.
기존 Pages Router의 Page
↓
기존 Page 로직을 Client Component로 분리
↓
기존 동작을 최대한 유지
↓
새로운 app/page.tsx에서 Client Component 사용
↓
필요한 부분부터 Server Component로 점진적으로 전환
Metadata와 Routing API
Pages Router에서는 페이지의 <head>를 관리할 때 next/head를 사용했습니다.
import Head from 'next/head';
const Page = () => {
return (
<>
<Head>
<title>페이지 제목</title>
</Head>
</>
);
};
App Router에서는 Metadata API를 사용할 수 있습니다.
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '페이지 제목',
};
Routing Hook도 달라집니다.
Pages Router에서는 next/router의 useRouter()를 사용했습니다.
Pages Router
next/router
↓
useRouter()
App Router에서는 next/navigation을 사용합니다.
현재 경로나 Query String이 필요하다면 각각 usePathname(), useSearchParams()를 사용할 수 있습니다.
App Router
next/navigation
↓
useRouter()
usePathname()
useSearchParams()
즉 App Router 전환에서는 기존에 Framework와 밀접하게 연결되어 있던 API의 사용 방식도 함께 확인해야 합니다.
두 Router를 함께 사용할 때 주의할 점
pages와 app을 동시에 사용할 수 있다는 것은 점진적인 마이그레이션에 큰 장점이지만, 두 Router 사이의 Navigation이 완전히 동일하게 동작하는 것은 아닙니다.
Next.js 공식 문서에서는 Pages Router와 App Router가 각각 담당하는 Route 사이를 이동하면 Hard Navigation이 발생하며, next/link의 자동 Prefetch도 Router 경계를 넘어 동작하지 않는다고 설명합니다.
Next.js - Using App Router together with Pages Router
먼저 일반적인 Next.js의 Navigation 방식을 살펴보겠습니다.
Next.js의 <Link>를 이용해 같은 Router 안에서 이동하면 브라우저가 페이지 전체를 새로 불러오지 않고 Client-side Navigation을 수행합니다.
또한 <Link>는 사용자가 이동할 가능성이 있는 Route에 필요한 데이터를 미리 가져오는 Prefetch를 지원합니다.
<Link>가 화면에 표시
↓
이동할 Route를 미리 Prefetch
↓
사용자가 Link 클릭
↓
페이지 전체를 새로 Load하지 않고 Client-side Navigation 수행
반면 Hard Navigation은 브라우저가 이동할 페이지를 새로 Load하는 방식입니다.
예를 들어 점진적인 마이그레이션 과정에서 /products는 Pages Router, /my는 App Router가 담당하고 있다고 가정해보겠습니다.
pages/products.tsx
→ /products
app/my/page.tsx
→ /my
/products에서 다음과 같이 <Link>를 이용해 /my로 이동할 수 있습니다.
import Link from 'next/link';
const ProductsPage = () => {
return <Link href="/my">마이페이지</Link>;
};
export default ProductsPage;
하지만 두 Route를 담당하는 Router가 다르기 때문에 Router 경계를 넘어 이동하게 됩니다.
/products (Pages Router)
↓
<Link>로 /my 이동
↓
Pages Router → App Router 경계를 넘어 이동
↓
Hard Navigation 발생
↓
/my 페이지를 새로 Load
또한 이처럼 Router 경계를 넘어가는 경우에는 next/link의 자동 Prefetch도 동작하지 않습니다.
즉 pages와 app을 함께 사용할 수는 있지만, 두 Router 사이를 이동할 때는 같은 Router 내부에서 이동할 때와 Navigation 방식에 차이가 생길 수 있습니다.
따라서 두 Router를 함께 운영하는 기간에는 Router 사이를 이동할 때의 Navigation 경험까지 함께 확인할 필요가 있습니다.
결국 Next.js 13에서 App Router가 제공되기 시작했다는 이유만으로 기존 Pages Router 전체를 즉시 변경할 필요는 없습니다.
운영 중인 서비스에서는 기존 기능에 미치는 영향을 최소화하면서 새로운 구조를 작은 단위로 도입하고 검증하는 것도 충분히 합리적인 마이그레이션 전략이 될 수 있습니다.
Hard Navigation의 비용 줄이기
Pages Router와 App Router 사이에서 발생하는 Hard Navigation 자체를 없애는 것은 아니지만, 이동할 페이지를 미리 준비하여 페이지 전환에 걸리는 시간을 줄일 수 있습니다.
Vercel에서는 이러한 Hard Navigation의 성능을 개선하는 방법으로 Prefetching과 Prerendering을 소개합니다.
*Vercel - Optimizing hard navigations
Prefetching은 사용자가 이동하기 전에 다음 페이지에 필요한 리소스를 미리 가져오는 방식입니다.
Prerendering은 여기서 더 나아가 다음 페이지를 미리 렌더링하여 바로 보여줄 수 있도록 준비하는 방식입니다.
Hard Navigation이 필요한 Route
↓
다음 페이지를 미리 Prefetch / Prerender
↓
사용자가 Link 클릭
↓
미리 준비한 리소스 또는 미리 렌더링한 페이지 활용
↓
페이지 전환에 걸리는 시간 감소
즉 Hard Navigation을 Soft Navigation으로 바꾸는 것은 아니지만, 페이지 이동 전에 필요한 작업을 미리 수행하여 Hard Navigation으로 인한 지연을 줄일 수 있습니다.
마이그레이션 이후에는 무엇을 확인해야 할까?
Package 설치가 성공하고 개발 서버가 실행된다고 해서 마이그레이션이 끝난 것은 아닙니다.
버전 변경으로 인해 Build 과정이나 Runtime에서 기존과 다른 문제가 발생할 수 있기 때문에 여러 단계에서 확인할 필요가 있습니다.
Dependency 설치
↓
Type Check
↓
Lint
↓
Production Build
↓
개발 환경 실행
↓
주요 사용자 Flow 확인
↓
문제 발견
↓
원인 수정
↓
다시 검증
각 단계는 확인하는 문제가 조금씩 다릅니다.
Type Check
TypeScript를 사용한다면 Next.js, React 또는 함께 사용하는 Library를 Upgrade하면서 API와 Type Definition이 변경되어 기존 코드에서 새로운 Type Error가 발생할 수 있습니다.
Type Definition은 Library의 API를 TypeScript에서 어떤 Type으로 사용해야 하는지 정의한 정보입니다.
따라서 Library의 Version이 변경되면서 함수의 인자나 반환값, Component의 Props와 같은 Type이 변경되면 기존 코드가 새로운 Type 조건과 맞지 않을 수 있습니다.
Dependency Version 변경
↓
Library의 API 또는 Type Definition 변경
↓
기존 코드가 새로운 Type 조건과 맞지 않을 수 있음
↓
Type Check 실행
↓
Type Error 발견
Production Build
개발 서버에서 정상적으로 실행되더라도 Production Build에서 문제가 발견될 수 있습니다.
따라서 next dev가 실행되는지만 확인하는 것이 아니라 실제 Production Build가 정상적으로 완료되는지도 확인해아 합니다.
pnpm build
기존 기능 확인
Build가 성공했다는 것은 애플리케이션의 모든 기능이 기존과 동일하게 동작한다는 의미는 아닙니다.
특히 Routing, Image, Link처럼 Framework와 밀접하게 연결된 기능이나 서비스의 주요 사용자 Flow는 실제 동작도 함께 확인할 필요가 있습니다.
Compile 성공 ≠ 기존 기능 정상 동작 보장
항상 최신 Version을 사용해야 할까?
Framework를 꾸준히 Upgrade하는 것은 중요하지만, 새로운 Version이 나올 때마다 바로 Production 서비스에 적용하는 것이 항상 좋은 선택은 아닙니다.
새로운 Version에는 기능 추가와 성능 개선, Bug Fix 등이 포함될 수 있지만, 동시에 기존 코드와 호환되지 않는 변경이나 새로운 요구사항이 포함될 수도 있습니다.
기대할 수 있는 변화
├── 새로운 기능
├── 성능 개선
├── Bug Fix
└── 생태계 개선
함께 확인해야 할 변화
├── Breaking Change
├── Runtime 요구사항
├── Dependency 호환성
├── 기존 설정
└── 기존 기능에 미치는 영향
결국 중요한 것은 Version 숫자를 최신으로 유지하는 것 자체가 아니라 업그레이드로 얻는 이점과 변경 비용을 함께 판단하는 것입니다.
운영 중인 서비스라면 특히 한 번에 너무 많은 변화를 가져가기보다 변경 범위를 나누고 각 단계에서 검증할 수 있도록 구성하는 것이 안전합니다.
마치며
처음에는 Next.js 12에서 14로 올리는 작업을 단순한 Dependency Upgrade로 생각하기 쉽습니다.
하지만 실제로는 Framework의 요구사항부터 React와 Node.js Version, 관련 Dependency, Breaking Change, 기존 Architecture까지 여러 요소가 서로 연결되어 있었습니다.
Next.js 12
↓
Next.js 13 Upgrade Guide 확인
↓
Breaking Change 확인
↓
React / Node.js / 관련 Dependency 정비
↓
필요한 반복 변경은 Codemod 활용
↓
Next.js 13 Upgrade
↓
App Router 도입 여부 검토
↓
Type Check / Build / 주요 기능 검증
↓
Next.js 14 Upgrade Guide 확인
↓
관련 Dependency와 설정 다시 확인
↓
Next.js 14 Upgrade
이 과정에서는 한 번에 많은 것을 변경하기보다 각 단계의 변경 범위를 나누고 검증하는 방식으로 마이그레이션을 진행했습니다.
특히 Framework Version Upgrade와 App Router 전환을 별개의 작업으로 바라보았습니다.
이를 통해 문제가 발생하더라도 어떤 변경에서 문제가 시작되었는지 확인해야 할 범위를 줄일 수 있었습니다.
결국 Next.js의 메이저 Version을 Upgrade하는 과정은 단순히 package.json의 Version을 변경하는 작업만으로 끝나지 않습니다.
공식 Upgrade Guide를 기준으로 현재 프로젝트에 영향을 주는 변경을 확인하고, 관련 Dependency와 기존 기능을 함께 검증하면서 변경 범위를 관리하는 과정이 중요합니다.