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,
};
};
Apollo Client를 통해 로그인 요청을 수행하고, 성공적으로 받은 accessToken과 refreshToken을 앱과 웹 모두에 저장합니다.
여기서 글로벌스테이트는 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 })),
}));
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>;
}