본문으로 건너뛰기

내비게이션 차단

내비게이션 차단은 내비게이션이 발생하지 않도록 하는 방법입니다. 다음과 같은 상황에서 사용자가 내비게이션을 시도할 때 일반적으로 필요합니다.

  • 저장하지 않은 변경 사항이 있습니다.
  • 양식을 작성하는 중입니다.
  • 결제를 진행하는 중입니다.

이러한 상황에서는 다른 곳으로 내비게이션할지 확인하도록 프롬프트나 사용자 지정 UI를 표시해야 합니다.

  • 사용자가 확인하면 내비게이션이 평소처럼 계속됩니다.
  • 사용자가 취소하면 대기 중인 모든 내비게이션이 차단됩니다.

내비게이션 차단은 어떻게 작동하나요?

내비게이션 차단은 전체 기반 history API에 하나 이상의 "차단 요소" 계층을 추가합니다. 차단 요소가 있으면 다음 방법 중 하나로 내비게이션을 일시 중지합니다.

  • 사용자 지정 UI
    • 라우터 수준에서 제어하는 작업으로 내비게이션이 트리거되면 사용자가 동작을 확인할 수 있도록 원하는 작업을 수행하거나 원하는 UI를 표시할 수 있습니다. 각 차단 요소의 blocker 함수를 비동기적으로 순서대로 실행합니다. 차단 요소 함수가 하나라도 해결되거나 true를 반환하면 내비게이션을 허용하고, 다른 모든 차단 요소도 진행이 허용될 때까지 같은 방식으로 계속 실행합니다. 차단 요소 하나라도 해결되거나 false를 반환하면 내비게이션을 취소하고 나머지 blocker 함수는 무시합니다.
  • onbeforeunload 이벤트
    • 직접 제어할 수 없는 페이지 이벤트에는 브라우저의 onbeforeunload 이벤트를 사용합니다. 사용자가 탭이나 창을 닫거나 새로 고치거나 어떤 방식으로든 페이지 자산을 "언로드"하려고 하면 브라우저의 일반적인 "페이지를 나가시겠습니까?" 대화상자가 표시됩니다. 사용자가 확인하면 모든 차단 요소를 우회하고 페이지를 언로드합니다. 사용자가 취소하면 언로드를 취소하고 페이지를 현재 상태로 유지합니다.

내비게이션 차단은 어떻게 사용하나요?

내비게이션 차단을 사용하는 방법은 2가지입니다.

  • 훅/논리 기반 차단
  • 컴포넌트 기반 차단

훅/논리 기반 차단

양식이 수정된 상태라면 내비게이션을 막는다고 가정해 보겠습니다. useBlocker 훅을 사용하면 됩니다.

React

import { useBlocker } from '@tanstack/react-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = useState(false)

useBlocker({
shouldBlockFn: () => {
if (!formIsDirty) return false

const shouldLeave = confirm('Are you sure you want to leave?')
return !shouldLeave
},
})

// ...
}

Solid

import { useBlocker } from '@tanstack/solid-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = createSignal(false)

useBlocker({
shouldBlockFn: () => {
if (!formIsDirty()) return false

const shouldLeave = confirm('Are you sure you want to leave?')
return !shouldLeave
},
})

// ...
}

shouldBlockFn을 사용하면 currentnext 위치에 타입 안전하게 접근할 수 있습니다.

React

import { useBlocker } from '@tanstack/react-router'

function MyComponent() {
// always block going from /foo to /bar/123?hello=world
const { proceed, reset, status } = useBlocker({
shouldBlockFn: ({ current, next }) => {
return (
current.routeId === '/foo' &&
next.fullPath === '/bar/$id' &&
next.params.id === 123 &&
next.search.hello === 'world'
)
},
withResolver: true,
})

// ...
}

Solid

import { useBlocker } from '@tanstack/solid-router'

function MyComponent() {
// always block going from /foo to /bar/123?hello=world
const { proceed, reset, status } = useBlocker({
shouldBlockFn: ({ current, next }) => {
return (
current.routeId === '/foo' &&
next.fullPath === '/bar/$id' &&
next.params.id === 123 &&
next.search.hello === 'world'
)
},
withResolver: true,
})

// ...
}

shouldBlockFnfalse를 반환하더라도 페이지를 새로 고치거나 탭을 닫을 때 브라우저의 beforeunload 이벤트가 여전히 트리거될 수 있습니다. 이를 제어하려면 enableBeforeUnload 옵션을 사용해 beforeunload 핸들러를 조건부로 등록할 수 있습니다.

React

import { useBlocker } from '@tanstack/react-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = useState(false)

useBlocker({
{/* ... */}
enableBeforeUnload: formIsDirty, // or () => formIsDirty
})

// ...
}

Solid

import { useBlocker } from '@tanstack/solid-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = useState(false)

useBlocker({
{/* ... */}
enableBeforeUnload: formIsDirty(),
})

// ...
}

useBlocker 훅에 관한 자세한 내용은 API 레퍼런스에서 확인할 수 있습니다.

컴포넌트 기반 차단

논리/훅 기반 차단 외에도 Block 컴포넌트를 사용해 비슷한 결과를 얻을 수 있습니다.

React

import { Block } from '@tanstack/solid-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = useState(false)

return (
<Block
shouldBlockFn={() => {
if (!formIsDirty) return false

const shouldLeave = confirm('Are you sure you want to leave?')
return !shouldLeave
}}
enableBeforeUnload={formIsDirty}
/>
)

// OR

return (
<Block
shouldBlockFn={() => formIsDirty}
enableBeforeUnload={formIsDirty}
withResolver
>
{({ status, proceed, reset }) => <>{/* ... */}</>}
</Block>
)
}

Solid

import { Block } from '@tanstack/solid-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = createSignal(false)

return (
<Block
shouldBlockFn={() => {
if (!formIsDirty()) return false

const shouldLeave = confirm('Are you sure you want to leave?')
return !shouldLeave
}}
/>
)

// OR

return (
<Block shouldBlockFn={() => !formIsDirty} withResolver>
{({ status, proceed, reset }) => <>{/* ... */}</>}
</Block>
)
}

사용자 지정 UI는 어떻게 표시하나요?

대부분의 경우 훅에서 withResolver: false로 설정하고 shouldBlockFn 함수에서 window.confirm을 사용하면 충분합니다. 사용자에게 내비게이션이 차단되었음을 명확히 알리고 응답에 따라 차단을 해결하기 때문입니다.

그러나 상황에 따라 의도적으로 방해를 줄이고 앱의 디자인에 더 잘 통합된 사용자 지정 UI를 표시하고 싶을 수 있습니다.

참고: withResolvertrue이면 shouldBlockFn의 반환값으로 차단이 해결되지 않습니다.

리졸버를 사용하는 훅/논리 기반 사용자 지정 UI

React

import { useBlocker } from '@tanstack/react-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = useState(false)

const { proceed, reset, status } = useBlocker({
shouldBlockFn: () => formIsDirty,
withResolver: true,
})

// ...

return (
<>
{/* ... */}
{status === 'blocked' && (
<div>
<p>Are you sure you want to leave?</p>
<button onClick={proceed}>Yes</button>
<button onClick={reset}>No</button>
</div>
)}
</>
}

Solid

import { useBlocker } from '@tanstack/solid-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = createSignal(false)

const { proceed, reset, status } = useBlocker({
shouldBlockFn: () => formIsDirty(),
withResolver: true,
})

// ...

return (
<>
{/* ... */}
{status === 'blocked' && (
<div>
<p>Are you sure you want to leave?</p>
<button onClick={proceed}>Yes</button>
<button onClick={reset}>No</button>
</div>
)}
</>
}

리졸버를 사용하지 않는 훅/논리 기반 사용자 지정 UI

React

import { useBlocker } from '@tanstack/react-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = useState(false)

useBlocker({
shouldBlockFn: () => {
if (!formIsDirty) {
return false
}

const shouldBlock = new Promise<boolean>((resolve) => {
// Using a modal manager of your choice
modals.open({
title: 'Are you sure you want to leave?',
children: (
<SaveBlocker
confirm={() => {
modals.closeAll()
resolve(false)
}}
reject={() => {
modals.closeAll()
resolve(true)
}}
/>
),
onClose: () => resolve(true),
})
})
return shouldBlock
},
})

// ...
}

Solid

import { useBlocker } from '@tanstack/solid-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = createSignal(false)

useBlocker({
shouldBlockFn: () => {
if (!formIsDirty()) {
return false
}

const shouldBlock = new Promise<boolean>((resolve) => {
// Using a modal manager of your choice
modals.open({
title: 'Are you sure you want to leave?',
children: (
<SaveBlocker
confirm={() => {
modals.closeAll()
resolve(false)
}}
reject={() => {
modals.closeAll()
resolve(true)
}}
/>
),
onClose: () => resolve(true),
})
})
return shouldBlock
},
})

// ...
}

컴포넌트 기반 사용자 지정 UI

훅과 마찬가지로 Block 컴포넌트는 렌더 props로 동일한 상태와 함수를 반환합니다.

React

import { Block } from '@tanstack/react-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = useState(false)

return (
<Block shouldBlockFn={() => formIsDirty} withResolver>
{({ status, proceed, reset }) => (
<>
{/* ... */}
{status === 'blocked' && (
<div>
<p>Are you sure you want to leave?</p>
<button onClick={proceed}>Yes</button>
<button onClick={reset}>No</button>
</div>
)}
</>
)}
</Block>
)
}

Solid

import { Block } from '@tanstack/solid-router'

function MyComponent() {
const [formIsDirty, setFormIsDirty] = createSignal(false)

return (
<Block shouldBlockFn={() => formIsDirty()} withResolver>
{({ status, proceed, reset }) => (
<>
{/* ... */}
{status === 'blocked' && (
<div>
<p>Are you sure you want to leave?</p>
<button onClick={proceed}>Yes</button>
<button onClick={reset}>No</button>
</div>
)}
</>
)}
</Block>
)
}