시작하기
환경 변수 저장
.env
EXPO_PUBLIC_SUPABASE_URL=https://abc123.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=abcd1234
환경변수 사용
src/lib/supabaseClient.ts
// lib/supabaseClient.ts
// -------------------------------------------------------------
// 목적: Supabase 클라이언트를 안전하게 생성 (릴리스/OTA/디버그 공통)
// 핵심: EXPO_PUBLIC_ 접두 env를 process.env로 직접 참조
// -------------------------------------------------------------
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createClient } from '@supabase/supabase-js';
import 'react-native-url-polyfill/auto'; // RN에서 URL/fetch 관련 호환성 보정
// 1) EXPO_PUBLIC_ 접두 env를 "직접" 읽는다.
// - Expo는 번들 시점에 이 값을 코드에 인라인한다.
// - app.config.js의 extra를 거치지 않으므로 런타임 유실/다름 문제 방지.
const SUPABASE_URL = process.env.EXPO_PUBLIC_SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
========== 생략 가능 ==========
// 2) 방어 코드: 빌드/런타임 어디서든 즉시 원인 파악 가능하게
function isValidUrl(u?: string) {
if (!u) return false;
try {
// 유효하지 않은 값이면 여기서 예외가 난다.
new URL(u);
return true;
} catch {
return false;
}
}
if (!isValidUrl(SUPABASE_URL)) {
// ❗ 릴리스에서도 메시지가 명확히 보이도록 에러를 던진다.
// adb logcat / Crashlytics / Sentry에서 원인 추적이 쉬워짐.
throw new Error(
`Supabase URL invalid: "${String(SUPABASE_URL)}". ` +
`Check EXPO_PUBLIC_SUPABASE_URL (must start with http/https).`
);
}
if (!SUPABASE_ANON_KEY || SUPABASE_ANON_KEY.length < 20) {
throw new Error(
`Supabase anon key is missing or too short. ` +
`Set EXPO_PUBLIC_SUPABASE_ANON_KEY correctly.`
);
}
========== 생략 가능 ==========
// 3) 클라이언트 생성
export const supabase = createClient(SUPABASE_URL!, SUPABASE_ANON_KEY!, {
auth: {
storage: AsyncStorage, // RN에서 세션 유지
autoRefreshToken: true, // 토큰 자동 갱신
persistSession: true, // 앱 재시작해도 세션 유지
detectSessionInUrl: false, // RN에서는 필요 없음
},
});
Expo 프로젝트 환경변수 추가
Expo
Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React.
expo.dev
빌드
eas build -p android --profile production
Q&A
Q1. RN 개발환경에서 환경 변수 값은 어디에 위치해야 할까?
A. 리포 루트의 `.env`에 `EXPO_PUBLIC_*` 키로 저장하고, JS에서는 `process.env.EXPO_PUBLIC_*`로 직접 사용해.
+ 메모/주의: `.env`는 커밋 금지(.gitignore). 클라이언트에서 쓰는 값만 `EXPO_PUBLIC_` 접두사 사용. 서비스 롤 키 같은 비밀키는 절대 넣지 말고(서버 전용), `npm run prebuild` 같은 검증 스크립트로 필수 키 체크하면 좋아.
Q2. RN 배포환경(EAS)에서 환경 변수 값은 어디에 위치해야 할까?
A. **Expo 대시보드 > Environment variables**에 같은 `EXPO_PUBLIC_*` 키를 등록해. 빌드/OTA 시 JS 번들에 인라인돼.
+ 메모/주의: 한 곳(대시보드)만 “단일 진실원칙”으로 관리 권장. `eas.json`의 `"env"`도 가능하지만 혼선 생김. 비밀키는 클라이언트에 두지 말고 서버/Edge Functions에만.
Q3. 빌드 버전이 환경변수 값을 잘 읽으려면 어느 시점에 추가돼 있어야 할까?
A. **빌드/업데이트 작업 시작 전에** 대시보드에 설정돼 있어야 그 값이 결과물에 “박혀(인라인)” 들어가.
+ 메모/주의: 빌드/OTA 후에 값을 바꾸면 반영 안 됨 → 재빌드 또는 새 OTA 필요. 순서: “환경변수 세팅 → (prebuild로 검증) → 빌드/업데이트”.
Q4. `app.config.js` / `eas.json`에서 env 제거해도 될까?
A. 돼. 코드에서 `process.env.EXPO_PUBLIC_*`만 직접 쓰면 돼. `extra` 경유 제거하면 런타임에서 값 유실/혼선 방지에 좋음.
+ 메모/주의: 로컬에서 `.env` 읽을 편의용으로 `app.config.js`에 `import 'dotenv/config'`만 남겨두면 OK. `Constants.expoConfig?.extra`로 읽는 방식은 지양.
Q5. `app.config.js`에서 `import 'dotenv/config'`를 하는 이유는?
A. 로컬 개발/번들 시 `.env` 내용을 `process.env`에 자동 주입하려고. 그래야 `process.env.EXPO_PUBLIC_*`가 로컬에서도 정상 인라인돼.
+ 메모/주의: 클라우드(EAS) 빌드에선 대시보드 값이 주입되므로 `dotenv`는 영향 없음(로컬 편의용). `.env`는 리포 루트에 두기.
참고 코드
.env
EXPO_PUBLIC_SUPABASE_URL=https://abc123.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=abcd1234
.lib/supabaseClient.ts
// lib/supabaseClient.ts
// -------------------------------------------------------------
// 목적: Supabase 클라이언트를 안전하게 생성 (릴리스/OTA/디버그 공통)
// 핵심: EXPO_PUBLIC_ 접두 env를 process.env로 직접 참조
// -------------------------------------------------------------
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createClient } from '@supabase/supabase-js';
import 'react-native-url-polyfill/auto'; // RN에서 URL/fetch 관련 호환성 보정
// 1) EXPO_PUBLIC_ 접두 env를 "직접" 읽는다.
// - Expo는 번들 시점에 이 값을 코드에 인라인한다.
// - app.config.js의 extra를 거치지 않으므로 런타임 유실/다름 문제 방지.
const SUPABASE_URL = process.env.EXPO_PUBLIC_SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
========== 생략 가능 ==========
// 2) 방어 코드: 빌드/런타임 어디서든 즉시 원인 파악 가능하게
function isValidUrl(u?: string) {
if (!u) return false;
try {
// 유효하지 않은 값이면 여기서 예외가 난다.
new URL(u);
return true;
} catch {
return false;
}
}
if (!isValidUrl(SUPABASE_URL)) {
// ❗ 릴리스에서도 메시지가 명확히 보이도록 에러를 던진다.
// adb logcat / Crashlytics / Sentry에서 원인 추적이 쉬워짐.
throw new Error(
`Supabase URL invalid: "${String(SUPABASE_URL)}". ` +
`Check EXPO_PUBLIC_SUPABASE_URL (must start with http/https).`
);
}
if (!SUPABASE_ANON_KEY || SUPABASE_ANON_KEY.length < 20) {
throw new Error(
`Supabase anon key is missing or too short. ` +
`Set EXPO_PUBLIC_SUPABASE_ANON_KEY correctly.`
);
}
========== 생략 가능 ==========
// 3) 클라이언트 생성
export const supabase = createClient(SUPABASE_URL!, SUPABASE_ANON_KEY!, {
auth: {
storage: AsyncStorage, // RN에서 세션 유지
autoRefreshToken: true, // 토큰 자동 갱신
persistSession: true, // 앱 재시작해도 세션 유지
detectSessionInUrl: false, // RN에서는 필요 없음
},
});
app.config.js
/**
* app.config.js
* =============================================================================
*
* 📱 Expo 앱 설정 파일
*
* 이 파일은 Expo 앱의 전체적인 설정을 관리합니다:
* - 앱 메타데이터 (이름, 버전, 아이콘 등)
* - 플랫폼별 설정 (iOS, Android, Web)
* - OTA 업데이트 설정
* - 플러그인 및 실험적 기능 설정
*
* 🔧 주요 변경사항:
* - Supabase 환경변수는 더 이상 여기서 주입하지 않음
* - JS 코드에서 process.env.EXPO_PUBLIC_*를 직접 사용하여 혼선 방지
* - OTA 업데이트를 위한 runtimeVersion 설정 포함
*
* =============================================================================
*/
// dotenv 설정을 로드하여 환경변수를 사용할 수 있게 함
import 'dotenv/config'
export default {
expo: {
// 📱 앱 기본 정보
name: '주식고사', // 앱 스토어에 표시될 앱 이름
slug: 'stock-exam', // Expo 프로젝트 고유 식별자 (URL 친화적)
version: '1.0.0', // 앱 버전 (앱 스토어 업데이트시 증가)
// 🔄 OTA(Over-The-Air) 업데이트 설정
// 앱 스토어 업데이트 없이 JavaScript 번들을 실시간으로 업데이트할 수 있음
updates: {
// EAS Update 서비스 URL - 프로젝트별 고유 주소
url: 'https://u.expo.dev/21f2a9db-2523-4ff3-b912-672e555bf3b8',
},
// 🎨 UI/UX 설정
orientation: 'portrait', // 화면 방향 고정 (세로 모드만)
icon: './assets/images/icon.png', // 앱 아이콘 경로
scheme: 'stockexam', // 딥링크용 URL 스키마 (stockexam://...)
userInterfaceStyle: 'automatic', // 다크/라이트 모드 자동 감지
newArchEnabled: true, // React Native 새로운 아키텍처 활성화
// 🍎 iOS 플랫폼 설정
ios: {
supportsTablet: true, // iPad 지원 여부
bundleIdentifier: 'com.stockexam.app', // iOS 앱 고유 식별자 (앱 스토어)
// 🔄 iOS OTA 업데이트 정책
// appVersion 기반: app.json의 version과 동일한 runtimeVersion 사용
// 같은 runtimeVersion끼리만 OTA 업데이트 가능
runtimeVersion: { policy: 'appVersion' },
},
// 🤖 Android 플랫폼 설정
android: {
// 적응형 아이콘 설정 (Android 8.0+)
adaptiveIcon: {
foregroundImage: './assets/images/adaptive-icon.png', // 전경 이미지
backgroundColor: '#ffffff', // 배경색
},
package: 'com.stockexam.app', // Android 패키지명 (Google Play)
edgeToEdgeEnabled: true, // 전체 화면 모드 (상태바/네비바까지 사용)
// 🔄 Android OTA 업데이트 버전
// 문자열로 고정: 같은 runtimeVersion을 가진 빌드끼리만 OTA 업데이트 가능
// 주의: 이 값을 변경하면 이전 버전과 호환되지 않음
runtimeVersion: '1.0.0',
},
// 🌐 웹 플랫폼 설정 (Expo Web)
web: {
bundler: 'metro', // Metro 번들러 사용 (React Native와 동일)
output: 'static', // 정적 파일로 빌드 (SSG)
favicon: './assets/images/favicon.png', // 웹 파비콘
},
// 🔌 Expo 플러그인 설정
// 네이티브 코드 수정 없이 기능을 추가할 수 있는 플러그인들
plugins: [
// 파일 기반 라우팅 시스템 (app/ 폴더 구조 기반)
'expo-router',
// 스플래시 스크린 설정
[
'expo-splash-screen',
{
image: './assets/images/splash-icon.png', // 스플래시 화면 이미지
imageWidth: 200, // 이미지 너비 (픽셀)
resizeMode: 'contain', // 이미지 크기 조정 방식
backgroundColor: '#ffffff', // 스플래시 화면 배경색
},
],
],
// 🧪 실험적 기능 설정
experiments: {
typedRoutes: true, // TypeScript 라우트 타입 자동 생성
},
// 📦 추가 설정 및 메타데이터
extra: {
// EAS (Expo Application Services) 프로젝트 ID
// ❌ 주의: Supabase 환경변수는 더 이상 여기서 전달하지 않음
// ✅ 대신 process.env.EXPO_PUBLIC_* 방식을 사용하여 보안성과 명확성 향상
eas: { projectId: '21f2a9db-2523-4ff3-b912-672e555bf3b8' },
},
},
}
eas.json
{
"cli": {
"version": ">= 0.52.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"distribution": "internal",
"android": {
"gradleCommand": ":app:assembleDebug"
},
"channel": "development"
},
"production": {
"autoIncrement": true,
"android": {
"buildType": "app-bundle"
},
"channel": "production",
"hooks": {
"prebuild": "node scripts/verify-env.js"
}
}
}
}