[React Router] Route Guard 정리 (Zustand Principal 기반)

Lui.Slki·2026년 2월 13일

React

목록 보기
2/5

Why Route Guard?

사용자는 항상 내가 원하는 대로 동작하지 않는다.
내가 "로그인 버튼을 눌러서 로그인 하고 마이페이지는 드롭다운 내의 메뉴로 접근하세요" 라는 흐름을 만들어 놨어도 사용자가 주소창에 경로를 직접 입력해서 특정 페이지(ex.마이페이지)로 들어올 수 있다.

UI상에서는 숨겨져 있거나 버튼이 막혀있고, 백엔드에서 막아주고 있는 상태여도
URL 직접 접근은 언제든 가능하기 때문에 해당 문제들이 발생한다.

  • 인증이 없는 상태에서 페이지가 렌더되며 내부 API가 연달아 호출됨
    • 401/403이 계속 발생하거나, 화면이 깨진 상태로 보임
  • 경우에 따라선 빈 화면 / 무한 로딩 / 에러 토스트 반복 같은 UX가 발생
  • "이 페이지는 접근 가능한가?" 라는 책임이 컴포넌트에 분산되어
    각 페이지마다 인증 체크를 반복하게 됨

그래서 "이 경로는 로그인한 사용자만 접근 가능" 같은 규칙을 라우팅 레벨에서 중앙집중적으로 차단 하기위해 Route Guard를 사용한다.


"액션 가드" vs "라우트 가드"

액션 가드(Action Guard)

  • "예약하기", "좋아요", "결제하기" 같은 행동 버튼 클릭 시점에 로그인 체크
  • 로그인이 아니면 로그인 화면으로 보내고, 로그인 후 원래 화면으로 복귀

장점: 특정 기능만 보호 가능, UX가 직관적
단점: 라우트 자체는 접근 가능(페이지 단에서 한 번 더 막아야 완벽)

라우트 가드(Route Guard)

  • 로그인 필수 페이지 접근 자체를 차단
  • 보호된 라우트는 <ProtectedRoute><Page/></ProtectedRoute> 형태로 감싸서 진입을 막음

장점: 페이지 단에서 확실하게 막힘
하나만 사용하는 경우도 있지만, 결국에는 UX를 위해서는 프로젝트를 진행할때는 항상 2가지 전부 사용을 하는 모습이다.


ProtectedRoute 구현 (Zustand principal + bootstrap)

  • 새로고침 직후엔 isAuthenticated = false 일 수 있다.
    → 토큰이 있으면 bootstrap() 으로 principal 복구 시도 후 판정해야 안정적
  • redirect는 <Navigate /> 사용 (SPA 유지)
  • state로 "원래 가려던 경로" 를 넘기면 로그인 후 자동 복귀 가능

<ProtectedRoute><Page/></ProtectedRoute> 형태로 사용하기 때문에

function ProtectedRoute({ children }: { children: React.ReactNode })
  • children : 해당 props가 반드시 있고 그 값은 리액트가 렌더 할 수있는 무언가여야 한다.
  • React.ReactNode : wrapper 컴포넌트에서 children 타입은 보통 이걸 쓰던데, 아래 요소들을 포함하기 때문이라고 한다.
    • JSX 엘리먼트 <div /> , <MyComp />
    • 문자열
    • 숫자
    • 배열
    • null
  const loc = useLocation();
// 인증 boolean
  const isAuthenticated = usePrincipalState((s) => s.isAuthenticated);
// 복구 시도 시 필요
  const bootstrap = usePrincipalState((s) => s.bootstrap);
  const [ready, setReady] = useState(false);

useEffect(() => {
    (async () => {
      await bootstrap(); // 토큰 존재 시 principal 복구 시도
      setReady(true);
    })();
  }, []); // 의존성 배열 없이 마운트 1회만

// 로딩중 화면
if (!ready) return <div style={{ padding: 16 }}>로딩중...</div>;

// 인증되지 않은 상황에서는 로그인 화면으로 넘기고, 로그인 후 원래 본인이 접근하려던 곳으로 보내줌
if (!isAuthenticated) {
    return <Navigate to="/signin" replace state={{ from: loc }} />;
  }

  return <>{children}</>;

라우터에 적용 (Page 전체 보호)

Page가 <Outlet /> 을 렌더한다면, 상위에서 한 번만 감싸면 하위 라우트가 전부 보호된다.

<Route
  element={
    <ProtectedRoute>
      <Page />
    </ProtectedRoute>
  }
>
// ...여기있는 라우트들은 보호 대상    
</Route>

+ (추가) 이미 로그인 했는데 로그인 화면이 다시 뜨는 문제

뒤로가기 등으로 로그인 페이지에 다시 접근 가능한 상황에서는
로그인 페이지 자체에서 "로그인 상태면 즉시 튕기기" 처리를 하면 된다.

const isAuthenticated = usePrincipalState((s) => s.isAuthenticated);

 useEffect(() => {
    if (isAuthenticated) {
      navigate(fromPath, { replace: true });
    }
  }, [isAuthenticated]);

Think

  • URL 직접 접근 차단 : 버튼을 숨겨도 주소창으로 들어올 수 있으니 "페이지 단(라우팅 레벨)"에서 먼저 막기
  • 새로고침/재접속 대응 : 토큰이 있어도 상태가 비어 있을 수 있어서 bootstrap() 으로 복구 후 판정(깜빡임 및 오판 방지)
  • 원래 가려던 곳으로 복귀 : /signin으로 보낼 때 state.from 저장 → 로그인 성공 시 navigate(from, { replace:true })
  • 뒤로가기 UX : 로그인 성공/리다이렉트는 replace:true로 히스토리에서 로그인 페이지 제거(뒤로가면 로그인창 재등장 방지)
  • 이미 로그인인데 로그인 페이지 노출 방지 : 뒤로가기로 로그인 진입해도 로그인 상태면 즉시 from으로 튕기기
  • 액션 가드도 같이 : "행동" 은 클릭 시점에도 한 번 더 체크(로그인 전인데 UI 플로우만 진행되는 것 방지)
  • 로딩/에러 표현 최소화 : guard 판정 중엔 잛은 로딩(스켈레톤 혹은 text)만, alert 너무 많지 않게, 토스트 혹은 인라인 안내로

0개의 댓글