[react] 진짜 진짜 마지막 로그인 정리

Ell!·2021년 12월 20일
7

로그인을 위한 그동안의 여정들

이전 블로그에 정리했던 내용

스크림도르 프로젝트를 시작했던 8월부터 4개월 내내 로그인 프로세스에 대해 고민했던 것 같다. 이제는 정말정말 마지막 최종 정리를 해보려고 한다.

위의 블로그에도 적어 놓았지만 로그인을 위해 여기저기 서치도 많이 하고 여쭤보기도 많이 했었다. 좋은 블로그를 찾아서 jwt를 통한 로그인과 로그인 유지는 해냈었지만 여전히 local Storage를 사용하던 상황. 이를 어떻게든 처리하고 싶었다.

전반적인 프로세스

전반적인 프로세스는 다음과 같다.

로그인 처리

프론트에서 아이디비밀번호로 로그인 시도. 백엔드에서는 DB를 체크해서 우리 사이트 유저라면 두가지를 내려준다. Access TokenRefresh Token 이다. 각각 json payloadsecrue, http only tag를 달아준 cookie에 넣어주어 response를 돌려준다. (access token의 목적은 앞으로의 api 요청에 대한 접근권한이고, refresh token은 새로운 access token에 대한 발급 권한이다.)

엑세스 토큰 보관

이전에는 localStorage를 사용했었기 때문에, 다음과 같이 코드를 짰었다.

// login.jsx

const onValidateCodeSubmit = async code => {
    const email = getUserDataValues().email || dataFromLogin.email;
    try {
      setLoadingState(prev => ({ ...prev, codeSubmit: true }));
      const result = await authAPI.validateEmailAuth(
        code.validateCode,
        email,
        'signup',
      );
     ❤  window.localStorage.setItem('token', result.data.token); ❤
      window.sessionStorage.setItem('is_first', result.data.is_first);
      closeModal();
      window.location.reload();
    } catch (err) {
      setLoadingState(prev => ({ ...prev, codeSubmit: false }));
      alert('잘못된 코드입니다. 다시 확인해주세요.');
    }
  };

하트 표시를 보면 알겠지만 이렇게 로컬 스토리지에 저장 후,

axiosInstance.interceptors.request.use(
  config => {

    const token = authToken.getToken();
    config.headers.common['Authorization'] = `Bearer ${token}`;

    return config;
  },
  err => {
    /* 
    request를 보낼 때에 error 발생 경우, 여기서 catch 가능
    */
    return Promise.reject(err);
  },
);

axiosInstance를 만든 후 interceptor를 이용해서 모든 api call에 로컬 스토리지에 저장되어 있던 access token을 부착해왔다.

문제는, 사용자의 access token이 로컬스토리지에 노출되어있다는 점! 비록 유효 시간이 짧고 서버에서도 redis에서 블랙 처리를 해주지만 그래도 이를 감추고 싶었다.

그래서 auth class를 만들었다.

// getter, setter를 사용함으로써 직접적인 token 프로퍼티 접근을 막는다.

class AuthToken {
  constructor() {
    this._token = '';
  }

  getToken() {
    return this._token;
  }

  setToken(newToken) {
    this._token = newToken;
  }
}

export const authToken = new AuthToken(); 

authToken을 export해서 어느 모듈에서건 사용할 수 있게 해주었다. 따라서 이전에

window.localStorage.getItem()authToken.getToken() 으로 변경해주었다.

페이지 리로드에는? silent refresh

이제 두 가지 문제점에 봉착한다.

첫번째는 우리 사이트에 장기간 접속 중일 때다. access token의 만료시간은 짧기 때문에 만료된 access token으로 api call을 사용중이라면 401 에러가 뜨게 될 것이다.

이는 지금 사용중인 react-queryinterval기능으로 해결할 수 있었다.

//  useSilentRefresh Hook  

const [refreshStop, setRefreshStop] = useState(false);

  useQuery([queryKey.REFRESH], authAPI.silentRefresh, {
    refetchOnWindowFocus: false,
    refetchOnMount: false,
    refetchOnReconnect: false,
    retry: 2,
    refetchInterval: refreshStop ? false : 60 * 60 * 1000, // 1시간 인 상황
    refetchIntervalInBackground: true,
    onError: () => {
      setRefreshStop(true);
      authToken.setToken('');
    },
    onSuccess: data => {
      const token = data?.data?.access_token;
      // access 토큰을 받아와서 새로 저장~~~
      if (token) authToken.setToken(token);
    },
  });

refetchInterval, refetchIntervalInBackground 프로퍼티를 통해 쉽게 interval을 걸 수 있었다.

두번째 문제는 페이지를 리로드해서 현재 유저의 access token이 증발했을 때이다. 이는 cookie에 저장된 refresh token을 통해 해결할 수 있었다. 앞서 refresh token은 새로운 access token에 대한 발급 권한이라고 설명한 바가 있다.

우리 사이트가 reloading되면 가장 최상단인 app.js에서 useSilentRefresh 을 가장 먼저 실행시켜 비어있는 authToken을 채워주는 것이다. 그 다음 useCheckCurrentUser를 실행시켜서 현재 유저의 정보를 불러왔다.

// useCheckCurrentUser

  /* app을 켜자마자 refresh token을 이용해서 로그인 상태 확인. */
  // refresh 올바르면 1. access token과 2. 로그인한 유저 정보 받아오기

  const currentUserQuery = useQuery(
    [queryKey.CURRENT_USER],
    authAPI.checkCurrentUser,
    {
      refetchOnWindowFocus: false,
      refetchOnMount: true,
      refetchOnReconnect: true,
      retry: 2,
      staleTime: 24 * 60 * 60 * 1000,
      onError: () => {
        authToken.setToken('');
      },
    },
  );

위의 방법으로 유저가 페이지 새로고침을 진행해도 연속된 로그인 경험을 할 수 있게 만들어주었다.

구현을 우선시하면서 관련 개념들을 보고 지나간 경우가 많은데 시간 날 때마다 차근차근 개념 정리를 해야겠따.

참조

https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C

profile
더 나은 서비스를 고민하는 프론트엔드 개발자.

2개의 댓글

comment-user-thumbnail
2022년 11월 23일

좋은 글 잘 봤습니다 ! 혹시 전체코드를 보고싶은데 해당 코드가 작성된 깃허브주소 알려주실 수 있나요?

답글 달기
comment-user-thumbnail
2023년 5월 10일

잘보고 갑니다!

답글 달기