카카오 로그인 앱 재시작 문의

문의 시, 사용하시는 SDK 버전 정보와 디벨로퍼스 앱ID를 알려주세요.


IOS SDK 최신버전 사용 중이며 디벨로퍼스 APP ID는 934880입니다.
시작 화면에서 카카오톡 로그인 버튼을 누르면 카카오톡으로 연결되어 동의 항목이 뜬 이후 앱으로 되돌아가면 앱이 재시작되며 로그인이 완료되지 않습니다. 다시 시도해도 똑같은 과정만 반복됩니다.
모든 계정이 그런 것은 아니지만 꽤나 높은 빈도로 해당 문제가 발생하여 문의드립니다.
어떤 이유 때문에 이런 문제가 발생하는 것일까요 ?

if UserApi.isKakaoTalkLoginAvailable() {
            UserApi.shared.loginWithKakaoTalk {(oauthToken, error) in
                if let error = error {
                    print("🚩🚩\(error)")
                } else {
                    print("----🚩카카오 톡으로 로그인 성공🚩----")
                    self.baseView.kakaoButton.isEnabled = false
                    Amplitude.instance().logEvent("complete_onboarding_finish")
                    guard let kakaoToken = oauthToken?.accessToken else { return }
                    let queryDTO = KakaoLoginRequestDTO(accessToken: kakaoToken, social: "KAKAO", deviceToken: User.shared.deviceToken)
                    self.authNetwork(queryDTO: queryDTO)
                }
            }

그리고 /v1/api/talk/friends에서 403(-402)에러와 400(-10)이 발생하는 걸로 확인되는데,
400(-10)는 허용된 요청 수를 넘었을 때 라고 나와있는데, 쿼터를 넘지 않는 것을 확인했습니다.
403(-402)에러는 메시지를 보낼 때 나타나는 에러라고 확인되는데, 여기서는 단순하게 카카오톡 친구를 불러오는 부분인데 해당 에러가 발생하는 이유를 모르겠습니다.
두가지 에러 확인 부탁드립니다.

안녕하세요.

카카오톡 프로필 조회 (/v1/api/talk/friends) 는 일 3만건 쿼터 제한이 존재하며, 초당 50건 이내로 요청하셔야 합니다. -10 오류 발생된 케이스는 매우 빠른 시간내 프로필 조회가 연속 발생하여 발생한 오류 입니다.

만약, 로그인이나 특정 메뉴 진입 시 마다 해당 API사용 하시면 사용자 수에 따라 언제든지 쿼터 초과가 발생할 수 있습니다. 클라이언트에서 톡 프로필 조회를 하지 않으셔도 될것 같은데요 해당 API 사용하시는 이유가 어떻게 되실까요?


그리고, 앱이 재시작은 SDK가 아닌 서비스 구현에 따라 발생하는 것으로 보입니다.
로그인 중, 인디케이터를 표시하거나 하여 뷰가 달리 표시될것 같아 보이는데요

가능하시다면 로그인 화면 구성 코드와 로그인 전체 코드를 공유 부탁드립니다.

안녕하세요 !
우선 저희 서비스를 현재 사용 중인 카카오톡 친구들을 불러와서 친구 추가하기 위해 해당 API를 사용 중입니다.

//
//  KakaoLoginViewController.swift
//  YELLO-iOS
//
//  Created by 지희의 MAC on 2023/07/15.
//

import UIKit

import Amplitude
import KakaoSDKUser
import KakaoSDKTalk

class KakaoLoginViewController: UIViewController {
    // MARK: - Variables
    // MARK: Component
    let baseView = KakaoLoginView()
    
    // MARK: - Function
    // MARK: LifeCycle
    override func loadView() {
        view = baseView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        addTarget()
    }
    
    // MARK: Custom Function
    func addTarget() {
        baseView.kakaoButton.addTarget(self, action: #selector(kakaoLoginButtonDidTap), for: .touchUpInside)
        baseView.privacyButton.addTarget(self, action: #selector(privacyButtonDidTap), for: .touchUpInside)
    }
    
    func authNetwork(queryDTO: KakaoLoginRequestDTO) {
        NetworkService.shared.onboardingService.postTokenChange(queryDTO: queryDTO) { result in
            switch result {
            case .success(let data):
                if data.status == 403 {
                    UserApi.shared.me() {(user, error) in
                        if let error = error {
                            print(error)
                        } else {
                            print("me() success.")
                            _ = user
                            guard let user = user else { return }
                            guard let uuidInt = user.id else { return }
                            guard let kakaoUser = user.kakaoAccount else {return}
                            guard let email = kakaoUser.email else {return}
                            guard let profile = user.kakaoAccount?.profile?.profileImageUrl else {return}
                            User.shared.social = "KAKAO"
                            User.shared.uuid = String(uuidInt)
                            User.shared.email = email
                            User.shared.name = kakaoUser.name ?? ""
                            User.shared.gender = kakaoUser.gender?.rawValue.uppercased() ?? ""
                            User.shared.profileImage = profile.absoluteString
                        }
                        
                        UserApi.shared.scopes(scopes: ["friends"]) { (scopeInfo, error) in
                            if let error = error {
                                print(error)
                            } else {
                                /// 동의항목 확인하기
                                guard let scopeInfo = scopeInfo else { return }
                                guard let allowList = scopeInfo.scopes else { return }
                                if allowList[0].agreed {
                                    Amplitude.instance().logEvent("click_onboarding_kakao_friends")
                                    TalkApi.shared.friends(limit: 100) {(friends, error) in
                                        if let error = error {
                                            print(error)
                                        } else {
                                            var allFriends: [String] = []
                                            friends?.elements?.forEach({
                                                guard let id = $0.id else { return }
                                                allFriends.append(String(id))
                                            })
                                            User.shared.kakaoFriends = allFriends
                                        }
                                    }
                                }
                                
                                /// 확인 후 플로우 변경
                                let nextViewController = allowList[0].agreed ? SchoolSelectViewController() : KakaoConnectViewController()
                                let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as! SceneDelegate
                                self.navigationController?.pushViewController(nextViewController, animated: true)
                            }
                        }
                        
                    }
                } else if data.status == 201 {
                    guard let data = data.data else { return }
                    KeychainHandler.shared.accessToken = data.accessToken
                    KeychainHandler.shared.refreshToken = data.refreshToken
                    UserDefaults.standard.setValue(true, forKey: "isLoggined")
                    
                    User.shared.isResigned = data.isResigned
                    Amplitude.instance().logEvent("complete_onboarding_finish")
                    
                    let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as! SceneDelegate
                    
                    if isFirstTime() {
                        let rootViewController = PushSettingViewController()
                        sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: rootViewController)
                    } else if User.shared.isResigned {
                        let rootViewController = TutorialViewController()
                        sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: rootViewController)
                    } else {
                        let rootViewController = YELLOTabBarController()
                        let status = UserDefaults.standard.integer(forKey: "status")
                        rootViewController.startStatus = status
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                            sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: rootViewController)
                        }
                    }
                }
            default:
                print("network failure")
                return
            }
        }
    }
    
    // MARK: Objc Function
    @objc func kakaoLoginButtonDidTap() {
        baseView.kakaoButton.isEnabled = false
        Amplitude.instance().logEvent("click_onboarding_kakao")
        /// 카카오톡 실행 가능 여부 확인
        /// isKakaoTalkLoginAvailable() : 카톡 설치 되어있으면 true
        if UserApi.isKakaoTalkLoginAvailable() {
            UserApi.shared.loginWithKakaoTalk {(oauthToken, error) in
                if let error = error {
                    print("🚩🚩\(error)")
                } else {
                    print("----🚩카카오 톡으로 로그인 성공🚩----")
                    self.baseView.kakaoButton.isEnabled = false
                    Amplitude.instance().logEvent("complete_onboarding_finish")
                    guard let kakaoToken = oauthToken?.accessToken else { return }
                    let queryDTO = KakaoLoginRequestDTO(accessToken: kakaoToken, social: "KAKAO", deviceToken: User.shared.deviceToken)
                    self.authNetwork(queryDTO: queryDTO)
                }
            }
        } else {
            /// 카톡 없으면 -> 계정으로 로그인
            UserApi.shared.loginWithKakaoAccount { (oauthToken, error) in
                if let error = error {
                    print(error)
                } else {
                    print("카카오 계정으로 로그인 성공")
                    self.baseView.kakaoButton.isEnabled = false
                    Amplitude.instance().logEvent("complete_onboarding_finish")
                    guard let kakaoToken = oauthToken?.accessToken else { return }
                    let queryDTO = KakaoLoginRequestDTO(accessToken: kakaoToken, social: "KAKAO", deviceToken: User.shared.deviceToken)
                    self.authNetwork(queryDTO: queryDTO)
                }
            }
        }
    }
    
    @objc func privacyButtonDidTap() {
        guard let url = URL(string: "https://yell0.notion.site/97f57eaed6c749bbb134c7e8dc81ab3f") else { return }
        UIApplication.shared.open(url, options: [:], completionHandler: nil)
    }
    
}

전체 로그인 코드는 다음과 같습니다.
그리고 403(-402)에러는 어떤 에러인가요 ?

친구 목록 가져오기 API는 카카오 서비스 내 친구목록(friends) 동의 항목에 동의한 사용자만 사용할 수 있습니다.
-402 오류는 동의하지 않은 사용자를 대상으로 API 사용하여 발생되었습니다.

혹시 저 코드에서 재시작이 되는 원인은 파악이 안될까요 ?

안녕하세요. @mam07065

저희 서버 로그상에는 로그인 요청처리 완료 된걸로 보이며,
주신 답글을 보면 로그인 완료 콜백에 뷰 분기 로직이 있는것으로 보입니다.

SDK api 호출결과로 UI를 핸들링 하실경우 다음과 같이 메인쓰레드 내에서 하셔야 합니다.

if UserApi.isKakaoTalkLoginAvailable() {
    UserApi.shared.loginWithKakaoTalk {(oauthToken, error) in
        if let error = error {
            print("🚩🚩\(error)")
        } else {
            print("----🚩카카오 톡으로 로그인 성공🚩----")

            DispatchQueue.main.async {
                //UI 처리 로직 
                //...
            }
        }
    }
}
1개의 좋아요

감사합니다!
그리고 가끔 카카오 로그아웃을 할 때 토큰 만료로 불가할 때가 있는데 , 이 문제는 어떻게 해결해야할까요 ?

로그아웃 기능은 해당 사용자의 접근토큰을 만료시킬 때 사용됩니다.
사용된 접근 토큰이 이미 만료된 경우 불가 하오니 이 때는 정상 요청으로 보시면 좋을것 같습니다.

1개의 좋아요