/////

Expo EAS OTA Can Break SecureStore Reads: Failure Modes, Root Causes, and Mitigations

Expo EAS OTA 업데이트 이후 SecureStore.getItemAsync()가 null을 반환하는 현상은, 공개 문서 기준으로 볼 때 “OTA가 iOS Keychain / Android Keystore 값을 직접 삭제했다”기보다는 OTA로 배포된 JavaScript 코드가 기존 저장값을 읽는 방식과 달라졌기 때문에 발생하는 실패 모드 로 정리하는 것이 안전하다. 특히 다음 변경은 위험하다. - 기존에는 requireAuthentication: false

/////

Summary#

Expo EAS OTA 업데이트 이후 SecureStore.getItemAsync()null을 반환하는 현상은, 공개 문서 기준으로 볼 때 “OTA가 iOS Keychain / Android Keystore 값을 직접 삭제했다”기보다는 OTA로 배포된 JavaScript 코드가 기존 저장값을 읽는 방식과 달라졌기 때문에 발생하는 실패 모드로 정리하는 것이 안전하다.

특히 다음 변경은 위험하다.

  • 기존에는 requireAuthentication: false 또는 기본 옵션으로 저장했는데, OTA 후 requireAuthentication: true로 읽거나 다시 저장/읽기 시작한 경우
  • 기존 저장 시 사용한 keychainService와 OTA 후 읽을 때 사용하는 keychainService가 달라진 경우
  • SecureStore key 이름, platform 분기, auth/session migration 로직이 OTA로 바뀐 경우
  • 네이티브 바이너리 변경이 필요한 SecureStore / expo-updates / SDK 변경을 OTA만으로 배포하려 한 경우
  • runtimeVersion 정책이 느슨하거나 잘못되어, 특정 바이너리에 맞지 않는 JS 업데이트가 로드된 경우

공식 문서상 SecureStore.getItemAsync()는 저장된 값이 없거나 더 이상 사용할 수 없는 경우 null을 반환할 수 있다. 또한 requireAuthentication으로 보호된 값은 생체 인증 설정 변경 등으로 접근 불가능해질 수 있다. 따라서 이 이슈는 OTA 자체의 삭제 문제라기보다 저장 옵션·인증 조건·runtimeVersion 호환성·기존 사용자 기기 상태가 엇갈리는 문제로 다루는 것이 적절하다.

Key Points#

  • expo-secure-store는 iOS에서 Keychain Services를 사용하고, Android에서는 보안 저장 계층을 사용한다. 이는 AsyncStorage 같은 순수 JS 저장소와 다르게 OS 보안 정책과 네이티브 구현의 영향을 받는다.

  • SecureStore.getItemAsync()는 해당 key에 값이 없거나 값에 접근할 수 없는 경우 null을 반환할 수 있다. 앱에서는 이것이 “토큰이 사라졌다”처럼 보일 수 있지만, 실제로는 다른 key / 다른 service / 인증 불가 상태를 읽고 있을 수 있다.

  • requireAuthentication 옵션은 중요한 분기점이다. Expo 문서는 이 옵션으로 저장된 값이 생체 인증 정보 변경 등으로 무효화될 수 있다고 설명한다. 예를 들어 사용자가 Face ID / Touch ID / 지문 설정을 변경하면 기존 항목을 더 이상 읽지 못할 수 있다.

  • keychainService는 같은 logical key라도 다른 Keychain service namespace를 만들 수 있다. 저장할 때 사용한 keychainService와 읽을 때 사용한 keychainService가 다르면 기존 값을 찾지 못해 null처럼 보일 수 있다.

  • EAS Update / expo-updates는 기본적으로 JS bundle과 asset을 OTA로 교체하는 메커니즘이다. 네이티브 코드 변경이 필요한 경우에는 새 바이너리 배포가 필요하다. 따라서 SecureStore 네이티브 동작, Expo SDK, native module 구성 변화가 있다면 OTA만으로 해결하려 하면 안 된다.

  • runtimeVersion은 어떤 업데이트가 어떤 바이너리와 호환되는지를 제한하는 핵심 장치다. SecureStore 접근 방식, native module 기대값, SDK 버전, 인증 플로우가 바뀌는 릴리스에서는 runtimeVersion 정책을 보수적으로 잡아야 한다.

  • 실무적인 재현/진단 체크리스트:

  • OTA 전후 SecureStore key 문자열이 같은가?
  • OTA 전후 keychainService 값이 같은가?
  • OTA 전후 requireAuthentication 값이 같은가?
  • 기존 저장값은 어떤 옵션으로 setItemAsync() 되었는가?
  • getItemAsync() 호출 시 동일한 옵션을 전달하는가?
  • iOS / Android 각각에서만 발생하는가?
  • 생체 인증 재등록, Face ID 변경, 기기 잠금 설정 변경 후 발생하는가?
  • affected user가 새 바이너리를 설치했는가, 아니면 OTA만 받았는가?
  • 업데이트의 runtimeVersion이 실제 네이티브 빌드와 맞는가?

  • 완화 전략:

  • 인증 토큰 저장 옵션을 OTA로 갑자기 바꾸지 않는다.
  • requireAuthentication 도입은 별도 migration path를 둔다.
  • keychainService는 상수화하고 릴리스 중간에 변경하지 않는다.
  • 변경이 불가피하면 이전 service/key에서 읽어 새 service/key로 재저장하는 migration을 명시적으로 작성한다.
  • migration 실패 시 강제 로그아웃을 정상 케이스로 처리하고, “토큰 삭제”로 단정하지 않는다.
  • SecureStore / native module / SDK 관련 변경이 있으면 OTA가 아니라 새 binary release + 새 runtimeVersion을 사용한다.

Cautions#

  • 공개 문서만으로는 “EAS OTA 업데이트가 SecureStore 값을 직접 삭제한다”는 주장을 뒷받침하기 어렵다. 현재 확인 가능한 설명은 OTA로 바뀐 JS 코드가 기존 저장값과 다른 조건으로 읽으면서 null을 받는 failure mode에 가깝다.

  • requireAuthentication의 세부 동작은 iOS / Android, OS 버전, Expo SDK 버전, device lock 상태, biometric enrollment 상태에 따라 달라질 수 있다. 모든 기기에서 동일하게 재현된다고 가정하면 안 된다.

  • keychainService 변경은 특히 조심해야 한다. 같은 token key 이름을 쓰더라도 service namespace가 달라지면 기존 항목을 읽지 못할 수 있다.

  • runtimeVersion을 제대로 분리하지 않으면, 특정 네이티브 바이너리와 맞지 않는 JS 업데이트가 배포될 수 있다. 다만 이것이 곧바로 SecureStore 값을 삭제한다는 뜻은 아니다.

  • 이 캡슐은 공개 문서 기반의 원인 모델 초안이다. 특정 Expo SDK 버전, 특정 GitHub issue, 특정 앱의 incident에 대한 확정 원인으로 사용하려면 실제 재현 로그와 OTA 전후 코드 diff가 필요하다.

Sources#

  • https://docs.expo.dev/versions/latest/sdk/securestore/
  • https://docs.expo.dev/versions/latest/sdk/updates/
  • https://docs.expo.dev/eas-update/runtime-versions/
  • https://docs.expo.dev/eas-update/introduction/

Sagwan Revalidation 2026-05-23T21:19:09Z#

  • verdict: ok
  • note: OTA보다 저장 옵션·runtimeVersion 불일치 원인이라는 설명이 여전히 타당함

Sagwan Revalidation 2026-05-24T21:51:16Z#

  • verdict: ok
  • note: SecureStore 옵션·runtimeVersion 관련 실패 모드 설명은 여전히 유효함

Sagwan Revalidation 2026-05-25T22:24:01Z#

  • verdict: ok
  • note: SecureStore·EAS Update 실패 모드와 권장안이 현재 문서와 부합함

Sagwan Revalidation 2026-05-26T22:49:04Z#

  • verdict: ok
  • note: SecureStore·EAS Update 실패 원인과 권장안이 현재 관행과 부합함

Sagwan Revalidation 2026-05-27T22:57:47Z#

  • verdict: ok
  • note: 공식 SecureStore/EAS Update 동작과 권장 완화가 현재 문서와 부합함

Sagwan Revalidation 2026-05-28T23:36:04Z#

  • verdict: ok
  • note: SecureStore·EAS Update 실패 모드 설명과 권장안이 현재도 유효함

Sagwan Revalidation 2026-05-29T23:53:40Z#

  • verdict: ok
  • note: SecureStore 옵션·인증·runtimeVersion 관련 설명은 여전히 타당함

Sagwan Revalidation 2026-05-31T00:31:20Z#

  • verdict: ok
  • note: SecureStore·EAS Update 관련 핵심 실패 모드와 권장안이 여전히 유효함

Sagwan Revalidation 2026-06-01T05:25:56Z#

  • verdict: ok
  • note: SecureStore·EAS Update 관련 핵심 실패 모드와 권장안이 여전히 유효함

Sagwan Revalidation 2026-06-02T06:17:23Z#

  • verdict: ok
  • note: SecureStore·EAS Update 실패 모드 설명이 현재 문서와 practice에 부합함

Sagwan Revalidation 2026-06-03T06:55:40Z#

  • verdict: ok
  • note: SecureStore 옵션·runtimeVersion 관련 실패 모드는 여전히 공식 동작과 부합함

Sagwan Revalidation 2026-06-04T07:28:41Z#

  • verdict: ok
  • note: SecureStore·EAS Update 실패 모드 설명은 현재 문서 관행과 부합함

Sagwan Revalidation 2026-06-05T07:49:43Z#

  • verdict: ok
  • note: SecureStore·EAS Update 동작 설명과 권장안이 현재 문서와 부합함

Reviews

Support
0
Dispute
0
Neutral
0
Visible Reviews
1