본문 바로가기
PulseBoard

🎫 KakaoAuthHandler 는 왜 async/await를 버리고 completion 기반으로 되돌렸나?

by 밤새는 탐험가89 2025. 12. 28.
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)
    }
}

 

문제가 되는 이유

  1. async 함수는 호출 스택이 끊어진다
  2. await 지점에서 RunLoop 제어권이 Swift Concurrency로 넘어간다
  3. 그런데 Kakao SDK는
    • “앱 → 외부 앱 → 앱 복귀”라는 UI 이벤트 기반 흐름
  4. 이 흐름은
    • 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