728x90
SMALL
기존 로그인 구현 코드
private extension KakaoAuthHandler {
/// 카카오톡 로그인
///
/// Kakao SDK의 callback 기반 API를
/// Swift async/await 스타일로 래핑합니다.
func loginWithKakaoTalk() async throws -> String {
try await withCheckedThrowingContinuation { continuation in
UserApi.shared.loginWithKakaoTalk { oauthToken, error in
// 1️⃣ Kakao SDK 에러 발생
if let error = error {
continuation.resume(throwing: error)
return
}
// 2️⃣ accessToken 추출 실패
guard let accessToken = oauthToken?.accessToken else {
continuation.resume(throwing: KakaoAuthError.failedToGetToken)
return
}
// 3️⃣ 로그인 성공 → accessToken 반환
continuation.resume(returning: accessToken)
}
}
}
/// 카카오 계정 로그인 (웹 로그인)
///
/// 카카오톡이 설치되어 있지 않은 경우 사용됩니다.
func loginWithKakaoAccount() async throws -> String {
try await withCheckedThrowingContinuation { continuation in
UserApi.shared.loginWithKakaoAccount { oauthToken, error in
// 1️⃣ Kakao SDK 에러 발생
if let error = error {
continuation.resume(throwing: error)
return
}
// 2️⃣ accessToken 추출 실패
guard let accessToken = oauthToken?.accessToken else {
continuation.resume(throwing: KakaoAuthError.failedToGetToken)
return
}
// 3️⃣ 로그인 성공 → accessToken 반환
continuation.resume(returning: accessToken)
}
}
}
}
1️⃣ 왜 async/await 방식에서 문제가 났나
결론부터 말하면
Kakao SDK는 “비동기 로직”이 아니라 “UI 라이프사이클에 강하게 묶인 SDK”
Kakao SDK의 특징
- 내부에서 UIApplication.open(...) 호출
- 외부 앱(카카오톡) 실행 → 다시 앱으로 복귀
- SceneDelegate / AppDelegate와 깊게 연동
- Main Thread + RunLoop 흐름이 보장되어야 함
❌ async/await + continuation의 근본적 문제
개선 전 코드에서 이 부분 👇
try await withCheckedThrowingContinuation { continuation in
UserApi.shared.loginWithKakaoTalk { oauthToken, error in
...
continuation.resume(returning: accessToken)
}
}
문제가 되는 이유
- async 함수는 호출 스택이 끊어진다
- await 지점에서 RunLoop 제어권이 Swift Concurrency로 넘어간다
- 그런데 Kakao SDK는
- “앱 → 외부 앱 → 앱 복귀”라는 UI 이벤트 기반 흐름
- 이 흐름은
- callback 기반일 때 가장 안정적
- continuation은 이 복귀 시점을 보장하지 못함
📌 그래서 실제 증상은 이렇게 나타났지:
- 카카오톡이 안 뜨거나
- 떴다가 바로 닫히거나
- 복귀 후 callback이 여러 번 호출되거나
- SceneDelegate에서 URL을 받았는데 흐름이 꼬임
이건 네 코드 문제가 아니라 SDK 문제...
2️⃣ 개선 전 코드의 구조적 한계 정리
개선 전 KakaoAuthHandler의 특징
@MainActor
func login() async throws -> String
문제 포인트
| 항목 | 문제 |
| @MainActor | 함수 진입만 Main Thread 보장, 내부 callback은 SDK에 위임 |
| async/await | 외부 앱 전환 + 복귀 흐름과 궁합 ❌ |
| withCheckedThrowingContinuation | UI SDK에는 과한 추상화 |
| 반환 타입 | “언제 끝나는지”가 명확하지 않음 |
📌 즉
“네트워크 비동기”에는 좋은 패턴인데,
“외부 앱 인증 SDK”에는 오히려 위험한 패턴
3️⃣ 개선 후 코드가 해결한 핵심 포인트
🔥 핵심 변경 요약
| 항목 | 개선 전 | 개선 후 |
| 비동기 방식 | async/await | completion handler |
| Main Thread 보장 | @MainActor | DispatchQueue.main.async |
| SDK 호출 방식 | continuation 래핑 | SDK 원형 유지 |
| 책임 범위 | 흐릿함 | accessToken 획득까지만 |
개선 후 코드가 “안전한 이유”
1️⃣ Kakao SDK를 있는 그대로 사용
UserApi.shared.loginWithKakaoTalk { oauthToken, error in
...
}
- SDK 내부 흐름을 건드리지 않음
- SceneDelegate 복귀 타이밍과 정확히 맞물림
2️⃣ Main Thread를 명시적으로 보장
DispatchQueue.main.async {
if UserApi.isKakaoTalkLoginAvailable() {
...
}
}
3️⃣ 책임이 명확해짐
KakaoAuthHandler의 책임은 딱 하나:
“Kakao SDK로부터 accessToken을 얻는다”
- Firebase ❌
- 서버 ❌
- Custom Token ❌
👉 이 분리는 지금 구조에서 완벽함.
개선 후 전체 KakaoAuthHandler.swfit
import Foundation
import KakaoSDKAuth
import KakaoSDKUser
// MARK: - KakaoAuthHandler
/// Kakao SDK를 사용해 실제 로그인 로직을 수행하는 구현체입니다.
///
/// 책임:
/// 1. 카카오톡 설치 여부 판단
/// 2. 카카오톡 / 카카오계정 로그인 분기
/// 3. 로그인 성공 시 accessToken 반환
///
/// ❗️Firebase, 서버 통신(Custom Token)은 여기서 다루지 않습니다.
///
/// ⚠️ 중요:
/// Kakao SDK는 로그인 과정에서 외부 앱(카카오톡)을 실행한 뒤
/// 다시 앱으로 복귀하는 UI 기반 인증 흐름을 사용합니다.
///
/// 이 과정은 iOS RunLoop 및 SceneDelegate와 강하게 결합되어 있으므로,
/// Swift Concurrency(async/await)로 래핑할 경우
/// 인증 흐름이 끊기거나 예기치 않은 동작이 발생할 수 있습니다.
///
/// 따라서 본 구현에서는 async/await를 사용하지 않고
/// Kakao SDK가 제공하는 callback 기반 API를 그대로 사용합니다.
final class KakaoAuthHandler {
// MARK: - Public
/// Kakao 로그인 진입점
///
/// 이 메서드는 Kakao SDK의 UI 흐름을 보존하기 위해
/// callback 기반으로 설계되었습니다.
///
/// - Note:
/// accessToken 획득 이후의 Firebase 인증 및 서버 통신은
/// 상위 레이어(AuthService / SocialAuthCoordinator)에서 처리합니다.
func login(completion: @escaping (Result<String, Error>) -> Void) {
// ⚠️ 반드시 Main Thread에서 Kakao SDK 호출
DispatchQueue.main.async {
if UserApi.isKakaoTalkLoginAvailable() {
self.loginWithKakaoTalk(completion: completion)
} else {
self.loginWithKakaoAccount(completion: completion)
}
}
}
}
// MARK: - Private Login Methods
private extension KakaoAuthHandler {
/// 카카오톡 로그인
///
/// 카카오톡 앱을 통해 로그인합니다.
/// Kakao SDK 내부에서 UIApplication.open(...)이 호출되므로
/// 반드시 Main Thread에서 실행되어야 합니다.
func loginWithKakaoTalk(
completion: @escaping (Result<String, Error>) -> Void
) {
UserApi.shared.loginWithKakaoTalk { oauthToken, error in
// 1️⃣ Kakao SDK 에러
if let error = error {
completion(.failure(error))
return
}
// 2️⃣ accessToken 추출 실패
guard let accessToken = oauthToken?.accessToken else {
completion(.failure(KakaoAuthError.failedToGetToken))
return
}
// 3️⃣ 로그인 성공
completion(.success(accessToken))
}
}
/// 카카오 계정 로그인 (웹 로그인)
///
/// 카카오톡이 설치되어 있지 않은 경우 사용됩니다.
func loginWithKakaoAccount(
completion: @escaping (Result<String, Error>) -> Void
) {
UserApi.shared.loginWithKakaoAccount { oauthToken, error in
// 1️⃣ Kakao SDK 에러
if let error = error {
completion(.failure(error))
return
}
// 2️⃣ accessToken 추출 실패
guard let accessToken = oauthToken?.accessToken else {
completion(.failure(KakaoAuthError.failedToGetToken))
return
}
// 3️⃣ 로그인 성공
completion(.success(accessToken))
}
}
}
// MARK: - KakaoAuthError
/// Kakao 로그인 과정에서 발생할 수 있는 에러 정의
enum KakaoAuthError: Error {
/// accessToken을 정상적으로 얻지 못한 경우
case failedToGetToken
}728x90
LIST
'PulseBoard' 카테고리의 다른 글
| 🤔 SocialAuthCoordinator 생성 조건 및 구조 설계 (0) | 2025.12.29 |
|---|---|
| 🤔 async, await VC CompletionHandler 언제 써야하나?(KakaoAuthHandler) (0) | 2025.12.28 |
| Firebase Functions + Kakao 로그인 문제 해결 정리 (0) | 2025.12.27 |
| 🤔 카카오, 네이버 로그인 - accessToken 처리 방향 (1) | 2025.12.26 |
| KakaoAuthHandler는 “무엇을 기준으로 설계했나?” (0) | 2025.12.26 |