컴포넌트를 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 라우팅 컴포넌트를 만들 수 있습니다.