Spotify 소셜 로그인 하기 - 3편

이은지·2022년 7월 28일
1
post-thumbnail

해결할 문제

새로고침을 하지 않은 채로 1시간이 경과했다고 가정하자.
이 상황에서 유저가 플레이 버튼을 누른다면?
토큰이 expired 상태이기 때문에 플레이가 되지 않는다.
물론 새로고침을 하면 해결되겠지만, 이는 유저에게 불편함을 준다.

지난 시간에 발견한 문제를 해결해보고자 했다.
이러한 불편함이 실제로 발생하는지 확인해보기 위해, 1시간을 기다렸다.
그런데, 1시간이 지났을 때 확인해보니 내 예상과는 다르게 로그인이 자동으로 풀려 있었다(!)

앗싸 개이득 하고 넘어갔어도 됐지만, 뭔가 이상해서 한 번 확인을 해봤다.

문제를 해결하려고 했는데? 새로운 문제(❶) 발견

const useCheckToken = () => {
  const token = useToken();
  const setToken = useSetToken();
  const router = useRouter();

  useEffect(() => {
    if (localStorage.getItem("access_token") !== null) {
      setToken(JSON.parse(localStorage.getItem("access_token")!));
    }
    if (token) {
      isTokenValid(token).then((isValid) => {
        if (!isValid) {
          setToken(null);
          router.push("/login");
        } else {
          router.push("/");
        }
      });
    } else {
      router.push("/login");
    }
  }, [router, setToken, token]);
};

알고보니, 위 코드의 useEffect가 계~속해서 반복적으로 호출되고 있었다.
useEffect가 반복 호출 되면서 토큰 유효성 검사하는 API를 계속 호출했기 때문에, 1시간이 지나면 자동으로 로그인이 풀렸던 것이다.

새로운 문제(❶) 트러블슈팅

원인 분석

useEffect의 deps 배열을 확인해보니, router가 문제였다.
deps 배열에서 router를 삭제하면 무한 리렌더링이 발생하지 않았다.
혹시나 해서 동일한 코드를 Home 컴포넌트 안으로 옮겨봤지만, 동일한 문제가 발생했다.

그냥 router를 deps 배열에서 삭제할 수도 있었지만... 저번에 읽었던 공식문서에서 "useEffect의 deps 배열에서 특정 값을 빼고 싶다면 애초에 useEffect내에서 해당 값이 쓰이지 않게 코드를 바꿔야 한다"라고 적혀 있던 게 생각이 났다. useEffect 내에서 router를 꼭 사용해야 하기 때문에 코드를 바꿀 수는 없고, 왜 deps 배열에 router를 넣는 게 무한 리렌더링 에러를 발생시키는지를 알아내야 했다.

해결 방법

구글링을 해보니, 나와 같은 문제를 겪은 사람을 찾을 수 있었다.
https://stackoverflow.com/questions/69203538/useeffect-dependencies-when-using-nextjs-router
위 글에 대한 답변으로, next.js github Issue에 동일한 문제에 대한 글이 올라와있는 것도 확인할 수 있었다.(링크)

일종의 버그인 것으로 보인다.

Currently, this is a bug.
It seems that the useRouter methods changes useRouter itself. So every time you call one of these methods, useRouter is changing and that leads to this loop.
And the other problem with this is that Next.js is not memorizing useRouter, so it changes even if the value is the same.

해결 방법으로는, router를 useRef로 저장해두는 방법이 제시되어 있었다.
router를 useRef로 저장해두고, 해당 router의 push 메서드를 리턴해 다른 곳에서도 사용할 수 있게 하는 커스텀 훅의 코드가 올라와 있어서, 이 코드를 적용했다.

useRef라는 훅은 참 신기한 용도로 많이 쓰이는 것 같다 ㅇㅅㅇ... 나중에 한 번 각잡고 공부해 봐야겠다. 아무튼!

import { useRouter } from 'next/router'
import type { NextRouter } from 'next/router'
import { useRef, useState } from 'react'

export default function usePush(): NextRouter['push'] {
    const router = useRouter()
    const routerRef = useRef(router)

    routerRef.current = router

    const [{ push }] = useState<Pick<NextRouter, 'push'>>({
        push: path => routerRef.current.push(path),
    })
    return push
}

응답에 올라와 있었던 이 코드를 hooks/usePush.ts에 추가하고, 기존 router.push를 usePush가 리턴하는 push로 대체했더니 문제가 해결되었다.


이제 새로 발견한 문제도 해결 했겠다. 다시 원래 해결하려던 문제를 해결하려고 하는데 다시금 새로운 문제에 직면했다.

또다시 새로운 문제(❷) 발견

두번째 새로운 문제: 새로고침 시 로그인이 풀린다.

새로운 문제(❷) 트러블슈팅

원인 분석

👩🏻‍💻 내 생각: 3번 라인에서 setToken한 연산이 token에 바로 반영되어 5번 라인 if(token)이 True를 리턴할 것이다.

💻실제 동작:
3번 라인의 setToken이 반영되지 않은 채로 5번 라인 실행. 이때는 token==null 이므로 15번 라인의 push('/login')에 의해 로그인 페이지로 리다이렉트 된다.
setToken에 의해 token이 변경되었으므로 deps arr에 의해 useEffect가 다시 실행된다. 이때는 token에 값이 제대로 들어 있으나 이미 로그인 페이지로 리다이렉트된 후이기 때문에, 제대로 동작하지 않는다.

useEffect의 상세한 동작 방식을 확인해볼 수 있었던 에러였다.

해결 방법

시도1: ❌

이를 해결하기 위해서는, token이 애시당초부터 null이 아닌 로컬스토리지에 들어 있는 값으로 설정되어야 한다고 생각했다. 두 가지 방법이 있었다.

  • index.tsx의 useCheckToken을 수정하여, 새로고침 시마다 로컬스토리지를 읽어와 setToken한다.
  • useToken을 수정한다. 로컬스토리지에 값이 존재할 시 해당 값을 useToken의 초기값으로 설정한다.

useToken의 용도 자체가 token의 상태를 관리하는 것이기 때문에, 두번째 방법이 더 적절하다고 판단했다.

그러나 결론적으로 이 방법은 사용할 수 없었다.
useToken 내에서 초기값을 설정하는 시점에 로컬스토리지에 접근할 수 없었기 때문이다.
SSR방식인 NextJS에서는 페이지에 client가 로드되고 window객체가 정의될 때까지 로컬스토리지에 접근할 수 없었다. 따라서 NextJS에서 로컬스토리지를 사용하기 위해서는 useEffect 내에서 사용해야 했는데, 그럴 경우 useToken의 초기값이 여전히 null이 되므로 문제가 해결되지 않는다.

시도2: ⭕️
따라서 첫번째 방법을 택했다. 코드를 아래와 같이 수정했다.

token이 null일 경우 무조건 로컬스토리지를 읽어와 반영하도록 로직을 바꿨다.
다음 로직을 진행하기 전에 무조건 로컬스토리지의 토큰이 반영되므로 문제가 해결된다!


이렇게 코드를 고친 후 토큰이 만료되길 기다려 확인해보니 내가 원래 해결하고 싶었던 문제가 잘 재현되었다... 새로고침 시 로그인 페이지로 리다이렉트 되어 다시 로그인을 하도록 동작하는 것도 확인 했다. 이제 한 시간마다 자동으로 토큰 유효성을 확인하게끔 해야 하는데, 이를 위해 react-query를 써야 할 지 어떨지 고민이다. 과연 setInterval이 얼만큼 안정적으로 작동할 것인가?

또한 대부분의 경우 잘 동작하는데, 새로고침 시에 token이 null인 상태로 onSpotifyWebPlaybackSDKRead 함수가 동작해 화면이 먹통이 될 때가 있다. useEffect랑 Context랑 순서가 꼬여서 발생하는 문제 같은데, 위 문제를 포함해 어떻게 해결해야 할지 고민 해봐야겠다.

0개의 댓글