본문 바로가기

Frontend

[React] SPA에서 페이지 이탈 감지하기

SPA 방식에서 페이지 이탈 감지하기

React를 사용하여 SPA의 router를 관리하기 위한 라이브러리로 React-router-dom을 많이 사용한다.
이번에는 SPA 에서 React-router-dom에서 제공하는 useBlocker를 활용하여 페이지 이탈을 감지하고 이를 유저에게 다이얼로그로 알려주는 코드를 작성해보려고 한다.

준비물

  • React-router-dom V6 설치
npm install react-router-dom@6
  • v6 부터 useBlocker를 사용할 수 있다.

상황

  1. 유저가 입력값을 수정할 수 있는 페이지가 존재한다.

유저가 입력값을 수정할 수 있는 페이지

  1. 유저가 입력값을 수정한 이후 "임시저장"을 하지 않은 상태로 현재 페이지를 이탈할 경우, 다이얼로그를 띄워 알림을 준다.

접근 방법

  1. 유저가 입력값을 수정했다는 상태값 선언
function EditPage() {
    const [isFormDirty, setIsFormDirty] = useState(false);

      const onFormDirtyChange = useCallback((isDirty: boolean) => {
        setIsFormDirty(isDirty);
      }, []);

    return (
        <div>
              <EditTableComponent onFormDirtyChange={onFormDirtyChange} />
          </div>
    )
}
function EditTableComponent({onFormDirtyChange}) {
  const methods = useForm();

  // Input 값 초기화되면 isFormDirty값 false로 초기화
  useEffect(() => {
    methods.reset();
    onFormDirtyChange(false);
  }, [methods, onFormDirtyChange]);

  // form 값 변경 감지
  useEffect(() => {
    const subscription = methods.watch(() => {
      onFormDirtyChange(true);
    });

    return () => {
      subscription.unsubscribe();
    };
  }, [methods, onFormDirtyChange]); 

  return (
      <FormProvider {...methods}>
        ...
      </FormProvider>
  )
}
  1. useBlocker 훅 생성
import {useNavigate,unstable_useBlocker as useRouterBlocker} from 'react-router-dom';

export function useBlocker(
  shouldBlock: (args: {
    currentLocation: Location;
    nextLocation: Location;
  }) => boolean,
) {
  const blocker = useRouterBlocker(shouldBlock);
  const navigate = useNavigate();

  return {
    ...blocker,
    reset: () => {
      blocker.reset?.();
    },
    proceed: () => {
      blocker.reset?.();
      if (blocker.location) {
        navigate(blocker.location.pathname);
      }
    },
  };
}

useBlocker 훅은 unstable_useBlocker 함수를 사용하고 boolean 값을 반환하는 shouldBlock 함수를 인자로 받아 blocker 객체를 반환한다.
reset 메서드는 blocker 객체를 초기화하고, proceed 객체는 사용자의 행동(페이지 이동)을 처리한다.

  1. useBlocker 훅을 통해 다이얼로그 띄우기
function EditPage() {
    const [isFormDirty, setIsFormDirty] = useState(false);
    const [showLeaveDialog, setShowLeaveDialog] = useState(false);

    const onFormDirtyChange = useCallback((isDirty: boolean) => {
      setIsFormDirty(isDirty);
    }, []);

    const blocker = useBlocker(({ currentLocation, nextLocation }) => {
      if (isFormDirty && currentLocation.pathname !== nextLocation.pathname) {
        setShowLeaveDialog(true);
        return true;
      }
      return false;
    });

    return (
        <div>
          <EditTableComponent onFormDirtyChange={onFormDirtyChange} />
          <Dialog
            isShow={showLeaveDialog}
            onClose={() => {
              setShowLeaveDialog(false);
              blocker.reset();
            }}
            className={styles.dialogContent}
            icon={<Icons_warning1 className={styles.warningIcon} />}
            header={t('알림')}
            footer={
                <>
                 <Button
                     type="button"
                     color="primary"
                     size="md"
                     onClick={() => {
                       setShowLeaveDialog(false);
                       blocker.reset();
                     }}
                      >
                    {t('머무르기')}
                  </Button>
                  <Button
                  type="button"
                  color="secondary"
                  size="md"
                  onClick={() => {
                    setShowLeaveDialog(false);
                    setIsFormDirty(false);
                    blocker.proceed();
                  }}
                  >
                   {t('나가기')}
                  </Button>
                </>
             }
            >
               <p>{t('페이지를 벗어나시겠습니까?')}</p>
               <p>{t('저장되지 않은 변경사항은 사라집니다.')}</p>
            </Dialog>
        </div>
    )
}

회고

이번 기회에 SPA 방식에서 페이지 이탈을 감지하고 다이얼로그로 사용자에게 알림을 주는 기능을 구현했다.
처음 해보는 거라 생각을 많이 하게 되었고 앞으로 자주 사용하게 될 것 같아 hook으로 만들었다.
내가 만든 코드가 누군가에 큰 도움이 되었으면 좋겠다.