개요
React Native 앱에서 Supabase를 활용한 카카오 소셜 로그인 구현 방법을 설명합니다.
전체 로그인 프로세스
import { supabase } from '@/lib/supabaseClient'
import { mixinFlex } from '@/styles/mixins'
import { theme } from '@/styles/theme'
import styled from '@emotion/native'
import { Ionicons } from '@expo/vector-icons'
import { router } from 'expo-router'
import { useEffect, useState } from 'react'
import { Alert, BackHandler, Image, TouchableOpacity, View } from 'react-native'
import WebView from 'react-native-webview'
const LoginScreen = () => {
const [showWebView, setShowWebView] = useState(false)
const [authUrl, setAuthUrl] = useState('')
////////// 카카오 로그인 버튼 클릭 함수
const signInWithKakao = async () => {
try {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'kakao',
options: {
skipBrowserRedirect: true, // 브라우저 리다이렉트 건너뛰고 로그인 URL만 받음(직접 브라우저를 열고 제어 가능)
},
})
// 에러 발생 시 에러 처리
if (error) throw error
// URL이 있다면
if (data?.url) {
// URL 상태 업데이트
setAuthUrl(data.url)
// WebView 표시
setShowWebView(true)
}
} catch(error) {
// 에러 발생 시 로그인 페이지로 이동
router.replace('/auth/login')
throw error
}
}
////////// WebView URL 변경 감지 함수
const handleNavigationStateChange = async (navState: any) => {
// access_token이 포함된 URL인지 확인
if (navState.url.includes('access_token=')) {
try {
// URL에서 access_token 추출
const params = new URLSearchParams(navState.url.split('#')[1])
const accessToken = params.get('access_token')
const refreshToken = params.get('refresh_token')
// 세션 설정
const {
data: { session },
error,
} = await supabase.auth.setSession({
access_token: accessToken!,
refresh_token: refreshToken!,
})
if (error) throw error
if (session) {
setShowWebView(false)
// 홈 화면으로 이동
router.replace('/auth/callback')
}
} catch {
Alert.alert('로그인 실패')
router.replace('/auth/login')
}
}
}
// 백핸들러(뒤로가기 버튼 눌렀을 때)
useEffect(() => {
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
router.replace('/') // 홈으로 이동
return true // 기본 동작 방지
})
return () => backHandler.remove()
}, [])
return (
<>
{showWebView ? (
<WebView source={{ uri: authUrl }} onNavigationStateChange={handleNavigationStateChange} />
) : (
<Container>
{/* 뒤로가기 버튼 */}
<BackButton onPress={() => router.replace('/')}>
<Ionicons
name='chevron-back'
size={theme.iconSizes.lg}
color={theme.colors.core.white}
/>
</BackButton>
{/* 로고 */}
<Logo source={require('@assets/images/icon.png')} style={{ width: 100, height: 100 }} />
{/* 카카오 로그인 버튼 */}
<TouchableOpacity onPress={signInWithKakao}>
<KakaoLogin source={require('@assets/images/kakao-login.png')} />
</TouchableOpacity>
</Container>
)}
</>
)
}
export default LoginScreen
const Container = styled(View)`
${mixinFlex('column', 'center', 'center')}
flex: 1;
background-color: ${theme.colors.background.default};
row-gap: 24px;
`
const Logo = styled(Image)`
border-radius: ${`${theme.border.radius.full}px`};
`
const BackButton = styled.TouchableOpacity`
position: absolute;
top: 16px;
left: 16px;
`
const KakaoLogin = styled(Image)``
프로세스 설명
로그인 시작 - 사용자 액션
카카오 로그인 버튼 클릭 시 signInWithKakao 실행
<TouchableOpacity onPress={signInWithKakao}>
<KakaoLogin source={require('@assets/images/kakao-login.png')} />
</TouchableOpacity>
동작:
사용자가 카카오 로그인 버튼 터치
signInWithKakao() 함수 호출
OAuth URL 획득 - 웹뷰 초기화
signInWithKakao 함수 내부에서 로그인 URL을 받아 URL 상태를 업데이트하고 웹뷰 상태를 true로 변경
const signInWithKakao = async () => {
// Supabase OAuth URL 요청
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'kakao',
options: { skipBrowserRedirect: true },
})
if (data?.url) {
setAuthUrl(data.url) // 카카오 로그인 URL 저장
setShowWebView(true) // 웹뷰 표시 활성화
}
}
동작:
- Supabase에 카카오 OAuth URL 요청
- 받은 URL로 `authUrl` 상태 업데이트
- `showWebView`를 `true`로 변경
- 결과: 화면이 로그인 버튼 → 웹뷰로 전환
사용자 인증 - 카카오 로그인 수행
사용자가 로그인 정보를 입력 후 로그인을 시도하면 인증 URL로 리다이렉트됨
{showWebView ? (
<WebView
source={{ uri: authUrl }} // 카카오 로그인 페이지 표시
onNavigationStateChange={handleNavigationStateChange}
/>
) : (
// 로그인 버튼 화면
)}
동작:
- 웹뷰에 카카오 로그인 페이지 표시
- 사용자가 카카오 계정/비밀번호 입력
- 카카오 서버에서 인증 처리
- 인증 성공 시 Supabase 콜백 URL로 리다이렉트
- **URL 변경: `
https://kauth.kakao.com/...
` → `stockexam://auth/callback#access_token=...`**
토큰 추출 - 세션 설정
리다이렉트 과정에서 onNavigationStateChange이 웹뷰의 URL 변화를 감지하고 URL에서 액세스 토큰을 추출 및 세션을 설정
const handleNavigationStateChange = async (navState) => {
// access_token이 포함된 URL 감지
if (navState.url.includes('access_token=')) {
// URL에서 토큰 추출
const params = new URLSearchParams(navState.url.split('#')[1])
const accessToken = params.get('access_token')
const refreshToken = params.get('refresh_token')
// Supabase 세션 설정
const { data: { session }, error } = await supabase.auth.setSession({
access_token: accessToken!,
refresh_token: refreshToken!,
})
if (session) {
// 다음 단계로...
}
}
}
동작:
- `onNavigationStateChange`가 URL 변경 감지
- URL에 `access_token` 포함 여부 확인
- URLSearchParams로 토큰 파싱
- `supabase.auth.setSession()`으로 세션 설정
- 결과: 사용자 로그인 상태 활성화
로그인 완료 - 화면 전환
세션 설정을 마쳤다면 웹뷰를 닫고 /auth/callback으로 리다이렉트
if (session) {
setShowWebView(false) // 웹뷰 숨기기
router.replace('/auth/callback') // callback 페이지로 이동
}
동작:
- `setShowWebView(false)` - 웹뷰 화면 닫기
- `router.replace('/auth/callback')` - callback 화면으로 이동
- 결과: 로그인 후속 처리 화면으로 전환
주요 설정사항
1.앱 설정
// app.config.js
export default {
expo: {
scheme: 'stockexam' // 커스텀 URL 스킴
}
}
2. 필요한 패키지
npm install @supabase/supabase-js @react-native-async-storage/async-storage react-native-webview
주의사항
1. 웹과 앱의 차이
- 웹: 쿠키로 자동 세션 관리
- 앱: WebView와 앱이 분리되어 수동 세션 설정 필요
2. 테스트
- Expo Go가 아닌 개발자 빌드로 테스트 필요
- USB 디버깅으로 실제 기기에서 테스트
3. 보안
- Supabase URL Configuration에 정확한 리다이렉트 URL 등록
- 토큰 관리 주의
용어 설명
- 커스텀 URL 스킴: 앱을 식별하는 고유한 URL 형식 (예: stockexam://)
- 딥링크: 앱의 특정 화면으로 직접 이동할 수 있는 링크
- OAuth: 사용자 인증을 위한 개방형 표준 프로토콜