🇺🇸 EN

🔐 Sign in with Apple

가장 프라이버시 친화적인 로그인. 이메일 숨김, 2FA 자동, Face ID 인증!

⭐ 난이도: ⭐⭐ ⏱️ 예상 시간: 1-2h 📂 App Services

✨ Sign in with Apple이란?

Apple ID로 간편 로그인합니다. 이메일을 숨길 수 있고, 2단계 인증이 자동으로 처리됩니다. 다른 소셜 로그인이 있다면 필수 구현입니다.

🎯 기본 구현

SignInManager.swift
import AuthenticationServices

class SignInManager: NSObject, ASAuthorizationControllerDelegate {
    func signInWithApple() {
        let provider = ASAuthorizationAppleIDProvider()
        let request = provider.createRequest()

        // 요청할 정보 (이름, 이메일)
        request.requestedScopes = [.fullName, .email]

        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests()
    }

    // 성공 시
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
            let userID = credential.user  // 고유 ID (변하지 않음)
            let email = credential.email  // 첫 로그인 시만 제공
            let fullName = credential.fullName  // 첫 로그인 시만 제공
            let identityToken = credential.identityToken  // JWT 토큰

            // 백엔드로 identityToken 전송하여 검증
            if let tokenData = identityToken,
               let tokenString = String(data: tokenData, encoding: .utf8) {
                verifyTokenWithBackend(tokenString, userID: userID)
            }

            // UserDefaults에 userID 저장
            UserDefaults.standard.set(userID, forKey: "appleUserID")
        }
    }

    // 실패 시
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: Error
    ) {
        print("로그인 실패: \(error.localizedDescription)")
    }

    func verifyTokenWithBackend(_ token: String, userID: String) {
        // 백엔드 API 호출
    }
}

extension SignInManager: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return UIApplication.shared.windows.first!
    }
}

🎨 SwiftUI에서 Sign in with Apple 버튼

SignInButton.swift
import SwiftUI
import AuthenticationServices

struct SignInWithAppleButton: View {
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        SignInWithAppleButton(
            .signIn,
            onRequest: { request in
                request.requestedScopes = [.fullName, .email]
            },
            onCompletion: { result in
                switch result {
                case .success(let authorization):
                    handleAuthorization(authorization)
                case .failure(let error):
                    print("에러: \(error)")
                }
            }
        )
        .signInWithAppleButtonStyle(
            colorScheme == .dark ? .white : .black
        )
        .frame(height: 50)
    }

    func handleAuthorization(_ authorization: ASAuthorization) {
        if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
            print("사용자 ID: \(credential.user)")
            print("이메일: \(credential.email ?? \"없음\")")
        }
    }
}

// 사용 예시
struct LoginView: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("로그인")
                .font(.largeTitle.bold())

            SignInWithAppleButton()
                .padding(.horizontal)
        }
    }
}

🔄 자동 로그인 상태 체크

CredentialCheck.swift
import AuthenticationServices

func checkCredentialState() {
    guard let userID = UserDefaults.standard.string(forKey: "appleUserID") else {
        // 저장된 userID 없음
        return
    }

    let provider = ASAuthorizationAppleIDProvider()
    provider.getCredentialState(forUserID: userID) { state, error in
        switch state {
        case .authorized:
            print("로그인 유지 중")
            // 자동 로그인 처리

        case .revoked:
            print("로그인 취소됨")
            // 로그아웃 처리
            UserDefaults.standard.removeObject(forKey: "appleUserID")

        case .notFound:
            print("사용자 없음")

        case .transferred:
            print("다른 팀으로 이전됨")

        @unknown default:
            break
        }
    }
}

// 앱 시작 시 체크
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    checkCredentialState()
    return true
}

📧 이메일 릴레이 (Email Relay)

사용자가 "이메일 숨기기"를 선택하면 Apple이 프록시 이메일(`privaterelay@icloud.com`)을 제공합니다.

EmailRelay.swift
func handleCredential(_ credential: ASAuthorizationAppleIDCredential) {
    if let email = credential.email {
        // 첫 로그인 시만 제공됨
        print("이메일: \(email)")

        // privaterelay@icloud.com 형태면 프록시 이메일
        if email.contains("privaterelay") {
            print("사용자가 이메일을 숨김")
        }

        // ⚠️ 중요: 첫 로그인 시만 제공되므로 반드시 저장!
        UserDefaults.standard.set(email, forKey: "userEmail")
    }
}

🔑 백엔드 토큰 검증

BackendVerification.swift
// identityToken은 JWT 형식입니다
// 백엔드에서 Apple 공개키로 검증해야 합니다

func sendTokenToBackend(_ credential: ASAuthorizationAppleIDCredential) async {
    guard let tokenData = credential.identityToken,
          let token = String(data: tokenData, encoding: .utf8) else {
        return
    }

    // authorizationCode도 백엔드로 전송 (refresh token 발급용)
    guard let codeData = credential.authorizationCode,
          let code = String(data: codeData, encoding: .utf8) else {
        return
    }

    // API 호출
    let body: [String: Any] = [
        "identityToken": token,
        "authorizationCode": code,
        "user": credential.user
    ]

    // POST to your backend
}

/* 백엔드 검증 (Node.js 예시)
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const client = jwksClient({
  jwksUri: 'https://appleid.apple.com/auth/keys'
});

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    callback(null, key.publicKey || key.rsaPublicKey);
  });
}

jwt.verify(identityToken, getKey, (err, decoded) => {
  if (err) {
    // 토큰 검증 실패
  } else {
    // decoded.sub === userID
    // decoded.email === 사용자 이메일
  }
});
*/

📱 기존 계정 업그레이드

AccountUpgrade.swift
import AuthenticationServices

// 기존 이메일/비밀번호 로그인을 Apple ID로 업그레이드
func upgradeToAppleID(existingEmail: String) {
    let provider = ASAuthorizationAppleIDProvider()
    let request = provider.createRequest()
    request.requestedScopes = [.fullName]

    // 기존 계정 연결을 위한 힌트
    request.user = existingEmail

    let controller = ASAuthorizationController(authorizationRequests: [request])
    // ... delegate 설정
    controller.performRequests()
}

🚨 로그아웃 & 계정 삭제

Revocation.swift
// 로그아웃
func signOut() {
    UserDefaults.standard.removeObject(forKey: "appleUserID")
    UserDefaults.standard.removeObject(forKey: "userEmail")
    // UI를 로그인 화면으로
}

// 계정 삭제 (백엔드에서 Apple API 호출 필요)
/*
사용자가 "설정 > Apple ID > 암호 및 보안 > Apple로 로그인을 사용하는 앱"에서
앱을 제거하면 로그인이 취소됩니다.

백엔드에서는 Apple의 Token Revocation API를 호출해야 합니다:
POST https://appleid.apple.com/auth/revoke

이후 사용자 데이터를 삭제하고 계정을 비활성화합니다.
*/

⚙️ 앱 설정 (Capabilities)

Xcode 설정
1. Xcode에서 프로젝트 선택
2. Signing & Capabilities 탭
3. "+ Capability" 클릭
4. "Sign in with Apple" 추가

5. Apple Developer 콘솔에서:
   - Identifier에 "Sign in with Apple" 활성화
   - Services ID 생성 (웹 로그인용, 선택)
   - Key 생성 (백엔드 검증용)

✅ 필수 요구사항 (App Review)

⚠️ 필수!
앱에 다른 소셜 로그인(Google, Facebook 등)이 있다면,
Sign in with Apple을 반드시 구현해야 합니다.
그렇지 않으면 App Store 심사에서 리젝됩니다.

💡 HIG 체크리스트
✅ 공식 Sign in with Apple 버튼 사용
✅ 다른 로그인 버튼과 동일한 크기/위치
✅ 이메일/이름은 첫 로그인 시만 제공됨 (저장 필수)
✅ 토큰은 백엔드에서 검증
✅ 앱 시작 시 credential 상태 체크

📦 학습 자료

💻
GitHub 프로젝트
🍎
Apple HIG 원문
📖
Apple 공식 문서

📎 Apple 공식 자료

📘 공식 문서 💻 샘플 코드 🎬 WWDC 세션