\(@^0^@)/

[알리미 7] 프로젝트 업그레이드 3 : Axios Interceptor, React-persist, Logout 본문

프로젝트&웨비나 회고/개인 프로젝트

[알리미 7] 프로젝트 업그레이드 3 : Axios Interceptor, React-persist, Logout

minjuuu 2022. 9. 21. 20:19
728x90

Axios Interceptor

어떤 api 요청을 하는 과정에서 만료된 refreshToken으로 인해 요청이 거부된 경우에는 어떻게 해야 할까?
axios interceptor를 활용하여 refreshToken을 재생성 후 다시 원하는 api 요청을 시도할 수 있다.

즉, 사용자의 accessToken이 만료되었다면 자동적으로 로그아웃 돼서 다시 로그인을 요청하는 것이 일반적인 루트인데,
지난 포스팅의 usefreshToken 커스텀 훅과 Axios Interceptor를 활용하여, accessToken이 만료됐다면 새로운 token을 생성해서 기존의 token에 덮어 씌운다.
이렇게 될 경우 사용자는 token이 만료되었는지, 새로운 token으로 덮어 씌웠는지 알지도 못하고, 알 방법도 없다.
token으로 인해서 다시 로그인할 경우도 훨씬 줄어들기 때문에, 웹을 이용할 때 더욱 편리하고 스무스하게 사용할 수 있다.

이론과 계획은 완벽했지만, 막상 실전에 들어와 코딩을 해보니 난관에 부딪혔다ㅠ
token이 만료되었지만 새로운 token으로 스왑 되지 않고, 계속 로그아웃되어 로그인 창으로 리다이렉션 되었다.
정말 오랫동안 고민하고 해결해보려 하였지만 정확히 어떤 부분이 문제인지, 또 어떻게 그 부분을 해결해야 할지 감이 안 잡혀서 일단은 보류하고 다른 부분들을 먼저 진행해야 할 것 같음.


React-Persist Login

이제까지 다른 프로젝트에서 로그인 기능을 구현할 때는 간편하게 LocalStorage에 token을 저장해왔다.
이 부분의 장점으로는 다른 웹사이트를 넘어갔다가 기존의 사이트에 다시 들어오거나 해당 웹사이트를 새로고침 할 경우,
LocalStorage에 token이 존재한다면 login상태가 유지시킬 수 있게 구현하는 것이 매우 간단하다는 것?
하지만 이런 방식으로 LocalStorage에서 token을 관리할 경우 내가 모르는 사람들이 (즉, 해커들) 내 token과 상태를 확인할 수 있고, 가져올 수 있으므로 보안에 취약하여 현업에서는 사실 사용하지 않는 방식이라고 알고 있다.

이번 프로젝트 때는 LocalStorage가 아닌 메모리에 저장하여 token을 관리하는 방법을 선택했고,
그럴 경우에는 다른 사이트로 넘어갔다가 기존의 사이트로 복귀하거나, 사이트를 새로고침해도 별도로 token을 웹에 저장시켜놓은 것이 아니기에, 다시 로그인이 필요하여 로그인 페이지로 리다이렉션 되고 있다.

지속적으로 로그인을 해야 하는 것이 번거롭기 때문에 React-persist를 사용하여 영구적으로 로그인 상태를 유지시켜보려고 한다.
(물론, 보안이 매우 중요한 사이트인 경우는 번거롭지만 계속 로그인시키는 것이 더 괜찮은 방법일지도 모르겠다..)
또한, 사용자가 이용하는 기기가 공공장소가 아니라, 신뢰할 수 있는 기기에서만 로그인 상태를 영구적으로 유지할 수 있도록 하여 사용자가 직접 react-persist의 사용 유무를 설정할 수 있도록 구현하였다.


const usePersist = atom({
  key: "usePersist",
  default: JSON.parse(localStorage.getItem("persist")) || false,
});

const getPersist = selector({
  key: "getPersist",
  get: ({ get }) => {
    const persist = get(usePersist);
    return persist;
  },
});

리코일을 통해 하나의 상태를 만들어주었음.
기본값은 LocalStorage에 persist가 존재한다면 persist고, 그렇지 않다면 false이다.

 

기존에는 만료된 token에 대해서만 다뤘던 Persist login을 관리하는 페이지에서
persist를 추가하여 persist가 존재한다면 로그인을 유지하고, 존재하지 않는다면 로그아웃돼서 로그인 페이지로 리다이렉션 된다.

return (
  <>
    {!persist
    ? <Outlet />
    : isLoading 
      ? <p>Loading...</p>
      : <Outlet />}
  </>
);

또한, 이중 삼항 연산자를 사용하여, persist와 isLoading의 유무에 따라 어떻게 return 할지에 대해 로직을 구현하였음.

const [persist, setPersist] = useRecoilState(usePersist);

const handleTogglePersist = () => {
  setPersist((prev) => !prev);
};

useEffect(() => {
  localStorage.setItem("persist", persist);
}, [persist]);
<div>
  <input
    type='checkbox'
    id='persist'
    onChange={handleTogglePersist}
    checked={persist}
  />
  <label htmlFor='persist'>로그인 유지</label>
</div>

마지막으로, 해당 기능을 컴포넌트에 추가하고 스타일링하는 작업!
전역으로부터 LocalStorage의 persist 상태 값을 받아온다.
로그인 유지 버튼을 클릭하면 handleTogglePersist를 통해 true 상태로 바뀌고, useEffect에서 persist가 업데이트되면 LocalStorage에 set 된다.

결과적으로, 버튼을 클릭하고 로그인을 할 경우에 새로고침 또는 다른 사이트를 왔다 갔다 하여도 로그인이 유지되고,
로그인 유지를 해지 후 로그인 할 경우, 새로고침 또는 다른 사이트로 넘어갔다가 복귀할 경우에 로그인이 해지되어 재 로그인하도록 로그인 페이지로 리다이렉션 된다.


Logout

로그아웃 기능 구현은 api 연결하여 쉽게 구현 가능할 것이라고 생각하였는데, 리코일이 익숙하지 않아서 그런 것일까..?
완성하는데 생각보다 시간이 오래 걸렸고, 많은 시련을 주었다ㅠ
로그아웃은 제대로 작동하지만, 전역으로 관리하는 유저 상태가 제대로 사라지지 않는 현상이 발생하였다.

위의 화면과 같이 로그아웃은 정상적으로 동작하였는데, 상태 관리가 제대로 되지 않아서 user 정보는 아직 남아있는 것을 확인할 수 있음.

const logoutFetch = async (token) => {
  const [auth, setAuth] = useRecoilState(useAuth);
  setAuth({});
    
  try {
    await axios.get(LOGOUT_URL, {
      headers: { Authorization: `Bearer ${token}` },
      withCredentials: true,
		});
	} catch (err) {
    console.error(err);
	}

  return logoutFetch;
};

로그아웃 api 콜 전에 상태 관리를 비워주기만 하면 된다고 생각하여 해당 로직으로 구현하였는데, 작동이 되지 않고 에러가 발생하였음.

오랜만에 보는 유효하지 않은 hook call error..

전역 상태 관리 부분에서 error가 난 것을 확인했지만, 정확히 어떤 부분이 문제인지 바로 파악이 안 됐음.
recoil에 대한 이해가 부족한 것 같다는 생각이 들어, 공식문서를 다시 훑기 시작했음.
그러다가 useRecoilValue라는 상태를 읽는 hook을 알게 되었고, 기존에 auth state를 읽을 때 사용했던 useRecoilState보다 더욱 적합하다는 생각이 들어 리팩터링을 진행하였다.

결론은 recoil과는 관련 없이, 내가 로직을 구성하는 데에 있어서 전체적인 플로우를 제대로 파악하지 못하고 엉뚱한 곳에서 상태 관리를 비워주는 것 때문에 생긴 오류였다.

const logoutFetch = async (token) => {
  try {
    await axios.get(LOGOUT_URL, {
      headers: { Authorization: `Bearer ${token}` },
      withCredentials: true,
    });
  } catch (err) {
    console.error(err);
  }

  return logoutFetch;
};
const [auth, setAuth] = useRecoilState(useAuth);
const authState = useAuthState();
const token = authState.accessToken;
let navigate = useNavigate();

const handleLogOut = async () => {
  await logoutFetch(token);
  setAuth({});
  navigate("/login");
};

로그아웃을 클릭하여 handleLogOut을 호출시키고, logoutFetch가 끝난 후에 상태 관리를 비워주도록 로직을 작성하였음.


 

728x90