티스토리 뷰

카테고리 없음

리액트 네이티브 Expo에서 Apollo GraphQL 토큰 관리 문제 해결하기

YG - 96년생 , 강아지 있음, 개발자 희망 2025. 3. 17. 08:27

리액트 네이티브 Expo에서 Apollo GraphQL 토큰 관리 문제 해결하기

문제 상황

리액트 네이티브 Expo 환경에서 GraphQL Apollo 클라이언트를 사용하는 중, 액세스 토큰과 리프레시 토큰 처리에서 문제가 발생했습니다.

 

기존에는 getItemAsync와 같은 비동기 함수를 사용해 토큰을 스토리지에서 가져오고 있었는데, 이로 인해 첫 요청 시 Authorization 헤더가 비어있는 상태로 요청이 전송되는 문제가 있었습니다.

원인 분석

 

문제의 핵심은 비동기적으로 토큰을 가져오는 과정에서 발생했습니다. 백엔드 로그를 확인해보니, 요청이 두 번 발생하는데:

  1. 첫 번째 요청: 토큰이 없이 전송됨
  2. 두 번째 요청: 토큰이 포함되어 전송됨

이는 getItemAsync의 비동기 특성 때문에, 토큰을 가져오기 전에 첫 요청이 먼저 전송되어 발생하는 현상이었습니다.

 

해결 방법

1. 동기식 스토리지 접근 구현

expo-sqlite/kv-store를 사용하여 동기식으로 스토리지에 접근할 수 있는 유틸리티 함수를 구현했습니다:

import { SQLiteStorageSetItemUpdateFunction, Storage } from 'expo-sqlite/kv-store';

// 비동기 함수 (기존 방식)
export const getItemAsync = async (key: string) => {
  const value = await Storage.getItem(key);
  return value;
};

// 동기 함수 (문제 해결)
export const getItemSync = (key: string) => {
  const value = Storage.getItemSync(key);
  return value;
};

export const setItemAsync = async (
  key: string,
  value: string | SQLiteStorageSetItemUpdateFunction
) => {
  await Storage.setItem(key, value);
};

export const setItemSync = (
  key: string,
  value: string | SQLiteStorageSetItemUpdateFunction
) => {
  Storage.setItemSync(key, value);
};

export const deleteItemAsync = (key: string) => {
  Storage.removeItemAsync(key);
};

 

2. Apollo 클라이언트 설정 수정

Apollo 클라이언트의 authLink를 수정하여 동기식으로 토큰을 가져오도록 했습니다:

const authLink = new ApolloLink((operation, forward) => {
  const token = getItemSync(ACCESS_TOKEN); // 비동기 -> 동기로 변경

  operation.setContext(({ headers = {} }) => ({
    headers: {
      ...headers,
      Authorization: token ? `Bearer ${token}` : '',
    },
  }));

  return forward(operation);
});

 

3. 에러 핸들링 로직 수정

인증 에러 발생 시 리프레시 토큰을 사용해 새 토큰을 발급받는 로직도 동기식으로 수정했습니다:

const errorLink = onError(({ graphQLErrors, operation, forward }) => {
  if (graphQLErrors) {
    for (const err of graphQLErrors) {
      if (err.extensions?.code === 'UNAUTHENTICATED') {
        return new Observable<FetchResult>((observer) => {
          const refreshToken = getItemSync(REFRESH_TOKEN); // 동기식으로 가져옴
          const token = getItemSync(ACCESS_TOKEN);
          
          // 리프레시 토큰 요청 및 처리...
          fetch(fetchUrl, {
            // ...요청 로직
          })
          .then((response) => response.json())
          .then((result: RefreshTokenResult) => {
            // 새 토큰 저장 시에도 동기식 사용
            setItemSync(ACCESS_TOKEN, newAccessToken);
            setItemSync(REFRESH_TOKEN, newRefreshToken);
            
            // 요청 재시도...
          })
        });
      }
    }
  }
  // ...
});

 

서버 측 구현

서버 측에서는 NestJS를 사용하여 JWT 미들웨어와 토큰 관리를 구현했습니다. 웹 버전에서는 액세스 토큰은 로컬 스토리지, 리프레시 토큰은 쿠키로 관리했지만, 앱에서는 개발 효율성을 위해 둘 다 스토리지에 저장하는 방식을 선택했습니다.

결론

이 문제는 비동기 스토리지 접근 방식에서 동기식으로 변경함으로써 해결할 수 있었습니다.

 

Expo 환경에서 Apollo Client를 사용할 때 인증 헤더를 설정하는 경우, getItemSync와 setItemSync와 같은 동기 함수를 사용하면 첫 요청부터 올바르게 토큰이 포함되도록 할 수 있습니다.

 

 

웹과 앱 환경에서의 토큰 관리 방식은 다를 수 있으며, 개발 환경과 사용자 경험을 고려하여 적절한 방식을 선택해야 합니다. 이 경우에는 사용자가 일주일에 한 번씩 로그인해야 하는 불편함을 줄이기 위해 리프레시 토큰을 유지하는 방식을 선택했습니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
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 29 30
글 보관함