2025-01-10

(번역) Next 14 버전과 Next 15 버전의 차이 정리

Table of contents

본문

React 19

  • reactreact-dom의 최소 버전은 19입니다.
  • useFormStateuseActionState로 대체 되었습니다. useFormState는 React 19에서 여전히 사용할 수 있지만 미래에는 삭제 될 예정입니다. pending과 같은 추가적인 프로퍼티들을 포함하는 useActionState 사용을 권장합니다.
  • 이제 useFormStatusdata, method 그리고 action과 같은 추가적인 key들을 포함합니다. 만약 React 19를 사용하고 있지 않다면, pending key만 사용할 수 있습니다.
  • React 19 업그레이드 가이드에 대한 내용을 확인해 보세요.

Tip: 만약 타입스크립트를 사용하고 있다면, @types/react@types/react-dom을 최신 버전으로 업그레이드 하세요.

비동기 Request APIs(Breaking change)

이전에 런타임 정보에 의존적이었던 동기적인 Dynamic APIs는 이제 비동기화 되었습니다:

cookies

비동기 사용을 권장

import { cookies } from 'next/headers';

// Before
const cookieStore = cookies();
const token = cookieStore.get('token');

// After
const cookieStore = await cookies();
const token = cookieStore.get('token');

임시 동기 사용 시

import { cookies, type UnsafeUnwrappedCookies } from 'next/headers';

// Before
const cookieStore = cookies();
const token = cookieStore.get('token');

// After
const cookieStore = cookies() as unknown as UnsafeUnwrappedCookies
// Dev 환경에서 주의 알림이 뜸
const token = cookieStore.get('token');

headers

비동기 사용을 권장

import { headers } from 'next/headers';

// Before
const headersList = headers();
const userAgent = headersList.get('user-agent');

// After
const headersList = await headers();
const userAgent = headersList.get('user-agent');

임시 동기 사용 시

import { headers, type UnsafeUnwrappedHeaders } from 'next/headers';

// Before
const headersList = headers();
const userAgent = headersList.get('user-agent');

// After
const headersList = headers() as unknown as UnsafeUnwrappedHeaders;
// Dev 환경에서 주의 알림이 뜸
const userAgent = headersList.get('user-agent');

draftMode

비동기 사용을 권장

import { draftMode } from 'next/headers';

// Before
const { isEnabled } = draftMode();

// After
const { isEnabled } = await draftMode();

임시 동기 사용 시

import { draftMode, type UnsafeUnwrappedDraftMode } from 'next/headers';

// Before
const { isEnabled } = draftMode();

// After
// Dev 환경에서 주의 알림이 뜸
const { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftMode;

params & searchParams

비동기 Layout

// Before
type Params = { slug: string };

export function generateMetadata({ params }: { params: Params }) {
  const { slug } = params;
};

export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Params;
}) {
  const { slug } = params;
}

// After
type Params = Promise<{ slug: string }>;

export async function generateMetadata({ params }: { params: Params }) {
  const { slug } = await params;
}

export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Params;
}) {
  const { slug } = await params;
}

동기 Layout

// Before
type Params = { slug: string };

export default function Layout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Params;
}) {
  const { slug } = params;
}

// After
import { use } from 'react';

type Params = Promise<{ slug: string; }>;

export default function Layout(props: {
  children: React.ReactNode;
  params: Params;
}) {
  const params = use(props.params);
  const slug = params.slug;
}

비동기 Page

// Before
type Params = { slug: string; };
type SearchParams = { [key: string]: string | string[] | undefined };

export function generateMetadata({
  params,
  searchParams,
}: {
  params: Params;
  searchParams: SearchParams;
}) {
  const { slug } = params;
  const { query } = searchParams;
}

export default async function Page({
  params,
  searchParams,
}: {
  params: Params;
  searchParams: SearchParams;
}) {
  const { slug } = params;
  const { query } = searchParams;
}

// After
type Params = Promise<{ slug: string; }>;
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>;

export async function generateMetadata(props: {
  params: Params;
  searchParams: SearchParams;
}) {
  const params = await props.params;
  const searchParams = await props.searchParams;
  const slug = params.slug;
  const query = searchParams.query;
}

export default async function Page(props: {
  params: Params;
  searchParams: SearchParams;
}) {
  const params = await props.params;
  const searchParams = await props.searchParams;
  const slug = params.slug;
  const query = searchParams.query;
}

동기 Page

'use client'

// Before
type Params = { slug: string; };
type SearchParams = { [key: string]: string | string[] | undefined };

export default function Page({
  params,
  searchParams,
}: {
  params: Params;
  searchParams: SearchParams;
}) {
  const { slug } = params;
  const { query } = searchParams;
}

// After
import { use } from 'react';

type Params = Promise<{ slug: string }>;
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>;

export default function Page(props: {
  params: Params;
  searchParams: SearchParams;
}) {
  const params = use(props.params);
  const searchParams = use(props.searchParams);
  const slug = params.slug;
  const query = searchParams.query;
}

Route 핸들러

// Before
type Params = { slug: string; };

export async function GET(request: Request, segmentData: { params: Params }) {
  const params = segmentData.params;
  const slug = params.slug;
}

// After
type Params = Promise<{ slug: string; }>;

export async function GET(request: Request, sementData: { params: Params }) {
  const params = await segmentData.params;
  const slug = params.slug;
}

runtime 설정(Breaking change)

runtime 부분의 설정은 이전에 edgeexperimental-edge를 값으로서 지원해왔습니다. 두 설정값 모두 같은 것을 참조하고 있어 옵션 간소화를 위해 이제 experimental-edge을 사용하면 에러를 발생시킬 것입니다. 따라서 이 에러를 수정하기 위해서 runtimeedge로 변경하기를 권장합니다.

fetch requests

더이상 fetch requests는 기본적으로 캐시하지 않도록 변경됩니다.

특정 fetch requests를 캐싱 상태로 인입하기 위해 cache: 'force-cache' 옵션을 전달할 수 있습니다.

export default async function RootLayout() {
  const a = await fetch('https://...'); // 캐시 되지 않음
  const b = await fetch('https://...', { cache: 'force-cache' }) // 캐시 됨

  // ...
}

한 layout 또는 한 페이지의 모든 fetch requests를 캐싱 상태로 인입하기 위해서는 export const fetchCache = 'default-cache' segment config 옵션을 사용할 수 있습니다. 만약 개별 fetch requests에 cache 옵션을 지정하면, 그 옵션이 대신 사용됩니다.

// 이 layout은 root layout이기 때문에
// 개별 캐시 옵션을 가지고 있지 않은 앱의 모든 fetch requests는 캐시됩니다.
export const fetchCache = 'default-cache';

export default async function RootLayout() {
  const a = await fetch('https://...'); // 캐시 됨
  const b = await fetch('https://...', { cache: 'no-store' }); // 캐시 되지 않음

  // ...
}

Route 핸들러

더이상 Route 핸들러에서 GET 함수는 기본적으로 캐시하지 않게 됩니다. GET 메서드들을 캐싱 상태로 인입하기 위해서는 export const dynamic = 'force-static'과 같은 route config 옵션을 사용할 수 있습니다.

export const dynamic = 'force-static';

export async function GET() {}

Client-side Router 캐시

<Link> 또는 useRouter를 통한 pages 사이에서 navigating(페이지 변경)할 때, page segments는 더이상 client-side router 캐시에서 사용되지 않습니다. 그러나, 브라우저 뒤로가기와 앞으로 가기 그리고 공유 layouts는 여전히 재사용 됩니다.

Page segments를 캐싱 상태로 인입하기 위해, staleTimes config 옵션을 사용할 수 있습니다:

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,
      static: 180,
    },
  },
}

module.exports = nextConfig;

Layoutsloading states는 여전히 캐시되고 navigation에서 재사용 됩니다.

next/font

@next/font 패키지는 내장된 next/font으로 인해 제거됩니다.

// Before
import { Inter } from '@next/font/google';

// After
import { Inter } from 'next/font/google';

bundlePagesRouterDependencies

experimental.bundlePagesExternals는 이제 안정화 되어 bundlePagesRouterDependencies로 명칭이 변경 되었습니다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Before
  experimental: {
    bundlePagesExternals: true,
  },

  // After
  bundlePagesRouterDependencies: true,
}

module.exports = nextConfig;

serverExternalPackages

experimental.serverComponentsExternalPackages는 이제 안정화 되어 serverExternalPackages로 명칭이 변경 되었습니다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Before
  experimental: {
    serverComponentsExternalPackages: ['package-name'],
  },

  // After
  serverExternalPackages: ['package-name'],
}

module.exports = nextConfig;

Speed Insights

Next 15에서는 Speed Insights를 위한 자동 장치들이 제거 되었습니다.

Speed Insights를 계속해서 사용하길 원한다면, Vercel Speed Insights QuckStart 문서를 참고 하세요.

NextRequest Geolocation

이 프로퍼티들은 호스팅 제공자에 의해 얻을 수 있는 값들이기 때문에 NextRequest에서 geoip 프로퍼티들이 삭제됩니다.

만약 Vercel을 사용하고 있다면 @vercel/function에서 geolocationipAddress 함수들로 대체할 수 있습니다:

import { geolocation } from '@vercel/functions';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { city } = geolocation(request);

  // ...
}
import { ipAddress } from '@vercel/functions';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const ip = ipAddress(request);

  // ...
}