테마
템플릿
새 프로젝트에 복사해 값만 채우는 파일들이다. 마크다운은 왼쪽 메뉴에서, 코드 파일은 아래에서 본다.
쓰는 법은 셋 다 같다.
대상 프로젝트의 "복사 위치"로 복사한다.
{{플레이스홀더}}를 출시 값 입력 양식 §0의 값으로 치환한다.남은 자리가 없는지 확인한다.
shgrep -n '{{' <복사한 파일> # 0건이어야 한다
launch-values.md를 가장 먼저 복사한다. 나머지 템플릿이 전부 그 파일의 값을 읽어 채워진다.
마크다운 양식
| 템플릿 | 언제 쓰나 | 복사 위치 | 관련 단계 |
|---|---|---|---|
| 출시 값 입력 양식 | 맨 처음. 콘솔에서 받은 값을 여기 한 곳에 모은다. 모든 {{플레이스홀더}}의 정의처다 | docs/ops/launch-values.local.md (.gitignore에 *.local.md) | 02 계정 개설부터 전 단계 |
| 에셋 폴더 구조 | 아이콘 원본과 스토어 스크린샷을 만들기 직전. 파일 이름과 폴더를 먼저 정한다 | docs/design/icons/ · docs/store-assets/ 폴더 규칙 | 09 에셋 |
| 개인정보처리방침 골격 | 법정 문서 초안을 쓸 때. 조문마다 "반드시 들어갈 것"만 있고 문장은 없다 | 랜딩의 법정 문서 페이지 또는 언어 사전 | 10 법정 문서 10.2 |
| 이용약관 골격 | 위와 같다. 유료 구독이 있으면 제9~11조를 반드시 채운다 | 위와 같다 | 10 법정 문서 10.4 |
| 스토어 설명문 | 두 콘솔에 붙여 넣을 문안의 정본. 콘솔에서 직접 고치지 않는다 | docs/ops/store-listing.md | 13 스토어 등록 · 13a · 13b |
| 데이터 안전 · App Privacy 답변 | 두 스토어가 같은 사실을 다른 폼으로 묻는다. 답을 한곳에 모아 어긋나지 않게 한다 | docs/ops/data-safety-answers.md | 13b Play Console 13b.7 · 13a 13a.2 |
| 심사 메모 | 심사 정보를 채우기 직전. 한국어와 English를 이어 붙여 넣는다 | docs/ops/review-notes.md | 14 심사 14.1 |
| 릴리스 로그 | 첫 빌드부터. 실패한 빌드의 원인 한 줄이 가장 쓸모 있다 | docs/ops/release-log.md | 12 빌드 · 서명 · 15 출시 후 15.4 |
코드 파일
| 템플릿 | 언제 쓰나 | 복사 위치 | 관련 단계 |
|---|---|---|---|
| eas.json | EAS 빌드 프로필을 처음 만들 때. base를 나머지 프로필이 extends 한다 | apps/mobile/eas.json | 12 빌드 · 서명 12.1 · 12.8 |
| app.config.skeleton.ts | Expo 앱을 만든 직후. 정적 app.json 대신 쓴다 — 키가 없는 통합은 플러그인을 붙이지 않아야 빌드가 된다 | apps/mobile/app.config.ts | 01 프로젝트 셋업 1.4 · 07 · 08 |
| env.example | 01의 env 규약을 정할 때. 접두사별로 어디에 들어가는 값인지 주석이 붙어 있다 | 저장소 루트의 .env.example | 01 프로젝트 셋업 1.5 · 12 12.3 |
app.config.ts와 eas.json은 서로를 본다. eas.json의 환경 변수가 빌드 시점에 app.config.ts로 들어가므로, 둘 중 하나만 고치면 로컬에서는 되는데 EAS 빌드에서 값이 비는 일이 생긴다(12.3).
eas.json
json
{
"cli": {
"version": ">= 24.0.0",
"appVersionSource": "remote"
},
"build": {
"base": {
"node": "22.23.1",
"pnpm": "12.3.4",
"env": {
"EXPO_NO_DOTENV": "1",
"EXPO_APPLE_TEAM_ID": "{{APPLE_TEAM_ID}}",
"EXPO_APPLE_TEAM_TYPE": "{{APPLE_TEAM_TYPE}}"
}
},
"development": {
"extends": "base",
"developmentClient": true,
"distribution": "internal",
"channel": "development",
"ios": {
"simulator": true
}
},
"preview": {
"extends": "base",
"distribution": "internal",
"channel": "preview"
},
"production": {
"extends": "base",
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {
"ios": {
"ascAppId": "{{ASC_APP_ID}}",
"appleTeamId": "{{APPLE_TEAM_ID}}"
},
"android": {
"track": "internal",
"releaseStatus": "draft"
}
}
}
}app.config.skeleton.ts
ts
/**
* app.config.ts 골격 — apps/mobile/app.config.ts로 복사해 쓴다.
*
* `{{플레이스홀더}}`는 templates/launch-values.md §0의 값으로 치환한다.
* 치환이 끝나면 `grep -n '{{' app.config.ts`가 0건이어야 한다.
*
* 정적 app.json이 아니라 동적 설정인 이유는 두 가지다.
* 1. 값이 env에서 온다(빌드 프로필마다 다르다).
* 2. 계정이 아직 없는 통합은 플러그인을 **붙이지 않아야** 한다.
* 이 플러그인들은 값이 없으면 prebuild 단계에서 throw 하므로,
* 조건 없이 나열하면 키가 없는 사람은 앱을 빌드조차 못 한다.
*
* 절차 설명: guide/01-project-setup.md §1.4
*/
import { existsSync } from 'node:fs';
import { resolve } from 'node:path';
import type { ConfigContext, ExpoConfig } from 'expo/config';
// ---------------------------------------------------------------------------
// 식별자 — 한 곳에서만 쓴다. 스토어에 올라간 뒤에는 바꿀 수 없다.
// ---------------------------------------------------------------------------
const BUNDLE_ID = '{{BUNDLE_ID}}'; // iOS 번들 ID = Android 패키지명
const APP_SCHEME = '{{APP_SCHEME}}'; // 딥링크 스킴 (스킴://)
const APP_SLUG = '{{APP_SLUG}}'; // Expo slug. EAS 프로젝트와 같아야 한다
const EAS_ACCOUNT = '{{EAS_ACCOUNT}}'; // eas whoami 의 결과와 같아야 한다
// 유니버설 링크 / App Links 호스트. 랜딩이 검증 파일을 서빙하는 도메인이다.
// guide/11-landing.md 참고.
const LINK_DOMAIN = '{{DOMAIN}}';
// [선택] 위젯·확장을 쓸 때만. 관례상 'group.' + 번들 ID.
// const APP_GROUP = '{{APP_GROUP}}';
// 구글의 공개 테스트 광고 ID. 실제 ID는 env에서 오므로, 광고 계정이 없는
// 클론에서도 빌드가 된다. 스토어 빌드 전에 실제 ID가 들어갔는지 확인한다.
const TEST_ADMOB_IOS_APP_ID = 'ca-app-pub-3940256099942544~1458002511';
const TEST_ADMOB_ANDROID_APP_ID = 'ca-app-pub-3940256099942544~3347511713';
const env = process.env;
// ---------------------------------------------------------------------------
// env 읽기 헬퍼
// ---------------------------------------------------------------------------
/** 빈 문자열과 공백만 있는 값을 undefined로 만든다. */
function str(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
/** 파일 경로 env. 없으면 저장소 안의 기본 경로를 본다. */
function filePath(value: string | undefined, fallback: string): string {
return str(value) ?? fallback;
}
/** production 프로필에서만 필수인 값. */
function requiredInProduction(name: string, value: string | undefined): void {
if (env.EAS_BUILD_PROFILE === 'production' && !value) {
throw new Error(
`${name} is required for the production build profile. ` +
'Set it in the EAS project environment before building.',
);
}
}
const appMode = str(env.EXPO_PUBLIC_APP_MODE) === 'demo' ? 'demo' : 'live';
const easProjectId = str(env.EXPO_PUBLIC_EAS_PROJECT_ID);
const appleTeamId = str(env.EXPO_PUBLIC_APPLE_TEAM_ID);
const iosAppId = str(env.EXPO_PUBLIC_IOS_APP_ID);
const linkDomain = str(env.EXPO_PUBLIC_UNIVERSAL_LINK_DOMAIN) ?? LINK_DOMAIN;
// Google 로그인. iOS URL 스킴(역방향 클라이언트 ID)이 있을 때만 플러그인을 붙인다.
const googleIosUrlScheme = str(env.EXPO_PUBLIC_GOOGLE_IOS_URL_SCHEME);
// Firebase(Crashlytics) 설정 파일 **경로**. 값이 아니라 파일이다.
// 두 파일이 실제로 있을 때만 플러그인을 붙인다 — 하나라도 없으면
// @react-native-firebase 플러그인이 prebuild에서 throw 한다.
// EAS에서는 `--type file` 환경 변수라 빌더가 절대 경로로 풀어 준다.
const googleServicesIos = filePath(
env.GOOGLE_SERVICES_INFO_PLIST,
'./firebase/GoogleService-Info.plist',
);
const googleServicesAndroid = filePath(env.GOOGLE_SERVICES_JSON, './firebase/google-services.json');
const firebaseEnabled =
existsSync(resolve(__dirname, googleServicesIos)) &&
existsSync(resolve(__dirname, googleServicesAndroid));
// ⚠️ react-native.config.js 의 같은 조건과 **표현을 똑같이** 유지한다.
// 오토링크는 플러그인이 아니라 package.json 을 보고 네이티브 모듈을 링크한다.
// ---------------------------------------------------------------------------
// 프로덕션 게이트 — 백엔드 없는 스토어 빌드를 막는다
// ---------------------------------------------------------------------------
requiredInProduction('EXPO_PUBLIC_SUPABASE_URL', str(env.EXPO_PUBLIC_SUPABASE_URL));
// 스토어 리뷰 링크·강제 업데이트 링크가 이 숫자 ID로 만들어진다.
// 가짜 값을 넣으면 사용자가 404 페이지로 간다.
requiredInProduction('EXPO_PUBLIC_IOS_APP_ID', iosAppId);
// iOS 권한 문구. 심사는 목적이 적히지 않은 권한 요청을 반려한다.
const PHOTOS_PERMISSION = '사진을 고르기 위해 사진 보관함에 접근합니다.';
const CAMERA_PERMISSION = '사진을 바로 찍기 위해 카메라를 사용합니다.';
const TRACKING_PERMISSION = '맞춤 광고에 사용됩니다. 허용하지 않아도 모든 기능을 쓸 수 있어요.';
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: '{{APP_NAME_EN}}', // EAS·대시보드·스토어가 이 값을 키로 쓴다
slug: APP_SLUG,
owner: EAS_ACCOUNT,
scheme: APP_SCHEME,
version: '1.0.0',
orientation: 'portrait',
userInterfaceStyle: 'automatic',
icon: './assets/icon.png',
// [선택] 스토어 표시 이름과 iOS 권한 문구의 현지화.
// locales: { ko: './locales/ko.json', en: './locales/en.json' },
ios: {
bundleIdentifier: BUNDLE_ID,
supportsTablet: false,
usesAppleSignIn: true,
...(appleTeamId ? { appleTeamId } : {}),
...(iosAppId ? { appStoreUrl: `https://apps.apple.com/app/id${iosAppId}` } : {}),
...(firebaseEnabled ? { googleServicesFile: googleServicesIos } : {}),
associatedDomains: [`applinks:${linkDomain}`, `webcredentials:${linkDomain}`],
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
NSPhotoLibraryUsageDescription: PHOTOS_PERMISSION,
NSCameraUsageDescription: CAMERA_PERMISSION,
},
},
android: {
package: BUNDLE_ID,
playStoreUrl: `https://play.google.com/store/apps/details?id=${BUNDLE_ID}`,
...(firebaseEnabled ? { googleServicesFile: googleServicesAndroid } : {}),
adaptiveIcon: {
backgroundColor: '#FFFFFF',
foregroundImage: './assets/adaptive-icon.png',
monochromeImage: './assets/adaptive-icon-monochrome.png',
},
// --- [선택] 푸시를 쓴다면 ---------------------------------------------
// Android 13부터 `POST_NOTIFICATIONS`가 런타임 권한이다. 선언하지 않으면
// 알림이 오류 없이 그냥 뜨지 않는다. 선언한 뒤 화면에서 직접 요청한다.
// ⚠️ `SCHEDULE_EXACT_ALARM`은 Play의 **제한 권한**이다. 정확한 시각이
// 필요한 예약 알림이 실제로 있을 때만 넣고, 없으면 지운다 —
// 선언해 두면 Play Console에서 사용 사유를 소명해야 한다.
// permissions: [
// 'android.permission.POST_NOTIFICATIONS',
// 'android.permission.SCHEDULE_EXACT_ALARM',
// ],
// guide/08-push.md 참고.
// ⚠️ `pathPrefix`를 빼면 이 호스트의 **모든** 경로를 앱이 가로챈다 —
// 랜딩 첫 화면도, 약관·개인정보처리방침 링크도 앱으로 열린다.
// 앱이 실제로 처리하는 경로만 적는다. 경로가 여럿이면 필터를 여러 개 둔다.
intentFilters: [
{
action: 'VIEW',
autoVerify: true,
data: [{ scheme: 'https', host: linkDomain, pathPrefix: '/join' }],
category: ['BROWSABLE', 'DEFAULT'],
},
],
},
// OTA 업데이트는 네이티브 지문에 묶인다. 네이티브가 바뀌면 지문이 바뀌고,
// 호환되지 않는 JS 번들이 기존 빌드로 나가지 않는다.
runtimeVersion: { policy: 'fingerprint' },
updates: {
...(easProjectId ? { url: `https://u.expo.dev/${easProjectId}` } : {}),
fallbackToCacheTimeout: 0,
},
extra: {
...(easProjectId ? { eas: { projectId: easProjectId } } : {}),
// 앱 코드는 env 대신 이 값을 읽어도 된다(빌드 시점에 박힌다).
appMode,
},
plugins: [
'expo-router',
'expo-localization',
'expo-secure-store',
'expo-web-browser',
'expo-apple-authentication',
[
'expo-splash-screen',
{
image: './assets/splash-icon.png',
imageWidth: 300,
resizeMode: 'contain',
backgroundColor: '#FFFFFF',
},
],
[
'expo-build-properties',
{
// [선택] Crashlytics — Firebase를 쓸 때만 정적 프레임워크로 바꾼다.
// 설정 파일이 없는 클론은 기본 Podfile 그대로 둔다.
//
// `@react-native-firebase`는 firebase-ios-sdk를 Swift Package Manager로
// 받는 것이 기본인데, 그 경로는 정적 링크에서 `pod install`을 중단시킨다.
// 그래서 SPM을 끄고(아래 `@react-native-firebase/app` 플러그인의
// `disableSPM`) CocoaPods + 정적 프레임워크로 간다 — SPM이 꺼졌을 때
// firebase-ios-sdk가 요구하는 조합이다.
//
// `useFrameworks`는 Podfile의 **모든** pod의 링크 방식을 바꾼다.
// `forceStaticLinking`은 지정한 pod만 그 프레임워크 빌드 안에서 정적
// 라이브러리로 남긴다 — 모듈러 헤더에 걸려 넘어지는 pod의 탈출구다.
// 다른 pod에서 링크 오류가 나면 그 이름을 이 배열에 추가한다.
// 자세한 사정: guide/07-crash-analytics.md
ios: firebaseEnabled
? { useFrameworks: 'static' as const, forceStaticLinking: ['RNFBApp', 'RNFBCrashlytics'] }
: {},
android: {
compileSdkVersion: 36,
targetSdkVersion: 36,
minSdkVersion: 24,
// 릴리스 빌드만 축소·난독화한다. 디버그 빌드는 그대로 둔다.
enableMinifyInReleaseBuilds: true,
enableShrinkResourcesInReleaseBuilds: true,
},
},
],
[
'expo-image-picker',
{ photosPermission: PHOTOS_PERMISSION, cameraPermission: CAMERA_PERMISSION },
],
['expo-tracking-transparency', { userTrackingPermission: TRACKING_PERMISSION }],
[
'react-native-google-mobile-ads',
{
iosAppId: str(env.EXPO_PUBLIC_ADMOB_IOS_APP_ID) ?? TEST_ADMOB_IOS_APP_ID,
androidAppId: str(env.EXPO_PUBLIC_ADMOB_ANDROID_APP_ID) ?? TEST_ADMOB_ANDROID_APP_ID,
delayAppMeasurementInit: true,
userTrackingUsageDescription: TRACKING_PERMISSION,
},
],
// --- 조건부: 값이 있을 때만 붙는다 -------------------------------------
...(googleIosUrlScheme
? [
['@react-native-google-signin/google-signin', { iosUrlScheme: googleIosUrlScheme }] as [
string,
Record<string, unknown>,
],
]
: []),
...(firebaseEnabled
? ([
['@react-native-firebase/app', { ios: { disableSPM: true } }],
'@react-native-firebase/crashlytics',
] as (string | [string, Record<string, unknown>])[])
: []),
// --- [선택] 푸시를 쓴다면 ---------------------------------------------
// `expo install expo-notifications` 뒤에 주석을 푼다. 아이콘·색을 비워 두면
// Android 상태바에 흰 사각형이 뜬다. 위 android.permissions도 같이 푼다.
// guide/08-push.md 참고.
// ['expo-notifications', {
// icon: './assets/notification-icon.png', // Android 상태바 아이콘(단색 PNG)
// color: '#000000',
// }],
// --- [선택] 위젯을 쓴다면 ---------------------------------------------
// iOS 위젯과 Android 위젯은 서로 다른 라이브러리이고,
// iOS 쪽은 App Group({{APP_GROUP}})을 Apple 포털에도 등록해야 한다.
// ['expo-widgets', { groupIdentifier: APP_GROUP, widgets: [] }],
// ['react-native-android-widget', { widgets: [] }],
],
});
// ---------------------------------------------------------------------------
// [선택] 네이티브 설정을 코드로 고쳐야 할 때
//
// ios/ 와 android/ 는 커밋하지 않는 생성물이다. 직접 고치면 다음
// `expo prebuild --clean`에서 사라지고, EAS 빌더에는 애초에 반영되지 않는다.
// 그래서 모든 네이티브 수정은 config plugin으로 쓴다.
//
// import { withProjectBuildGradle, withGradleProperties } from 'expo/config-plugins';
//
// function withGradleFixes(config: ExpoConfig): ExpoConfig {
// return withProjectBuildGradle(config, (mod) => {
// // 앵커 문자열을 못 찾으면 조용히 넘어가지 말고 throw 한다.
// // 그래야 템플릿이 바뀌었을 때 prebuild가 그 자리에서 알려 준다.
// return mod;
// });
// }
//
// 실제로 필요했던 사례(광고 SDK의 Kotlin 메타데이터, WorkManager 중복 클래스)는
// guide/01-project-setup.md의 함정 절에 있다. R8 축소·난독화 쪽은
// guide/12-build-signing.md.
// ---------------------------------------------------------------------------env.example
sh
# 프로젝트 env 양식 — 저장소 루트의 `.env.example`로 복사한다.
#
# 값은 전부 비어 있다. 채운 파일은 커밋하지 않는다.
# .env 서버·CLI·Edge Function 용 (루트)
# apps/mobile/.env.local 앱 용
# .gitignore 에 `.env` 와 `.env*.local` 을 넣는다.
#
# 접두사 규칙 (guide/01-project-setup.md §1.5)
# EXPO_PUBLIC_* → 앱 번들에 그대로 박힌다. 시크릿 금지.
# NEXT_PUBLIC_* → 랜딩 브라우저 번들에 박힌다. 시크릿 금지.
# 접두사 없음 → 서버·CLI 전용. 여기에만 시크릿을 둔다.
#
# 각 값을 어느 콘솔에서 받아 오는지는 templates/launch-values.md 에 있다.
# 값이 채워진 줄은 프로젝트 값이 아니라 안전한 기본값이다.
# ---------------------------------------------------------------------------
# apps/mobile (Expo) — 클라이언트, 정의상 공개 값
# ---------------------------------------------------------------------------
# demo | live. demo 는 '요청'일 뿐이고 개발 빌드와 development/preview 채널에서만
# 실제로 켜진다. production 은 백엔드 URL이 없으면 설정 단계에서 throw 한다.
EXPO_PUBLIC_APP_MODE=demo
EXPO_PUBLIC_SUPABASE_URL= # Project Settings › API › Project URL
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY= # publishable key (구버전이면 anon key)
# 커스텀 SMTP를 붙이기 전에는 false로 둔다. 기본 SMTP는 한도가 낮아 심사 중에 막힌다.
EXPO_PUBLIC_EMAIL_OTP_ENABLED=false
# 카카오는 앱에 클라이언트 키가 없다. 서버에 프로바이더가 켜졌는지만 알려 주는 플래그다.
EXPO_PUBLIC_KAKAO_LOGIN_ENABLED=false
# RevenueCat 공개 SDK 키 (appl_ / goog_ 로 시작)
EXPO_PUBLIC_RC_IOS_KEY=
EXPO_PUBLIC_RC_ANDROID_KEY=
# AdMob — 비우면 구글 공개 테스트 ID로 떨어진다. 스토어 빌드 전에 실제 값으로 채운다.
EXPO_PUBLIC_ADMOB_IOS_APP_ID= # ca-app-pub-…~… (앱 ID)
EXPO_PUBLIC_ADMOB_ANDROID_APP_ID=
EXPO_PUBLIC_ADMOB_BANNER_IOS= # ca-app-pub-…/… (광고 단위 ID)
EXPO_PUBLIC_ADMOB_BANNER_ANDROID=
# Google 로그인
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID= # 유형: 웹 애플리케이션. Android도 이 값을 본다
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID= # 유형: iOS
EXPO_PUBLIC_GOOGLE_IOS_URL_SCHEME= # iOS 클라이언트의 역방향 클라이언트 ID
# 링크·문의
EXPO_PUBLIC_UNIVERSAL_LINK_DOMAIN= # {{DOMAIN}} — 스킴도 경로도 없는 호스트만
EXPO_PUBLIC_APP_URL= # {{WEB_ORIGIN}} — 약관·지원 링크의 기준
EXPO_PUBLIC_SUPPORT_EMAIL= # {{SUPPORT_EMAIL}}
# Apple / EAS
EXPO_PUBLIC_APPLE_TEAM_ID= # Membership › Team ID → {{APPLE_TEAM_ID}}
# eas.json 의 build.base.env 에 박는 두 이름. 비대화형 빌드가 팀을 묻지 않게 한다.
EXPO_APPLE_TEAM_ID= # 위와 같은 값 → {{APPLE_TEAM_ID}}
EXPO_APPLE_TEAM_TYPE= # INDIVIDUAL | COMPANY_OR_ORGANIZATION → {{APPLE_TEAM_TYPE}}
EXPO_PUBLIC_IOS_APP_ID= # ASC 앱 생성 후 숫자 ID → {{ASC_APP_ID}}
EXPO_PUBLIC_EAS_PROJECT_ID= # `eas init` 결과 → {{EAS_PROJECT_ID}}
# 심사용 고정 코드 로그인의 앱 쪽 두 값. 아무것도 인증하지 않는다 — 판정은 서버 시크릿이
# 한다(아래 Edge Function 시크릿 절의 REVIEWER_*). guide/04-auth.md §4.7.
EXPO_PUBLIC_REVIEWER_EMAIL= # [선택 — 심사 기간에만. 승인 뒤 unset] 같은 주소 → {{REVIEWER_EMAIL}}
EXPO_PUBLIC_REVIEWER_OTP_HINT= # [선택 — 심사 기간에만. 승인 뒤 unset] 배너에 찍는 코드. REVIEWER_OTP와 같은 값
# Firebase 설정 파일의 **경로**다(값이 아니다). 로컬에서는 비워 두고 파일만
# apps/mobile/firebase/ 에 넣어도 된다. EAS에서는 `--type file` 환경 변수로 넣는다.
GOOGLE_SERVICES_INFO_PLIST=
GOOGLE_SERVICES_JSON=
# ---------------------------------------------------------------------------
# 백엔드 CLI — 마이그레이션·설정 푸시에 쓴다 (루트 .env)
# ---------------------------------------------------------------------------
SUPABASE_PROJECT_REF= # Project Settings › General → {{SUPABASE_REF}}
SUPABASE_SECRET_KEY= # secret key — 서버 전용. 앱에 넣지 않는다
SUPABASE_DB_PASSWORD= # 프로젝트 생성 시 정한 DB 비밀번호
# ---------------------------------------------------------------------------
# 인증 메일 SMTP — `supabase config push` 가 셸에서 읽는다
# ---------------------------------------------------------------------------
SUPABASE_AUTH_SMTP_HOST= # {{SMTP_HOST}}
# 포트는 env가 아니다. config.toml `[auth.email.smtp] port = 465` 고정. guide/04-auth.md 참고.
SUPABASE_AUTH_SMTP_USER= # {{SMTP_USER}}
SUPABASE_AUTH_SMTP_PASS= # 2단계 인증을 쓰면 '메일 전용 비밀번호'
SUPABASE_AUTH_SMTP_ADMIN_EMAIL= # 발신자로 표시할 주소
# ---------------------------------------------------------------------------
# 소셜 로그인 프로바이더 — 같은 방식으로 셸에서 읽힌다
# ---------------------------------------------------------------------------
# Apple: 네이티브 로그인만 쓰면 CLIENT_ID 는 번들 ID 하나, SECRET 은 비워 둔다.
# 웹(Services ID) 플로우를 켤 때만 Services ID 를 콤마로 덧붙이고 .p8 로 JWT 를 만든다.
SUPABASE_AUTH_APPLE_CLIENT_ID= # {{BUNDLE_ID}}
SUPABASE_AUTH_APPLE_SECRET=
APPLE_SIGNIN_KEY_ID= # 위 JWT 를 만든 키의 Key ID
# Google: 웹 클라이언트의 ID/시크릿. 네이티브 ID 는 위 EXPO_PUBLIC_* 에 있다.
SUPABASE_AUTH_GOOGLE_CLIENT_ID=
SUPABASE_AUTH_GOOGLE_SECRET=
GOOGLE_ANDROID_CLIENT_ID= # [참고] 콘솔 등록만 필요. 앱 env 에는 안 들어간다
# 카카오: REST API 키와 Client Secret(상태를 '사용함'으로 켠 뒤)
SUPABASE_AUTH_KAKAO_CLIENT_ID=
SUPABASE_AUTH_KAKAO_SECRET=
# ---------------------------------------------------------------------------
# Edge Function 시크릿 — 파일이 아니라 `supabase secrets set` 으로 넣는다
# ---------------------------------------------------------------------------
RC_SECRET_API_KEY= # RevenueCat Secret API key v1 (sk_ 로 시작)
RC_WEBHOOK_AUTH= # 임의의 긴 문자열. 웹훅 Authorization 헤더와 같은 값
EXPO_ACCESS_TOKEN= # [선택] 서버에서 푸시를 보낼 때
# 심사용 고정 코드 로그인 — guide/04-auth.md §4.7. 네 값이 두 짝이다.
REVIEWER_EMAIL= # [선택 — 심사 기간에만. 승인 뒤 unset] 허용 목록. 주소 하나만
REVIEWER_OTP= # [선택 — 심사 기간에만. 승인 뒤 unset] 진짜 6자리 코드. 상수 시간 비교
# ---------------------------------------------------------------------------
# 스토어 제출 (eas submit) — 파일은 저장소 밖에 둔다
# ---------------------------------------------------------------------------
ASC_APP_SPECIFIC_PASSWORD= # [선택] API Key 를 쓰면 비워 둔다
ASC_API_KEY_ID= # [선택] App Store Connect API
ASC_API_ISSUER_ID= # [선택]
ASC_API_KEY_P8_PATH= # [선택] 내려받은 .p8 경로
PLAY_SERVICE_ACCOUNT_JSON_PATH= # [선택] Play 서비스 계정 키 경로
PLAY_APP_SIGNING_SHA256= # [나중] 앱 서명 키 인증서 SHA-256 (AA:BB:CC:…)
# ---------------------------------------------------------------------------
# 참고 값 — env 로 읽지는 않지만 한곳에 두면 편하다
# ---------------------------------------------------------------------------
FIREBASE_PROJECT_ID= # {{FIREBASE_PROJECT_ID}}
ADMOB_PUBLISHER_ID= # {{ADMOB_PUBLISHER_ID}} — 랜딩의 /app-ads.txt 에 들어간다
# ---------------------------------------------------------------------------
# apps/landing (Next.js, 공개 웹)
# ---------------------------------------------------------------------------
NEXT_PUBLIC_APP_URL= # {{WEB_ORIGIN}} — canonical·OG·사이트맵의 기준
NEXT_PUBLIC_SUPPORT_EMAIL= # {{SUPPORT_EMAIL}}
NEXT_PUBLIC_IOS_APP_ID= # {{ASC_APP_ID}} — 비어 있으면 App Store 배지를 숨긴다
NEXT_PUBLIC_ANDROID_PACKAGE= # {{BUNDLE_ID}}
NEXT_PUBLIC_PRICE_MONTHLY= # {{PRICE_MONTHLY}}
NEXT_PUBLIC_PRICE_YEARLY= # {{PRICE_YEARLY}}
# ---------------------------------------------------------------------------
# apps/admin (선택, 로컬 전용 운영 콘솔) — 배포본에 넣지 않는다
# ---------------------------------------------------------------------------
SUPABASE_URL= # {{SUPABASE_REF}}에서 파생된다: https://{{SUPABASE_REF}}.supabase.co
ADMIN_PASSWORD=
ADMIN_SESSION_SECRET= # openssl rand -hex 32
# 사업자 정보(약관·개인정보처리방침에 들어가는 값)는 env 가 아니다.
# templates/launch-values.md §12 에 적고 문서 생성에만 쓴다.