Supabase + Flutter Apple 로그인 연동 가이드
이 글에서 다루는 내용
Supabase와 Flutter를 사용해 Apple 로그인을 연동하는 전체 과정을 순서대로 정리합니다. Apple Developer Console 설정부터 Xcode Capability 추가, Service ID 생성, Supabase Dashboard 설정, Flutter 코드 구현까지 iOS와 Android 모두 다루며, 6개월마다 갱신이 필요한 Secret Key 관리 방법과 Supabase 없이 Spring Boot로 직접 구현할 경우의 난이도도 함께 설명합니다.
작업 순서 전체 흐름
1. Apple Developer — App ID 설정
2. Apple Developer — Key (.p8) 생성
3. Apple Developer — Service ID 생성 + Redirect URL 등록 (Android/웹 사용 시)
4. Xcode — Sign In with Apple Capability 추가
5. Android — AndroidManifest.xml 설정 (Android 사용 시)
6. Supabase Dashboard — Apple Provider 설정
7. Flutter 코드 구현
8. Secret Key 6개월 갱신 관리
1. Apple Developer — App ID 설정
Apple Developer Console 접속 후 아래 순서로 진행합니다.
Certificates, Identifiers & Profiles
→ Identifiers → 앱 App ID 클릭
→ Sign In with Apple 체크박스 활성화
→ Save
App ID에 Sign In with Apple을 활성화해야 해당 앱이 Apple 로그인을 사용할 권한을 얻습니다. 이 설정 없이는 Apple 서버가 인증 요청 자체를 거부합니다.
Modify App Capabilities 경고 메시지
Capability를 추가/삭제할 때 아래 경고창이 뜹니다.
"Adding or removing any capabilities will invalidate any provisioning profiles that include this App ID and they must be regenerated for future use."
이 문장의 의미는 다음과 같습니다.
Capability를 변경하면
→ 이 App ID가 포함된 Provisioning Profile들이 모두 무효화됩니다
→ 앞으로 사용하려면 새로 생성해야 합니다
invalidate = 유효했던 것이 더 이상 유효하지 않은 상태로 바뀐다는 뜻입니다. 신용카드로 비유하면, 카드 정보가 바뀌어 기존 카드가 정지되고 새 카드를 재발급받아야 하는 상황과 같습니다.
Xcode에서 Automatically manage signing을 사용 중이라면 Xcode가 자동으로 Profile을 재생성하므로 그냥 Continue를 누르면 됩니다.
2. Apple Developer — Key (.p8) 생성
Supabase가 Apple 서버와 통신할 때 사용하는 인증 키입니다.
Certificates, Identifiers & Profiles
→ Keys → [+] 버튼
→ Key 이름 입력
→ Sign In with Apple 체크 → Configure
→ Primary App ID 선택
→ Save → Continue → Register
→ Download (.p8 파일 저장)
.p8파일은 딱 한 번만 다운로드 가능합니다. 반드시 안전한 곳에 보관하세요.
다운로드한 .p8 파일을 열면 아래와 같은 형식입니다.
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg...
-----END PRIVATE KEY-----
이 내용 전체를 Supabase의 Secret Key 필드에 붙여넣습니다.
3. Apple Developer — Service ID 생성 + Redirect URL 등록
Service ID는 웹 및 Android 환경에서 OAuth 흐름에 사용되는 별도 식별자입니다. iOS 네이티브만 사용할 경우 이 단계는 건너뛰어도 됩니다.
iOS 네이티브 vs Android/웹 비교
| 항목 | iOS 네이티브만 | Android/웹 포함 |
|---|---|---|
| App ID 활성화 | ✅ 필요 | ✅ 필요 |
| Service ID 생성 | ❌ 불필요 | ✅ 필요 |
| Xcode Capability 추가 | ✅ 필요 | ✅ 필요 |
| .p8 Key 생성 | ✅ 필요 | ✅ 필요 |
| Supabase Client IDs | Bundle ID 하나만 | Bundle ID + Service ID |
Service ID는 같은 앱 하나를 플랫폼별로 다른 식별자로 구분하는 것입니다. 두 개의 서비스가 아니라 동일한 앱을 iOS는 Bundle ID로, Android/웹은 Service ID로 식별합니다.
iOS 네이티브 로그인 → App Bundle ID 사용 (com.ytylab.natum)
Android / 웹 로그인 → Service ID 사용 (com.ytylab.natum.service)
Service ID 생성
Certificates, Identifiers & Profiles
→ Identifiers → [+] → Services IDs 선택
→ Continue
Description: Natum Service
Identifier: com.ytylab.natum.service ← Bundle ID와 반드시 다르게
→ Continue → Register
Redirect URL 등록
Identifiers → 방금 만든 Service ID 클릭
→ Sign In with Apple 체크 → [Configure]
Primary App ID: com.ytylab.natum 선택
Domains and Subdomains:
acxqzhbxhkyvftelqbbx.supabase.co ← https:// 없이 도메인만 입력
Return URLs:
https://acxqzhbxhkyvftelqbbx.supabase.co/auth/v1/callback
→ Next → Done → Save
Supabase Callback URL은 Dashboard → Authentication → Providers → Apple 화면 하단의 Callback URL (for OAuth) 에서 확인할 수 있습니다.
자주 하는 실수
| 실수 | 올바른 방법 |
|---|---|
| Service ID를 App ID와 동일하게 설정 | 반드시 다르게 (.service 등 접미사 추가) |
Domain에 https:// 포함 |
도메인만 입력 |
| Return URL 오타 | Supabase URL 그대로 복사 붙여넣기 |
4. Xcode — Sign In with Apple Capability 추가
Apple Developer Console 설정은 서버 측 권한 등록이고, Xcode 설정은 앱 바이너리에 실제 권한을 부여합니다. 둘 다 해야 iOS 네이티브 Apple 로그인이 작동합니다.
Xcode → Runner 프로젝트 클릭
→ Runner Target 선택
→ Signing & Capabilities 탭
→ [+ Capability] 클릭
→ "Sign in with Apple" 검색 후 더블클릭
Capability를 추가하면 Runner.entitlements 파일이 자동으로 생성 또는 수정됩니다.
<!-- Runner.entitlements -->
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
Capability 추가 없이 실행하면 아래 오류가 발생합니다.
AuthorizationError: The operation couldn't be completed.
(com.apple.AuthenticationServices.AuthorizationError error 1000.)
5. Android — AndroidManifest.xml 설정
Android에서는 네이티브 시트 대신 웹뷰(Custom Tab) 기반 OAuth로 동작합니다.
<!-- android/app/src/main/AndroidManifest.xml -->
<activity
android:name="com.aboutyou.dart_packages.sign_in_with_apple.SignInWithAppleCallback"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="signinwithapple"
android:path="callback" />
</intent-filter>
</activity>
iOS vs Android 동작 방식 비교
| 항목 | iOS | Android |
|---|---|---|
| 방식 | 네이티브 시스템 시트 | 웹뷰 / Custom Tab |
| Face ID / Touch ID | ✅ | ❌ |
| 이메일 숨기기 | ✅ | ✅ |
| Service ID 필요 | ❌ | ✅ |
6. Supabase Dashboard — Apple Provider 설정
Authentication → Providers → Apple
각 필드 입력값
| 필드 | 입력값 | 설명 |
|---|---|---|
| Enable Sign in with Apple | ON | 현재 OFF 상태이므로 반드시 변경 |
| Client IDs (iOS만) | com.ytylab.natum |
Xcode Bundle ID |
| Client IDs (iOS + Android) | com.ytylab.natum, com.ytylab.natum.service |
콤마로 구분해서 둘 다 입력 |
| Secret Key | .p8 파일 전체 내용 |
-----BEGIN PRIVATE KEY----- 부터 끝까지 전체 복사 |
| Allow users without an email | OFF 유지 권장 | Apple 이메일 숨기기 사용자도 허용하려면 ON |
| Callback URL | 읽기 전용 | Apple Developer Console Service ID Return URL에 등록하는 용도 |
값을 가져오는 위치
| 필드 | Apple Developer Console 위치 |
|---|---|
| Client IDs | Identifiers → App ID의 Bundle ID |
| Service ID | Identifiers → Service ID의 Identifier |
| Secret Key | Keys → 생성한 Key의 .p8 파일 내용 전체 |
Apple OAuth secret keys expire every 6 months 경고
Supabase Apple Provider 설정 화면 하단에 표시되는 경고입니다. Apple OAuth Client Secret(JWT)의 최대 유효기간이 6개월로 제한되어 있다는 뜻입니다. 만료되면 웹 및 Android 기반 로그인이 즉시 중단됩니다. iOS 네이티브 방식은 영향 없습니다.
7. Flutter 패키지 설치 및 코드 구현
pubspec.yaml
dependencies:
supabase_flutter: ^2.x.x
sign_in_with_apple: ^6.x.x
crypto: ^3.x.x
Apple 로그인 구현 (iOS / Android 통합)
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class AppleAuthService {
final _supabase = Supabase.instance.client;
String _generateNonce([int length = 32]) {
const charset =
'0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
final random = Random.secure();
return List.generate(length, (_) => charset[random.nextInt(charset.length)])
.join();
}
String _sha256ofString(String input) {
final bytes = utf8.encode(input);
final digest = sha256.convert(bytes);
return digest.toString();
}
Future<AuthResponse> signInWithApple() async {
final rawNonce = _generateNonce();
final hashedNonce = _sha256ofString(rawNonce);
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
nonce: hashedNonce,
// Android에서만 webAuthenticationOptions 사용
webAuthenticationOptions: Platform.isAndroid
? WebAuthenticationOptions(
clientId: 'com.ytylab.natum.service', // Service ID
redirectUri: Uri.parse(
'https://acxqzhbxhkyvftelqbbx.supabase.co/auth/v1/callback',
),
)
: null,
);
final idToken = appleCredential.identityToken;
if (idToken == null) throw Exception('Apple ID Token이 없습니다.');
return await _supabase.auth.signInWithIdToken(
provider: OAuthProvider.apple,
idToken: idToken,
nonce: rawNonce, // 원본 nonce 전달
);
}
}
인증 흐름 핵심 포인트
rawNonce 생성 (랜덤 문자열)
↓
SHA256 해시 → Apple에 전달
↓
Apple → idToken 반환
↓
Supabase에 idToken + rawNonce(원본) 전달
↓
Supabase가 nonce 검증 후 로그인 완료
인증 상태 감지
Supabase.instance.client.auth.onAuthStateChange.listen((data) {
switch (data.event) {
case AuthChangeEvent.signedIn:
// 메인 화면으로 이동
break;
case AuthChangeEvent.signedOut:
// 로그인 화면으로 이동
break;
default:
break;
}
});
주의사항
| 항목 | 내용 |
|---|---|
| 이메일 최초 1회만 | Apple은 이메일을 최초 로그인 시에만 제공. DB에 저장 필수 |
| nonce 필수 | 없으면 Supabase에서 401 오류 발생 |
| Apple 버튼 디자인 | App Store 정책상 공식 디자인 가이드라인 준수 필요 |
| 테스트 | Android는 실제 단말 필요 (에뮬레이터 불안정) |
8. Secret Key 6개월 갱신 관리
Supabase는 자동 갱신을 지원하지 않아 수동으로 새 Secret Key를 생성해 Dashboard에 붙여넣어야 합니다.
Apple Developer Console → Keys → 기존 Key 삭제
→ 새 Key 생성 (.p8 다운로드)
→ Supabase Dashboard → Apple Provider → Secret Key 교체
| 관리 방법 | 설명 |
|---|---|
| 캘린더 알림 | 설정일 기준 5개월 후 알림 등록 |
| Supabase 모니터링 | Auth 에러율 급증 시 Slack 알림 연동 |
| CI/CD 스크립트 | Supabase Management API로 자동 갱신 구현 |
Supabase 없이 Spring Boot로 직접 구현한다면?
난이도 ★★★★☆
Supabase가 대신 처리하는 것들을 전부 직접 구현해야 합니다.
| Supabase가 해주는 것 | Spring Boot 직접 구현 시 |
|---|---|
| Apple JWT 검증 | 직접 구현 |
| Client Secret 생성 및 관리 | 직접 구현 |
| 토큰 발급 / 갱신 / 만료 처리 | 직접 구현 |
| 6개월 Secret Key 갱신 | 직접 구현 |
| OAuth 콜백 처리 | 직접 구현 |
| 세션 관리 | 직접 구현 |
| DB 유저 생성/매핑 | 직접 구현 |
특히 어려운 포인트
Client Secret JWT 직접 생성 — .p8 파일로 ES256 알고리즘 서명 JWT를 직접 만들어야 합니다.
Apple 공개키 동적 검증 — Apple이 공개키를 주기적으로 교체하므로 /auth/keys 엔드포인트에서 실시간으로 가져와 idToken을 검증해야 합니다.
OAuth 콜백 서버 직접 구현 — Android/웹용 리다이렉트 흐름 전체를 Spring Security로 직접 처리해야 합니다.
6개월 갱신 자동화 — 만료 감지와 갱신 스케줄러까지 직접 만들어야 합니다.
Java 라이브러리 옵션
전용 라이브러리가 일부 존재하지만 idToken 검증 등 일부만 도와주는 수준입니다.
apple-signin (mikereem) — idToken 검증에 특화
<dependency>
<groupId>com.github.mikereem</groupId>
<artifactId>apple-signin</artifactId>
<version>1.0</version>
</dependency>
appleauth-java (Accedia) — Apple REST API 통신 캡슐화. GitHub에서 직접 추가 필요.
JJWT + BouncyCastle 조합 — 라이브러리 없이 직접 구현할 때 사용
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.11.5</version>
</dependency>
라이브러리가 해주는 것 vs 여전히 직접 해야 하는 것
| 항목 | 라이브러리 처리 | 직접 구현 |
|---|---|---|
| idToken JWT 검증 | ✅ | |
| Apple 공개키 fetch | ✅ | |
| Client Secret 생성 | ✅ 일부 | |
| OAuth 콜백 처리 | ❌ | ✅ |
| 세션/토큰 관리 | ❌ | ✅ |
| 6개월 갱신 자동화 | ❌ | ✅ |
| Android 웹뷰 흐름 | ❌ | ✅ |
Spring Boot가 메인 백엔드라면 Supabase Auth + Spring Boot 비즈니스 로직 조합이 가장 현실적입니다.