[친구하자] Capacitor 앱에서 Kakao/Google 네이티브 로그인 구현 정리

Capacitor로 빌드한 Android 앱에서 카카오/구글 네이티브 로그인을 구현한 방법에 대해 정리해보았습니다.

  • Capacitor로 빌드한 React 앱에서 카카오와 구글 네이티브 로그인을 구현하는 방법을 정리해 보았다.
  • 관련되서 정리되어있는게 없는것 같아서.. 내가 나중에 찾아볼 수 있게 일단 문서를 만들어보았다.
  • 웹에서는 일반 OAuth 리다이렉트를 사용하지만, 모바일 앱에서는 네이티브 SDK를 직접 사용하는 방식으로 구현했다. (kakao, google)
    • kakao developer에는 flutter문서에서 크로스 플랫폼에서는 커스텀 URL 스킴을 사용하라고 권장한다. 하지만 나는 플러그인을 사용하는 방식으로 구현해보았다.

네이티브 SDK 직접 사용 구헌 방식 (카카오, 구글)

네이티브 SDK 로그인 방식이란?

  • 카카오/구글이 제공하는 Android/iOS 전용 SDK를 사용한 로그인 방식
  • 앱 간 통신 방식을 사용한다.
    • 즉, 기기에 설치된 카카오톡/구글 앱과 직접 통신하여 인증한다.
  • WebView를 우회하여 브라우저 리다이렉트 없이 앱 레벨에서 토큰을 교환한다.

이 방식이 하이브리드 앱에서 가능한 이유

  • ⭐️Capacitor의 플러그인 시스템⭐️이 핵심!

설명 이미지1

  • WebView의 JavaScript 코드가 네이티브 코드를 호출할 수 있게 해줌
  • KakaoLoginPlugin.goLogin()을 통해 실제로 Android의 카카오 SDK 실행
  • 결과를 다시 JavaScript로 전달

*앱 구조 비교

// 안드로이드에서 실행 시
if (Capacitor.isNativePlatform()) {
  // ✅ 네이티브 SDK 사용 (앱간 통신)
  // 카카오톡 앱 → 내 앱 (빠르고 안정적)
  const result = await KakaoLoginPlugin.goLogin();
}

// 웹 브라우저에서 실행 시
else {
  // ✅ OAuth 리다이렉트 사용 (브라우저 기반)
  // 브라우저 → 카카오 웹 → 콜백 URL
  window.location.href = "https://kauth.kakao.com/oauth/...";
}

이렇게 하는 이유

  • 네이티브 로그인의 장점:
    • 사용자가 카카오톡이 설치되어 있으면 앱 전환만으로 즉시 로그인
    • 브라우저 리다이렉트보다 UX가 훨씬 부드러움
    • DeepLink, Custom URL Scheme 문제 없음
  • 웹 로그인:
    • 브라우저에서는 네이티브 SDK를 쓸 수 없으니 전통적인 OAuth 방식 사용

1. 카카오 네이티브 로그인 구현

1.1 플러그인 설치

pnpm add capacitor-kakao-login-plugin
npx cap sync

1.2 Android 설정

1.2.1 Kakao SDK 의존성 추가

android/app/build.gradle:

dependencies {
    // ... 기존 의존성 ...
    implementation "com.kakao.sdk:v2-common:2.20.1"
    implementation "com.kakao.sdk:v2-auth:2.20.1"
}

android/build.gradle:

allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url 'https://devrepo.kakao.com/nexus/content/groups/public/' }
    }
}

1.2.2 AndroidManifest.xml 설정

android/app/src/main/AndroidManifest.xml:

<application>
    <!-- 카카오 SDK 메타데이터 -->
    <meta-data
        android:name="com.kakao.sdk.AppKey"
        android:value="@string/kakao_app_key" />

    <activity
        android:name=".MainActivity"
        android:exported="true">
        <!-- 카카오 로그인 리다이렉트 -->
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:host="kakaolink" android:scheme="@string/kakao_scheme" />
        </intent-filter>
    </activity>

    <!-- 카카오 인증 코드 핸들러 -->
    <activity
        android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:host="oauth" android:scheme="@string/kakao_scheme" />
        </intent-filter>
    </activity>
</application>

1.2.3 strings.xml 설정

android/app/src/main/res/values/strings.xml:

<resources>
    <string name="kakao_app_key">YOUR_KAKAO_NATIVE_APP_KEY</string>
    <string name="kakao_scheme">kakaoYOUR_KAKAO_NATIVE_APP_KEY</string>
</resources>

1.2.4 MainActivity.java 초기화

android/app/src/main/java/com/yourpackage/app/MainActivity.java:

import com.kakao.sdk.common.KakaoSdk;

public class MainActivity extends BridgeActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 카카오 SDK 초기화
        KakaoSdk.init(this, getResources().getString(R.string.kakao_app_key));
    }
}

1.3 iOS 설정

1.3.1 Info.plist 설정

ios/App/App/Info.plist:

<key>KAKAO_APP_KEY</key>
<string>YOUR_KAKAO_NATIVE_APP_KEY</string>

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>kakaoYOUR_KAKAO_NATIVE_APP_KEY</string>
            <string>com.yourapp.bundleid</string>
        </array>
    </dict>
</array>

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>kakaoYOUR_KAKAO_NATIVE_APP_KEY</string>
    <string>kakaokompassauth</string>
    <string>storykompassauth</string>
    <string>kakaolink</string>
    <string>storylink</string>
    <string>kakaotalk</string>
    <string>kakaotalk-5.9.7</string>
    <string>kakaostory-2.9.0</string>
</array>

1.3.2 AppDelegate.swift 초기화

ios/App/App/AppDelegate.swift:

import KakaoSDKAuth
import KakaoSDKCommon

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let key = Bundle.main.infoDictionary?["KAKAO_APP_KEY"] as? String
    if let kakaoKey = key {
        KakaoSDK.initSDK(appKey: kakaoKey)
    }
    return true
}

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    if (AuthApi.isKakaoTalkLoginUrl(url)) {
        return AuthController.handleOpenUrl(url: url)
    }
    return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}

1.4 프론트엔드 코드

client/lib/auth.ts:

import { KakaoLoginPlugin } from "capacitor-kakao-login-plugin";

export const startSocialLogin = async (
  provider: OAuthProvider
): Promise<void> => {
  const isMobile = Capacitor.isNativePlatform();

  if (isMobile && provider === "kakao") {
    try {
      // 카카오 네이티브 로그인 실행
      const kakaoResult = await KakaoLoginPlugin.goLogin();

      // 카카오 액세스 토큰을 백엔드로 전달
      const result = await processKakaoNativeLogin(kakaoResult.accessToken);

      if (result) {
        window.dispatchEvent(
          new CustomEvent("oauth-login-success", {
            detail: { userInfo: result.data.user_info },
          })
        );
      }
    } catch (error) {
      window.dispatchEvent(
        new CustomEvent("oauth-login-error", {
          detail: { error: error.message },
        })
      );
    }
  } else {
    // 웹: 일반 OAuth 리다이렉트
    window.location.href = config.data.authorization_url;
  }
};

// 백엔드로 카카오 토큰 전달
export const processKakaoNativeLogin = async (
  kakaoAccessToken: string
): Promise<OAuthLoginResponse> => {
  const response = await fetch(`${getApiUrl()}/v1/auth/oauth/kakao/native`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      kakao_access_token: kakaoAccessToken,
      device_info: `${navigator.platform} - ${navigator.userAgent}`,
    }),
    credentials: "include",
  });

  return await response.json();
};

1.5 카카오 개발자 콘솔 설정

  1. 키 해시 등록 (Android)

    • Android Studio Logcat에서 SHA-1, SHA-256 키 해시 확인
    • 카카오 개발자 콘솔 → 내 애플리케이션 → 플랫폼 → Android → 키 해시 등록
  2. 리다이렉트 URI 설정

    • Android: kakao{NATIVE_APP_KEY}://oauth
    • iOS: kakao{NATIVE_APP_KEY}://oauth

2. 구글 네이티브 로그인 구현

2.1 플러그인 설치

pnpm add @codetrix-studio/capacitor-google-auth
npx cap sync

2.2 Google Console 설정

2.2.1 웹 클라이언트 ID 생성

  1. Google Console → API 및 서비스 → 사용자 인증 정보
    • 사용자 인증 정보 만들기 → OAuth 2.0 클라이언트 ID
  2. 애플리케이션 유형: 웹 애플리케이션
  3. 생성된 클라이언트 ID를 복사

2.2.2 Android 클라이언트 ID 생성

    • 사용자 인증 정보 만들기 → OAuth 2.0 클라이언트 ID
  1. 애플리케이션 유형: Android
  2. 패키지 이름: com.yourapp.bundleid
  3. SHA-1 인증서 지문 등록 (중요!)

SHA-1 지문 확인 방법:

# 디버그 키스토어
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

# 또는 MainActivity에서 로그로 확인

SHA-1 지문을 콜론 없이 등록

2.2.3 iOS 클라이언트 ID 생성 (선택)

    • 사용자 인증 정보 만들기 → OAuth 2.0 클라이언트 ID
  1. 애플리케이션 유형: iOS
  2. 번들 ID: com.yourapp.bundleid
  3. GoogleService.plist 파일 다운로드

2.3 Android 설정

2.3.1 strings.xml 설정

android/app/src/main/res/values/strings.xml:

<resources>
    <!-- 웹 클라이언트 ID (server_client_id) -->
    <string name="server_client_id">YOUR_WEB_CLIENT_ID.apps.googleusercontent.com</string>
</resources>

참고: Android 클라이언트 ID는 코드에 넣을 필요 없습니다. 플러그인이 자동으로 사용합니다.

2.3.2 AndroidManifest.xml 권한 추가

<uses-permission android:name="android.permission.GET_ACCOUNTS" />

2.3.3 capacitor.config.ts 설정

export default {
  plugins: {
    GoogleAuth: {
      scopes: ["profile", "email"],
      serverClientId: "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com",
      forceCodeForRefreshToken: true,
    },
  },
};

2.4 iOS 설정

2.4.1 GoogleService.plist 추가

  1. Google Console에서 다운로드한 GoogleService.plist 파일을
  2. ios/App/App/ 폴더에 복사

2.4.2 Info.plist 설정

ios/App/App/Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>com.yourapp.bundleid</string>
            <!-- GoogleService.plist의 REVERSED_CLIENT_ID 값 -->
            <string>YOUR_REVERSED_CLIENT_ID</string>
        </array>
    </dict>
</array>

REVERSED_CLIENT_IDGoogleService.plist 파일에서 확인할 수 있습니다.

2.5 프론트엔드 코드

client/lib/auth.ts:

import { GoogleAuth } from "@codetrix-studio/capacitor-google-auth";

export const startSocialLogin = async (
  provider: OAuthProvider
): Promise<void> => {
  const isMobile = Capacitor.isNativePlatform();

  if (isMobile && provider === "google") {
    try {
      // 구글 플러그인 초기화 (scopes 포함)
      await GoogleAuth.initialize({
        scopes: ["profile", "email"],
      });

      // 구글 네이티브 로그인 실행
      const googleResult = await GoogleAuth.signIn();

      // 구글 ID 토큰을 백엔드로 전달
      if (!googleResult.authentication?.idToken) {
        throw new Error("구글 ID 토큰을 받지 못했습니다.");
      }

      const result = await processGoogleNativeLogin(
        googleResult.authentication.idToken
      );

      if (result) {
        window.dispatchEvent(
          new CustomEvent("oauth-login-success", {
            detail: { userInfo: result.data.user_info },
          })
        );
      }
    } catch (error) {
      window.dispatchEvent(
        new CustomEvent("oauth-login-error", {
          detail: { error: error.message },
        })
      );
    }
  } else {
    // 웹: 일반 OAuth 리다이렉트
    window.location.href = config.data.authorization_url;
  }
};

// 백엔드로 구글 ID 토큰 전달
export const processGoogleNativeLogin = async (
  googleIdToken: string
): Promise<OAuthLoginResponse> => {
  const response = await fetch(`${getApiUrl()}/v1/auth/oauth/google/native`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      google_id_token: googleIdToken,
      device_info: `${navigator.platform} - ${navigator.userAgent}`,
    }),
    credentials: "include",
  });

  return await response.json();
};

2.6 웹 설정 (선택)

index.html:

<meta
  name="google-signin-client_id"
  content="YOUR_WEB_CLIENT_ID.apps.googleusercontent.com"
/>

3. 백엔드 API 엔드포인트

3.1 카카오 네이티브 로그인

POST /v1/auth/oauth/kakao/native
Content-Type: application/json

{
  "kakao_access_token": "카카오_액세스_토큰",
  "device_info": "플랫폼 정보"
}

3.2 구글 네이티브 로그인

POST /v1/auth/oauth/google/native
Content-Type: application/json

{
  "google_id_token": "구글_ID_토큰",
  "device_info": "플랫폼 정보"
}

4. 체크리스트

카카오 로그인

  • capacitor-kakao-login-plugin 설치
  • Android: Kakao SDK 의존성 추가
  • Android: AndroidManifest.xml 설정
  • Android: strings.xml에 앱 키 설정
  • Android: MainActivity.java에서 SDK 초기화
  • iOS: Info.plist 설정
  • iOS: AppDelegate.swift에서 SDK 초기화
  • 카카오 개발자 콘솔에 키 해시 등록

구글 로그인

  • @codetrix-studio/capacitor-google-auth 설치
  • Google Console에 웹 클라이언트 ID 생성
  • Google Console에 Android 클라이언트 ID 생성 (SHA-1 지문 등록)
  • Google Console에 iOS 클라이언트 ID 생성 (선택)
  • Android: strings.xml에 웹 클라이언트 ID 설정
  • Android: capacitor.config.ts 설정
  • iOS: GoogleService.plist 추가
  • iOS: Info.plistREVERSED_CLIENT_ID 추가
  • 웹: index.html에 meta tag 추가

5. 참고 자료