TechFeedTechFeed
Frontend

리다이렉트, NEXT_REDIRECT, 서버 함수 | 저장은 됐는데 화면이 안 바뀌면?

서버 함수의 redirect는 값을 돌려주지 않고 NEXT_REDIRECT를 던집니다. try 안에 두면 카카오 로그인 저장은 되고 화면만 콜백에 남습니다. 이동은 try 밖이거나 catch 첫 줄의 unstable_rethrow 다음이어야 합니다. 303과 307, 프록시 이동과 자리를 가릅니다. Next.js, 프론트, 한국 1인 개발. 2026년 9월 문서.

by

저장은 됐는데 주소가 그대로면, 서버 함수의 redirect가 try 안에서 NEXT_REDIRECT로 잡힌 자리입니다. 이 함수는 값을 돌려주지 않고 오류를 던져, 프레임워크가 이동을 처리하게 합니다.


카카오 콜백에서 사용자 행은 들어갔는데 화면만 그 주소에 남아 있으면, 예외 처리가 그 던지기를 삼킨 것입니다. 자바스크립트 없는 폼 제출은 303이고, 그 외 임시 이동은 307입니다. 영구 이동은 permanentRedirect의 308입니다.


호출은 try 밖으로 빼거나, catch 첫 줄에서 unstable_rethrow로 프레임워크 오류를 다시 던지세요. 설정 파일의 redirects와 프록시의 응답 이동은 렌더 전입니다. 설명은 redirect 문서와 리다이렉트 가이드에 있습니다.


redirect는 돌려주지 않고 던진다

서버 함수와 라우트 핸들러에서 redirect의 반환 타입은 없습니다. 호출 즉시 NEXT_REDIRECT라는 내부 오류가 나고, 넥스트가 그 오류를 보고 응답을 이동으로 바꿉니다. 성공 값을 리턴하는 함수와 같은 칸에 두면 안 됩니다.


return redirect('/home')도 함수가 값을 만들기 전에 던지기 때문에, return 자체는 이동을 막지 않습니다. 막는 것은 return이 아니라 그 호출을 감싼 try입니다. return을 붙여도 try 안이면 catch가 받습니다.


로그에 digest만 보이고 화면이 조용하면, 앱 오류가 아니라 이 내부 오류를 먹어 버린 경우부터 보세요. 없는 글을 500으로 바꾸는 흐름은 not-found와 error 파일 글의 자리입니다. 그쪽은 notFound를 던져 404 칸을 여는 이야기이고, 여기에는 저장 뒤 주소가 안 바뀌는 이야기만 둡니다.


먼저 볼 한 줄 | 행은 있는데 화면만 그대로면, redirect가 try 안에 있는지부터 찾습니다. 데이터베이스를 롤백하는 순서가 아닙니다.


모니터 뒤를 본 빈 작업 책상. 저장 뒤 주소가 안 바뀔 때 서버 함수를 보는 자리
행이 들어갔는데 주소가 그대로면 redirect가 try 안에 있다

try 밖에 두거나 첫 줄에서 다시 던지기

카카오 로그인 콜백을 서버 함수로 닫는 경우를 예로 듭니다. 사용자 행을 넣은 뒤 홈으로 보내야 하는데, 저장과 이동을 한 try에 넣으면 행만 남습니다. catch가 실패 객체를 돌려주면 클라이언트는 그 객체를 성공 응답으로 보고 제자리에 있습니다.


저장 중에 난 진짜 예외만 catch가 받게 하고, 이동은 그 블록이 끝난 다음에 호출하세요. 조건 때문에 이동이 블록 안에 있어야 하면, catch의 첫 줄에서 unstable_rethrow를 부릅니다. 이 함수는 리다이렉트, 영구 리다이렉트, notFound처럼 프레임워크가 쓰는 오류를 다시 던집니다. 그 다음에 앱의 실패 메시지를 돌려주세요.


동적 렌더를 끊는 내부 오류도 같이 다시 던져지므로, 그 줄을 앱 버그 처리보다 앞에 두세요. 쿠키를 정적으로 읽다 빌드가 갈리는 문제와 이 이동 오류를 한 수정으로 묶지 않습니다. 문서 이름은 아직 unstable_rethrow입니다. 공개된 isRedirectError를 직접 만들어 거르지 말고, next/navigation의 이 함수를 쓰세요.


저장만 try, 이동은 밖
'use server' import { redirect } from 'next/navigation' export async function finishKakaoLogin(userId) { try { await saveSession(userId) } catch (err) { console.error('session save failed') return { ok: false } } redirect('/home') }
블록 안에 둘 때는 첫 줄에서 다시 던지기
'use server' import { redirect, unstable_rethrow } from 'next/navigation' export async function finishKakaoLogin(userId) { try { const user = await saveSession(userId) if (!user) redirect('/login') } catch (err) { unstable_rethrow(err) console.error('session save failed') return { ok: false } } }

두 번째 예는 사용자가 없을 때만 로그인으로 보냅니다. redirect가 try 안에 있으므로 unstable_rethrow가 없으면 그 이동도 실패 객체가 됩니다. 첫 예처럼 밖으로 뺄 수 있으면 밖으로 빼는 쪽이 읽기 쉽습니다.


넥스트 16.3의 catchError 경계는 그 경계 안에서 난 리다이렉트를 일반 오류로 삼키지 않도록 되어 있습니다. 파일에 직접 쓴 try는 그 경계가 아닙니다. 경계가 있어도 함수 안의 try는 따로 다시 던져야 합니다. 재던지기 문서는 unstable_rethrow에 있습니다.


  • [ ] 이동 호출이 try 밖이거나, catch 첫 줄이 unstable_rethrow다
  • [ ] 실패 반환은 진짜 저장 예외에만 있다
  • [ ] 행이 생긴 뒤에도 주소가 콜백이면 이 순서를 다시 본다
  • [ ] 로그의 digest를 앱 예외 메시지와 한 줄로 적지 않는다

머리 위로 묶인 네트워크 케이블. 서버 함수의 이동과 저장 예외를 나누는 장면
저장 예외만 잡고, 이동 오류는 프레임워크로 돌려보낸다

303과 307, 히스토리에 남는 방식

같은 redirect라도 누가 부르는지에 따라 브라우저가 받는 상태가 갈립니다. 숫자를 코드에 박기 전에 표를 보세요.


호출브라우저가 보는 것언제
redirect, 그 외307 임시 이동서버 컴포넌트, 라우트 핸들러
redirect, 서버 함수 폼303, 이어서 GET자바스크립트 없이 제출
redirect, 서버 함수클라이언트 이동자바스크립트가 있을 때
permanentRedirect308 영구 이동주소가 앞으로 바뀌지 않을 때

카카오 콜백 폼을 자바스크립트 없이 제출하면 303이어야 다음 요청이 GET이 됩니다. 307로 POST가 반복되면 새로고침마다 저장이 한 번 더 돌 수 있습니다. 스크립트가 있으면 전체 새로고침 대신 클라이언트 이동이 나갑니다. 개발자 도구의 문서 요청만 보고 307이 없다고 실패로 단정하지 마세요.


히스토리 옵션 type의 기본은 서버 함수에서 push, 그 외에는 replace입니다. 서버 컴포넌트 렌더 중의 호출에는 이 옵션이 적용되지 않습니다. 뒤로 가기를 누르면 폼으로 돌아와야 하면 서버 함수의 기본 push를 유지하세요. 로그인 중간 주소를 히스토리에서 지우고 싶으면 replace를 넘깁니다. 영구 이동이 아닌데 permanentRedirect를 쓰면 브라우저와 중간 캐시가 308을 기억합니다. 콜백 주소는 임시이므로 redirect를 쓰세요.


클릭 핸들러와 요청 앞 이동은 다른 칸

클라이언트 컴포넌트의 렌더 중에 redirect를 부르는 것은 허용됩니다. 버튼 onClick 안에서는 허용되지 않습니다. 클릭으로 옮기려면 useRouter의 push나 replace를 쓰세요. 서버 컴포넌트에서 클릭 함수를 자식에게 넘기다 깨지는 문제는 이벤트 핸들러 글입니다. 그 글은 함수를 넘기는 자리이고, 여기서는 서버 함수가 던진 이동을 try가 먹는 자리만 다룹니다.


클릭은 라우터, 서버 함수의 redirect와 분리
'use client' import { useRouter } from 'next/navigation' export function GoHomeButton() { const router = useRouter() return ( <button type="button" onClick={() => router.push('/home')}> 홈으로 </button> ) }

요청이 렌더에 닿기 전에 옮기는 길은 두 가지입니다. next.config의 redirects와, 프록시 파일의 NextResponse.redirect입니다. 둘 다 서버 함수 안의 redirect와 다른 칸입니다. 로그인 여부를 보고 관리자 경로를 막는 일은 proxy.ts 글을 보세요. 파일 이름만 바꾸고 함수가 안 도는 문제와, 저장 뒤에 화면이 안 바뀌는 문제를 한 패치로 고치지 않습니다.


자리쓰는 것try에 물리는가
서버 함수, 라우트 핸들러redirect, permanentRedirect물린다. 밖 또는 재던지기
클라이언트 클릭useRouterredirect를 클릭에 두지 않음
요청 앞설정 redirects, 프록시 응답서버 함수 try와 무관
없는 글notFound역시 던져지므로 재던지기

제출 버튼이 두 번 눌려 행이 두 개 생기는 문제는 이동 오류와 별개입니다. 버튼 비활성은 폼 상태 훅 글에서 다룹니다. 한국에서 혼자 카카오 로그인을 붙이면, 콘솔에는 사용자가 생겼는데 프리뷰 화면만 콜백에 남는 순서가 자주 나옵니다. 콘솔 행을 지우는 대신 서버 함수의 try 범위를 먼저 줄이세요.


스위치에 꽂힌 이더넷 케이블. 렌더 전 이동과 서버 함수 이동을 나누는 장면
프록시와 설정 파일의 이동은 서버 함수 try 밖이다

참고 자료


내부 연계: 이벤트 핸들러, not-found와 error, 폼 이중 제출, proxy.ts.


상태 코드와 재던지기 이름은 2026년 9월 29일 넥스트 문서 기준입니다. 문서 갱신일은 2026년 7월 28일로 적힌 redirect 페이지를 봤습니다.


자주 묻는 질문

return redirect를 쓰면 이동이 되나요?

try 밖이라면 return을 붙여도 던지기는 같습니다. try 안이라면 return을 붙여도 catch가 NEXT_REDIRECT를 받습니다. 막을지 말지는 return이 아니라 try 범위입니다.


데이터베이스에는 사용자가 있는데 화면이 콜백입니다.

저장은 커밋되고 이동만 삼켜진 순서입니다. redirect를 try 밖으로 옮기거나 catch 첫 줄에 unstable_rethrow를 두세요. 행을 지우는 작업은 그 다음에도 주소가 그대로일 때 합니다.


307이 없고 303만 보입니다.

자바스크립트 없는 서버 함수 폼은 303으로 POST를 GET으로 바꿉니다. 스크립트가 있으면 문서 요청 대신 클라이언트 이동이 나갈 수 있습니다. 네트워크 탭의 종류를 먼저 나누세요.


버튼 onClick 안에서 redirect가 실패합니다.

클릭 핸들러에서는 useRouter를 씁니다. redirect는 렌더 중이나 서버 함수, 라우트 핸들러에서 던지는 함수입니다. 서버 컴포넌트에서 클릭 함수를 넘기는 문제와는 파일을 나누는 쪽이 맞습니다.


proxy.ts를 고쳤는데도 저장 뒤 화면이 그대로입니다.

프록시는 요청이 렌더에 닿기 전의 이동입니다. 서버 함수가 행을 넣은 뒤에 던진 NEXT_REDIRECT는 그 파일 밖입니다. 함수의 try를 보세요.


notFound도 같이 삼켜지나요?

notFound도 오류를 던집니다. 같은 catch가 받으면 없는 글이 실패 객체나 500처럼 보일 수 있습니다. unstable_rethrow가 그 오류도 다시 던지므로, 404 칸은 에러 파일 글의 순서로 확인하세요.


화면이 안 바뀌는 서버 함수는 redirect가 던진 NEXT_REDIRECT를 try가 받은 상태입니다. 이동은 블록 밖으로 빼거나, catch 첫 줄에서 unstable_rethrow로 프레임워크에 돌려보내세요. 303과 307, 클릭의 라우터, 프록시의 응답 이동은 각각 다른 칸입니다. 관련 글: not-found, proxy.ts, 이벤트 핸들러.


로그인 콜백에서 사용자 행이 생긴 뒤 주소만 제자리면, 실패 응답을 돌려준 예외 블록이 이동을 받아 먹은 순서입니다. 행을 되돌리기 전에 블록 범위를 줄이세요. 스크립트 없는 폼은 따라오는 요청이 조회로 바뀌어야 새로고침이 저장을 반복하지 않습니다. 스크립트가 있으면 문서 요청이 아니라 화면 안 이동으로 보일 수 있으니 기록 탭의 종류를 먼저 나누세요. 영구 이동은 주소가 앞으로 유지될 때만 쓰고, 로그인 중간 주소에는 임시 이동을 남깁니다. 클릭으로 보내는 버튼은 화면 쪽 이동 도구를 쓰고, 요청이 그려지기 전의 이동은 설정과 앞단 파일에 둡니다. 없는 글을 여는 호출도 같은 방식으로 던져지므로, 예외 블록의 첫 줄에서 프레임워크 오류를 다시 올려 보내야 없음 칸이 열립니다. 저장이 두 번 들어가는 버튼 문제는 이동이 삼켜진 문제와 따로 보세요. 화면이 바뀌지 않는 동안 콘솔의 사용자 행을 성공으로 세면, 다음 배포에서도 같은 블록이 남습니다. 주소가 바뀐 뒤의 새로고침 한 번을 끝난 기준으로 두세요. 중간 주소가 뒤로 가기에 남는지까지 보면 임시 이동과 영구 이동을 잘못 고른 자리가 드러납니다. 주소가 바뀐 요청과 제자리에 남은 요청을 같은 성공으로 적지 않으면, 다음 사람이 행만 보고 끝났다고 판단합니다. 행이 생긴 시각과 주소가 바뀐 시각을 따로 적으면, 이동이 삼켜진 배포를 다음 로그에서 바로 찾을 수 있습니다. 그 두 시각이 같으면 이동까지 끝난 것입니다.


redirectNEXT_REDIRECT서버 함수tryunstable_rethrowNext.js카카오프론트

함께 보면 좋은 문제 해결

EXPLORE / Frontend

이어서 읽어보기

전체 토픽 둘러보기 →