블로그 목록

Expo Router 보호된 라우트 만들기 — Stack.Protected로 로그인 가드 짜기

앱에 로그인 붙이면 항상 나오는 문제. "로그인 안 한 사람이 홈 탭에 접근하면 어떻게 막지?"

예전엔 각 화면 상단에서 useEffect로 토큰 확인하고 router.replace('/login') 때리는 식으로 처리했다. 화면마다 같은 코드가 반복되고, 리다이렉트되기 전에 보호된 화면이 한 프레임 깜빡이는 것도 거슬렸다. 레이아웃 마운트 순서 때문에 "Attempted to navigate before mounting the Root Layout" 에러도 종종 만났다.

Expo Router v5(SDK 53)부터는 Stack.Protected라는 선언적 가드가 생겼다. 조건이 안 맞으면 그 그룹 자체가 라우팅에서 빠져서, 접근하면 자동으로 다른 화면으로 튕긴다.

인증 상태부터

먼저 로그인 여부를 어디서든 읽을 수 있게 context로 빼둔다. 실제 토큰은 expo-secure-store에 넣는 걸 추천.

// context/auth.tsx
import { createContext, useContext, useState } from 'react';

const AuthContext = createContext<{
  isLoggedIn: boolean;
  signIn: () => void;
  signOut: () => void;
}>(null!);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  return (
    <AuthContext.Provider
      value={{
        isLoggedIn,
        signIn: () => setIsLoggedIn(true),
        signOut: () => setIsLoggedIn(false),
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

라우트를 그룹으로 감싸기

핵심은 루트 레이아웃이다. guard prop에 조건을 주고, 그 안에 보호할 화면(또는 (app) 같은 그룹)을 넣는다.

// app/_layout.tsx
import { Stack } from 'expo-router';
import { AuthProvider, useAuth } from '../context/auth';

function RootNavigator() {
  const { isLoggedIn } = useAuth();
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Protected guard={isLoggedIn}>
        <Stack.Screen name="(app)" />
      </Stack.Protected>

      <Stack.Protected guard={!isLoggedIn}>
        <Stack.Screen name="login" />
      </Stack.Protected>
    </Stack>
  );
}

export default function RootLayout() {
  return (
    <AuthProvider>
      <RootNavigator />
    </AuthProvider>
  );
}

guardfalse가 되면 그 안의 라우트는 존재하지 않는 것처럼 취급된다. 로그아웃하면 (app) 그룹이 통째로 빠지면서 자동으로 login으로 이동한다. router.replace를 직접 부를 일이 없다.

걸렸던 부분

  • 가드 조건이 준비되기 전(예: SecureStore에서 토큰 읽는 중)에는 null을 리턴하거나 스플래시를 유지해야 한다. 로딩 중에 isLoggedInfalse로 잡히면 로그인 화면이 한 번 깜빡인다.
  • Stack.ProtectedStack 안에서만 동작한다. Tabs에 바로는 못 쓰니, 탭 전체를 보호하려면 탭 그룹을 Stack.Screen으로 감싸서 그 위에 가드를 건다.

화면마다 흩어져 있던 인증 체크가 레이아웃 한 곳으로 모이니까 훨씬 읽기 쉬워졌다. 조건만 바꾸면 온보딩 완료 여부, 결제 여부 같은 다른 가드도 똑같은 패턴으로 추가할 수 있다.