🌱 ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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을 포함한 세션 정보가 제공된다. 정보를 업데이트하는 로직이 필요하다. 

     

    기본적으로 localStorage에 저장됨.

     

     기본적으로 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,
      },
    })​


    SupportedStorage type
    SupportedStorage type

    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

     

    댓글

🍀 Y0ungZ dev blog.
스크롤 버튼