프론트엔드/RN

RN(React-Native) 환경변수 사용방법ㅣ릴리즈 버전 환경변수 오류 해결

순코딩 2025. 9. 16. 11:19

시작하기

환경 변수 저장

.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 프로젝트 환경변수 추가

https://expo.dev/

 

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"
      }
    }
  }
}