소셜 로그인 설정
Google, Apple 소셜 로그인을 연결합니다.
한눈에 보기
Unveily SDK는 플랜별로 소셜 로그인 제공 범위가 다릅니다.
| 소셜 | Basic | Standard | Pro |
|---|---|---|---|
| ✅ | ✅ | ✅ | |
| Apple (iOS) | ✅ | ✅ | ✅ |
| Apple (Android) | ✅ | ✅ | ✅ |
| 카카오 | — | ✅ | ✅ |
| 네이버 | — | ✅ | ✅ |
| LINE | — | ✅ | ✅ |
| Meta | — | — | ✅ |
Google과 Apple 모두 Firebase Auth를 통해 처리됩니다 — 별도의 백엔드가 필요 없습니다.
두 제공사 모두 Firebase Console에서 설정하며, 동일한 google-services.json 파일이 두 로그인에 모두 적용됩니다.
SDK는 Google 및 Apple 로그인에 대해 Firebase ID Token (idToken)을 반환하며,
서버에서 Firebase Admin SDK를 사용해 검증할 수 있습니다.
iOS 프로바이더 지원 현황
현재 iOS에서는 Apple과 Google 로그인만 동작합니다. iOS에서 카카오·네이버·LINE·Meta를 호출하면 향후 iOS SDK 업데이트 전까지 SDK_NOT_READY 에러가 반환될 수 있습니다. Android는 모든 프로바이더를 지원합니다. Apple/Google은 두 플랫폼 모두에서 동작합니다.
또한 iOS의 Apple 로그인은 idToken과 함께 authorizationCode (Apple 전용)를 반환합니다. 위의 "Google + Apple은 idToken 반환" 설명은 Firebase 경로(Android 및 iOS의 Firebase 처리분)에 해당합니다.
1. 키 설정 파일
assets/config/social_login_config.json에 각 소셜의 SDK 키를 입력합니다.
{
"kakao": {
"nativeAppKey": "여기에_카카오_네이티브_앱_키"
},
"naver": {
"clientId": "여기에_네이버_클라이언트_ID",
"clientSecret": "여기에_네이버_클라이언트_시크릿",
"appName": "앱 이름"
},
"line": {
"channelId": "여기에_LINE_채널_ID"
},
"meta": {
"appId": "여기에_메타_앱_ID",
"clientToken": "여기에_메타_클라이언트_토큰"
}
}- 키가 비어 있으면 해당 소셜은 자동 비활성화됩니다.
- Google 및 Apple:
google-services.json+ Firebase Console로 설정합니다. 이 파일에 별도 항목이 필요 없습니다.
2. build.gradle.kts Placeholder 수정
카카오와 Meta는 딥링크 콜백을 위해 build.gradle.kts의 manifest placeholder를 업데이트해야 합니다.
// app/build.gradle.kts — defaultConfig 내부
defaultConfig {
// 카카오: "kakao" + nativeAppKey
manifestPlaceholders["kakaoScheme"] = "kakao여기에_카카오_네이티브_앱_키"
// Meta: "fb" + appId
manifestPlaceholders["facebookLoginProtocolScheme"] = "fb여기에_메타_앱_ID"
}3. 소셜 버튼 노출 제어
assets/config.json의 socialLogin 섹션에서 버튼 노출 여부를 on/off 합니다.
{
"socialLogin": {
"google": true,
"apple": true,
"kakao": true,
"naver": true,
"line": false,
"meta": false
}
}true: 버튼 노출 허용 (단, 플랜에서도 허용돼야 실제 동작)false: 버튼 미노출
4. 개발자 콘솔 등록 절차
- Firebase Console → 프로젝트 생성
- Authentication → 로그인 방법 → Google 사용 설정
google-services.json다운로드 →app/폴더에 교체
Apple (iOS 및 Android)
iOS와 Android 모두 Apple 로그인은 Firebase Auth를 통해 처리됩니다 — 동일한 Firebase 프로젝트가 두 플랫폼을 모두 지원합니다.
- Apple Developer → Certificates, Identifiers & Profiles:
- Services ID 생성 (역도메인 형식, 예:
com.yourcompany.app) - Sign In with Apple이 활성화된 Key 생성 → Team ID, Key ID 메모 후
.p8키 다운로드
- Services ID 생성 (역도메인 형식, 예:
- Firebase Console → Authentication → 로그인 방법 → Apple 사용 설정
- Team ID, Services ID, Key ID 입력 후
.p8개인 키 내용 붙여넣기
- Team ID, Services ID, Key ID 입력 후
- 동일한
google-services.json이 Google과 Apple 모두에 적용됩니다 — 추가 파일 불필요.
Firebase가 Apple의 전체 OAuth 플로우(Android의 Chrome Custom Tab 포함)를 내부적으로 처리합니다. 서버 측 콜백 엔드포인트나 커스텀 URL 스킴이 필요 없습니다.
카카오
- 카카오 개발자 콘솔 → 내 애플리케이션 → 애플리케이션 추가
- 앱 키 → Native 앱 키 복사
- 플랫폼 → Android → 패키지명 및 마켓 URL 등록
- 카카오 로그인 → 활성화 → 동의 항목 설정 (닉네임, 이메일 등)
social_login_config.json→kakao.nativeAppKey입력build.gradle.kts→manifestPlaceholders["kakaoScheme"] = "kakao{Native앱키}"수정
네이버
- 네이버 개발자 센터 → Application → 애플리케이션 등록
- 사용 API → 네아로(네이버 아이디로 로그인) 선택
- Android → 패키지명 등록
- Client ID / Client Secret 복사
social_login_config.json→naver.clientId,naver.clientSecret,naver.appName입력
LINE
- LINE Developers → Providers → Create → Channel 생성
- Channel 종류 → LINE Login 선택
- Android Package Name 등록
- Channel ID 복사
social_login_config.json→line.channelId입력
Meta (Facebook)
- Meta for Developers → My Apps → Create App
- 제품 추가 → Facebook Login 선택
- 설정 → 기본 설정 → 앱 ID 및 클라이언트 토큰 복사
- Android → 패키지 이름 및 키 해시 등록
social_login_config.json→meta.appId,meta.clientToken입력build.gradle.kts→manifestPlaceholders["facebookLoginProtocolScheme"] = "fb{앱ID}"수정
5. JS Bridge API
로그인
window.unveilyBridge.auth.socialLogin({
provider: 'google' // 'apple' | 'kakao' | 'naver' | 'line' | 'meta'
});로그인 콜백
페이지에 다음 함수를 선언해두면 결과를 수신합니다.
function onSocialLoginResult(result) {
if (result.success) {
console.log(result.provider); // 'google' | 'apple' | 'kakao' | ...
console.log(result.uid); // Firebase UID (Google/Apple) 또는 소셜 고유 ID
console.log(result.displayName); // 이름 (null 가능)
console.log(result.email); // 이메일 (null 가능)
console.log(result.profileImage); // 프로필 이미지 URL (null 가능)
console.log(result.idToken); // Firebase ID Token (Google + Apple만 해당; 카카오/네이버/LINE/Meta는 null)
// idToken으로 서버 측 Firebase Admin SDK를 통해 로그인 검증
} else {
console.error(result.error);
// 주요 에러 코드:
// FEATURE_NOT_ALLOWED → 티어 미충족 (result.requiredTier 포함)
// PROVIDER_DISABLED → config.json에서 false로 설정됨
// SDK_NOT_CONFIGURED → social_login_config.json 키 미입력
// SDK_NOT_READY → iOS에서 아직 미지원 프로바이더 (카카오/네이버/LINE/Meta)
// LOGIN_CANCELLED → 사용자가 로그인 화면을 닫음
}
}로그아웃
window.unveilyBridge.auth.logout({ provider: 'google' });연동 해제 (소셜 제공사 연결 해제)
해당 소셜 제공사의 연결만 해제합니다. 계정 자체는 삭제되지 않습니다.
window.unveilyBridge.auth.revoke({ provider: 'kakao' });Google / Apple 연동 해제
revoke는 Firebase user.unlink()를 호출합니다 — 해당 제공사 연결만 해제됩니다.
Firebase 계정은 그대로 유지됩니다.
계정 탈퇴(삭제)는 서버에서 user.delete()를 호출하는 별도 처리가 필요합니다.
로그아웃/연동해제 콜백
function onSocialAuthResult(result) {
// result.success — true/false
// result.action — 'logout' | 'revoke'
// result.provider — 'google' | 'apple' | 'kakao' | ...
// result.error — 실패 시 에러 코드
}6. 에러 코드 참조
| 에러 코드 | 설명 |
|---|---|
FEATURE_NOT_ALLOWED | 현재 플랜에서 허용되지 않는 소셜 (+ requiredTier 포함) |
PROVIDER_DISABLED | config.json에서 해당 소셜이 false |
SDK_NOT_CONFIGURED | social_login_config.json 키가 비어 있음 |
SDK_NOT_READY | iOS에서 아직 지원되지 않는 프로바이더 (카카오/네이버/LINE/Meta — 향후 iOS SDK 업데이트 예정) |
LOGIN_CANCELLED | 사용자가 로그인 화면을 닫음 |
UNKNOWN_PROVIDER | 지원하지 않는 provider 값 |
GOOGLE_ID_TOKEN_NULL | Google ID 토큰 발급 실패 |
FIREBASE_AUTH_FAILED | Firebase 로그인 자격증명 거부됨 |
APPLE_NO_USER | Apple 로그인 성공했으나 Firebase가 사용자를 반환하지 않음 |
APPLE_ERROR | Apple 로그인 실패 (일반) |
GOOGLE_REVOKE_FAILED | Google 제공사 연결 해제 실패 |
APPLE_REVOKE_FAILED | Apple 제공사 연결 해제 실패 |
NAVER_NO_TOKEN | 네이버 액세스 토큰 없음 |
LINE_INIT_FAILED | LINE SDK 초기화 실패 |
7. profileImage 처리 규칙
| 소셜 | 제공 여부 |
|---|---|
거의 항상 제공 (없으면 null) | |
| Apple | 항상 null (원천 미제공) |
| 카카오 | 동의 항목 설정 시 제공 |
| 네이버 | 동의 항목 설정 시 제공 |
| LINE | 사용자 프로필 공개 설정 시 제공 |
| Meta | 공개 프로필 설정 시 제공 |
null인 경우 고객사에서 기본 이미지를 표시하도록 처리하세요.
function onSocialLoginResult(result) {
const avatar = result.profileImage
?? '/images/default-avatar.png';
document.getElementById('avatar').src = avatar;
}8. Apple displayName 캐시
Apple은 최초 로그인 시에만 사용자 이름을 제공합니다. SDK는 이를 로컬에 캐시합니다.
| 동작 | 처리 |
|---|---|
| 첫 로그인 (displayName 있음) | 캐시 저장 후 전달 |
| 재로그인 (displayName null) | 캐시된 값으로 대체 전달 |
| 로그아웃 | 캐시 유지 (재로그인 대비) |
| 연동 해제 (revoke) | 캐시 삭제 (재연동 시 Apple이 새로 제공) |
| 플랫폼 | 캐시 저장소 | 키 형식 |
|---|---|---|
| iOS | UserDefaults | apple_name_{uid} |
| Android | SharedPreferences("unveily_apple_name_cache") | apple_name_{uid} |
9. 완성 예제
// 로그인 버튼 클릭
document.getElementById('btn-google').addEventListener('click', () => {
window.unveilyBridge.auth.socialLogin({ provider: 'google' });
});
// 결과 처리
function onSocialLoginResult(result) {
if (!result.success) {
if (result.error === 'FEATURE_NOT_ALLOWED') {
alert(`${result.requiredTier} 플랜 이상에서 사용 가능합니다.`);
} else if (result.error !== 'LOGIN_CANCELLED') {
alert('로그인에 실패했습니다.');
}
return;
}
// 서버에 전송 — Google/Apple은 idToken으로 서버 측 검증
sendToServer(result);
}
async function sendToServer(result) {
await fetch('/api/auth/social', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider: result.provider,
uid: result.uid,
idToken: result.idToken, // Firebase ID Token (Google/Apple) — 서버 측 검증용
email: result.email,
displayName: result.displayName
})
});
}서버 측 Firebase ID Token 검증
Google 및 Apple 로그인의 경우, Firebase Admin SDK를 사용해 서버에서 idToken을 검증합니다.
// npm install firebase-admin
import admin from 'firebase-admin';
admin.initializeApp({ credential: admin.credential.applicationDefault() });
app.post('/api/auth/social', async (req, res) => {
const { provider, uid, idToken, email, displayName } = req.body;
// Google 및 Apple의 Firebase ID Token 검증
if (idToken) {
const decoded = await admin.auth().verifyIdToken(idToken);
if (decoded.uid !== uid) return res.status(401).json({ error: 'UID mismatch' });
}
// uid로 사용자 조회 또는 생성
let user = await db.users.findOne({ socialProvider: provider, socialId: uid });
if (!user) {
user = await db.users.create({ socialProvider: provider, socialId: uid, email, name: displayName });
}
req.session.userId = user.id;
res.json({ ok: true, user: { id: user.id, name: user.name } });
});// NuGet: FirebaseAdmin
using FirebaseAdmin;
using FirebaseAdmin.Auth;
[HttpPost("auth/social")]
public async Task<IActionResult> SocialLogin([FromBody] SocialLoginRequest request)
{
// Google 및 Apple의 Firebase ID Token 검증
if (!string.IsNullOrEmpty(request.IdToken))
{
var decoded = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(request.IdToken);
if (decoded.Uid != request.Uid)
return Unauthorized(new { error = "UID mismatch" });
}
var user = await _userService.FindOrCreateBySocialAsync(
request.Provider, request.Uid, request.Email, request.DisplayName);
HttpContext.Session.SetInt32("UserId", user.Id);
return Ok(new { ok = true, user = new { user.Id, user.Name } });
}// Maven: com.google.firebase:firebase-admin
@PostMapping("/api/auth/social")
public ResponseEntity<?> socialLogin(@RequestBody SocialLoginRequest request,
HttpSession session) throws FirebaseAuthException {
// Google 및 Apple의 Firebase ID Token 검증
if (request.getIdToken() != null) {
FirebaseToken decoded = FirebaseAuth.getInstance().verifyIdToken(request.getIdToken());
if (!decoded.getUid().equals(request.getUid()))
return ResponseEntity.status(401).body(Map.of("error", "UID mismatch"));
}
User user = userService.findOrCreateBySocial(
request.getProvider(), request.getUid(), request.getEmail(), request.getDisplayName());
session.setAttribute("userId", user.getId());
return ResponseEntity.ok(Map.of("ok", true, "userId", user.getId()));
}<?php
// composer require kreait/firebase-php
use Kreait\Firebase\Factory;
$data = json_decode(file_get_contents('php://input'), true);
$provider = $data['provider'];
$uid = $data['uid'];
$idToken = $data['idToken'] ?? null;
$email = $data['email'] ?? null;
$displayName = $data['displayName'] ?? null;
// Google 및 Apple의 Firebase ID Token 검증
if ($idToken) {
$firebase = (new Factory)->withServiceAccount('/path/to/serviceAccount.json');
$auth = $firebase->createAuth();
$decoded = $auth->verifyIdToken($idToken);
if ($decoded->claims()->get('sub') !== $uid) {
http_response_code(401);
echo json_encode(['error' => 'UID mismatch']);
exit;
}
}
$stmt = $pdo->prepare('SELECT * FROM users WHERE social_provider = ? AND social_id = ?');
$stmt->execute([$provider, $uid]);
$user = $stmt->fetch();
if (!$user) {
$pdo->prepare('INSERT INTO users (social_provider, social_id, email, name) VALUES (?, ?, ?, ?)')
->execute([$provider, $uid, $email, $displayName]);
$userId = $pdo->lastInsertId();
} else {
$userId = $user['id'];
}
$_SESSION['user_id'] = $userId;
echo json_encode(['ok' => true, 'userId' => $userId]);