오늘 배운 것

현재 서버에는 액세스 및 리프레시 토큰을 발급하는 API, 토큰이 유효한지 확인하는 API, 그리고 액세스 토큰이 만료되었을 경우 리프레시 토큰을 제시하면 새로운 액세스 토큰을 제공하는 API가 있다. 그러나 프론트 앱 서버에서는 현재 토큰이 만료되었을 경우, 리프레시 토큰을 통해 액세스 토큰을 갱신하는 별도의 작업은 하지 않고 있어서 이 작업이 필요하다고 느꼈다. 

 

우선은 프론트 코드에서 토큰을 갱신하는 작업을 하는 로직 작성을 위해 API_PATH 변수에 액세스 토큰을 갱신해주는 API 엔드포인트를 입력하고, 해당 API를 호출하기 위한 커스텀 훅도 만들어 주었다. 

renew: `${BASE_URL}/auth/token/refresh/`,
renewToken: refreshToken => {
  return handleRequest(() => axios.post(API_PATH.renew, { refreshToken }));
},

 

그리고 백엔드 서버에 가서, 해당 API를 어떤 방식으로 호출해야 하는지 알아보았다. 

 

해당 TokenRefreshView에서는 TokenRefreshSerializer를 사용하고 있었고, 해당 serializer에서는 refresh와 access 토큰 2개의 파라미터를 받고 있었다. 

 

TokenViewBase 함수가 어떻게 동작하는지 살펴보니, 핵심 로직은 post 메소드에 있는 것 같았다. 사용하려는 serializer가 유효한지 판단한 후, 해당 serializer의 validated_data를 가져오는 식이었다. 그렇다면 TokenRefreshSerializer의 validate 메소드를 보면 되겠다. validate 메소드에서는 RefreshToken 클래스에 request에서 보낸 정보를 담아 가져오고 있었다. 

 

아무튼 결론은, TokenRefreshView에 POST 방식으로(토큰 관련 데이터를 보내는 것이므로 GET은 아닐 것이라고 추측했다) 리프레시 토큰과 액세스 토큰 데이터를 보내면 되겠다. 


그럼 이제 프론트에서 해당 API로 요청을 보내보자. 주의할 점은 "기존에 다른 API에서 보낸 요청이 토큰 만료로 인해 실패했을 때" 해당 API를 호출해야 하기 때문에, 에러 처리 로직에서 해당 API를 호출하게 될 것이다. 사실 하나의 API에서만 이 작업을 하려면 그냥 queryClient를 사용할 때 onError 로직에 함수를 추가해주면 된다. 그런데 그게 아니라 거의 대부분의 API에서 이 작업을 공통적으로 해야 하므로, 로직을 공통화할 필요가 있다. 

 

그래서 블로그를 찾아보니, 여기서는 queryClient를 별도의 파일에, QueryClientProvider라는 컴포넌트로 QueryClient를 감싸서 그 안에서 기본 에러 로직을 작성해주는 식으로 작업하고 있었다. 그런데 이렇게 하는 경우, QueryClientProvider를 정의한 클래스와 실제 사용하는 클래스가 서로 다른 곳에 위치해 있기 때문에 useRef 훅을 사용해서 해당 QueryClient의 값을 계속 참고하도록 했다. 

 

그런데 우리는 이미 QueryClient를 정의한 곳에서 사용까지 하고 있었기 때문에 이 로직이 불필요하다고 생각해서, 그냥 new QueryClient()로 QueryClient를 생성하는 시점에 default option으로 정의해 주기로 했다. 그런데 그게 또 오류가 났다. 몇 번의 삽질 끝에, useMutation()을 사용해서 custom hook을 만드는 방식으로는 어쨌든 로직을 계속 반복해줘야 한다는 결론이 났다. 

 

그래서 모든 로직이 거쳐가는 handleRequest()라는, 각 API 요청별로 아주 간단한 에러 처리를 하는, 모든 요청이 공통적으로 지나가는 함수에서 try/catch 로직 부분에 다음과 같은 코드를 추가해 주었다. 

const handleRequest = async request => {
  try {
    const response = await request();
    return response.data;
  } catch (err) {
    console.log(JSON.stringify(err.response, null, 2));
    if (
      err.response.status === 401 &&
      err.response.data.message === 'Token expired'
    ) {
      try {
        const responseData = await Api.renewToken();
        AsyncStorage.setItem('accessToken', responseData.accessToken);
        AsyncStorage.setItem('refreshToken', responseData.refreshToken);
        const secondRequest = await request();
        return secondRequest.data;
      } catch (refreshError) {
        if (refreshError.response.status === 401) {
          router.replace('index');
        } else {
          throw refreshError;
        }
      }
    } else {
      throw err;
    }
  }
};

 

 궁금한 점

1. Simplejwt 라이브러리의 동작 원리가 궁금하다. 어떻게 이 설정들을 settings 및 urls에 명시하는 것 만으로도 토큰을 발급 및 갱신할 수 있었을까?

2. SimpleJwt 라이브러리에서 왜 굳이 RefreshToken 클래스에서 property로 accessToken을 넣었을지 궁금하다. 그냥 별도로 RefreshToken과 AccessToken을 만들어서 관리하는 방법은 별로였을까?

3. useRef는 구체적으로 언제 필요한 것일까? 잘 모르겠다. 

 

참고한 사이트

https://velog.io/@heeppyea/QueryClient-%EA%B8%B0%EB%B3%B8-%EC%84%A4%EC%A0%95-%ED%95%B4%EC%A3%BC%EA%B8%B0

+ Recent posts