React Native에서 인증 관리 로직 구현

useDeviceAuth를 활용해 앱의 인증 정보를 관리합니다. 여기서 accessToken은 앱의 상태로 관리하고, refreshToken은 보안이 강화된 스토리지(SecureStore)에 저장합니다.

// import {AsyncStorage} from 'react-native'; => 옛날 방식
// import AsyncStorage from "@react-native-async-storage/async-storage"; // 최신 방식
import * as SecureStore from "expo-secure-store";
import { useState } from "react";

export const useDeviceAuth = (onResponse) => {
  const [accessToken, setAccessToken] = useState("");

  const updateDeviceAuthForAccessTokenSet = (variables) => {
    setAccessToken(variables.accessToken);
    onResponse({
      updateDeviceAuthForAccessTokenSet: {
        message: "완료",
      },
    });
  };

  const updateDeviceAuthForRefreshTokenSet = async (variables) => {
    // 1. AsyncStorage => 로컬스토리지
    // await AsyncStorage.setItem("refreshToken", refreshToken);

    // 2. SecureStore => 암호화된 스토리지
    //    2-1) 안드로이드: SharedPreferences 저장소(keystore로 암호화하여 저장됨)
    //    2-2) IOS: Keychain 저장소(keychain으로 암호화하여 저장됨)
    await SecureStore.setItemAsync("refreshToken", variables.refreshToken);

    onResponse({
      updateDeviceAuthForRefreshTokenSet: {
        message: "완료",
      },
    });
  };

  const fetchDeviceAuthForAccessTokenSet = () => {
    onResponse({
      fetchDeviceAuthForAccessTokenSet: {
        accessToken,
      },
    });
  };

  const fetchDeviceAuthForRefreshTokenSet = async () => {
    const refreshToken = await SecureStore.getItemAsync("refreshToken");
    onResponse({
      fetchDeviceAuthForRefreshTokenSet: {
        refreshToken,
      },
    });
  };

  return {
    updateDeviceAuthForAccessTokenSet,
    updateDeviceAuthForRefreshTokenSet,
    fetchDeviceAuthForAccessTokenSet,
    fetchDeviceAuthForRefreshTokenSet,
  };
};

Next.js의 로그인 페이지에서 상태 관리

Apollo Client를 통해 로그인 요청을 수행하고, 성공적으로 받은 accessTokenrefreshToken을 앱과 웹 모두에 저장합니다.

여기서 글로벌스테이트는 zustand를 사용했습니다.

import { useDeviceSetting } from "@/commons/settings/05-01-device-setting-variables/hook";
import {
  useAccessTokenStore,
  useRefreshTokenStore,
} from "@/commons/stores/11-01-token-store";
import { gql, useMutation } from "@apollo/client";
import { useRouter } from "next/navigation";
import { useState } from "react";

const LOGIN = gql`
  mutation login($loginInput: LoginInput!) {
    login(loginInput: $loginInput) {
      accessToken
      refreshToken
    }
  }
`;

export default function LoginPage() {
  const router = useRouter();

  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [login] = useMutation(LOGIN);

  const { setAccessToken } = useAccessTokenStore();
  const { setRefreshToken } = useRefreshTokenStore();
  const { fetchApp } = useDeviceSetting();

  const onChangeEmail = (event) => {
    setEmail(event.target.value);
  };

  const onChangePassword = (event) => {
    setPassword(event.target.value);
  };

  const onClickLogin = async () => {
    try {
      // 1. 로그인 뮤테이션 날려서 accessToken 받아오기
      const result = await login({
        variables: {
          loginInput: { email, password },
        },
      });
      const { accessToken, refreshToken } = result.data.login;
      if (!accessToken || !refreshToken) {
        alert("로그인에 실패했습니다! 다시 시도해 주세요!");
        return;
      }

      // 2. 받아온 accessToken, refreshToken 글로벌스테이트에 저장하기
      //    2-1) 웹에서 사용
      setAccessToken(accessToken);
      setRefreshToken(refreshToken);

      // 3. 받아온 accessToken, refreshToken 모바일 디바이스에 저장하기
      //    3-1) 앱에서 사용할 때 필요(웹뷰 말고, react-native 컴포넌트들로 만든 페이지)
      //    3-2) 앱을 종료하고 다시 들어와서 웹이 새로고침될 때 다시 꺼내오려면 필요
      await fetchApp({
        query: "updateDeviceAuthForAccessTokenSet",
        variables: { accessToken },
      });
      await fetchApp({
        query: "updateDeviceAuthForRefreshTokenSet",
        variables: { refreshToken },
      });

      // 3. 로그인 성공 페이지로 이동하기
      router.push("/section11/11-01-login-success");
    } catch (error) {
      alert(error.message);
    }
  };

  return (
    <div>
      이메일: <input type="text" onChange={onChangeEmail} />
      비밀번호: <input type="password" onChange={onChangePassword} />
      <button onClick={onClickLogin}>로그인</button>
    </div>
  );
}
// accessToken, refreshToken 저장 코드
import { create } from "zustand";

export const useAccessTokenStore = create((set) => ({
  accessToken: "",
  setAccessToken: (accessToken) => set(() => ({ accessToken })),
}));

export const useRefreshTokenStore = create((set) => ({
  refreshToken: "",
  setRefreshToken: (refreshToken) => set(() => ({ refreshToken })),
}));

ApolloClient에 accessToken 포함하기

React Native 앱에서 웹뷰를 사용해 요청 시 accessToken을 헤더에 포함합니다.

"use client";

import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
import { useAccessTokenStore } from "@/commons/stores/11-01-token-store";

const GLOBAL_STORAGE = new InMemoryCache();

export default function ApolloSettingLogin({ children }) {
  const { accessToken } = useAccessTokenStore();

  const client = new ApolloClient({
    uri: "<https://main-hybrid.codebootcamp.co.kr/graphql>",
    cache: GLOBAL_STORAGE,
    headers: { Authorization: `Bearer ${accessToken}` },
  });

  return <ApolloProvider client={client}>{children}</ApolloProvider>;
}