Recent Posts
Recent Comments
Link
Today
Total
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
관리 메뉴

Jiseong's FE Dev blog

Frontend, 완벽한 자동로그인 관리 본문

React

Frontend, 완벽한 자동로그인 관리

jijiseong 2025. 12. 26. 01:32

Intro

로그인은 거의 모든 서비스에 들어간다.
로그인은 아주 쉬워보이면서도 구현할 때 마다 어렵다고 느낀다.
JWT 기반의 두 가지 자동로그인 방식의 비교, 예외 상황 처리, 최적화 등 여러가지 측면을 다뤄본다.
특히 병렬요청의 상황, Socket 통신등 API 통신 외의 통신이 도입 되었을 때의 처리를 다룬다.

자동로그인 방식 비교

커뮤니티에서는 크게 두가지 방식이 언급되는 것 같다.

  • Timer(setTimeout, setInterval) 기반의 로그인 연장 처리
  • 401 응답에 대해 retry 방식

결론부터 말하면 내가 생각하는 best practice는 401 응답에 대해 retry 방식이다.

Timer 방식의 자동로그인

Timer 방식은 setTimeout, setInterval API를 사용하여 구현할 수 있다. accessToken 만료 1분전, 5분전 등으로 정해놓고, accessToken을 재발급하는 방식이다.

Timer 방식은 허점이 많다. Timer function은 정확한 시점에 동작하지않는다. JS가 복잡하게 동작하거나, 브라우저가 백그라운드 상태로 들어가거나 다양한 상황에서 Timer는 밀려 동작할 수 있다. 이로인해 발생하는 다양한 문제가 있다.

accessToken의 유효기간 1시간이라고 가정하자. 1시간 1초가 지난 시점에 refresh 요청을 한다면, 1초 동안은 비로그인 상태가 되는것이다.
이 1초 동안 어떠한 API 통신이 이루어졌다면, 401 응답을 받을 것이다. 1초가아니라 10초, 1분, 10분이 될 수도있다.
아주 긴 form을 작성해야하는 서비스에서 이러한 상황에서 로그인이 끊긴다면 최악의 UX가 될거다.

401 retry 방식의 자동로그인

API 401 응답을 받으면 refresh 요청을 하고 기존 요청을 재요청하는 방식이다.

Timer 방식과는 다르게, accessToken 유효기간이 지나도 상관없다.어떠한 요청의 응답이 401이 발생한다면, refresh하고 재요청을하며 로그인 상태가 무조건 유지되게한다.

병렬요청 발생시 자동로그인 관리

병렬 처리 안 했을때 문제점

API 요청이 병렬로 발생하는 경우를 잘 처리해야한다.

3개의 요청이 발생했다 가정하자. 3개의 401 응답을 받게 될 것이고, 3개의 refresh 요청을 보낸다. 서버는 첫 refresh 요청을 처리한 순간, 나머지 2개의 요청의 refresh token은 만료된 토큰으로 처리한다. 결과적으로 client는 1개의 성공응답, 2개의 401응답을 받게된다.

singleton 패턴 기반의 refresh 상태 관리

구현 방법은 지피티나 클로드 물어보자. 잘 알려준다.
singleton 패턴으로 상태 관리하는게 핵심이다.
앱 전역에서 리프레시가 진행 중인지, 401 받은 요청을 관리해야한다.

class RefreshController() {
  isRefreshing: boolean
  failedRequests: Array
};

const refreshController = new RefreshController();
export default refreshController;

isRefreshing이 false인 상태일 때만 refresh 요청보낸다. 실패한 요청은 failedRquests에 저장한다.
이렇게 병렬요청인 상황을 처리할 수 있다.

다양한 통신에서의 자동로그인 처리

API 통신 외, Socket, SSE 통신 등 다른 통신을 할 때도 자동로그인 처리를 해야한다. 그래서 Singleton으로 구현했다. Socket, SSE는 연결 시점에만 토큰 유효성 검사를 한다는 특징이 있다.

로그인 상태 관리

로그인이 되었다를 어떻게 판별할 것인가. 아주아주 많은 시도, 고민이 있었다.

  • API 통신을 기반으로 서버상태로 관리하는 방법
  • 클라이언트 상태를 선언하여 optimistic하게 관리하는 방법

서버 상태 vs 클라이언트 상태

서버 상태를 기반으로 로그인 판별

서버 상태는 정확하다. 로그인이란 것은 서버에서 이루어지기 때문이다. 하지만 비동기적으로 처리해야하고, 로딩 중일때는 과연 비로그인인가 로그인인가도 고민해야한다.

첫 번째로 문제는 구현상의 문제가 있었다. react에서 서버상태를 다루려면 보통 tanstack query를 사용한다. me를 조회하는 API를 기반으로 로그인 상태로 사용했다.

const { data:me } = useQuery(getMeQueryOptions());
const isLoggedIn = Boolean(me);

accessToken, refreshToken이 모두 만료된 상태에서 다른 API 요청에서 401이 발생한다면, meQuery를 refetch 해야한다. isLoggedIn을 false 상태로 만들기 위해서다. 하지만 이 때, meQuery에서도 401이 발생하고, refresh - retry 를 시도한다. 401이 발생했기 때문에 meQuery를 다시 refetch하고 무한루프에 빠진다.

또, refetching 상태에서는 data가 null 값으로 되므로 isLoggedInfalse가 된다. 자동로그인 순간에 유저는 깜빡이는 UI를 보게된다. 물론 keepPrevioseData 같은 tanstack query API를 사용하면 된다. 하지만 이미 복잡해진다. 버그를 고치기위해 무언가를 도입하고, 또 버그가 생기고, 또 추가하게 된다.

  • 최적화의 문제
  • 구현의 복잡성

클라이언트 상태를 기반으로 로그인 판별

처음에는 '클라이언트 상태는 정확하지 않다'에 꽂혔다. 하지만 지금은 클라이언트 상태를 선언하는것이 UX 적으로, DX 적으로 모두 좋다 결론이났다.
optimistic하게 관리하는 것이 포인트다. 'accessToken이 존재하면 로그인상태다' 라고 가정하는 것이다.(accessToken을 js 메모리에 저장한다면 refreshToken기반으로 판별해도 문제없다.)

이 때, 문제가 될 만한 상황은 딱 하나 있다. accessToken과 refreshToken이 존재는 하지만 모두 만료된 토큰일 때 이다. 문제라기보다는 깜빡임이 한번 발생할것이다. 하지만 이런 상황은 케이스가 적고, 큰 문제가 아니라는 생각이다.

예를 들면, 프론트엔드에서는 로그인이 되었다고 가정하고 백엔드에 요청을 보낸다. 401응답이오고 refresh 시도를 하지만 또 401 응답이 온다. 유저는 로그인 상태일 때의 화면을 잠깐 보게되었다가 로그인화면으로 이동하는 식의 UI를 보게된다.

하지만 이 케이스만 제외한다면 모든 상황에서 UX적으로 좋다. 보통의 유저는 로그인이 된 상태의 UI를 로딩 지연없이 볼 수 있다.

Context API 기반으로 구현한다면 아래처럼 구현할 수 있다.

function SessionProvider(){
  const [isLoggedIn, setIsLoggedIn] = useState(false);


  const login = ({accessToken, refreshToken}) => { 
    // web-stroage, cookie 등에 저장
    setIsLoggedIn(true);
  }

  const logout = ({accessToken, refreshToken}) => { 
    // web-stroage 저장된 토큰 삭제
    setIsLoggedIn(false);
  }

  return(
    <_SessionProvider value={{isLoggedIn, login, logout}}>
      {children}
    </_SessionProvider>
  )
}
  • 구현의 단순함
  • 속도

하지만 여기서도 구현상의 문제가 생겼다. axios, custom api 요청 함수에 보통 interceptor가 정의되어 있을것이다. 'react 외부에서는 isLoggedIn 상태를 제어할 수 없다'가 문제였다. refresh 요청은 보통 interceptor에에 정의 되어있다. interceptor에서 어떻게 dispatch를 하는가? observer pattern으로 문제를 해결 했다.

react 외부와 연동하기 위해 useEffect를 사용한다.
(의사 코드만 제공한다. 구현은 클로드에게 물어봐라)

useEffect(() => {
  authEvent.on('LOGIN', () => {
    //...
    setIsLoggedIn(true)
  });
  authEvent.on('LOGOUT', () => {
    //...
    setIsLoggedIn(false)
  });

  return () => {
    authEvent.off('LOGIN');
    authEvent.off('LOGOUT');
  }
}, [])
// interceptor 내부
// refresh가 성공한 직후
authEvent.emit('LOGIN');

// refresh가 실패했다면
authEvent.emit('LOGOUT');

마무리

프로젝트를 진행할 때마다 로그인은 어려웠다 느꼈다. 아직 완벽한것 같진 않다. 분명 어딘가 부족한 점이 있을 것이다. 하지만 계속 발전하고 있음을 느낀다. 더 안정적인 로그인을 제공할 때 까지 계속 고민 해봐야겠다.