개요
Next.js 15 버전이 릴리스 되었습니다. 이번 글에서는 Next.js 15 버전의 변경점을 알아보고, 블로그에 적용해 보겠습니다.
또 나왔다고? 🫨
변경점
지난 2월과 10월에 올라온 두 버전의 RC(Release Candidate)를 거쳐 넥스트의 스테이블한 15 버전이 공식적으로 릴리스 되었습니다. 그 중 몇 가지 변경점을 살펴보겠습니다.
@next/codemod
CLI를 통해 물 흐르듯 업그레이드하기
Next.js 15 버전에서는 @next/codemod
CLI를 통해 자동으로 코드베이스를 변경할 수 있게 되었습니다.
npx @next/codemod@canary upgrade latest
위와 같은 명령어를 통해 최신의 스테이블 버전 또는 프리 릴리스 버전으로 코드베이스를 업그레이드할 수 있습니다.
비동기 요청 APIs (Breaking Change)
전통적인 SSR 방식에서는 서버가 컨텐츠를 렌더링 하기 전에 요청을 기다렸습니다. 하지만, 모든 컴포넌트들이 요청 데이터에 의존하지 않으므로, 이러한 기다림은 불필요합니다. 이상적으로, 서버는 요청이 도착하기 전에 할 수 있는 모든 준비를 끝마쳐야 합니다. 이러한 준비를 마치기 위해, 우리는 언제 요청을 기다릴지 알 수 있어야 합니다.
그러므로, 특정 요청에 의존하는 데이터(headers
, cookies
, params
, searchParams
)들은 비동기 API로 전환이 필요합니다.
앞서 추가된 @next/codemod
CLI를 사용하면 코드베이스를 쉽게 마이그레이션할 수 있습니다.
npx @next/codemod@canary next-async-request-api .
해당 CLI를 통해 코드 변경을 진행하면, 다음과 같이 변경됩니다.
// as-is
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata | undefined> {
// params await 없이 사용
const post = getAllPosts().find((post) => post.slug === params.slug);
// ...
}
//to-be
export async function generateMetadata(props: {
params: Promise<{ slug: string }>;
}): Promise<Metadata | undefined> {
const { slug } = await props.params;
const post = getAllPosts().find((post) => post.slug === params.slug);
// ...
}
캐싱 시멘틱
넥스트의 앱 라우터는 최상의 성능을 위해 캐시를 기본적으로 사용하며 필요에 의해 설정을 해제할 수 있습니다.
15 버전에서 GET
라우트 핸들러와 클라이언트 라우터 캐시는 기본적으로 캐시를 사용하지 않도록 변경되었습니다.
GET
라우트 핸들러는 더 이상 기본적으로 캐시 되지 않습니다.
넥스트 14에서 GET
메서드를 사용하는 라우트 핸들러는 동적 함수와 동적 옵션을 사용하여도 기본적으로 캐시 되었습니다.
이제 15 버전에서는 GET
라우트 핸들러는 기본적으로 캐시 되지 않습니다.
만약 캐시를 사용하고 싶다면, 정적 라우트 설정인 export dynamic = "force-static"
를 통해 캐시를 사용할 수 있습니다.
메타 데이터 파일과 관련된 핸들러(sitemap.ts
, opengraph-image
...)의 경우는 여전히 캐시됩니다.
클라이언트 라우터 캐시는 더 이상 기본적으로 페이지 컴포넌트를 캐시 되지 않습니다.
넥스트 14.2 버전에서 라우터 캐시를 위한 staleTimes
플래그가 추가되었습니다.
넥스트 15에서 해당 플래그는 여전히 유효하지만, 기본 동작은 0
으로 변경되었습니다.
앱을 이동할 때, 네비게이션을 통해 활성화된 클라이언트는 항상 페이지 컴포넌트의 최신 데이터를 반영합니다.
그러나, 여전히 변함없이 유지되는 중요한 동작들이 있습니다:
- 공유된 레이아웃 데이터는 서버에서 리페치되지 않으며, partial rendering을 지원합니다.
- 앞/뒤 네비게이션은 브라우저가 스크롤 포지션을 보존하도록 캐시를 사용합니다.
loading.js
는 5분 동안 캐시 됩니다.
next.config
를 통해 클라이언트 라우터 캐시 동작을 제어할 수 있습니다:
const nextConfig = {
experimental: {
staleTimes: {
dynamic: 30,
},
},
};
export default nextConfig;
리액트 19
리액트 19는 아직 RC 상태이지만, 넥스트 팀은 리액트 팀과 긴밀한 협업을 통해 리액트 팀으로부터 리액트 19의 안정성에 대한 확신을 받았으며 넥스트 15 버전에서 리액트 19를 적용해 릴리스하게 되었습니다.
리액트 컴파일러 (Experimental)
리액트 컴파일러는 리액트의 실험적인 컴파일러로 수동으로 메모이제이션하던(useMemo
, useCallback
) 작업을 자동으로 처리해줍니다.
npm install babel-plugin-react-compiler
const nextConfig = {
experimental: {
reactCompiler: true,
},
};
export default nextConfig;
리액트 컴파일러를 사용하기 위해선, 바벨 플러그인을 설치해야 합니다. 이는 느린 개발 환경과 빌드 시간을 유발할 수 있습니다.
Turbopack Dev
next dev --turbo
는 스테이블하며 빠른 개발 환경을 제공할 수 있게 되었습니다.
실제 블로그 프로젝트에 적용한 결과:

as-is
터보팩 미적용 시 빌드 시간

to-be
터보팩 적용 시 빌드 시간
실제 블로그에 적용한 결과, 빌드 1230ms
-> 547ms
로 빌드 시간이 절반으로 줄어들었습니다.
정적 라우트 인디케이터
개발 환경에서 넥스트 15는 정적 라우트 인디케이터를 제공합니다. 해당 인디케이터를 통해, 각 라우트가 정적인 라우트인지 동적인 라우트인지 확인할 수 있습니다.

서버 액션(함수) 보안 향상
서버 액션은 클라이언트에서 직접 호출할 수 있는 서버 측 함수입니다.
함수는 use server
지시어를 갖는 파일에 정의되어 있으며, 비동기 함수로 사용됩니다.
서버 액션 또는 유틸리티 함수가 중요하지 않더라도, 해당 코드는 공개적으로 HTTP 엔드 포인트를 노출합니다. 이런 동작은 기술적으로 올바르지만, 의도치 않게 해당 함수가 노출될 수 있습니다.
보안을 향상하기 위해, 다음과 같은 변경이 이루어졌습니다:
- Dead code elimination: 사용되지 않는 서버 액션의 ID가 클라이언트 자바스크립트 번들에 노출되지 않으며, 번들 크기가 줄어들고 성능이 향상됩니다.
- Secure action IDs: 넥스트는 클라이언트에서 서버 액션이 참조되고 호출될 수 있도록 추측할 수 없는 비결정적인 ID를 생성합니다.
개발 환경 및 빌드 개선
서버 컴포넌트 HMR
개발 환경에서, 서버 컴포넌트는 저장 시 재호출됩니다. 이는 fetch
함수 및 서드 파티 서비스 역시 호출된다는 것을 뜻합니다.
개발 환경의 성능과 API 호출 비용을 줄이기 위해, HMR이 이전 렌더의 fetch
응답 값을 재사용할 수 있도록 보장합니다.
앱 라우터를 위한 빠른 정적 생산
느린 네트워크 요청을 위한, 정적 생산의 빌드 시간을 최적화합니다.
이전 방식은 클라이언트 사이드 네비게이션을 위한 데이터 생산과 초기 페이지 방문 시 HTML 렌더링을 위해 두 번의 렌더링이 필요했습니다. 이제, 첫 번째 렌더를 재사용 하여, 작업 부하와 빌드 시간을 줄일 수 있게 되었습니다.
추가적으로, 정적 생산 워커들은 fetch
캐시를 페이지 간 공유합니다.
이를 통해 동일한 데이터를 여러 번 요청하는 것을 방지할 수 있습니다.
향상된 정적 생산 제어 (Experimental)
또한 다양한 사용 사례에 대한 정적 생산 제어를 위한 실험적인 기능이 추가되었습니다.
넥스트 팀은 추가적인 리소스 사용과 메모리 누수를 유발할 수 있으니, 특별한 요구 사항이 없을 경우 기본 설정을 사용하는 것을 추천한다고 합니다.
const nextConfig = {
experimental: {
// 넥스트가 실패한 정적 페이지를 몇 번 재시도할 것인지
// 빌드 실패 전
staticGenerationRetryCount: 1
// 워커 당 처리할 최대 페이지 수
staticGenerationMaxConcurrency: 8
// 새로운 워커가 돌기 전에 각 워커가 처리할 최소 페이지 수
staticGenerationMinPagesPerWorker: 25
},
}
export default nextConfig;
이외에도 추가적인 변경점들은 공식 블로그의 릴리즈 노트에서 확인할 수 있습니다.