← ALL ENTRIES

Expo


📋 요약 한 줄 요약 Expo는 React Native로 앱을 만들 때 필요한 것들을 미리 묶어놓은 도구 모음이다. React Native의 대체재가 아니라 그 위에 얹히는 층으로, 네이티브 프로젝트(Xcode/Android Studio)를 직접 열지 않고도 카메라·파일시스템 같은 네이티브 기능을 쓰고 빌드까지 할 수 있게 해준다. 핵심 아이디어는 “네이티브 코드를 손으로 관리하지 않는다”.

1. React Native와 무슨 관계인가

React Native는 JS/TS로 쓴 코드를 iOS·Android의 진짜 네이티브 UI로 그려주는 프레임워크다. 하지만 RN만 쓰면 직접 해야 하는 게 많다.

  • ios/, android/ 네이티브 프로젝트를 직접 들고 관리
  • 라이브러리 하나 붙일 때마다 Podfile·Gradle·권한 설정을 손으로 수정
  • 빌드하려면 macOS + Xcode(iOS), Android Studio(Android) 필요

Expo는 이 뒤치다꺼리를 대신한다. 개발자는 JS/TS와 설정 파일(app.json) 하나만 만지고, 네이티브 쪽은 Expo가 생성·관리한다.

[ 내 코드 (TS/React) ]

[ Expo SDK — expo-camera, expo-file-system, expo-font ... ]

[ React Native ]

[ iOS / Android 네이티브 ]

2. 핵심 개념

2.1 Expo SDK

네이티브 기능을 감싼 공식 모듈 묶음. expo-image-picker(사진 선택), expo-file-system(파일), expo-font(폰트), expo-notifications(푸시) 같은 것들이다.

중요한 점: SDK는 버전 단위로 통째로 움직인다. SDK 57을 쓰면 그 안의 모듈들도 57 계열로 맞춰야 하고, 모듈 API가 메이저 버전에서 갈아엎히는 경우가 있다. (예: expo-file-system 57은 예전 함수 호출 방식 대신 new File(uri) 같은 객체 API를 쓴다.) 그래서 Expo는 검색 결과보다 해당 버전 공식 문서를 봐야 한다 — 오래된 블로그 글의 코드가 그대로 안 도는 일이 잦다.

2.2 Expo Go vs Development Build

Expo GoDevelopment Build (dev client)
뭔가스토어에서 받는 미리 만들어진 껍데기 앱내 프로젝트로 직접 빌드한 개발용 앱
설치앱스토어/플레이스토어에서 바로EAS 등으로 빌드해서 설치
쓸 수 있는 것Expo SDK에 포함된 모듈만네이티브 라이브러리 아무거나
언제초기 개발, 빠른 확인서드파티 네이티브 모듈이 필요해질 때

Expo Go는 이미 빌드된 앱이라 그 안에 들어있지 않은 네이티브 코드는 못 쓴다. 소셜 로그인 SDK처럼 Expo SDK 밖의 네이티브 모듈이 필요해지는 순간 dev client로 넘어가야 한다.

2.3 CNG (Continuous Native Generation)

ios/, android/ 폴더를 저장소에 두지 않고, app.json 설정으로부터 빌드할 때마다 새로 생성하는 방식. npx expo prebuild가 그 생성 작업을 한다.

  • 장점: 네이티브 프로젝트 파일 충돌·머지 지옥이 없고, SDK 업그레이드가 설정 갱신으로 끝난다
  • 대가: 네이티브 파일을 손으로 고치면 다음 생성 때 날아간다. 고쳐야 하면 config plugin으로 “이렇게 고쳐라”를 코드로 적어둔다

2.4 app.json / config plugin

앱 이름, 아이콘, 번들 ID, 권한 문구 등을 한 파일에서 선언한다. 네이티브 설정이 필요한 라이브러리는 플러그인으로 등록하면 prebuild 때 알아서 반영된다.

"plugins": [
  ["expo-image-picker", {
    "photosPermission": "추억 사진을 기록에 넣기 위해 사진 접근이 필요해요."
  }],
  "expo-font"
]

권한 문구를 Info.plist/AndroidManifest.xml에 직접 쓰지 않고 여기서 선언한다는 게 요점.

2.5 Expo Router — 파일 기반 라우팅

SDK 49 이후 Expo가 미는 표준 네비게이션. app/ 폴더의 파일 구조가 곧 화면 경로가 된다. React Navigation을 직접 설정하는 대신 파일을 만들면 라우트가 생긴다.

app/
├── _layout.tsx          # 공통 레이아웃 (전역 Provider, 테마)
├── index.tsx            # "/"        홈
├── record/
│   ├── _layout.tsx      # 이 그룹의 레이아웃 (스택/탭)
│   ├── index.tsx        # "/record"
│   └── [date].tsx       # "/record/2026-08-05"  동적 경로
└── (tabs)/              # 괄호 폴더 = 경로에 안 들어가는 그룹
    ├── home.tsx         # "/home"
    └── settings.tsx     # "/settings"
import { Link, router, useLocalSearchParams } from 'expo-router';

// 선언형 이동
<Link href="/record/2026-08-05">기록 보기</Link>

// 명령형 이동
router.push('/record/2026-08-05');
router.back();

// 동적 경로 파라미터 받기 — [date].tsx 안에서
const { date } = useLocalSearchParams<{ date: string }>();
  • [date].tsx 대괄호는 동적 세그먼트, (tabs) 괄호는 경로에 포함되지 않는 그룹 폴더(레이아웃만 공유)
  • 딥링크가 공짜로 따라온다 — myapp://record/2026-08-05가 해당 화면으로 바로 연결된다. 소셜 로그인 콜백을 받을 때 이 점이 중요해진다
  • 웹까지 같은 코드로 도는 게 설계 의도(React Native Web)

2.6 EAS (Expo Application Services)

Expo가 운영하는 클라우드 서비스.

  • EAS Build — 클라우드에서 앱을 빌드한다. macOS 없이 iOS 빌드가 가능한 게 실질적으로 가장 큰 이점. 프로파일별로 산출물을 나눈다 (내부 배포용 APK / 스토어용 AAB 등)
  • EAS Submit — 빌드 결과를 스토어에 업로드
  • EAS Update (OTA) — JS 번들만 교체해서 스토어 심사 없이 배포. 단 JS 레벨 변경만 가능하고, 네이티브 모듈이 바뀌면 새로 빌드해야 한다

2.7 환경변수와 시크릿

RN 앱은 번들이 기기에 통째로 내려가므로 “앱에 넣는 값은 다 공개된다” 를 전제로 나눠야 한다.

# .env — EXPO_PUBLIC_ 접두사가 붙은 것만 앱 코드에서 읽힌다
EXPO_PUBLIC_API_URL=https://api.example.com
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
  • EXPO_PUBLIC_*빌드 시점에 번들에 박제된다. API 주소나 클라이언트 ID처럼 노출돼도 되는 값만 넣는다
  • 서버 시크릿(DB 비밀번호, 서비스 키)은 앱에 절대 넣지 않는다. 필요하면 백엔드를 경유한다
  • 빌드 과정에만 필요한 값(스토어 자격증명 등)은 EAS Secrets(eas secret:create)로 클라우드에 두고 번들에는 포함시키지 않는다
  • 소셜 로그인 키는 클라이언트에 두지 않는다. 시크릿은 물론 클라이언트 키까지 서버가 들고 프로바이더와 왕복하고, 앱에는 자체 발급한 인증 토큰만 내려준다 (4장 “방법 B” 참고). 네이티브 SDK를 직접 붙이는 경우에만 앱 키가 불가피하게 들어간다

3. 자주 쓰는 명령어

명령어용도
npx create-expo-app@latest <name>프로젝트 생성
npx expo start개발 서버 실행 (--dev-client 붙이면 dev build용)
npx expo install <pkg>SDK 버전에 맞는 버전으로 설치 — npm install 대신 이걸 쓴다
npx expo install --check설치된 패키지가 현재 SDK와 맞는지 점검
npx expo prebuild --cleanios/·android/ 재생성 (CNG)
npx expo-doctor버전 충돌·설정 문제 진단
eas build --profile development --platform androiddev client 빌드
eas build --profile production --platform all스토어용 빌드
eas update --branch productionOTA 배포 (JS만)

💡 TIP 막혔을 때 첫 두 가지 원인 모를 빌드·런타임 오류는 대개 버전 불일치다. npx expo-doctornpx expo install --check를 먼저 돌려보면 상당수가 여기서 잡힌다.

4. 한계 / 주의점

  • 네이티브를 아예 안 봐도 되는 건 아니다. Expo SDK 밖의 기능이 필요해지면 dev client + config plugin을 알아야 한다
  • 버전 결합이 강하다. RN·React·Expo SDK 버전이 서로 묶여 있어서 임의로 올리면 깨진다. npx expo install을 쓰면 SDK에 맞는 버전으로 설치해준다
  • 앱 용량이 순정 RN보다 큰 편
  • 문서 버전 확인이 필수. 위에 쓴 대로 메이저 버전에서 API가 바뀐다

자주 걸리는 예: 소셜 로그인

Expo Go의 한계가 가장 먼저 드러나는 지점. 이유는 두 겹이다.

① 네이티브 SDK가 Expo Go 안에 없다. 카카오·네이버가 제공하는 로그인 SDK는 네이티브 모듈이다. Expo Go는 이미 컴파일된 앱이라 그 안에 없는 네이티브 코드는 npm install을 해도 런타임에 존재하지 않는다. 모듈이 null이라는 에러로 나타난다.

② Expo Go에서 실행 주체는 내 앱이 아니다. 소셜 로그인 플랫폼에 앱을 등록할 때 안드로이드 패키지명 / iOS 번들 ID / 키 해시 / 리다이렉트 URI를 등록하는데, Expo Go로 띄우면 그걸 실행하는 건 Expo Go 앱(host.exp.exponent)이다. 내 앱 기준으로 등록한 값과 맞지 않고, 로그인 후 돌아오는 딥링크 스킴도 myapp://가 아니라 Expo Go의 exp:// 계열이라 어긋난다.

단, “소셜 로그인이 안 된다”는 뜻은 아니다. 위 두 문제는 네이티브 SDK를 앱에 직접 넣는 방식의 문제고, 브라우저 기반 웹 OAuth로 가면 우회된다. 방법은 두 가지다.

방법 A — 앱이 직접 OAuth를 돈다 (expo-auth-session)

expo-auth-session은 하부의 expo-web-browser(시스템 브라우저를 띄우고 리다이렉트로 앱에 제어권을 돌려주는 모듈) 위에서 OAuth 흐름 자체 — PKCE, state 검증, 인가 코드 → 토큰 교환 — 를 대신 처리해준다. 네이티브 SDK가 필요 없으니 Expo Go에서도 개발 확인은 된다.

걸리는 것 두 가지:

  • 리다이렉트 URI — 배포용으로는 myapp://auth 같은 커스텀 스킴을 등록해야 하는데 Expo Go에선 실행 주체가 Expo Go라 exp:// 계열로 돌아온다. 결국 dev build가 필요하다. (예전엔 auth.expo.io 프록시가 이를 우회해줬으나 SDK 48에서 제거됐다.)
  • 클라이언트 시크릿 — 카카오·네이버 REST 방식의 토큰 교환에는 시크릿이 필요한데, 앱에 넣으면 번들에 박제되어 그대로 노출된다 (2.7 참고).

방법 B — 백엔드가 OAuth를 돌고 앱은 브라우저만 연다 (권장)

앱은 expo-web-browseropenAuthSessionAsync내 백엔드의 인증 시작 URL을 열기만 한다. 프로바이더와의 왕복은 서버가 전부 처리하고, 끝나면 커스텀 스킴으로 자체 발급 토큰만 들려 앱으로 돌려보낸다.

앱 ──openAuthSessionAsync──> 내 백엔드 /oauth/kakao

                          (서버가 프로바이더와 왕복
                           · 시크릿 사용 · 토큰 검증)

앱 <──myapp://auth?token=…──── 자체 JWT 발급
  • 시크릿·클라이언트 키가 앱에 들어가지 않는다. 방법 A의 두 번째 문제가 구조적으로 사라진다
  • 등록할 리다이렉트 URI가 https 하나로 끝난다. 플랫폼별 스킴을 프로바이더마다 등록할 필요가 없다
  • 프로바이더 설정이 바뀌어도 앱 재배포가 필요 없다
  • 프론트는 인증 결과 토큰만 받으므로 서버가 인증 정책의 단일 지점이 된다

⚠️ 주의 웹에서 쓰던 HttpOnly 쿠키 리프레시는 그대로 못 가져온다 RN은 브라우저가 아니라 네이티브 앱이라 쿠키 저장소 동작이 다르고 앱 재시작 후 유지가 보장되지 않는다. 리프레시 토큰은 expo-secure-store(iOS 키체인 / Android Keystore)에 저장하고 헤더로 실어 보내는 방식이 정석이다. 웹과 모바일의 토큰 보관 전략은 분기해서 설계한다.

두 방법 모두 배포 단계에선 dev build가 필요하다(커스텀 스킴). Apple 로그인은 네이티브 entitlement가 필요해 expo-apple-authentication + dev build가 필수이고, 이것만은 웹 OAuth로 대체할 수 없다.

정리하면 “소셜 로그인 = Expo Go 졸업 시점” 이라는 결론은 어느 방법을 골라도 유효하다. Expo Go로는 개발 중 임시 확인까지만 가능하다.

링크