React Native 토큰 기반 인증 구현 개선하기

oversleep·2025년 2월 20일
0

app-development

목록 보기
18/38
post-thumbnail

들어가며

최근 백엔드 팀에서 토큰 기반 인증 방식의 변경 요청이 있었습니다.
기존에는 단일 토큰을 사용하여 인증을 처리했는데, 이를 accessTokenrefreshToken을 사용하는 더 안전하고 효율적인 방식으로 변경하게 되었습니다.

백엔드 변경사항

  1. 토큰 만료 시간 차등 적용

    • accessToken: 1시간
    • refreshToken: 14일
  2. 로그인 응답 구조 변경

    • jti (JWT ID)
    • accessToken
    • refreshToken 포함
  3. API 인증 프로세스 변경

    • 모든 인증 요청에 accessToken을 헤더에 포함
    • accessToken 만료 시 refreshToken으로 새로운 토큰 발급
    • refreshToken 만료 시 재로그인 필요

구현 상세

1. API 인터셉터 구현

API 요청과 응답을 처리하는 인터셉터를 구현하여 토큰 관리를 자동화했습니다.

// api/axios-interceptor.ts
const axiosInstance = axios.create({
  baseURL: "http://13.125.58.70:8080",
  timeout: 5000,
  headers: {
    "Content-Type": "application/json",
  },
});

// Request Interceptor
axiosInstance.interceptors.request.use(
  async (config) => {
    // 인증이 필요없는 요청 처리
    if (
      config.url === "/auth/login" ||
      config.url === "/auth/signup" ||
      config.url === "/auth/refresh"
    ) {
      return config;
    }

    // 토큰 확인 및 헤더 추가
    const token = await AsyncStorage.getItem("accessToken");
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    
    return config;
  }
);

2. 토큰 갱신 로직

accessToken 만료 시 자동으로 refreshToken을 사용하여 새로운 토큰을 발급받습니다.

// Response Interceptor
axiosInstance.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const refreshToken = await AsyncStorage.getItem("refreshToken");
        const response = await axios.post("/auth/refresh", { refreshToken });

        const { accessToken, refreshToken: newRefreshToken } = response.data;
        await AsyncStorage.multiSet([
          ["accessToken", accessToken],
          ["refreshToken", newRefreshToken],
        ]);

        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
        return axiosInstance(originalRequest);
      } catch (refreshError) {
        // refreshToken 만료 시 로그아웃 처리
        await AsyncStorage.multiRemove([
          "jti",
          "accessToken",
          "refreshToken",
          "isLoggedIn",
        ]);
        navigate("Login");
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

3. 로그인 처리

로그인 성공 시 받은 토큰들을 안전하게 저장합니다.

export const handleLogin = async ({
  email,
  password,
  navigation,
  axiosInstance,
  onError,
  onSuccess,
}: LoginParams): Promise<boolean> => {
  try {
    const response = await axiosInstance.post<LoginResponse>("/auth/login", {
      email,
      password,
    });

    if (response.data?.accessToken) {
      const decodedToken = decodeToken(response.data.accessToken);
      const userId = decodedToken?.userId;

      await AsyncStorage.multiSet([
        ["jti", response.data.jti],
        ["accessToken", response.data.accessToken],
        ["refreshToken", response.data.refreshToken],
        ["isLoggedIn", "true"],
        ["userId", userId?.toString() ?? ""],
      ]);

      return true;
    }
    return false;
  } catch (error) {
    // 에러 처리
    return false;
  }
};

4. 로그아웃 처리

로그아웃 시 모든 인증 관련 데이터를 안전하게 제거합니다.

export const useLogout = () => {
  const navigation = useNavigation<TNavigationProp>();

  const handleLogout = async () => {
    try {
      await AsyncStorage.multiRemove([
        "jti",
        "isLoggedIn",
        "accessToken",
        "refreshToken",
        "userId",
      ]);

      navigation.reset({
        index: 0,
        routes: [{ name: "Start" }],
      });
    } catch (error) {
      console.error("로그아웃 실패:", error);
    }
  };

  return handleLogout;
};

개선된 점

  1. 보안성 강화

    • 짧은 수명의 accessToken 사용으로 토큰 탈취 위험 감소
    • refreshToken을 통한 안전한 토큰 갱신
  2. 사용자 경험 개선

    • 토큰 만료 시 자동 갱신으로 끊김 없는 서비스 제공
    • 불필요한 재로그인 최소화
  3. 유지보수성 향상

    • 인터셉터를 통한 중앙화된 토큰 관리
    • 명확한 토큰 갱신 프로세스

마치며

이번 인증 방식 변경을 통해 보안성과 사용자 경험을 모두 개선할 수 있었습니다.
특히 토큰 갱신 프로세스를 자동화함으로써 프론트엔드 로직이 더욱 안정적이고 견고해졌습니다.

향후 계획으로는 토큰 관리에 대한 모니터링 강화와 에러 처리 개선을 고려하고 있습니다.

profile
궁금한 것, 했던 것, 시행착오 그리고 기억하고 싶은 것들을 기록합니다.

0개의 댓글