컴포넌트를 a tag
로 만들고 useLinkClickHandler
를 활용한 컴포넌트를 만들면 접근성을 준수한 SPA
라우팅 컴포넌트를 만들 수 있습니다.
react-router 를 쓰다보면 종종 특정 컴포넌트를 클릭할 때 location 을 바꾸기 위해 useNavigate
를 활용합니다.
예를 들면, 다음과 같은 코드입니다.
function Component() {
const navigate = useNavigate();
// ...
return (
<Button
onClick={(e) => {
e.preventDefault();
navigate.to('~~~');
}}
/>
);
}
위 코드는 우리가 원하는 요구사항을 대부분 충족시켜줍니다.
그러나, 이런 구현들을 직접하게 될 경우 예상치 못한 edge case 들이 발생하게 됩니다.
예를 들어 마우스의 휠 중간 버튼을 클릭 했을 때 새 탭이 열리거나, command, ctrl
등의 키와 함께 클릭 시 새 탭이 열려야 되는데 그렇지 못하는 문제들이 발생합니다.
react-router 는 이에 대응할 수 있는 useLinkClickHandler
라는 훅을 제공해줍니다.
Usage
useLinkClickHandler 는 react-router-dom 이 제공하는 <Link>
컴포넌트가 아닌, 커스텀한 navigator 를 구현할 때 click event handler 로 사용할 수 있는 함수를 반환해줍니다.
다음과 같이 사용합니다.
import { useHref, useLinkClickHandler } from 'react-router-dom';
const StyledLink = styled('a', { color: 'fuchsia' });
const Link = React.forwardRef(({ onClick, replace = false, state, target, to, ...rest }, ref) => {
let href = useHref(to);
let handleClick = useLinkClickHandler(to, {
replace,
state,
target,
});
return (
<StyledLink
{...rest}
href={href}
onClick={(event) => {
onClick?.(event);
if (!event.defaultPrevented) {
handleClick(event);
}
}}
ref={ref}
target={target}
/>
);
});
참고로, useHref
는 접근성을 위해 basename 을 포함한 full path 를 반환해주는 훅입니다.
Implementation
useLinkClickHandler 는 다음과 같이 구현되어 있습니다.
/**
* Handles the click behavior for router `<Link>` components. This is useful if
* you need to create custom `<Link>` components with the same click behavior we
* use in our exported `<Link>`.
*/
export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
to: To,
{
target,
replace: replaceProp,
state,
}: {
target?: React.HTMLAttributeAnchorTarget;
replace?: boolean;
state?: any;
} = {}
): (event: React.MouseEvent<E, MouseEvent>) => void {
let navigate = useNavigate();
let location = useLocation();
let path = useResolvedPath(to);
return React.useCallback(
(event: React.MouseEvent<E, MouseEvent>) => {
if (
event.button === 0 && // Ignore everything but left clicks
(!target || target === "_self") && // Let browser handle "target=_blank" etc.
!isModifiedEvent(event) // Ignore clicks with modifier keys
) {
event.preventDefault();
// If the URL hasn't changed, a regular <a> will do a replace instead of
// a push, so do the same here.
let replace =
!!replaceProp || createPath(location) === createPath(path);
navigate(to, { replace, state });
}
},
[location, navigate, path, replaceProp, state, target, to]
);
}
몇 가지 주목해야 할 점이 있습니다.
event.button
은 마우스가 눌렸을 때 이벤트를 trigger 한 버튼이 어떤 것인지 눌렸는 지 확인할 수 있는 값입니다.
event.button
이 0 인 경우는 메인 버튼, 즉 마우스 왼쪽 버튼이 클릭되었음을 의미합니다.
즉 useLinkClickHandler
는 클릭된 버튼이 마우스 왼쪽 버튼이 아니라면 별다른 동작을 하지 않습니다.
anchor element 는 linked URL 을 보여줘야 할 때 탭인지, 윈도우인지 (browsing context) 보여주기 위한 속성값인 target
을 받을 수 있습니다.
target 으론 _self, _blank, _parent, _top
등의 속성이 있습니다.
useLinkClickHandler
는 target 이 _self
이거나, 받은 target 이 없는 경우 동작을 수행합니다.
isModifiedEvent 는 클릭 시 ctrl, command
등의 키와 함께 눌렸는지 확인하는 함수입니다.
function isModifiedEvent(event: React.MouseEvent) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
useLinkClickHandler
는 modifier
키들과 함께 클릭이 된 경우 동작을 수행하지 않습니다.
replace
주석에도 설명되어 있다시피 만약, URL 이 변경되지 않았는데 linkClickHandler 가 호출이 되는 경우 push 대신 replace 로 동작을 수행하게 됩니다.
정리
컴포넌트를 a tag
로 만들고 useLinkClickHandler
를 활용한 컴포넌트를 만들면 접근성을 준수한 SPA
라우팅 컴포넌트를 만들 수 있습니다.