왜 Next.js 네비게이션이 예상대로 작동하지 않을까
Source: Dev.to

우리는 메뉴 항목을 클릭했을 때 가끔 기대한 페이지로 이동하지 않는 문제를 겪었습니다. 주소 표시줄의 URL은 정상적으로 변경되지만 화면에 표시되는 내용은 업데이트되지 않았습니다. 메뉴를 반복해서 클릭하면 가끔씩 네비게이션이 정상적으로 동작하기도 했습니다. 이 문제는 로컬 개발 환경에서는 나타나지 않았으며, 오직 프로덕션 환경에서만 발생했습니다.
아래는 우리가 발견한 내용과 해결 방법입니다.
이유
Next.js가 “ 클릭 시 RSC 요청을 보냄
App Router에서 “를 클릭하면 전체 페이지를 다시 로드하는 대신 클라이언트‑사이드 내비게이션이 발생합니다. 다음 페이지를 렌더링하기 위해 Next.js는 React Server Components(RSC)와 관련된 내부 요청을 보내어 전환에 필요한 서버‑렌더링 데이터를 가져옵니다. 이를 통해 문서를 전체적으로 다시 로드하지 않고도 페이지를 업데이트할 수 있습니다.
네트워크 탭에서 발견한 내용
내비게이션 중 네트워크 탭을 살펴보면 다음과 같은 응답 헤더를 확인할 수 있습니다:
x-middleware-rewrite: /rate-limit-error?retryAfter=1&_rsc=xxxx
x-nextjs-rewritten-path: /rate-limit-error
x-middleware-rewrite– 미들웨어가 내부 RSC 요청을rate-limit-error라우트로 재작성했습니다.x-nextjs-rewritten-path– 최종적으로 Next.js가/rate-limit-error경로로 요청을 처리했습니다.
즉, 내부 RSC 요청이 레이트‑리밋 오류 페이지로 리다이렉트되고 있었습니다.
문제 흐름
- 사용자가 “를 클릭합니다.
- Next.js가 다음 페이지 데이터를 가져오기 위해 내부 RSC 요청을 보냅니다.
- 레이트‑리밋 미들웨어가 이 내부 요청에도 잘못 레이트‑리밋을 적용합니다.
- 미들웨어가 요청을
/rate-limit-error로 재작성합니다. - 브라우저 URL은 올바르게 유지되지만, 가져온 콘텐츠는 다른 라우트에서 온 것입니다.
- 페이지가 깨지거나 기대한 대로 업데이트되지 않습니다.
Let’s fix
Before
미들웨어가 레이트 제한을 너무 넓게 적용하여 내부 Next.js 요청에까지 영향을 주었습니다.
// Rate limit applied too broadly
const shouldRateLimit = true; // ← too broad
// Incomplete RSC detection
const hasRscParam = requestUrl.includes('_rsc=');
const hasRscHeader = request.headers.get('rsc') === '1';
const isRSCRequest = hasRscParam || hasRscHeader;
// Internal requests were not excluded
if (shouldRateLimit && isRateLimited) {
return NextResponse.rewrite(
new URL('/rate-limit-error', request.url)
);
}
After
레이트 제한을 API 라우트와 GET이 아닌 요청에만 적용하고, 내부 Next.js (RSC/라우터) 요청은 명시적으로 제외했습니다.
// Rate limit only APIs and non-GET requests
const isApiRoute = pathname.startsWith('/api/');
const isNonGetRequest = request.method !== 'GET';
const shouldRateLimit = isApiRoute || isNonGetRequest;
// Accurate RSC detection
const hasRscParam = requestUrl.includes('_rsc=');
const hasRscHeader = request.headers.get('rsc') === '1';
const hasRscAccept =
request.headers.get('accept')?.includes('text/x-component');
const isRSCRequest = hasRscParam || hasRscHeader || hasRscAccept;
// Exclude internal navigation requests
if (shouldRateLimit && !isRSCRequest && isRateLimited) {
return NextResponse.rewrite(
new URL('/rate-limit-error', request.url)
);
}
향후 유사한 문제를 방지하기 위해
- API 라우트와 GET이 아닌 요청에만 속도 제한을 적용합니다.
- RSC 및 라우터 관련 요청과 같은 내부 Next.js 요청은 미들웨어 로직에서 명시적으로 제외합니다.
- 특히 클라이언트‑사이드 네비게이션에서 미들웨어의 rewrite 사용에 주의합니다.
- 네비게이션이 일관되지 않을 경우, Network 탭과 응답 헤더를 확인하여 예상치 못한 rewrite가 있는지 검사합니다.
이러한 관행을 따르면 안정적인 네비게이션을 보장하고 디버깅이 어려운 프로덕션 문제를 방지할 수 있습니다.