-
[WizSched] Supabase onAuthStateChange 활용하기Front-End/Project 2024. 2. 2. 16:22
🍀 목차
문제
onAuthStateChange?
정리
Session
구현문제
OAuth를 통해 유저로그인을 하는 코드는 아래와 같았다.
const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'github' })
그런데 이
signInWithOAuth
메서드는 redirect를 유발하고, 해당 코드가 성공하는 순간 작성된 redirect URL로 이동한다. 이 사실을 알지 못하고 해당 코드 아래에 data가 있으면 유저 상태를 변경하는 로직을 작성했고, 그 유저 상태에는 NULL만 들어가게 되었다. 공식 문서를 부분적으로 읽다 보니 제대로 이해하지 못하고 작성하여 발생한 문제 같다. Supabase의 auth는onAuthStateChange
라는 auth 관련 event가 발생하면 처리할 수 있는 메서드를 제공한다. 해당 메서드와 React Context API를 활용하여 전역적으로 사용할 수 있는 유저 데이터를 만들어보자!onAuthStateChange?
supabase.auth.onAuthStateChange 메서드는 auth 관련 event가 발생할 때마다 알림을 받는다. 콜백 함수의 파라미터는 이벤트 객체인
event
, 현재 사용자 세션에 대한 정보를 제공하는session
이다.event의 종류는 아래와 같다.
INITIAL_SESSION
: Supabase 클라이언트가 구성되고 초기 세션이 로드된 직후 emit 되는 event. 혹은 애플리케이션이 로드되는 동안 사용자가 이미 로그인되어 있는 경우.SIGNED_IN
: user 세션이 재설정될 때마다, 로그인등을 포함하여 발생한다. 사용자가 로그인한 상태에서도 발생할 수 있다(정확히 예측할 수 없다).SIGNED_OUT
: 로그아웃,supabase.auth.signOut()
을 부른 후, 사용자 세션이 만료되었을 경우(사용자가 다른 장치에서 로그아웃, 세션 시간 초과, 다른 장치에서 로그인 등)TOKEN_REFRESHED
: 로그인한 사용자에 대해 새 액세스 및 Refresh token을 가져올 때마다 발생한다. 애플리케이션에서 사용할 수 있도록 Access token(JWT : Json Web Token)을 memory에 저장하는 것을 권장한다.supabase.auth.getSession()
을 같은 목적으로 자주 호출하는 것은 피한다(세션 정보를 자주 가져오는 것보다 토큰 갱신마다 발생하는 해당 이벤트를 이용하여 세션 정보를 업데이트하는 것을 권장). 세션의 새로고침 시점을 추적하는 백그라운드 프로세스가 있으므로 이 이벤트를 통해 항상 유효한 토큰을 받을 수 있다. 이벤트의 빈도는 프로젝트에 구성된 JWT expiry limit과 관련 있다.- 등등...
onAuthStateChange는 애플리케이션 UI를 최신 상태로 유지하기 위해 여러 탭에서 발생한다. 그렇기에 열려 있는 탭의 수에 따라 매우 자주 실행될 수 있다. 콜백 외부에서 수행될 수 있는 최대한 많은 작업을 defer(setTimeout 등으로 연기) 하거나 debounce(검색어 타이핑 시 타이핑마다 검색 요청을 보내는 것이 아닌 사용자 타이핑 완료 후 검색 요청이 갈 수 있도록 지연시키는 기술)하는 것이 효율적일 수 있다.
주의할 점은 onAuthStateChange의 콜백은 async function일 수 있고, 변경 사항을 처리하는 동안은 동기적으로 실행된다. Supabase의 다른 메서드를 호출할 때 await을 사용하면 데드락이 만들어질 수도 있다는 것이다.
그렇기에
1. async function을 콜백으로 사용하지 않는다.
2. async 콜백에서 await 호출 수를 제한한다. 여러 개의 await은 순환적으로 자원을 기다리게 할 수 있다(순환 대기). 또한, await은 비동기 작업 완료 때까지 대기하기에 여러 작업을 병렬로 실행하는 것이 더 효율적일 수 있다.
3. 콜백 함수에 다른 Supabase 메서드를 사용하지 않는다. 정말 필요한 경우에는 콜백 실행 완료 된 후 함수를 dispatch(작업을 나중에 실행하는 점에서 defer과 비슷하나 비동기 작업의 콜백 완료 후 특정 함수를 실행시키는 것임)한다. 방법은 아래와 같다.
supabase.auth.onAuthStateChange((event, session) => { setTimeout(async () => { // 다른 Supabase 함수에서 await을 사용 // 이는 콜백이 완료된 직후에 실행된다. }, 0) })
정리
onAuthStateChange를 React Context API 등을 사용하여 전역적으로 auth event를 감지할 수 있도록 한다. 또한, 해당 메서드의 event에 대한 행위를 정리해 보면
- SIGNED_IN : 필요한 정보(oauth_provider_token, oauth_provider_refresh_token)를 LocalStorage, SessionStorage, 메모리(React State) 등에 저장한다.
- SIGNED_OUT : 로그인 시 저장했던 정보들을 삭제한다.
- TOKEN_REFRESHED : 새로운 Access token을 포함한 세션 정보가 제공된다. 정보를 업데이트하는 로직이 필요하다.
기본적으로 Supabase는 로그인, 로그아웃 관련 LocalStorage
setItem
,removeItem
등의 기능을 제공한다. 로그인 후 LocalStorage를 확인해 보면 위 사진처럼sb-Reference ID-auth-token
형식으로 저장된 것을 볼 수 있다(key 형식이 저게 고정인가 해서 관련 정보를 찾다 발견하지 못해 Supabase AI에게 물어봤는데 정확하지 않은 정보를 알려주거나 없는 페이지를 알려줘서 해당 형식을 상수로 관리할 수 있는지는 확실하지 않다). 참고로 Project의 Reference ID는대시보드 -> Settings
의 General에서 확인할 수 있다.Access token을 브라우저 저장소에 저장하는 것에 대해서는 여러 논의가 많은 것 같다.
그렇기에 이 부분은 본인이 생각하는 안전한 방법으로 구현하는 것도 좋아 보였다.
Access token은 브라우저 저장소에는 저장하지 않고 로컬 변수로 관리, Refresh token만 저장(HTTP Only + Secure 쿠키 등) 하여 리로드, 리프레시가 되어 Access token이 사라진다면 Refresh token을 활용하여 새로 발급받거나 로그인 페이지로 다시 이동시키거나 등... 자세히 보기
+) HTTP-Only 쿠키를 사용해서 Access, Refresh token을 저장하는 방법은 Supabase를 제외하고 별도의 서버가 없는 프로젝트 환경에서는 응답 헤더에 담긴 식별값에 접근할 수 없기 때문에 Web Storage나 메모리에 저장하는 방법을 사용한다.
+) 별도의 Custom Storage를 구현해 사용하고 싶다면 Supabase 클라이언트 생성 시 Storage 옵션을 재정의할 수 있다.
import { createClient } from '@supabase/supabase-js' const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { auth: { storage: customStorageObject, }, })
customStorageObject는 Storage 인터페이스의 getItem, setItem, removeItem을 구현하여야 한다.Session
Supabase의 세션(session)은 사용자가 로그인할 때 생성된다. 기본적으로는 무기한 지속, 사용자는 여러 장치에서 활성 세션을 무제한으로 보유할 수 있다. 세션은 JWT 형식의 Supabase Auth Access token과 고유 문자열인 Refresh token으로 표시된다.
Access token은 5분에서 1시간 사이의 짧은 수명을 가지고 Refresh token은 만료되진 않지만 한 번만 사용할 수 있다(일반적으로는 그렇다. 해당 설계에는 두 가지 예외가 있는데 해당 문서에 나와있다). 한 번 교환하여 새 Access token과 Refresh token 쌍을 얻을 수 있다.
일반적으로 아래의 경우 세션이 중지된다.
- 사용자가 로그아웃을 클릭했을 때
- 사용자의 비밀번호 변경 등 보안에 민감한 작업 수행 시
- 비활성화로 인한 시간 초과
- 최대 수명 도달
- 사용자가 다른 장치에서 로그인
Supabase의 scope 등으로 로그아웃 시에도 다른 기기나 브라우저 세션은 종료하지 않도록 할 수 있다.
구현
Listen to auth events -> Use React Context for the User's session
부분을 참고했다.const SessionContext = React.createContext(null) function main() { const [session, setSession] = React.useState(null) React.useEffect(() => { const subscription = supabase.auth.onAuthStateChange( (event, session) => { if (event === 'SIGNED_OUT') { setSession(null) } else if (session) { setSession(session) } }) return () => { subscription.unsubscribe() } }, []) return ( <SessionContext.Provider value={session}> <App /> </SessionContext.Provider> ) }
해당 코드를 기반으로 추가로 user 정보를 많이 쓸 것 같아 session 안에 있는 user도 별도의 상태로 관리하였고, SIGNED_IN, TOKEN_REFRESHED일 때는 user, session 정보를 세팅하고 SIGNED_OUT은 그대로 해 주었다.
이제 해당 context를 사용해서 헤더에 로그인 감지를 할 수 있게 되었다!
참고자료
https://supabase.com/docs/reference/javascript/auth-onauthstatechange
https://supabase.com/docs/guides/auth/sessions