보호된 경로
앱의 일부는 로그인한 사용자에게만 보여야 합니다. 대시보드, 계정 페이지, 개인 데이터를 가져오는 어디든 말입니다. React Router에는 전용 "보호된 경로" 기능이 없으며, 실제로 필요하지도 않습니다. 라우팅과 중첩 경로의 개념들이 함께 작동하여 이 패턴을 만듭니다. 사용자가 로그인했는지 확인하고 자신의 자식을 렌더링하거나 로그인 페이지로 리다이렉트하는 레이아웃 경로입니다.
코드를 작성하기 전에 명확히 할 점이 하나 있습니다. 클라이언트에서 경로를 보호하는 것은 사용자 경험 기능입니다. 로그인하지 않은 방문자가 자신의 데이터를 가져오려다 깨진 빈 화면을 보는 것을 방지합니다. 하지만 이것은 보안이 아닙니다. 클라이언트 번들의 모든 것은 검사 가능하고 우회 가능하기 때문입니다.
실제 보호는 서버에서 일어나며, 인증되지 않은 요청에는 데이터를 제공하지 않아야 합니다. 인증에서는 그 강제가 어떻게 작동하는지 다룹니다. 이 장은 클라이언트 부분에 관한 것입니다.
auth-required 레이아웃 경로
레이아웃 경로는 공유 UI와 자신의 자식을 위한 Outlet을 렌더링하는 부모 경로입니다. 경로 없는 레이아웃 경로는 URL 세그먼트를 전혀 추가하지 않습니다. 순전히 감싸기 위해 존재합니다. 이것이 로그인 확인을 하기에 완벽한 장소입니다. 보호하려는 모든 경로를 경로 없는 레이아웃 경로로 감싸면, 유일한 역할은 Outlet이 렌더링될지 결정하는 것입니다.
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="login" element={<Login />} />
<Route element={<AuthRequired />}>
<Route path="host" element={<Dashboard />} />
<Route path="host/vans" element={<HostVans />} />
<Route path="host/vans/:id" element={<HostVanDetail />} />
</Route>
</Route><Route element={<AuthRequired />}> 안에 중첩된 모든 것은 이제 보호 뒤에 있습니다. host/vans/:id 같은 매개변수가 있는 경로도 포함됩니다. 나중에 새 페이지를 보호하는 것은 한 줄입니다. 그 경로를 래퍼 안으로 옮기면 됩니다.
보호장치 자체는 두 가지 분기를 가집니다. 사용자가 로그인했으면 Outlet을 렌더링하여 일치하는 자식이 나타나도록 합니다. 그렇지 않으면 대신 사용자를 로그인 페이지로 보내는 것을 렌더링합니다. 아무것도 렌더링하지 않는 것이 핵심입니다. 보호된 컴포넌트가 렌더링되지 않으면 그 안의 모든 데이터 가져오기도 시작되지 않습니다.
import { Outlet, Navigate } from 'react-router-dom'
export default function AuthRequired() {
const authenticated = false // 실제 세션 확인을 위한 자리 표시자
if (!authenticated) {
return <Navigate to="/login" />
}
return <Outlet />
}이 과정은 확인을 boolean으로 가짜로 처리하고, 나중에 localStorage의 값으로 처리하여 라우팅에 집중할 수 있게 합니다. 실제 확인은 세션 상태를 읽으며, 보통 context를 통해 제공됩니다. 다시 말해, 인증에서는 그 상태가 실제로 어디서 오는지 다룹니다.
버전 참고
여기의 예제는 v6과 v7 모두에서 작동하는 react-router-dom에서 import합니다. 라우팅에서는 v7이 패키지에 어떤 변화를 주었는지 다룹니다. 이 섹션 전체에서 이 과정은 v6을 가르칩니다.
Navigate 컴포넌트
Link는 앵커를 렌더링하고 클릭될 때까지 기다립니다. Navigate는 기다리는 것을 건너뜁니다. 렌더링되는 순간, 라우터는 사용자를 to 경로로 이동시킵니다. 이것이 렌더 로직을 위한 리다이렉트 도구를 만드는 것입니다.
컴포넌트는 렌더 중간에 사용자가 여기에 있으면 안 된다고 결정하고 UI 대신 <Navigate to="/login" />을 반환합니다. 또한 이벤트 핸들러에서 리다이렉트하기 위한 함수를 반환하는 useNavigate 훅도 있습니다. 로그인 폼 자체가 성공적인 제출 후 사용자를 다음으로 보내는 방법입니다.
아무 설명 없이 맨 로그인 페이지에 도착하는 것은 혼란스럽습니다. 보호장치는 사용자가 거기 끝나게 된 이유를 말해야 합니다. Navigate는 Link와 같은 state prop을 받으므로, 리다이렉트는 목적지에 이유를 전달할 수 있습니다.
if (!authenticated) {
return (
<Navigate
to="/login"
state={{ message: 'You must log in first' }}
/>
)
}import { useLocation } from 'react-router-dom'
function Login() {
const location = useLocation()
return (
<>
{location.state?.message && <h3>{location.state.message}</h3>}
<h1>Sign in to your account</h1>
{/* form... */}
</>
)
}옵셔널 체이닝이 중요합니다. 왜냐하면 로그인 페이지로 직접 클릭하는 사람은 보호장치를 통과하지 않았으므로 location.state가 null로 설정되어 있기 때문입니다.
히스토리 스택과 replace
지금까지의 패턴에는 느낄 수 있는 버그가 있습니다. 로그인하지 않은 상태에서 보호된 페이지를 방문하고, 리다이렉트되어, 로그인한 후 브라우저의 뒤로 가기 버튼을 누릅니다. 모든 일이 일어나기 전에 있던 페이지 대신, "먼저 로그인해야 합니다"라는 메시지와 함께 로그인 페이지에 다시 도착합니다.
브라우저는 히스토리 스택을 유지합니다. 모든 네비게이션은 새 항목을 푸시하고, 뒤로 가기는 이전 항목으로 팝합니다. 리다이렉트는 보호된 페이지 바로 뒤에 /login을 스택에 푸시했고, 로그인 폼의 자신의 네비게이션은 그 위에 보호된 페이지를 푸시했습니다. 뒤로 가기는 당신을 남겨진 것 바로 안으로 걷게 합니다.
해결책은 새로 푸시하는 대신 현재 항목을 바꾸는 리다이렉트를 만드는 것입니다. Navigate 컴포넌트에는 replace prop이 있습니다. useNavigate 함수에는 옵션입니다.
// AuthRequired에서: 리다이렉트는 자신을 차단된 페이지로 교체합니다
return <Navigate to="/login" state={{ message: 'You must log in first' }} replace />
// Login에서 성공적인 제출 후
navigate('/host', { replace: true })둘 다 적용하면, 로그인 우회는 히스토리에 남지 않습니다. 보호된 페이지에서 뒤로 가기는 이제 사용자가 이전에 있던 곳으로 가고, 로그인 페이지는 실수로 다시 방문될 수 없습니다. 좋은 경험 법칙입니다: 사용자가 요청하지 않은 모든 네비게이션(이것이 리다이렉트입니다)은 replace해야 합니다.
향하던 곳으로 돌아가기
하나의 거친 모서리가 남아 있습니다. 로그인 폼이 navigate('/host', ...)을 하드코딩하므로, /host/vans/2에 도달하려던 사용자는 로그인 후 /host로 떨어집니다. 보호장치는 차단된 URL을 알고 있습니다. AuthRequired 안의 useLocation이 사용자가 렌더링하려던 페이지를 나타내기 때문입니다. 같은 네비게이션 상태에서 함께 전달합니다.
export default function AuthRequired() {
const location = useLocation()
const authenticated = false
if (!authenticated) {
return (
<Navigate
to="/login"
state={{
message: 'You must log in first',
from: location.pathname + location.search + location.hash,
}}
replace
/>
)
}
return <Outlet />
}pathname만으로는 전체 주소가 아닙니다. /host/vans?type=luxury에서 차단된 사용자는 필터된 목록으로 향하고 있었습니다. 쿼리 문자열을 떨어뜨리면 필터가 제거됩니다. search와 hash를 추가하면 라우터가 추적하는 URL의 모든 부분을 재현합니다. 로그인 페이지는 값을 다시 읽습니다. 로그인 페이지로 직접 온 사람은 상태가 없으므로 폴백이 있습니다.
function Login() {
const location = useLocation()
const navigate = useNavigate()
const from = location.state?.from || '/host'
function handleLogin() {
// ...성공하면:
navigate(from, { replace: true })
}
// ...
}이제 보호된 페이지로의 공유 링크는 로그인 우회를 견딥니다. 차단되고, 리다이렉트되고, 로그인했고, 원하던 정확한 URL로 전달됩니다.
Outlet을 렌더링하고 페이지들은 정상적으로 나타납니다. 그들이 그렇지 않으면, Navigate를 렌더링하며, 나타나는 순간 로그인 페이지로 휘감깁니다. replace를 추가하면 뒤로 가기의 메모리에서 리다이렉트를 유지하고, 향하던 곳을 함께 전달하면 로그인 페이지가 나중에 그곳으로 직접 보낼 수 있습니다.
이것은 사용자를 위한 친절입니다. 서버는 여전히 데이터 자체를 보호해야 합니다.
다음: React는 어떻게 렌더링합니까, 다음에 나오는 모든 성능 결정 뒤의 정신적 모델.

