Maru@maru

Dev Hub

Next.js 15 비동기 Request API와 캐싱 정책 변화: 무엇이 바뀌었고 어떻게 대응할까

Next.js가 버전 15로 올라오면서 서버 컴포넌트와 라우트 핸들러를 다루는 방식에 큰 변화를 주었습니다. 그동안 당연하게 동기식으로 읽어오던 쿠키나 헤더, URL 파라미터가 비동기 API로 전환되었고, 개발자들 사이에서 호불호가 크게 갈렸던 기본 fetch 캐싱 정책도 완전히 뒤바뀌었습니다.

앱 라우터 환경에서 웹 서비스를 개발해 온 팀이라면 마이그레이션 과정에서 빌드 에러나 런타임 경고를 마주하게 됩니다. 어떤 배경에서 이런 변화가 생겼고, 실제 코드 작성법은 어떻게 달라지는지 정리해 보겠습니다.

무엇이 달라졌는가

핵심 변경 사항은 두 가지입니다. 첫째는 요청 관련 객체 접근이 Promise를 반환하도록 바뀐 점이고, 둘째는 fetch 요청과 GET 라우트 핸들러의 기본 캐싱이 비활성화된 점입니다.

기존 Next.js 13과 14에서는 cookies()나 headers()를 동기 함수처럼 호출해 바로 값을 꺼냈습니다. 페이지나 레이아웃 컴포넌트가 받는 params와 searchParams prop 역시 평범한 일반 객체였습니다. Next.js 15부터는 이 모든 대상이 Promise 기반으로 동작하므로 await를 붙여서 꺼내야 합니다.

캐싱의 경우 Next.js 14까지는 fetch 요청 시 개발자가 별도 설정을 하지 않으면 자동으로 데이터를 영구 캐싱(force-cache)했습니다. 이 때문에 데이터가 실시간으로 갱신되지 않아 혼란을 겪는 개발자가 많았습니다. Next.js 15는 이 동작을 버리고 웹 표준과 동일하게 매 요청마다 새로 받아오는 no-store를 기본값으로 지정했습니다.

왜 동기 API를 비동기로 바꿨을까

얼핏 보면 코드에 await를 하나 더 추가해야 해서 번거롭게 느껴질 수 있습니다. Vercel 팀이 이 인터페이스를 전면 수정한 이유는 서버 렌더링 최적화와 스트리밍 효율성 때문입니다.

서버가 페이지 렌더링을 시작할 때 특정 컴포넌트가 쿠키나 헤더를 읽으려 하면, 기존 구조에서는 실제 요청 헤더가 완전히 도착할 때까지 서버 렌더링 전체가 대기 상태에 빠졌습니다. 요청 정보가 비동기로 바뀌면 프레임워크는 요청 헤더가 완전히 평가되기 전이라도 정적인 레이아웃이나 데이터가 필요 없는 상위 트리를 먼저 스트리밍할 수 있습니다.

정적 렌더링과 동적 렌더링의 경계를 명확하게 구분하려는 의도도 큽니다. 비동기 핸들러를 사용하면 런타임에 어떤 컴포넌트가 요청 컨텍스트에 의존하는지 정확하게 추적할 수 있어, 향후 도입될 컴파일러 및 부분 사전 렌더링 최적화의 토대가 됩니다.

실제 코드 작성법: cookies()와 params

Next.js 15 환경에서 쿠키를 읽거나 동적 라우트의 파라미터를 다루려면 호출부에 await를 명시해야 합니다.

tsx

Next.js 15에서는 이전 방식으로 const \{ id \} = params;처럼 직접 구조 분해 할당을 시도하면 개발 모드에서 런타임 경고가 출력됩니다. 공식 마이그레이션 코드모드를 실행하면 기존 컴포넌트의 파라미터와 헤더 호출부에 await를 일괄 적용할 수 있습니다.

fetch 기본값 변경과 캐싱 대응 전략

기본 캐싱이 해제되면서 이제 Next.js의 fetch는 브라우저 네이티브 동작과 동일하게 매번 백엔드 서버에 새 요청을 보냅니다. 라우트 핸들러의 GET 요청 역시 정적으로 굳어지지 않고 매번 실행됩니다.

자주 변하지 않는 외부 API 데이터를 호출하거나 이전처럼 강력한 정적 캐싱을 유지하고 싶다면 명시적으로 옵션을 지정해야 합니다.

ts

실시간성이 중요한 대시보드나 회원 전용 페이지에서는 이전처럼 일일이 \{ cache: 'no-store' \}를 붙이지 않아도 되므로 의도치 않은 캐시 오염 버그가 크게 줄어듭니다. 반면 자주 호출되는 공통 마스터 데이터나 콘텐츠 성격의 페이지를 운영한다면 캐시 누락으로 백엔드 트래픽이 늘어날 수 있으니 force-cache 옵션이 필요한지 점검해 보는 것이 좋습니다.

Loading comments…