@gj-kit/expo-auth
Expo·React Native·웹을 한 벌의 코드로 다루는 token refresh 코어입니다. transient 분기를 빠뜨리면 컴파일이 실패합니다.
- npm
0.1.2 - 테스트 140건 이상 통과 (unit·native·web)
- @ts-expect-error 컴파일 가드 19건
- 런타임 의존성 0, optional peer 1개
- ./testing 테스트 더블 5종, peer 불필요
계속 학습하기
섹션 제목: “계속 학습하기”왜 필요한가
섹션 제목: “왜 필요한가”직접 짠 refresh 코드는 5xx·timeout·CORS 실패를 확정 거절로 오분류해 사용자를 로그아웃시킵니다. 갱신 결과를 raw switch로 소비하는데 TypeScript가 exhaustiveness를 강제하지 않고, 빠뜨리는 분기는 늘 transient이기 때문입니다. 여기에 Platform.OS 분기가 웹 번들까지 expo-secure-store를 끌고 들어오고, 두 탭이 같은 단일 사용 refresh token을 두고 경합하고, 재진입 여부를 boolean 플래그로 다루다 401 재시도가 다시 refresh 경로로 떨어지고, signOut 뒤 옵션 없이 다시 로그인하면 세션 로그인이 조용히 durable로 승격돼 공용 PC에서 사용자가 고른 범위가 무너집니다.
무엇으로 막는가
섹션 제목: “무엇으로 막는가”- transient 누락은 컴파일 에러입니다 — matchRefreshOutcome은 다섯 결말을 전부 핸들러 키로 받으므로, 하나라도 빠지면 “Property ‘transient’ is missing”으로 컴파일이 실패합니다.
- 5xx에서는 저장된 토큰에 손대지 않습니다 — transient 결말에서는 코어가 storage에 아무것도 쓰지 않고, runAuthorized는 원래 에러를 그대로 다시 던져 사용자를 로그인 화면으로 보내지 않습니다.
- 재시도 두 번은 표현할 수 없습니다 — runAuthorized는 기본값 없는 shouldRetryAfterRefresh를 요구하고, 단 한 번 수행하는 재시도는 refresh 경로 밖에서 실행되므로 공개 API에 재진입 스위치 자체가 없습니다.
- signIn은 persistence를 잊을 수 없습니다 — signIn의 두 번째 인자에서 persistence는 필수 옵션이라, “세션 로그인 → signOut → 옵션 없는 재로그인”이 토큰을 조용히 durable로 승격시키는 일이 생기지 않습니다.
- 웹 번들에 SecureStore가 없습니다 —
./storage하나가 exports 조건으로 갈라지므로 앱에는 Platform.OS 분기가 한 줄도 없고, 브라우저 그래프에는 expo-secure-store가 들어가지 않습니다.
Golden path
섹션 제목: “Golden path”완료 상태: 로그인 상태를 저장하고 동시 refresh를 조정하는 앱 소유 세션 하나를 만듭니다.
1. 설치
섹션 제목: “1. 설치”pnpm add @gj-kit/expo-auth2. 앱이 소유할 경계를 정합니다
섹션 제목: “2. 앱이 소유할 경계를 정합니다”storage adapter를 한 번 선택하고, 앱의 refresh endpoint 콜백만 연결합니다.
3. 최소 연결부터 시작합니다
섹션 제목: “3. 최소 연결부터 시작합니다”먼저 아래 코드를 복사한 뒤, 위에서 언급한 앱 소유 값만 교체하세요.
import { createAuthSession, type RefreshRequest } from '@gj-kit/expo-auth';import { createTokenStorage, createWebLocksRefreshLock } from '@gj-kit/expo-auth/storage';
declare const refresh: RefreshRequest; // Your API client classifies rotated, invalid, or transient.
export const session = createAuthSession({ storage: createTokenStorage({ keyPrefix: 'myapp.auth' }), lock: createWebLocksRefreshLock({ name: 'myapp.auth' }), refresh,});실제로는 이렇게 걸립니다
섹션 제목: “실제로는 이렇게 걸립니다”refresh()의 다섯 결말을 키로 소비하므로, 네트워크 오류를 오탐 로그아웃으로 바꾸는 누락이 컴파일 단계에서 걸립니다. tokens는 refreshed·adopted로 좁힌 뒤에만 접근할 수 있고, refresh 콜백이 throw하면 던진 값이 cause에 담긴 채 transient로 도착합니다.
import { matchRefreshOutcome, type RefreshOutcome } from '@gj-kit/expo-auth';
declare const outcome: RefreshOutcome; // the app owns this — it is `await session.refresh()`declare const goToSignIn: () => null; // the app owns navigationdeclare const report: (cause: unknown) => void; // the app owns telemetry
export const accessToken = matchRefreshOutcome<string | null>(outcome, { refreshed: ({ tokens }) => tokens.accessToken, // `tokens` exists here and on `adopted` only adopted: ({ tokens }) => tokens.accessToken, 'signed-out': () => goToSignIn(), invalid: () => goToSignIn(), // a definitive server rejection transient: ({ cause }) => { report(cause); // stored tokens are left untouched — a 5xx is not a sign-out return null; },});// Delete the `transient` line above and tsc refuses the call:// error TS2345: ... Property 'transient' is missing in type ... but required in type ...사용할 때
섹션 제목: “사용할 때”모바일과 브라우저 클라이언트 전체에서 재사용할 토큰 refresh 및 영속 경계가 필요할 때 사용합니다.
사용하지 않을 때
섹션 제목: “사용하지 않을 때”앱 route, identity provider 정책, telemetry, API client 소유권을 패키지에 넣지 마세요.
환경과 peer
섹션 제목: “환경과 peer”엔진: node >=20
| Peer | 지원 범위 | 필요한 위치 |
|---|---|---|
expo-secure-store |
>=14.0.0 |
문서화된 optional subpath에서만 |
공개 subpath
섹션 제목: “공개 subpath”| Entry | Exported symbols |
|---|---|
Root entry |
19 |
./storage |
3 |
./testing |
8 |
안전 경계
섹션 제목: “안전 경계”토큰은 secret으로 취급하세요. 제공된 오류 계약을 사용하고 토큰 문자열이나 원본 authorization 응답을 로그에 남기지 마세요.
API reference
섹션 제목: “API reference”전체 API reference에는 선언 파일에서 생성한 모든 공개 함수, 클래스, 타입, 상수, 오류 계약이 있습니다. 각 항목은 import path, release signature, package version을 표시합니다.
패키지 관계
섹션 제목: “패키지 관계”| 관계 | 패키지 | 함께 쓰는 이유 |
|---|---|---|
| 단독 사용 | — | 다른 GJ Kit 패키지 없이 도입할 수 있습니다. |
| 선택 조합 | @gj-kit/expo-media |
앱 미디어 API에 세션 소유 authorization callback을 제공할 때 함께 사용합니다. |