일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- BFC
- 제어컴포넌트
- parent padding
- DOM
- transition
- accordian
- ignore padding
- 서초구보건소 #무료CPR교육
- QueryClient
- Carousel
- debouncing
- 함수형프로그래밍
- 부모요소의 패딩 무시
- ?? #null병합연산자
- tailwindCSS
- useQueryClient
- 조건부스타일
- 이즐 #ezl #욕나오는 #교통카드
- react
- BlockFormattingContext
- twoarrow
- createPortal
- es6
- 문제해결
- 화살표2개
- 리액트
- CustomHook
- vite
- 부모패딩
- alias설정
- Today
- Total
프론트엔드 첫걸음
[React Router] useBlocker 이해하기 본문
개요
사용자의 입력을 요구하는 서비스에서는 화면을 떠날 때 '떠나시겠습니까?' 를 묻는 경우가 있다.
이 때 useBlocker를 잘 사용하면 더 좋은 코드를 작성할 수 있을것이다.
useBlocker란?
useBlocker는 React Router에서 네비게이션 차단 기능을 제공하는 훅이다.
사용자가 페이지를 이동하려 할 때 특정 조건이 충족되면 네비게이션을 차단할 수 있다.
즉, 페이지 이탈을 방지하는 역할을 한다.
useBlocker 코드
React-Router 에서 useBlocker 코드를 찾아보자.
//1. 매개변수
export function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker {
let { router, basename } = useDataRouterContext(DataRouterHook.UseBlocker);
let state = useDataRouterState(DataRouterStateHook.UseBlocker);
let [blockerKey, setBlockerKey] = React.useState("");
let blockerFunction = React.useCallback<BlockerFunction>(
(arg) => {
//함수아니면(boolean)이면 그대로 반환
if (typeof shouldBlock !== "function") {
return !!shouldBlock;
}
if (basename === "/") {
return shouldBlock(arg);
}
// If they provided us a function and we've got an active basename, strip
// it from the locations we expose to the user to match the behavior of
// useLocation
// 만약 사용자가 함수를 제공했고 활성화된 basename이 있다면,
// useLocation의 동작과 일치하도록 사용자에게 노출하는 위치 정보에서 basename을 제거한다.
let { currentLocation, nextLocation, historyAction } = arg;
return shouldBlock({
currentLocation: {
...currentLocation,
pathname:
stripBasename(currentLocation.pathname, basename) ||
currentLocation.pathname,
},
nextLocation: {
...nextLocation,
pathname:
stripBasename(nextLocation.pathname, basename) ||
nextLocation.pathname,
},
historyAction,
});
},
[basename, shouldBlock]
);
// This effect is in charge of blocker key assignment and deletion (which is
// tightly coupled to the key)
// 이 effect는 blocker 키를 할당하고 삭제하는 역할을 한다(키와 밀접하게 연결되어 있음).
React.useEffect(() => {
let key = String(++blockerId);
setBlockerKey(key);
return () => router.deleteBlocker(key);
}, [router]);
// This effect handles assigning the blockerFunction. This is to handle
// unstable blocker function identities, and happens only after the prior
// effect so we don't get an orphaned blockerFunction in the router with a
// key of "". Until then we just have the IDLE_BLOCKER.
// 이 effect는 blockerFunction을 할당하는 역할을 한다.
// 이는 blockerFunction의 불안정한 함수 식별자를 처리하기 위한 것으로,
// 이전 effect 이후에만 실행되어야 한다.
// 그렇지 않으면 키가 ""인 orphaned(고아 상태의) blockerFunction이
// 라우터에 남게 된다.
// 그 전까지는 IDLE_BLOCKER를 사용한다.
React.useEffect(() => {
if (blockerKey !== "") {
router.getBlocker(blockerKey, blockerFunction);
}
}, [router, blockerKey, blockerFunction]);
// DataRouterContext는 메모이제이션되므로,
// router.state가 아닌 state에서 blocker를 가져오는 것이 더 좋다.
// 이렇게 하면 blocker 상태가 업데이트될 때 올바르게 반영될 수 있다.
// Prefer the blocker from state not router.state since DataRouterContext
// is memoized so this ensures we update on blocker state updates
return blockerKey && state.blockers.has(blockerKey)
? state.blockers.get(blockerKey)!
: IDLE_BLOCKER;
}
매개변수
export function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker
- shouldBlock:
- boolean 값일 경우: true이면 네비게이션 차단, false이면 차단 안 함.
- BlockerFunction일 경우: (arg) => boolean 형태의 함수로, 현재 위치(currentLocation), 이동할 위치(nextLocation), 히스토리 액션(historyAction)을 받아 네비게이션 차단 여부를 결정.
- Blocker는 Block 상태에 대한 타입
타입
declare function useBlocker(
shouldBlock: boolean | BlockerFunction
): Blocker;
type BlockerFunction = (args: {
currentLocation: Location;
nextLocation: Location;
historyAction: HistoryAction;
}) => boolean;
type Blocker =
| {
state: "unblocked";
reset: undefined;
proceed: undefined;
location: undefined;
}
| {
state: "blocked";
reset(): void;
proceed(): void;
location: Location;
}
| {
state: "proceeding";
reset: undefined;
proceed: undefined;
location: Location;
};
interface Location<State = any> extends Path {
state: State;
key: string;
}
interface Path {
pathname: string;
search: string;
hash: string;
}
enum HistoryAction {
Pop = "POP",
Push = "PUSH",
Replace = "REPLACE",
}
- 속성으로 state, location 갖고, 메서드로 reset과 proceed 가짐
- 속성 (Properties)
- state (blocker의 현재 상태)
- unblocked - blocker가 대기 상태이며, 아직 네비게이션을 차단하지 않음.
- blocked - blocker가 네비게이션을 차단한 상태.
- proceeding - blocker가 proceed() 호출을 통해 차단된 네비게이션을 진행하는 중.
- location
- 차단된 네비게이션의 대상 위치(가려고 했던곳)
- blocked 일 때의 location은 가려고 했던곳.
(proceeding 일때의 location은 blocker.proceed() 호출 후 이동 중인 위치.)
- state (blocker의 현재 상태)
* blocked와 proceeding을 분리한 이유
- "이동이 차단된 상태"와 "이동이 진행 중인 상태"를 명확히 구분해야 함.
- UI에서 다르게 처리할 수 있도록 설계 (예: blocked 상태에서는 confirm 창, proceeding 상태에서는 로딩 UI)
- reset()을 호출할 수 있는 상태(blocked)와 필요 없는 상태(proceeding)를 구분
- 메서드 (Methods)
- proceed()
- blocked 상태일 때, blocker.proceed()를 호출하면 차단된 위치로 네비게이션을 진행할 수 있음.
- reset()
- blocked 상태일 때, blocker.reset()을 호출하면 blocker를 다시 unblocked 상태로 되돌려 현재 위치에 머물도록 함.
- proceed()
내부 동작
1) router, basename, state 가져오기
let { router, basename } = useDataRouterContext(DataRouterHook.UseBlocker);
let state = useDataRouterState(DataRouterStateHook.UseBlocker);
- router: React Router의 내부 DataRouter 인스턴스를 가져와서 차단 기능을 수행할 수 있도록 함.
- basename: 현재 라우트의 기본경로 (ex. /app)
- state : 현재 useBlocker의 상태
2) blockerFunction 정의
export type BlockerFunction = (args: {
currentLocation: Location;
nextLocation: Location;
historyAction: NavigationType;
}) => boolean;
let blockerFunction = React.useCallback<BlockerFunction>(
(arg) => {
if (typeof shouldBlock !== "function") {
return !!shouldBlock;
}
if (basename === "/") {
return shouldBlock(arg);
}
// basename이 존재하면, nextLocation과 currentLocation의 pathname에서 basename을 제거
let { currentLocation, nextLocation, historyAction } = arg;
return shouldBlock({
currentLocation: {
...currentLocation,
pathname:
stripBasename(currentLocation.pathname, basename) ||
currentLocation.pathname,
},
nextLocation: {
...nextLocation,
pathname:
stripBasename(nextLocation.pathname, basename) ||
nextLocation.pathname,
},
historyAction,
});
},
[basename, shouldBlock]
);
- shouldBlock이 boolean이면 그대로 반환.
- shouldBlock이 함수이면 BlockerFunction으로 실행.
- { currentLocation, nextLocation, historyAction } 는 useBlock에 넘겨주는 shouldBlock함수의 매개변수(현재위치, 다음위치, 이동방식)
- basename을 스트립해서(stripBasename) shouldBlock을 호출
- basename이 /app이고 nextLocation.pathname이 /app/settings라면 settings로 변환.
3) blockerKey 할당 후 blockerFunction 등록
React.useEffect(() => {
let key = String(++blockerId);
setBlockerKey(key);
return () => router.deleteBlocker(key);
}, [router]);
React.useEffect(() => {
if (blockerKey !== "") {
router.getBlocker(blockerKey, blockerFunction);
}
}, [router, blockerKey, blockerFunction]);
- 첫 번째 useEffect:
blockerKey를 먼저 생성 (setBlockerKey(key))
cleanup 함수에서 router.deleteBlocker(key)를 호출하여 언마운트 시 제거 - 두 번째 useEffect:
blockerKey가 ""이 아닐 때 router.getBlocker(blockerKey, blockerFunction)을 실행
즉, blockerKey가 설정된 후에만 blockerFunction을 등록하도록 순서를 조정
4) blocker 상태 반환
return blockerKey && state.blockers.has(blockerKey)
? state.blockers.get(blockerKey)!
: IDLE_BLOCKER;
- state.blockers에서 blockerKey에 해당하는 blocker상태를 가져와 반환.
- 존재하지 않으면 기본값 IDLE_BLOCKER 반환.
*만약 blockerKey 없이 router.getBlocker("", blockerFunction)이 실행되면, 빈 키를 가진 blocker가 등록될 수 있음.
이런 상태에서는 useBlocker가 정상적으로 동작하지 않음.
그래서 blockerKey가 설정될 때까지 IDLE_BLOCKER(차단이 없는 기본 상태)를 유지하는 방식으로 처리.
function getBlocker(key: string, fn: BlockerFunction) {
let blocker: Blocker = state.blockers.get(key) || IDLE_BLOCKER;
if (blockerFunctions.get(key) !== fn) {
blockerFunctions.set(key, fn);
}
return blocker;
}
*RouterState는 React Router가 내부적으로 관리하는 상태정보에 대한 인터페이스로 ,
이 RouterState가 가진 속성중의 하나가 blockers(활성화 된 blocker들의 맵).
*getBlocker로 Blocker상태(BlockerUnblocked | BlockerBlocked | BlockerProceeding)를 반환함.
요약
- shouldBlock이 true이거나 특정 조건을 만족하면 네비게이션을 차단한다.
- 차단된 상태에서 blocker.proceed()를 호출하면 이동을 허용하고, blocker.reset()을 호출하면 이동을 취소한다.
- blocker.state를 통해 현재 상태(blocked, proceeding, unblocked)를 확인하고 적절한 UI 처리를 할 수 있다.
사용 예시
import { useBlocker } from "./useBlocker";
import useEffect From 'react'
function FormPage() {
const [isDirty, setIsDirty] = React.useState(false);
const blocker = useBlocker(() => isDirty);
useEffect(() => {
if (blocker.state === "blocked") {
if (window.confirm("정말로 나가시겠습니까? 변경 사항이 저장되지 않습니다.")) {
blocker.proceed(); //이동하려던 페이지로 이동
} else {
blocker.reset(); //unblocked 상태로 되돌려 현재 위치에 머물도록 함.
//새로고침이 아니라 네비게이션 차단 상태(blocked)를 해제하고 현재 페이지에 그대로 머무르게 됨.
}
}
}, [blocker]);
return (
<form>
<input onChange={() => setIsDirty(true)} />
<button type="submit">저장</button>
</form>
);
}
[[https://reactrouter.com/6.29.0/hooks/use-blocker#useblocker]]
useBlocker v6.29.0 | React Router
reactrouter.com
결론
사용자의 입력이 필요한 페이지에서는, 입력 도중 페이지를 벗어나려 할 때 이를 막고 사용자에게 경고하는 기능이 필요하다. useBlocker를 활용하면 이러한 동작을 효과적으로 구현할 수 있다.
useBlocker를 활용하면 React Router 기반으로 깔끔하고 확장 가능한 네비게이션 차단 로직을 구현할 수 있으며, 불필요한 window.onbeforeunload 남용을 줄일 수 있다.
'개발 공부 > React' 카테고리의 다른 글
useLocation (0) | 2023.07.18 |
---|---|
김민태 프론트엔드 강의 - React 만들기 (0) | 2023.03.11 |
forEach(parent.appendChild.bind(parent)) 이해하기 (0) | 2023.03.03 |
[칼럼] React 컴포넌트를 커스텀 훅으로 제공하기 (0) | 2023.02.22 |
QueryClient 대신 useQueryClient 사용하는 이유 (0) | 2023.02.20 |