본문으로 건너뛰기

애플과 Capacitor을 사용하여 로그인하는 방법 (2026)

Add Sign in with Apple to a Capacitor 8 app on iOS, Android, and web: Apple Developer setup, plugin code, backend token checks, and common errors.

기사 저자

마틴 도나디우

작가

발레리아

리뷰어

조던

편집자

애플과 함께 사용하는 Capacitor (2026)

애플로 로그인 기능을 Capacitor 앱에 추가하려면 설치 @capgo/capacitor-social-loginSign in with Apple 기능을 앱 ID에 활성화하고 Xcode에서 활성화한 다음 SocialLogin.initialize({ apple: {} })그런 다음 SocialLogin.login({ provider: 'apple' }). iOS에서 이 앱은 native Apple sheet를 열고 identity token을 반환합니다. Android와 웹은 Apple의 웹 OAuth 흐름을 사용하며, Services ID와 HTTPS redirect URL이 필요합니다.

이 안내서에서는 Apple Developer 설정, iOS, Android 및 웹용 클라이언트 code 설정, 백엔드에서 토큰을 확인하는 방법, 그리고 가장 자주 발생하는 오류에 대해 설명합니다. 이 안내서는 Capacitor 8과 Xcode 26을 대상으로 하며, App Store Connect는 2026년 4월부터 업로드를 위해 이 버전이 필요합니다.

시작하기 전에 필요한 것

요구 사항 왜
Apple Developer 프로그램의 유료 회원 가입 App IDs, Services IDs 및 Sign in with Apple 키는 회원만 사용할 수 있습니다.
Xcode 26이 설치된 Mac 2026년 iOS 앱을 빌드하고 업로드하려면 필요합니다.
Capacitor 8 프로젝트 플러그인 v8은 Capacitor 8 버전을 대상으로 합니다.
관리할 HTTPS 도메인 Android와 웹만 지원. 애플은 localhost plain HTTP 반환 URL
백엔드 엔드포인트 항상 토큰을 확인합니다. Android에서는 Apple의 콜백도 받습니다.

인증서를 아직 만들지 않은 경우, iOS 인증서 생성기 Mac 키 체인 댄스 없이 인증서를 생성합니다.

Sign in with Apple의 플랫폼별 작동 방식

애플은 native SDK를만들어 제공하기 때문에 플랫폼별로 흐름이 다릅니다.

플랫폼 기능 사용하는 Client ID 로그인에 필요한 백엔드
iOS 자연어 ASAuthorizationAppleIDProvider sheet 시트 앱의 번들 ID
아니요 안드로이드 Services ID 서비스 ID
네 웹 서비스 ID 아니오 (하지만 토큰을 확인하세요)

사용자 ID의 대상 (aud)은 클라이언트 ID를 따릅니다. iOS에서 발급된 토큰은 앱의 번들 ID를 포함하고, Android 및 웹에서 발급된 토큰은 서비스 ID를 포함합니다. 백엔드에서는 두 가지 모두를 수락해야 합니다.

1단계: Apple Developer 계정 구성

1단계: Apple Developer 계정 구성

  1. Open 열기 인증서, 식별자 및 프로필 식별자 선택.
  2. 앱 ID 열기 (예를 들어 Capacitor appId (예를 들어 com.example.app. 만든다.
  3. 아래 능력, 체크 애플 로그인 및 저장. 기본 App ID로 남겨두거나 여러 앱을 묶을 때만 변경.

기존 App ID의 기능을 변경하면 사용하는 프로비전 프로파일을 다시 생성해야 합니다. 새로운 권한이 포함되지 않은旧 프로파일은 사용할 수 없습니다.

서비스 ID 생성 (안드로이드 및 웹 전용)

  1. 에서 식별자, 클릭 + 및 선택 서비스 ID.
  2. ‘예시 웹 로그인’과 같은 설명과 ‘예시 웹 로그인’과 같은 식별자를 사용하세요. 이 값은 안드로이드와 웹에서 사용됩니다. com.example.app.signin새로운 서비스 ID를 열고 ‘예시 웹 로그인’과 같은 설명과 ‘예시 웹 로그인’과 같은 식별자를 사용하세요. clientId 애플 로그인
  3. , 설정, 클릭 Configure.
  4. (예를 들어
  5. 등록한 도메인 목록에 추가하십시오 도메인과 서브 도메인 (예를 들어 scheme이 없는 경우) auth.example.com).
  6. Apple 로그인에 사용할 리턴 URL을 추가하십시오. 리턴 URL 전체 경로와 함께, 예를 들어 https:// . 웹 로그인에 사용할 경우 로그인 호출하는 페이지 URL을, 슬래시(/)가 있는 경우와 없는 경우 모두 입력하십시오. https://auth.example.com/apple/callbackApple 로그인 키를 생성하십시오 (Android 및 서버 토큰 교환)

Apple 로그인 키를 생성하려면

  1. , 키, +클릭 Apple 로그인, 설정 기본 App ID를 선택하기 위해
  2. 등록 키와 다운로드할 .p8 파일을 등록하세요. Apple은 파일을 한번만 다운로드할 수 있으니 주의하세요.
  3. 주의 사항 Key ID 그리고 팀 ID (개발자 포털의 상단 오른쪽과 회원 가입 아래에 표시됩니다.)

서버는 키를 사용하여 Apple의 토큰 엔드포인트로 보낸 JWT를 서명합니다. client_secret 이메일 출처를 등록하세요. 이메일을 보내는 경우

이메일 소스 등록

사용자는 선택할 수 있습니다. 이메일 숨기기, 이 주소가 제공됩니다. Apple은 등록한 도메인과 주소만 전달합니다. @privaterelay.appleid.com 서비스 > Apple과 함께 로그인하기 . 이메일 전송을 위해 이 설정을 건너뛰면 비밀번호 재설정 및 수표 이메일이 조용히 사라집니다.Step 2: Capacitor 플러그인 설치

Capacitor 플러그인은 여러 제공자를 포함합니다. Apple (및 Google)만 사용하는 경우 다른 제공자를 끄세요.

bun add @capgo/capacitor-social-login
bunx cap sync

소프트웨어 SDK가 배포되지 않도록 하세요: capacitor.config.ts Capacitor 플러그인을 다시 설치하세요.

import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'Example',
  webDir: 'dist',
  plugins: {
    SocialLogin: {
      providers: {
        apple: true,
        google: false,
        facebook: false,
        twitter: false,
      },
    },
  },
};

export default config;

Run bunx cap sync Capacitor 플러그인을 다시 설치하세요. AD_ID Google Play Console에서 SDK이 발생시키는 허용 경고입니다.

Step 3: iOS 설정

  1. 실행 bunx cap open ios.
  2. 선택하세요 App Capacitor을 사용하여 Apple과 함께 로그인하는 방법 Capacitor을 사용하여 Apple과 함께 로그인하는 방법, click + Capability, 그리고 추가 Capacitor을 사용하여 Apple과 함께 로그인하는 방법.
  3. Capacitor을 사용하여 Apple과 함께 로그인하는 방법

Capacitor을 사용하여 Apple과 함께 로그인하는 방법 com.apple.developer.applesignin into App.entitlements. 만약 CI에서 빌드하거나 Capgo 빌드를 커밋하지 않으면, 클라우드 빌드는 실패하는 앱을 생성합니다. ASAuthorizationError code 1000.

No Info.plist iOS에서 Apple에 대한 AppDelegate Step 4: Android 설정

Apple은 Android

Apple has no Android SDK. The plugin opens Apple’s authorize page in a Custom Tab with your Services ID. Apple then sends the result as a form POST to your HTTPS return URL. Your server exchanges the code, then redirects to a deep link that reopens the app with the tokens.

를 재개합니다. android/app/src/main/AndroidManifest.xml애플 로그인에 사용하는 Capacitor를 사용하는 방법 MainActivity <activity> 요소:

<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:scheme="com.example.app" android:host="apple-login" />
</intent-filter>

플러그인으로 인텐트를 전달하세요

편집 MainActivity.java 이러한 플러그인으로 도달하는 깊은 링크가 되도록 편집하세요:

package com.example.app;

import android.content.Intent;
import android.net.Uri;
import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginHandle;
import ee.forgr.capacitor.social.login.SocialLoginPlugin;

public class MainActivity extends BridgeActivity {

    @Override
    protected void onNewIntent(Intent intent) {
        Uri data = intent.getData();
        if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null
                && "apple-login".equals(data.getHost())) {
            PluginHandle handle = getBridge().getPlugin("SocialLogin");
            if (handle != null) {
                Plugin plugin = handle.getInstance();
                if (plugin instanceof SocialLoginPlugin) {
                    ((SocialLoginPlugin) plugin).handleAppleLoginIntent(intent);
                    return;
                }
            }
        }
        super.onNewIntent(intent);
    }
}

호스트에서 필터링하면 다른 깊은 링크(비밀번호 초기화, 마케팅 링크)가 작동합니다.

콜백 엔드포인트

애플이 code, id_token, state첫 로그인 시 user JSON 문자열을 반환 URL로 전송합니다. 엔드포인트는 code를 교환하고 앱으로 리다이렉트해야 하며, 플러그인이 파싱하는 계약입니다. Hono 예제를 사용하여 최소한의 success=true 플러그인이 토큰을 깊은 링크 쿼리 문자열에서 읽습니다. 따라서 앱이 동일한 커스텀 스키마를 등록하는 경우 로그에 표시하거나 다른 앱이 읽을 수 있습니다. 단기 토큰만 전송하세요. jose:

import { Hono } from 'hono';
import { SignJWT, importPKCS8, createRemoteJWKSet, jwtVerify } from 'jose';
import { saveAppleRefreshToken } from './db'; // your own persistence layer

const app = new Hono();

const TEAM_ID = process.env.APPLE_TEAM_ID!;
const KEY_ID = process.env.APPLE_KEY_ID!;
const SERVICE_ID = process.env.APPLE_SERVICE_ID!; // com.example.app.signin
const PRIVATE_KEY = process.env.APPLE_PRIVATE_KEY!; // contents of the .p8 file
const APP_REDIRECT = 'com.example.app://apple-login';
const APPLE_JWKS = createRemoteJWKSet(new URL('https://appleid.apple.com/auth/keys'));

async function appleClientSecret() {
  const key = await importPKCS8(PRIVATE_KEY, 'ES256');
  return new SignJWT({})
    .setProtectedHeader({ alg: 'ES256', kid: KEY_ID })
    .setIssuer(TEAM_ID)
    .setSubject(SERVICE_ID)
    .setAudience('https://appleid.apple.com')
    .setIssuedAt()
    .setExpirationTime('5m')
    .sign(key);
}

app.post('/apple/callback', async (c) => {
  const form = await c.req.parseBody();
  const code = String(form.code ?? '');
  if (!code) {
    return c.redirect(`${APP_REDIRECT}?success=false`);
  }

  // `form.user` is only present on the first authorization. Store it now.

  const res = await fetch('https://appleid.apple.com/auth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: SERVICE_ID,
      client_secret: await appleClientSecret(),
      code,
      grant_type: 'authorization_code',
      redirect_uri: 'https://auth.example.com/apple/callback',
    }),
  });
  if (!res.ok) {
    return c.redirect(`${APP_REDIRECT}?success=false`);
  }
  const tokens = await res.json();

  // Verify the identity token, then keep the long-lived refresh token on the server.
  // Only the short-lived tokens go back to the app in the deep link.
  const { payload } = await jwtVerify(tokens.id_token, APPLE_JWKS, {
    issuer: 'https://appleid.apple.com',
    audience: SERVICE_ID,
  });
  if (tokens.refresh_token) {
    await saveAppleRefreshToken(String(payload.sub), tokens.refresh_token);
  }
  const params = new URLSearchParams({
    success: 'true',
    access_token: tokens.access_token,
    id_token: tokens.id_token,
  });
  return c.redirect(`${APP_REDIRECT}?${params}`);
});

export default app;

The plugin reads the tokens from the deep link query string, so anything you put there can end up in logs or be read by another app that registers the same custom scheme. Send only the short-lived access_token 그리고 id_token, 새로고침 토큰이 아닌 id_token 서명과 nonce 서버에서 확인하기 전에, 앱의 역방향 번들 ID가 달린 고유한 스키마를 사용하세요.

The client_secret JWT는 6개월까지 유효할 수 있지만, 요청당 짧은 JWT를 생성하여 만료 오류를 피하세요. 파일을 비밀 관리자에 저장하세요, 앱 번들에 저장하지 마세요. .p8 5단계: 웹 설정

웹 빌드는 Apple JS와 Services ID를 사용합니다. 로그인 트리거 URL을 Services ID의 반환 URL에 정확히 추가하세요. Apple은

,이러한 URL을 허용하지 않습니다. 따라서 웹 로그인 테스트를 미리 배포된 도메인 또는 실제 호스트 이름이 있는 터널을 통해 진행하세요. localhost6단계: 초기화 및 로그인

앱 시작 시 한 번 초기화하고,

context redirectUrl Android와 웹에서만. iOS에서는 비어 있지 않은 redirectUrl 플러그인으로 POST를 보내는 URL을 지정하면, native flow에서 원치 않는 결과가 발생합니다.

import { Capacitor } from '@capacitor/core';
import { SocialLogin } from '@capgo/capacitor-social-login';

const platform = Capacitor.getPlatform();

export async function initAuth() {
  await SocialLogin.initialize({
    apple: {
      clientId: platform === 'ios' ? 'com.example.app' : 'com.example.app.signin',
      redirectUrl:
        platform === 'android'
          ? 'https://auth.example.com/apple/callback'
          : platform === 'web'
            ? 'https://app.example.com/login'
            : '',
    },
  });
}

버튼에서 로그인 트리거를 호출하세요. 서버에서 재생된 토큰을 거부하기 위해 랜덤한 nonce를 사용하세요:

import { SocialLogin } from '@capgo/capacitor-social-login';

export async function signInWithApple() {
  const nonce = crypto.randomUUID();

  try {
    const { result } = await SocialLogin.login({
      provider: 'apple',
      options: {
        scopes: ['email', 'name'],
        nonce,
      },
    });

    if (!result.idToken) {
      throw new Error('Apple did not return an identity token');
    }

    // Send to your backend. Name and email are only present on the first login.
    const response = await fetch('https://api.example.com/auth/apple', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        idToken: result.idToken,
        nonce,
        givenName: result.profile.givenName,
        familyName: result.profile.familyName,
      }),
    });
    return response.json();
  } catch (error: any) {
    if (error?.code === 'USER_CANCELLED') {
      return null; // user closed the sheet, not a failure
    }
    throw error;
  }
}

응답은 다음과 같습니다:

Field Content
result.idToken 애플에서 발급한 서명된 JWT, 서버에서 확인하는 것
result.profile.user 애플의 안정적인 사용자 ID (claim과 동일) sub claim)
result.profile.email 첫 번째 인증만
result.profile.givenName / familyName 기본 설정으로는 인증 __CAPGO_KEEP_0__을 보유하고, 실제 액세스 토큰이 아닌
result.accessToken Capacitor의 기본 설정으로는 인증 code을 저장하고, 실제 접근 토큰이 아닌 것입니다.
result.authorizationCode 초기화 시에 useProperTokenExchange: true

서버에서 Apple 리프레시 토큰이 필요하다면 (예를 들어 사용자가 계정을 삭제할 때 revoke 엔드포인트를 호출할 때), useProperTokenExchange: true 에서 apple 서버에서 교환합니다. authorizationCode 7단계: 백엔드에서 사용자 식별 토큰을 확인하세요

7단계: 백엔드에서 사용자 식별 토큰을 확인하세요

클라이언트가 보내는 프로필 필드에 절대 신뢰하지 마세요. Apple의 공개 키와 JWT 서명과 클레임을 확인하세요:

import { createRemoteJWKSet, jwtVerify } from 'jose';

const appleKeys = createRemoteJWKSet(new URL('https://appleid.apple.com/auth/keys'));

export async function verifyAppleToken(idToken: string, expectedNonce: string) {
  const { payload } = await jwtVerify(idToken, appleKeys, {
    issuer: 'https://appleid.apple.com',
    audience: ['com.example.app', 'com.example.app.signin'], // bundle ID + Services ID
  });

  if (payload.nonce !== expectedNonce) {
    throw new Error('Nonce mismatch');
  }

  return {
    appleUserId: payload.sub as string,
    email: payload.email as string | undefined,
    emailVerified: payload.email_verified === true || payload.email_verified === 'true',
  };
}

사용자 레코드를 sub에 저장하세요, 이메일이 아닌. 이메일은 중계 주소일 수 있고 사용자가 이를 공유하지 않도록 할 수 있습니다. 확인 후, 자신의 세션 토큰을 발급하세요. Supabase 또는 Firebase를 사용하는 경우, 토큰을 확인해 주세요. Supabase와 Capacitor 소셜 로그인.

4.8 지침

  • App Store 규칙을 참고하세요: Google, Facebook 또는 다른 세 번째 로그인 옵션을 제공한다면, Apple 로그인과 같은 개인 정보 보호에 중점을 둔 동등한 옵션도 제공해야 합니다.
  • 5.1.1(v) 지침: 사용자가 계정을 만들 수 있는 앱은 사용자가 앱에서 계정을 삭제할 수 있어야 합니다. Apple 로그인 사용자에게는 Apple의 삭제 엔드포인트를 호출해야 합니다. 자세한 내용은 계정 삭제 가이드를 참조하세요. https://appleid.apple.com/auth/revoke 버튼 디자인 : Apple의 승인된 버튼 스타일을 사용하고 다른 로그인 버튼과 같은 중요성을 지닌 버튼을 사용해야 합니다..
  • 문제 해결증상

가능한 원인

해결 __CAPGO_KEEP_0__ 1000 on iOS Fix
ASAuthorizationError code 1000 iOS에서 iOS 빌드 대상에서 capability 또는 권한이 누락됨 Xcode에서 Sign in with Apple 추가, 커밋 App.entitlements다시 프로파일 생성
Code 1001 iOS 버전 사용자가 취소함 플러그인이 거부함 USER_CANCELLED, 오류로 간주하지 않음
invalid_client iOS, Android 또는 웹에서 잘못됨 clientId (Services ID 대신 Bundle ID 사용 또는 키 내용이 잘못됨) client_secret Services ID 사용, 팀 ID, 키 ID 및 키 내용 확인
invalid_request: Invalid redirect_uri 등록된 URL과 정확히 일치하지 않음 URL을 완전히 추가하세요 https:// URL에 Services ID를 완전히 추가하세요. 끝에 슬래시(/)가 일치해야 합니다.
Android 로그인 후 앱이 다시 돌아오지 않습니다. Android 로그인 후 앱이 다시 돌아오지 않습니다. Compare com.example.app://apple-login 비교
apple.android.redirectUrl is null or empty 문자 하나하나 비교 redirectUrl 아니요 안드로이드
안드로이드에서 HTTPS callback URL을 초기화할 때 전달하세요. null 이름과 이메일은 첫 번째 인증이 아닙니다.
백엔드에서 토큰이 거부되었습니다: aud 일치하지 않음 서버는 하나의 청중만 허용합니다. 배달 ID와 서비스 ID를 모두 수락하십시오.

릴리스 후 배송 수정

대부분의 인증 문제는 TypeScript에서 발생합니다: 플랫폼당 잘못된 클라이언트 ID, 비어있는 비회한, 나쁜 오류 처리. 사용자에게 이러한 문제를 다음날에 배달할 수 있습니다. Capgo 라이브 업데이트 앱 리뷰를 기다리지 않고도. MainActivity 권한 변경, 안드로이드 매니페스트 또는

변경 사항은 네이티브이므로 새로운 스토어 빌드를 필요로합니다.

Live updates for Capacitor apps

웹-layer 버그가 활성화된 경우 Capgo을 통해 픽스를 배포하는 대신 앱 스토어 승인까지 며칠 기다리지 않고, 사용자는 배경에서 업데이트를 받으면서 네이티브 변경은 일반적인 검토 경로에 남겨둔다.

최신 뉴스

Capgo은 여러분에게 전문적인 모바일 앱을 만들기 위해 필요한 최고의 통찰력을 제공합니다.