React Native Academy · 강의

인증 흐름과 보호된 경로

회원가입, 로그인, 세션 지속성을 구현하고 인증 스택과 앱 스택 사이를 전환하는 루트 내비게이터를 설정한 다음, 인증되지 않은 사용자가 모든 데이터 화면에 접근하지 못하도록 보호합니다.

레슨 2/413개 단계

인증 흐름과 보호된 경로은(는) CoddyKit의 무료 React Native Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 React Native Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Authentication Architecture Overview

A production auth flow has three states your navigator must handle: loading (checking if a session exists from storage), unauthenticated (show auth screens), and authenticated (show app screens). The key insight is that your root navigator switches between the auth stack and the app stack based on session state — users cannot access data screens without a valid session, and authenticated users cannot land on the login screen.

// Auth states:
// 'loading'        -> Show a splash/loading screen
// 'unauthenticated' -> Show auth stack (Sign In, Sign Up)
// 'authenticated'   -> Show app stack (tabs, data screens)

// The navigator reads session from AuthContext:
const { session, isLoading } = useAuth();

if (isLoading) return <SplashScreen />;

return (
  <NavigationContainer>
    {session ? <AppNavigator /> : <AuthNavigator />}
  </NavigationContainer>
);

AuthContext Setup

Create an AuthContext that provides the current session, user object, sign-in, sign-up, and sign-out functions to the entire component tree. On mount, check AsyncStorage or Supabase's auto-restored session to determine if the user is already logged in from a previous app launch. This prevents the auth screens from flashing before the session is restored.

// src/context/AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { Session } from '@supabase/supabase-js';
import { supabase } from '../services/supabase';

interface AuthContextType {
  session: Session | null;
  isLoading: boolean;
  signOut: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType>(null!);

export function AuthProvider({ children }) {
  const [session, setSession] = useState<Session | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    supabase.auth.getSession().then(({ data }) => {
      setSession(data.session);
      setIsLoading(false);
    });

    const { data: listener } = supabase.auth.onAuthStateChange(
      (_event, session) => setSession(session)
    );
    return () => listener.subscription.unsubscribe();
  }, []);

  const signOut = async () => { await supabase.auth.signOut(); };

  return (
    <AuthContext.Provider value={{ session, isLoading, signOut }}>
      {children}
    </AuthContext.Provider>
  );
}

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

Wrapping the App with AuthProvider

Wrap your entire application in AuthProvider at the entry point so every component can access the session. Place it outside NavigationContainer since the navigator itself reads from auth context. Also wrap with QueryClientProvider if using React Query. The order matters: data providers should wrap navigation, not be nested inside specific screens.

// App.tsx (entry point)
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from './src/context/AuthContext';
import { RootNavigator } from './src/navigation/RootNavigator';

const queryClient = new QueryClient();

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <AuthProvider>
        <RootNavigator />
      </AuthProvider>
    </QueryClientProvider>
  );
}

Sign-Up Screen Implementation

The sign-up screen collects email and password, validates them locally, and calls supabase.auth.signUp(). On success, Supabase may require email confirmation — handle this case by showing a 'Check your email' message instead of immediately navigating. Use react-hook-form for validation to avoid manual state management for each field's error state.

// src/screens/auth/SignUpScreen.tsx
import { useForm, Controller } from 'react-hook-form';
import { supabase } from '../../services/supabase';

export function SignUpScreen({ navigation }) {
  const { control, handleSubmit, formState: { errors } } = useForm();
  const [message, setMessage] = useState('');

  const onSubmit = async ({ email, password }) => {
    const { error } = await supabase.auth.signUp({ email, password });
    if (error) {
      setMessage(error.message);
    } else {
      setMessage('Check your email to confirm your account!');
    }
  };

  return (
    <View>
      <Controller
        control={control}
        name='email'
        rules={{ required: true, pattern: /^[^@]+@[^@]+$/ }}
        render={({ field: { onChange, value } }) => (
          <TextInput
            value={value}
            onChangeText={onChange}
            placeholder='Email'
            keyboardType='email-address'
          />
        )}
      />
      {/* Password field similar pattern */}
      <Button title='Sign Up' onPress={handleSubmit(onSubmit)} />
      {message ? <Text>{message}</Text> : null}
    </View>
  );
}

Sign-In Screen and Session Persistence

The sign-in screen calls supabase.auth.signInWithPassword(). If successful, Supabase stores the session in AsyncStorage (configured during client setup) and the onAuthStateChange listener in AuthContext fires, updating the session state. The root navigator detects the new session and automatically navigates to the app stack — you do not need to call navigation.navigate manually after sign-in.

// Sign-in logic (simplified)
const signIn = async (email, password) => {
  setLoading(true);
  const { error } = await supabase.auth.signInWithPassword({
    email,
    password,
  });
  setLoading(false);

  if (error) {
    // Show error to user
    Alert.alert('Sign In Error', error.message);
    return;
  }

  // No navigation.navigate() needed!
  // AuthContext.onAuthStateChange fires automatically
  // -> session is set -> root navigator switches to AppNavigator
};

Root Navigator with Auth Guard

The root navigator is the gatekeeper that decides which navigation tree to show based on session state. It does not render any UI itself — it delegates to either AuthNavigator or AppNavigator. Because React Navigation re-renders when the navigator tree changes (auth → app), users are automatically redirected to the correct screens without explicit navigation calls on sign-in or sign-out.

// src/navigation/RootNavigator.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { useAuth } from '../context/AuthContext';
import { AuthNavigator } from './AuthNavigator';
import { AppTabNavigator } from './AppTabNavigator';
import { SplashScreen } from '../screens/SplashScreen';

const Stack = createStackNavigator();

export function RootNavigator() {
  const { session, isLoading } = useAuth();

  if (isLoading) {
    return <SplashScreen />; // Full-screen loading state
  }

  return (
    <NavigationContainer>
      {session ? (
        <AppTabNavigator />  // Authenticated: show app screens
      ) : (
        <AuthNavigator />    // Unauthenticated: show auth screens
      )}
    </NavigationContainer>
  );
}

Protecting Individual Screens

The root navigator pattern protects entire navigation trees. But sometimes you need in-screen protection — for example, a feature only available to premium users. Create a useRequireAuth hook that checks the session and navigates to login if missing. Or create a ProtectedRoute wrapper component. This pattern is useful for deep-linked screens that might be accessed before authentication.

// src/hooks/useRequireAuth.ts
import { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
import { useAuth } from '../context/AuthContext';

export function useRequireAuth() {
  const { session, isLoading } = useAuth();
  const navigation = useNavigation();

  useEffect(() => {
    if (!isLoading && !session) {
      navigation.navigate('SignIn' as never);
    }
  }, [session, isLoading, navigation]);

  return { session, isLoading };
}

// Usage in a screen:
export function PremiumFeatureScreen() {
  const { session } = useRequireAuth();
  if (!session) return null; // Navigation already happening
  return <View>...</View>;
}

Handling Token Expiry

Supabase JWT tokens expire (typically after 1 hour). The Supabase client configured with autoRefreshToken: true handles this automatically — it refreshes the token in the background before it expires. If refresh fails (e.g., the refresh token was revoked), onAuthStateChange fires with a SIGNED_OUT event, and your AuthContext clears the session. The navigator then automatically shows the auth screens, prompting the user to sign in again.

// Token expiry handling is automatic with Supabase:
// supabase client config (already set):
// {
//   auth: {
//     autoRefreshToken: true,  <- handles renewal automatically
//     persistSession: true,
//     storage: AsyncStorage,
//   }
// }

// Your AuthContext listener handles SIGNED_OUT:
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_OUT') {
    // session is null -> navigator shows auth screens
    setSession(null);
  }
  if (event === 'TOKEN_REFRESHED') {
    // New session with fresh tokens
    setSession(session);
  }
});

Password Reset Flow

Implement Forgot Password using Supabase's resetPasswordForEmail(). The user enters their email, Supabase sends a reset link, and when clicked, the link deep-links back to the app with a token. You configure the redirect URL in your Supabase dashboard's auth settings. On the app side, listen for the PASSWORD_RECOVERY auth event and navigate to a new password screen.

// Step 1: Send reset email
const resetPassword = async (email) => {
  const { error } = await supabase.auth.resetPasswordForEmail(email, {
    redirectTo: 'habittracker://reset-password',
  });
  if (!error) Alert.alert('Email sent', 'Check your inbox.');
};

// Step 2: Handle deep link in AuthContext
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'PASSWORD_RECOVERY') {
    // Navigate to update password screen
    navigationRef.current?.navigate('UpdatePassword');
  }
});

// Step 3: Update password screen
const updatePassword = async (newPassword) => {
  const { error } = await supabase.auth.updateUser({
    password: newPassword,
  });
  if (!error) navigation.navigate('Home');
};

Sign Out and Session Cleanup

Sign out must clear all local state, not just the Supabase session. Call supabase.auth.signOut(), which removes the session from AsyncStorage and fires the SIGNED_OUT event. Also clear your React Query cache so the next user who logs in on the same device does not see the previous user's data. Reset navigation state if you have any screen-specific state in the navigator.

// Complete sign-out
const signOut = async () => {
  // 1. Sign out from Supabase (clears AsyncStorage session)
  await supabase.auth.signOut();

  // 2. Clear React Query cache
  queryClient.clear();

  // 3. AuthContext detects SIGNED_OUT -> session = null
  // 4. RootNavigator switches to AuthNavigator automatically
  // No navigation.navigate() needed!
};

// In a Settings screen:
<TouchableOpacity onPress={signOut}>
  <Text>Sign Out</Text>
</TouchableOpacity>

Testing the Auth Flow

Test every auth path before moving to feature development: new user sign-up (email confirmation flow), existing user sign-in, wrong password error, session persistence after app restart, sign-out, and password reset. Automate the happy path with a Maestro test. Verify the navigator correctly transitions between auth and app states without flickering — the loading state prevents this flicker when checking AsyncStorage on startup.

# Maestro test for sign-in flow
---
appId: com.yourcompany.habittracker
---
- launchApp:
    clearState: true
- assertVisible: 'Sign In'
- tapOn: 'Email'
- inputText: 'test@example.com'
- tapOn: 'Password'
- inputText: 'TestPass123!'
- tapOn: 'Sign In'
- waitForAnimationToEnd
- assertVisible: 'My Habits'  # First app screen
- tapOn: 'Settings'
- tapOn: 'Sign Out'
- waitForAnimationToEnd
- assertVisible: 'Sign In'   # Back to auth screen

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: how to build an AuthContext that tracks session state and subscribes to Supabase auth events, how the root navigator uses session state to switch between auth and app navigation trees, and how to implement sign-up, sign-in, sign-out, and password reset flows. You also saw how to clear React Query cache on sign-out to prevent data leaks between users. Next up we build the core data feed with offline support.

무료로 시작

AI 튜터와 함께 JavaScript을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
30
레슨
120

자주 묻는 질문

“인증 흐름과 보호된 경로” 강의는 무료인가요?

네 — “인증 흐름과 보호된 경로” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 React Native Academy 강의 전체를 잠금 해제할 수 있습니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“인증 흐름과 보호된 경로”에서 뭘 배우나요?

회원가입, 로그인, 세션 지속성을 구현하고 인증 스택과 앱 스택 사이를 전환하는 루트 내비게이터를 설정한 다음, 인증되지 않은 사용자가 모든 데이터 화면에 접근하지 못하도록 보호합니다. 브라우저에서 직접 실행하는 실습 코드로 React Native Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

React Native Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 React Native Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“인증 흐름과 보호된 경로” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 React Native Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 React Native Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 아키텍처 및 기술 스택 계획하기
  2. 인증 흐름과 보호된 경로
  3. 핵심 기능: 오프라인을 지원하는 데이터 피드
  4. 다듬기, 테스트 및 양대 스토어 출시
← React Native Academy(으)로 돌아가기