ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [iOS][Spring] Kakao login 붙이기
    기타개발지식/풀스택개발 2026. 6. 19. 15:21

    개요

    앱에서 카카오 로그인을 붙일 때 제일 먼저 정리해야 하는 건 역할 분리다.

    iOS 앱은 카카오 SDK로 사용자를 로그인시키고, 카카오 access token을 받는다.

    Spring 백엔드는 그 access token으로 카카오 사용자 정보를 조회하고, 우리 서비스 회원을 찾거나 만든 다음, 우리 서비스용 access token과 refresh token을 내려준다.

    즉, 앱이 카카오 유저 ID를 그냥 문자열로 보내고 서버가 믿는 구조가 아니다.

    서버가 카카오 API를 직접 호출해서 이 토큰이 진짜인지 확인해야 한다.

    전체 흐름

    iOS 앱
      -> Kakao SDK 로그인
      -> Kakao access token 획득
      -> POST /api/auth/kakao-login
    
    Spring 백엔드
      -> Kakao 사용자 정보 API 호출
      -> provider = KAKAO, providerUserId = kakao id 로 회원 조회
      -> 없으면 회원 생성
      -> 우리 서비스 JWT access token / refresh token 발급
    
    iOS 앱
      -> 우리 서비스 토큰을 Keychain에 저장
      -> 이후 API 요청에 Authorization: Bearer {accessToken} 붙임

    여기서 핵심은 카카오 토큰과 우리 서비스 토큰을 구분하는 것이다.

    카카오 access token은 로그인 증명서처럼 한 번 서버에 넘기는 값이고, 앱에서 계속 들고 다닐 서비스 세션은 우리 백엔드가 발급한 토큰이다.

    Kakao Developers 설정

    먼저 Kakao Developers에서 앱을 만든다.

    Kakao Developers
      > 내 애플리케이션
      > 애플리케이션 추가하기

    앱을 만들고 나면 아래 메뉴들을 확인한다.

    앱 설정 > 앱 키
    앱 설정 > 플랫폼
    제품 설정 > 카카오 로그인
    제품 설정 > 카카오 로그인 > 동의항목

    iOS 앱에서 Kakao SDK를 쓸 거면 Native app key가 필요하다.

    그리고 iOS 플랫폼에 Bundle ID를 등록해야 한다.

    앱 설정 > 플랫폼 > iOS 플랫폼 등록
    Bundle ID: com.example.myapp

    Bundle ID는 Xcode의 Target > General > Identity 쪽에서 보는 그 값이다.

    카카오 문서 기준으로 iOS SDK는 등록된 Bundle ID의 native app에서만 사용할 수 있다.

    그 다음 카카오 로그인을 켠다.

    제품 설정 > 카카오 로그인 > 활성화 설정 ON

    동의항목에서는 앱에서 필요한 값만 켠다.

    처음에는 보통 닉네임, 프로필 이미지, 이메일 정도를 본다.

    다만 이메일은 사용자가 동의하지 않거나 카카오 계정 상태에 따라 없을 수 있어서, 백엔드에서 email을 필수 키로 잡으면 나중에 피곤해진다.

    나는 provider + providerUserId 조합을 회원의 외부 로그인 키로 쓰는 쪽이 낫다고 본다.

    Kakao Developers에서 실제로 누르는 순서

    위 내용을 실제 사이트 기준으로 다시 풀면 이렇다.

    Kakao Developers
      > 내 애플리케이션
      > 애플리케이션 추가하기
      > 앱 선택
      > 앱 설정
      > 플랫폼
      > 제품 설정
      > 카카오 로그인

    먼저 애플리케이션을 만든다.

    앱 이름, 회사명, 카테고리, 대표 도메인 정도를 넣고 저장하면 된다.

    Kakao Developers 애플리케이션 생성 화면

    앱을 만든 다음에는 앱 키를 확인한다.

    iOS Kakao SDK에서 쓰는 값은 Native app key다.

    Kakao Developers 플랫폼 키 화면

    iOS 앱에서는 Native app key 설정에 Bundle ID를 등록해야 한다.

    Xcode의 Bundle Identifier와 여기 입력한 Bundle ID가 다르면 로그인할 때 invalid android_key_hash or ios_bundle_id or web_site_url 계열 에러가 날 수 있다.

    Kakao Developers Native app key iOS Bundle ID 설정 화면

    그 다음 제품 설정의 카카오 로그인으로 들어가서 Usage settings를 ON으로 바꾼다.

    이걸 안 켜면 카카오 로그인 요청 시 KOE004가 날 수 있다.

    Kakao Login Usage settings ON 화면

    OpenID Connect를 쓸 거면 같은 화면에서 OpenID Connect도 ON으로 켠다.

    이 글의 기본 흐름은 access token으로 백엔드가 /v2/user/me를 조회하는 방식이지만, ID token 기반으로 갈 거면 이 설정이 필요하다.

    Kakao Login OpenID Connect ON 화면

    마지막으로 동의항목을 설정한다.

    닉네임, 프로필 이미지, 이메일 같은 값은 여기서 사용 여부와 동의 방식을 정한다.

    Kakao Login 동의항목 목록 화면

    각 항목의 Set 버튼을 누르면 동의 타입과 목적을 넣는 모달이 열린다.

    이메일을 Optional consent로 둘지, Required consent로 둘지는 서비스 정책에 따라 정해야 한다.

    다만 Required email은 Biz app이나 추가 심사가 필요할 수 있으니, 처음 붙일 때는 provider user id 기준으로 로그인되게 만드는 게 마음 편하다.

    Kakao Login 동의항목 설정 모달

    Redirect URI는 언제 쓰나

    카카오 REST API 방식으로 authorization code를 직접 받아서 서버에서 토큰 교환을 할 때는 Redirect URI가 필요하다.

    제품 설정 > 카카오 로그인 > Redirect URI

    예를 들면 이런 식이다.

    https://api.example.com/login/oauth2/code/kakao

    그런데 iOS Kakao SDK로 로그인하고 앱이 access token을 바로 받는 구조라면, 앱 로그인 흐름에서 이 redirect URI를 직접 쓰지는 않는다.

    그래도 운영 중 웹 로그인이나 백엔드 OAuth2 클라이언트 흐름을 같이 붙일 수 있어서, 서버 콜백 주소는 정리해두는 게 좋다.

    iOS 프로젝트 설정

    Kakao SDK를 추가한다.

    Swift Package Manager를 쓴다면 패키지는 아래를 사용한다.

    https://github.com/kakao/kakao-ios-sdk

    필요한 모듈은 보통 이 정도다.

    KakaoSDKCommon
    KakaoSDKAuth
    KakaoSDKUser

    Info.plist에는 URL Scheme을 넣는다.

    CFBundleURLTypes
      CFBundleURLSchemes
        kakao{NATIVE_APP_KEY}

    예를 들어 Native app key가 123456이면 URL scheme은 아래처럼 된다.

    kakao123456

    카카오톡 앱을 열 수 있는지 확인하려면 allowlist도 필요하다.

    LSApplicationQueriesSchemes
      kakaokompassauth
      kakaolink
      kakaoplus

    실제 앱에서는 Native app key를 코드에 대충 박아두기보다 xcconfig나 빌드 설정으로 분리해두는 게 좋다.

    iOS SDK 초기화

    SwiftUI App Life Cycle이면 App 진입점에서 초기화한다.

    import SwiftUI
    import KakaoSDKAuth
    import KakaoSDKCommon
    
    @main
    struct MyApp: App {
        init() {
            KakaoSDK.initSDK(appKey: "NATIVE_APP_KEY")
        }
    
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .onOpenURL { url in
                        if AuthApi.isKakaoTalkLoginUrl(url) {
                            _ = AuthController.handleOpenUrl(url: url)
                        }
                    }
            }
        }
    }

    여기서 handleOpenUrl이 빠지면 카카오톡 앱으로 갔다가 내 앱으로 돌아오는 흐름에서 로그인이 마무리되지 않는다.

    이게 은근히 자주 놓치는 부분이다.

    iOS 로그인 코드

    카카오톡이 설치되어 있으면 카카오톡 로그인을 먼저 시도하고, 없으면 카카오계정 로그인을 열면 된다.

    import KakaoSDKUser
    
    @MainActor
    final class KakaoLoginService {
        private let authClient: AuthClient
    
        init(authClient: AuthClient) {
            self.authClient = authClient
        }
    
        func login() async throws -> LoginResponse {
            let token = try await kakaoAccessToken()
    
            return try await authClient.kakaoLogin(
                accessToken: token,
                deviceId: DeviceID.current,
                timezone: TimeZone.current.identifier,
                locale: Locale.current.identifier
            )
        }
    
        private func kakaoAccessToken() async throws -> String {
            try await withCheckedThrowingContinuation { continuation in
                let completion: (OAuthToken?, Error?) -> Void = { token, error in
                    if let error {
                        continuation.resume(throwing: error)
                        return
                    }
    
                    guard let accessToken = token?.accessToken else {
                        continuation.resume(throwing: AuthError.missingProviderToken)
                        return
                    }
    
                    continuation.resume(returning: accessToken)
                }
    
                if UserApi.isKakaoTalkLoginAvailable() {
                    UserApi.shared.loginWithKakaoTalk(completion: completion)
                } else {
                    UserApi.shared.loginWithKakaoAccount(completion: completion)
                }
            }
        }
    }

    앱이 백엔드로 보내는 요청은 단순하게 잡는다.

    {
      "accessToken": "kakao-access-token",
      "deviceId": "device-uuid",
      "timezone": "Asia/Seoul",
      "locale": "ko-KR"
    }

    iOS AuthClient 예시

    struct KakaoLoginRequest: Encodable {
        let accessToken: String
        let deviceId: String
        let timezone: String
        let locale: String
    }
    
    struct LoginResponse: Decodable {
        let accessToken: String
        let refreshToken: String
        let memberId: Int
        let isExistingMember: Bool
    }
    
    final class AuthClient {
        private let baseURL = URL(string: "https://api.example.com")!
    
        func kakaoLogin(
            accessToken: String,
            deviceId: String,
            timezone: String,
            locale: String
        ) async throws -> LoginResponse {
            var request = URLRequest(url: baseURL.appending(path: "/api/auth/kakao-login"))
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = try JSONEncoder().encode(
                KakaoLoginRequest(
                    accessToken: accessToken,
                    deviceId: deviceId,
                    timezone: timezone,
                    locale: locale
                )
            )
    
            let (data, response) = try await URLSession.shared.data(for: request)
            guard (response as? HTTPURLResponse)?.statusCode == 200 else {
                throw AuthError.loginFailed
            }
    
            return try JSONDecoder().decode(LoginResponse.self, from: data)
        }
    }

    실제 프로젝트에서는 응답이 공통 래퍼라면 ApiResponse<LoginResponse> 같은 형태로 한 번 더 감싸서 디코딩하면 된다.

    Spring 요청 DTO

    public record KakaoLoginRequest(
            String accessToken,
            String deviceId,
            String timezone,
            String locale
    ) {
    }

    컨트롤러는 provider별 엔드포인트를 분리해두면 처음 구현할 때 보기가 편하다.

    @RestController
    @RequestMapping("/api/auth")
    public class AuthController {
    
        private final AuthService authService;
    
        public AuthController(AuthService authService) {
            this.authService = authService;
        }
    
        @PostMapping("/kakao-login")
        public LoginResponse kakaoLogin(@RequestBody KakaoLoginRequest request) {
            return authService.loginWithKakao(request);
        }
    }

    Spring에서 카카오 토큰 검증

    access token을 받으면 카카오 사용자 정보 API를 호출한다.

    GET https://kapi.kakao.com/v2/user/me
    Authorization: Bearer {kakao-access-token}

    Spring Boot 3 기준이면 RestClient로 간단히 만들 수 있다.

    @Component
    public class KakaoOAuthClient {
    
        private final RestClient restClient;
    
        public KakaoOAuthClient(RestClient.Builder builder) {
            this.restClient = builder
                    .baseUrl("https://kapi.kakao.com")
                    .build();
        }
    
        public KakaoUserResponse getUser(String accessToken) {
            return restClient.get()
                    .uri("/v2/user/me")
                    .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
                    .retrieve()
                    .body(KakaoUserResponse.class);
        }
    }

    응답 DTO는 필요한 필드만 받으면 된다.

    public record KakaoUserResponse(
            Long id,
            KakaoAccount kakao_account
    ) {
        public record KakaoAccount(
                String email,
                Profile profile
        ) {
        }
    
        public record Profile(
                String nickname,
                String profile_image_url
        ) {
        }
    }

    회원 처리

    카카오에서 받은 id를 우리 서비스의 provider user id로 쓴다.

    @Service
    public class AuthService {
    
        private final KakaoOAuthClient kakaoOAuthClient;
        private final MemberRepository memberRepository;
        private final TokenService tokenService;
    
        public LoginResponse loginWithKakao(KakaoLoginRequest request) {
            KakaoUserResponse kakaoUser = kakaoOAuthClient.getUser(request.accessToken());
    
            String providerUserId = String.valueOf(kakaoUser.id());
            String email = kakaoUser.kakao_account() == null
                    ? null
                    : kakaoUser.kakao_account().email();
    
            Member member = memberRepository
                    .findByProviderAndProviderUserId(Provider.KAKAO, providerUserId)
                    .orElseGet(() -> memberRepository.save(
                            Member.social(Provider.KAKAO, providerUserId, email)
                    ));
    
            TokenPair tokenPair = tokenService.issue(member.getId(), request.deviceId());
    
            return LoginResponse.from(member, tokenPair);
        }
    }

    DB에는 이런 유니크 키를 잡는다.

    provider + provider_user_id unique

    email unique만으로 소셜 로그인을 묶으면 안된다.

    카카오는 이메일이 없을 수도 있고, 나중에 동의 항목을 바꿨을 때 들어오는 값이 달라질 수도 있다.

    앱 세션 저장

    백엔드가 내려준 우리 서비스 토큰은 Keychain에 저장한다.

    final class AuthSession {
        func apply(_ response: LoginResponse) {
            Keychain.set(response.accessToken, forKey: "accessToken")
            Keychain.set(response.refreshToken, forKey: "refreshToken")
            UserDefaults.standard.set(response.memberId, forKey: "memberId")
        }
    }

    이후 보호된 API에는 카카오 토큰이 아니라 우리 서비스 access token을 붙인다.

    Authorization: Bearer {service-access-token}

    access token이 만료되면 refresh token으로 재발급하고, 재발급도 실패하면 로그아웃 처리한다.

    자주 막히는 부분

    카카오 로그인에서 자주 막히는 건 대부분 설정값이다.

    1. Bundle ID가 Kakao Developers에 등록한 값과 다름
    2. URL Scheme에 kakao{NATIVE_APP_KEY}를 안 넣음
    3. handleOpenUrl 처리를 안 해서 앱 복귀 후 로그인이 끝나지 않음
    4. 카카오 로그인을 활성화하지 않음
    5. 이메일 동의항목을 필수처럼 생각하고 DB 로직을 짬
    6. 카카오 access token을 우리 서비스 세션 토큰처럼 계속 사용함

    특히 마지막이 중요하다.

    앱 내부 API 인증은 우리 백엔드가 발급한 토큰으로 해야 한다.

    최종 정리

    Kakao login은 iOS 쪽 설정이 조금 번거롭고, Spring 쪽은 access token으로 사용자 정보 API를 호출하는 구조라 비교적 단순하다.

    처음에는 OIDC id token까지 욕심내지 말고, Kakao SDK access token을 백엔드로 보내고 서버에서 /v2/user/me를 호출하는 방식으로 끝까지 붙여보는 게 좋다.

    그 다음에 OpenID Connect, 카카오싱크, 추가 동의항목 같은 것을 붙이면 삽질 범위가 줄어든다.

    참고 링크

Designed by Tistory.