Expo SecureStore 사용법: 토큰 저장과 생체 인증 처리

2026.04.21·수정 2026.07.20·약 14분

Expo SecureStore 토큰 저장 핵심 요약

SecureStore는 기기의 보호된 저장 기능을 이용해 작은 key-value 값을 암호화해 저장하지만, 완전한 비밀 저장소·서버 보안·백업·세션 폐기를 대신하지 않습니다. access/refresh token처럼 앱 재시작 뒤 필요한 작은 값만 저장하고, 서버에서 토큰 만료·회전·폐기를 강제하세요. 비밀번호, 서비스 계정 키, 대용량 프로필, 복구 불가능한 원본 데이터는 넣지 않습니다.

버전 기준: 2026-07-19 Expo SDK 57 최신 문서의 expo-secure-store ~57.0.1 기준입니다. 실제 설치는 npx expo install expo-secure-store로 프로젝트 SDK와 맞추고 lockfile을 확인하세요.

1. “암호화됨”보다 저장할 값과 공격 경계를 먼저 정합니다

Expo SecureStore에 저장할 토큰과 저장하면 안 되는 서버 비밀·대용량 데이터를 나누는 기준
권장 이유·대안
짧은 access token 조건부 가능 만료를 짧게 하고 서버 검증·폐기 적용
refresh token 가능 회전·재사용 탐지·로그아웃 시 서버 폐기 필요
앱 잠금용 작은 secret 조건부 가능 생체 인증 실패·무효화 fallback 설계
사용자 비밀번호 저장 금지 인증 공급자의 token/session 사용
서비스 계정·API master key 저장 금지 서버 비밀 관리자에 보관, 앱에 배포하지 않음
대용량 프로필·복구 불가능한 원본 저장 금지 플랫폼이 큰 값을 거부할 수 있고 backup이 아님

SecureStore에서 값을 읽은 뒤에는 JavaScript 메모리와 앱 로직이 그 평문을 사용할 수 있습니다. 기기가 root/jailbreak 되었거나 실행 중인 앱 프로세스가 침해된 상황까지 절대 방어한다고 단정할 수 없습니다. 어떤 공격을 줄이려는지와 토큰이 탈취됐을 때 서버가 무엇을 할 수 있는지 함께 설계합니다.

2. Android와 iOS의 저장·삭제 동작은 같지 않습니다

Android

Expo 공식 문서상 값은 SharedPreferences에 저장되고 Android Keystore의 키로 암호화됩니다. 앱을 제거하면 Keystore 항목이 사라지므로 SecureStore 값도 보존되지 않습니다. 복원된 backup은 기존 키로 해독할 수 없기 때문에 config plugin이 해당 항목을 Android Auto Backup에서 제외하도록 구성합니다. Keystore의 키 추출 방지와 사용 제한 원리는 Android Keystore 공식 문서에서 확인하세요.

iOS

iOS에서는 Keychain의 generic password 항목을 사용합니다. 같은 bundle ID로 재설치할 때 값이 남을 수 있지만 Expo는 이 동작을 보장되는 영구 저장으로 의존하지 말라고 안내합니다. 로그아웃·계정 삭제 때 앱이 명시적으로 값을 지우고, 재설치 뒤 남은 credential도 서버가 유효성을 다시 판단해야 합니다. 기반 API는 Apple Keychain Services입니다.

상황 Android iOS 앱 처리
앱 업데이트 일반적으로 유지 일반적으로 유지 스키마·key migration 테스트
앱 삭제·재설치 보존되지 않음 남을 수 있으나 보장 아님 서버 재검증, signed-out fallback
생체 정보 변경 requireAuthentication 값이 무효화될 수 있음 null·오류를 로그아웃 또는 재인증으로 처리

3. 설치와 config plugin

npx expo install expo-secure-store
{
  "expo": {
    "plugins": [
      [
        "expo-secure-store",
        {
          "configureAndroidBackup": true,
          "faceIDPermission": "보호된 세션을 확인하기 위해 Face ID를 사용합니다."
        }
      ]
    ],
    "ios": { "config": { "usesNonExemptEncryption": false } }
  }
}

Face ID 설명 문구와 Android backup 설정은 native binary에 반영되므로 config를 바꾼 뒤 새 development build·production build를 만들어야 합니다. 생체 인증이 필요한 requireAuthentication 흐름은 Expo Go가 아니라 config가 포함된 실제 build와 실기기에서 확인합니다. 정확한 현재 옵션은 Expo SecureStore 공식 문서를 기준으로 합니다.

4. 토큰 저장·복원·삭제를 한 모듈로 묶습니다

Expo SecureStore 토큰 저장, 서버 refresh, 회전, 로그아웃 삭제와 실패 복구 흐름

저장 API를 화면에서 직접 흩어 쓰지 않습니다

import * as SecureStore from 'expo-secure-store';

const ACCESS_TOKEN = 'auth.access-token';
const REFRESH_TOKEN = 'auth.refresh-token';

export async function saveSession(accessToken: string, refreshToken: string) {
  await Promise.all([
    SecureStore.setItemAsync(ACCESS_TOKEN, accessToken),
    SecureStore.setItemAsync(REFRESH_TOKEN, refreshToken),
  ]);
}

export async function readRefreshToken() {
  return SecureStore.getItemAsync(REFRESH_TOKEN);
}

export async function clearSession() {
  await Promise.all([
    SecureStore.deleteItemAsync(ACCESS_TOKEN),
    SecureStore.deleteItemAsync(REFRESH_TOKEN),
  ]);
}

key 이름과 옵션을 중앙 모듈에 두면 저장할 때와 읽을 때 다른 keychainService·인증 옵션을 쓰는 실수를 줄일 수 있습니다. 로그에는 토큰 값이나 SecureStore 오류 객체의 민감한 부가정보를 출력하지 않습니다.

앱 시작 시 저장 값만 보고 로그인 완료로 판단하지 않습니다

type RefreshFailure = Error & { status?: number; code?: string };

const INVALID_REFRESH_CODES = new Set([
  'invalid_grant',
  'invalid_token',
  'token_revoked',
]);

function isRefreshCredentialInvalid(error: unknown) {
  if (typeof error !== 'object' || error === null) return false;
  const failure = error as RefreshFailure;
  return (
    failure.status === 401 ||
    failure.status === 403 ||
    INVALID_REFRESH_CODES.has(failure.code ?? '')
  );
}

export async function restoreSession() {
  const refreshToken = await readRefreshToken();
  if (!refreshToken) return { kind: 'signed-out' } as const;

  try {
    const session = await exchangeRefreshToken(refreshToken);
    await saveSession(session.accessToken, session.refreshToken);
    return { kind: 'signed-in', session } as const;
  } catch (error) {
    if (isRefreshCredentialInvalid(error)) {
      await clearSession();
      return { kind: 'signed-out' } as const;
    }

    // network·timeout·429·5xx는 token을 보존하고 상위 재시도 정책에 맡깁니다.
    throw error;
  }
}

저장된 refresh token이 있어도 만료·폐기·재사용 탐지·계정 정지 상태일 수 있습니다. 서버 교환이 성공한 뒤 session을 복원하되, 모든 실패를 로그아웃으로 취급하면 안 됩니다. 위 예제는 exchangeRefreshToken이 서비스 계약에 따라 “refresh credential 무효”를 401·403 또는 명시적 code로 정규화한다고 가정합니다. OAuth 2.0 규격의 invalid_grant도 만료·폐기·유효하지 않은 refresh token을 뜻합니다. 이때만 로컬 token을 지우고, network·timeout·429·5xx는 token을 보존한 채 상위로 전달해 제한된 재시도·backoff·offline UI를 적용합니다. 근거는 OAuth 2.0 token endpoint 오류 규격, 앱 인증 흐름은 Expo 인증 가이드를 참고하세요.

5. 생체 인증은 저장 암호화와 별도의 사용 조건입니다

import * as SecureStore from 'expo-secure-store';

const PROTECTED_KEY = 'auth.protected-refresh-token';
const protectedOptions = {
  requireAuthentication: true,
  authenticationPrompt: '세션을 계속하려면 본인 확인이 필요합니다.',
  keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
} as const;

export async function saveProtectedToken(value: string) {
  if (!SecureStore.canUseBiometricAuthentication()) return false;
  await SecureStore.setItemAsync(PROTECTED_KEY, value, protectedOptions);
  return true;
}

export async function readProtectedToken() {
  try {
    return await SecureStore.getItemAsync(PROTECTED_KEY, protectedOptions);
  } catch {
    return null;
  }
}

canUseBiometricAuthentication()은 충분히 안전한 생체 인증 사용 가능 여부를 확인합니다. requireAuthentication은 플랫폼별 prompt 시점이 다르고, 새 지문·얼굴 등록처럼 생체 설정이 바뀌면 저장 key가 무효화되어 읽기 결과가 null이 될 수 있습니다. 생체 인증 실패를 데이터 손실 오류로만 처리하지 말고 비밀번호·OAuth 재로그인 같은 복구 경로를 제공합니다.

동기식 getItem·setItem은 인증 prompt 동안 JavaScript thread를 막을 수 있으므로 화면 흐름에서는 async API를 우선 검토합니다. 실제 기기의 prompt, 취소, 여러 번 실패, 생체 정보 변경을 모두 테스트하세요.

6. 저장소 한계와 실기기 검증표

Expo 문서는 큰 payload를 underlying platform이 거부할 수 있고, 과거 일부 iOS에서 약 2,048바이트를 넘는 값이 거부된 사례를 설명합니다. 이 숫자를 보장 한도로 사용하지 말고 토큰처럼 작은 문자열만 저장하며 모든 write 오류를 처리합니다.

7. 명시적 결론과 다음 행동

결론: SecureStore는 작은 credential의 로컬 노출 위험을 줄이는 도구이며 인증 시스템 자체가 아닙니다. 서버의 짧은 만료·refresh token 회전·폐기와 앱의 실패 복구가 함께 있어야 합니다. 다음 행동은 저장 중인 key 목록을 작성해 토큰 외 데이터를 제거하고, Android/iOS 삭제·재설치·생체 변경 테스트를 실제 기기에서 수행하는 것입니다.

공식 근거

내부 학습 경로

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

“Expo SecureStore 사용법: 토큰 저장과 생체 인증 처리”에 대한 1개의 생각

댓글 남기기