지난 글에서는 Capacitor 플러그인과 Safe Area에 대해 알아봤습니다. 이번 글에서는 사용자 플로우를 따라 갈 때, 가장 처음에 나오는 로그인, 그 중에서도 소셜 로그인 3종(구글/카카오/애플) 적용 방법에 대해 알아보도록 하겠습니다.
소셜 로그인
기본적으로 구글, 카카오, 애플 로그인은 웹에서 처리를 했고, 앱 패키징만 하면 되는데 왜 소셜 로그인 적용 방식이 따로 나눠졌는지 궁금해하실 수 있습니다. 하지만, 실제 서비스에서는 웹 방식과 네이티브 방식이 나뉘는 이유가 있습니다.
구글: 구글은 기본적으로 모바일 앱에서 WebView 로그인을 제한합니다. 따라서 기존 웹 로그인 방식을 그대로 앱 패키징만 하는 경우 로그인이 되지 않습니다. 공식 SDK 또는 네이티브 로그인 플로우를 통해서 로그인을 권장합니다.
카카오: 카카오는 구글과 다르게 웹 로그인 방식으로 로그인이 가능합니다. 새로운 브라우저가 열리는건 Capacitor 플러그인 중, inAppBrowser을 통해 해결할 수 있습니다. 하지만 네이티브 방식을 사용하는 카카오톡이 설치되어 있는 경우 앱 연동 로그인을 지원합니다. 즉 UX를 더 챙길 수 있는거죠.
애플: 애플 로그인은 애플 스토어 심사 때문에 어쩔 수 없이 포함되는 로그인이죠. 네이티브와 웹 로그인 시 UI 차이는 없는 것 같습니다. 근데 소셜 로그인 플러그인에 구글이랑 같이 있으니까 써보죠?
구글 로그인
소셜 로그인을 위해서 사용한 Capacitor 플러그인은 @capgo/capacitor-social-login입니다. 우선 플러그인을 설치해줍시다. 저번에 말씀드린 것처럼 저는 base, capacitor-aos, capacitor-ios 3개의 프로젝트로 나눠서 관리하기 때문에 3개에 다 설치해줍시다.
npm install @capgo/capacitor-social-login
npx cap sync
다음은 capacitor.config.ts 수정입니다. 이건 base에 없으니 2개의 프로젝트를 수정해주면 되겠네요. 페이스북과 X(구 트위터) 로그인 기능이 필요하신 분들은 설정을 true로 바꾸시면 됩니다.
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: "com.gnaak.app",
appName: "gnaak",
webDir: "dist",
server: {
url: "https://gnaak.co.kr/",
url: "http://172.24.208.1:3000",
},
plugins: {
SocialLogin: {
providers: {
google: true,
facebook: false,
apple: true,
twitter: false
},
},
},
};
export default config;
그 다음은 SDK 초기화입니다. layout.tsx 혹은 main.tsx 등과 같이 사용자가 처음 들어왔을 때 SDK 초기화를 진행해주도록 합시다.
const [initialized, setInitialized] = useState(false);
const { setUser } = useAuth();
useEffect(() => {
const initSocialLogin = async () => {
try {
await SocialLogin.initialize({
google: {
webClientId: 'YOUR_WEB_CLIENT_ID',
iOSClientId: 'YOUR_IOS_CLIENT_ID',
iOSServerClientId: 'YOUR_WEB_CLIENT_ID',
mode: "online",
},
});
setInitialized(true);
} catch (error) {
console.error("초기화 실패:", error);
}
};
initSocialLogin();
}, []);
웹 클라이언트, IOS 클라이언트, Android 클라이언트(얘는 깃허브에는 설명이 없는데 있어야 됩니다. 생성을 하러 갑시다. 참고로 FCM을 사용하기 위해 Firebase에서 앱을 생성하는 경우에는 Google Cloud Console에도 자동으로 프로젝트가 생성되기 때문에 Firebase에서 생성하도록 합시다.아마도?)

아직 iOS는 필요 없지만, 나중에 iOS도 구글 로그인을 넣을거라면 미리 만들어 둡시다. 나머지는 웹에서 구글 로그인과 동일하지만, Android Client에 들어가서 SHA-1를 등록해줘야 합니다. SHA-1 확인 방법은 capacitor-aos, 혹은 android 폴더가 생긴 프로젝트에서 아래 명령어를 통해 확인할 수 있습니다.
cd android
./gradlew signingReport
개발 단계에서는 debug keystore를 사용하고, 구글 Play Store에 업로드 하면 Google이 App Signing Key로 서명해서 다시 배포해줍니다. 배포 이후 확인 방법은 Play Console - 설정 - 앱 무결성 - 앱 서명입니다.


SHA-1를 저장해줬으면, 이제 로그인을 이어서 진행합시다.
import { SocialLogin } from "@capgo/capacitor-social-login";
import { Capacitor } from "@capacitor/core";
import { handleLoggedIn } from "./handleLoggedIn";
// ...
const handleGoogleLogin = async () => {
if (!initialized) {
console.log("초기화 아직 안됨");
return;
}
try {
const result = await SocialLogin.login({
provider: "google",
options: {
style: "standard",
filterByAuthorizedAccounts: false,
// scopes: ["profile", "email"],
// scopes 사용하면 MainActivity 수정해야 하는데, scopes 없이도 정보 받아옴
},
});
if (result) {
await handleLoggedIn(result.result, "google", setUser);
}
} catch (error) {
console.log("로그인 실패", error);
}
};
const googleAuth = () => {
const scopeQuery = scopeParam
? `&scope=${encodeURIComponent(scopeParam)}`
: "";
window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${client_id}&redirect_uri=${encodeURIComponent(
redirect_uri
)}&response_type=code&access_type=offline${scopeQuery}`;
};
return (
<button onClick={Capacitor.isNativePlatform() ? handleGoogleLogin : googleAuth}>
<img src={google} alt="google login" className="absolute left-10" />
<span>Google로 로그인</span>
</button>
);
};
저는 기존 웹 방식과 네이티브 방식을 분기처리 해주기 위해서 Capacitor.isNativePlatform()을 사용했습니다. 아래는 결과입니다.
{
"provider": "google",
"result": {
"accessToken": {
"token": "ya29.a0Aa7pCA_lmLdMF7eDuoCLnhTCG4dcvDMVD7nUs9dysnYNuGfB2elItuFeVS7wTpLCNhsoXC35AW_5ZqWtqQrTkDL0Ct5qkquZeO0GsLzJY1-ZBlI0zfLIEXy8zmrLlTxllEmukBJiPFc-Q_jFQyAwd4SxC9x9J3i9vs9DfUww5PjVWt96RxOK8tRGxKatjcs4rntGtecaCgYKAZESARYSFQHGX2MirKcoA9x56fymaGgTVVvtRg0206"
},
"profile": {
"id": "103889678089733282612",
"name": "gnaaak",
"email": "o1027447735@gmail.com",
"givenName": "gnaaak",
"imageUrl": "https://lh3.googleusercontent.com/a/ACg8ocImb0UeHw38XNA8byCkw1TAwBhTnM4JMJFAEWPlTL5gSTwqqrw=s96-c"
},
"idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEzMGZkY2VmY2M4ZWQ3YmU2YmVkZmE2ZmM4Nzk3MjIwNDBjOTJiMzgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxMDU0ODY0NTA1MDY3LTNrbTFnYmZmZW41OWRtam1zNWhpY3NycGd0NDN2cWloLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiMTA1NDg2NDUwNTA2Ny1mdGdpYnFnamg2YTFkdWgwcTNxbTZyNjZpbzVjdmQydi5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjEwMzg4OTY3ODA4OTczMzI4MjYxMiIsImVtYWlsIjoibzEwMjc0NDc3MzVAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJnbmFhYWsiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUNnOG9jSW1iMFVlSHczOFhOQThieUNrdzFUQXdCaFRuTTRKTUpGQUVXUGxUTDVnU1R3cXFydz1zOTYtYyIsImdpdmVuX25hbWUiOiJnbmFhYWsiLCJpYXQiOjE3NjU0MjgxODIsImV4cCI6MTc2NTQzMTc4Mn0.J6vZ3U2PKTq1jmUDS4ESg62YIuHgDxzOvQItWKmMLB15FmQ8d2X3QzptjpT_JgZlJoCDGyt7rQlXbiWVhkl6P2zg5SM-Cgg1u_HTmsp2Ws4zzYz2K33BPHkX-Of0zT8xHqqwPEQL6Qqg0or6--uRONUV89IeyR9e2oiPJDUruu95lE0f1UhI09Ntt9tQBoINWiM6eutEdWxR81-lK-3Z85lBfzEMCn1fehvhMGb8SNcikY06s7lyrlOKceWmKwC4S-Qvjxs0a1idBLCO8C5dute1WQHFvVw0DRKb6ZbOkOZ7cHkEoXchWupeEcxcnpUrUnRda1zw1hqdZrZu02s0iA",
"responseType": "online"
}
}
카카오 로그인
카카오 로그인은 capgo에서 제공하는 플러그인을 통해 로그인 할 수 없어 다른 플러그인을 사용했습니다.
GitHub - nerdFrenzs/capacitor-kakao-login-plugin
Contribute to nerdFrenzs/capacitor-kakao-login-plugin development by creating an account on GitHub.
github.com
카카오 SDK를 사용하기 위해서는 네이티브 앱 키를 등록해줘야 합니다. 키 이름, Android 패키지명, 키 해시, iOS 번들 ID를 입력해줍시다. 키 해시는 위에 구글 로그인에 등록한 SHA-1 키 해시값입니다. 우선은 넘어갑시다.
android/build.gradle
카카오 Android SDK를 추가해줍시다. 카카오 SDK는 mavenCentral()에 없기 때문에 카카오 로그인을 위해서 추가해줘야 합니다.
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
maven {
url 'https://devrepo.kakao.com/nexus/content/groups/public/'
allowInsecureProtocol = true
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
app/build.gradle
카카오 SDK 의존성 추가
dependencies {
...
implementation "com.kakao.sdk:v2-user:2.20.0"
implementation "com.kakao.sdk:v2-auth:2.20.0"
}
android/app/src/main/java/com.gnaak.app/MainActivity.java
카카오 SDK 실행 여기서 위에서 넘겼던 키 해시 값을 로그로 찍어볼 수 있습니다. CLI로 하는거보다 이게 더 편하죠?
import com.kakao.sdk.common.KakaoSdk;
import com.kakao.sdk.common.util.Utility;
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
KakaoSdk.init(this, getString(R.string.kakao_app_key)); // 추가
String keyHash = Utility.INSTANCE.getKeyHash(this);
Log.d("KakaoKeyHash", "Current Key Hash from SDK: " + keyHash);
...
}
android/app/src/main/res/values/strings.xml
카카오 디벨로퍼스에서 받은 네이티브 앱 키 추가
<?xml version='1.0' encoding='utf-8'?>
<resources>
...
<string name="kakao_app_key">{native_app_key}</string>
<string name="kakao_scheme">kakao{native_app_key}</string>
</resources>
android/app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<package android:name="com.kakao.talk" />
</queries>
<meta-data
android:name="com.kakao.sdk.AppKey"
android:value="@string/kakao_app_key" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</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="kakaolink" android:scheme="@string/kakao_scheme" />
<data android:host="oauth" android:scheme="@string/kakao_scheme" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
이렇게 길고 긴 카카오 SDK 네이티브 처리가 끝이 났습니다. 이제 프론트엔드에서 구글과 마찬가지로 분기처리로 네이티브 로그인을 구현해봅시다.
import { KakaoLoginPlugin } from "capacitor-kakao-login-plugin";
import { handleLoggedIn } from "./handleLoggedIn";
import { Capacitor } from "@capacitor/core";
// ...
const handleKakaoLogin = async () => {
try {
const result = await KakaoLoginPlugin.goLogin();
if (result) {
const user_info = await KakaoLoginPlugin.getUserInfo();
if (user_info)
handleLoggedIn(user_info.value.kakaoAccount, "kakao", setUser);
}
} catch (error) {
console.log("카카오 로그인 에러", error);
}
};
const kakaoAuth = () => {
const scopeQuery = scopeParam ? `&scope=${scopeParam}` : "";
const url = `https://kauth.kakao.com/oauth/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=code${scopeQuery}&lang=ko`;
window.location.href = url;
};
return (
<button onClick={Capacitor.isNativePlatform() ? handleKakaoLogin : kakaoAuth} >
<img src={kakao} alt="kakao login" className="absolute w-5 h-5 left-10" />
<span>카카오 로그인</span>{" "}
</button>
);
};
애플 로그인
애플 로그인은 구글과 동일한 SDK를 사용합니다. SDK 사용 외에 inAppBrowser로 처리를 해보도록 합시다.
npm install cordova-plugin-inappbrowser
npm install @awesome-cordova-plugins/in-app-browser
npx cap sync
import { InAppBrowser } from "@awesome-cordova-plugins/in-app-browser";
const Apple = () => {
useEffect(() => {
const appleLoginUrl =
"https://appleid.apple.com/auth/authorize?" +
new URLSearchParams({
client_id: import.meta.env.VITE_APPLE_CLIENT_ID,
redirect_uri: import.meta.env.VITE_APPLE_REDIRECT_URI,
response_type: "code id_token",
response_mode: "form_post",
scope: "email name",
state: "state",
}).toString();
if (Capacitor.isNativePlatform()) {
InAppBrowser.create(
appleLoginUrl,
"_blank",
"location=no,hideurlbar=yes,fullscreen=no",
);
} else {
window.location.href = appleLoginUrl;
}
}, []);
return <></>;
};
export default Apple;
애플 로그인의 경우 기존의 콜백 애플 로그인 방식에서 네이티브인 경우 인앱 브라우저로 여는 부분만 달라지기 때문에 네이티브 코드 추가는 필요 없습니다.
로그인 처리
구글/카카오 SDK 사용 시 사용자 정보가 백엔드로 가지 않습니다. (애플은 인앱 브라우저를 사용했기 때문에 기존 웹 로그인 방식과 동일하게 쿠키에 저장됩니다) 프론트엔드에만 가지고 있는 정보를 다시 백엔드에 POST 요청을 통해 보내주도록 합시다.
import { Preferences } from "@capacitor/preferences";
import { baseURL } from "../common/useAPI";
/**네이티브에서 카카오 및 구글 SDK
* 사용자 정보 받은 후, 백엔드에 POST
*/
export const handleLoggedIn = async (
result,
type: string,
setUser: (user: UserInfo | null) => void
) => {
let email: string;
let nickname: string;
let profileImg: string;
let fcmToken: string;
const { value } = await Preferences.get({ key: "fcmToken" });
if (value) {
fcmToken = value;
}
if (type == "google") {
email = result.profile.email;
nickname = result.profile.name;
profileImg = result.profile.imageUrl;
} else if (type == "kakao") {
email = result.email;
nickname = result.profile.nickname;
profileImg = result.profile.profileImageUrl;
}
try {
const response = await fetch(`${baseURL}/api/auth/loggedIn`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
nickname,
profileImg,
fcmToken,
}),
});
if (!response.ok) {
throw new Error("로그인 요청 실패");
}
const data = await response.json();
await Preferences.set({
key: "auth",
value: JSON.stringify({
access_token: data.access_token,
refresh_token: data.refresh_token,
user_info: data.user_info,
}),
});
await new Promise((resolve) => setTimeout(resolve, 200));
} catch (error) {
console.error("로그인 에러:", error);
}
};
백엔드는 쿠키에 token을 저장하는게 아니라, JSON 형태로 보내주고, 받아온 값들을 Preferences에 저장하도록 합시다.
async def loggedIn(request, db):
body = await request.json()
nickname = body.get("nickname")
email = body.get("email")
profile_image = body.get("profileImg")
device_info = request.headers.get("user-agent")
user_obj = await auth_repository.get_or_create_user(nickname, email, profile_image, db)
access_token, refresh_token, encoded_info = create_for_native(user_obj)
await auth_repository.set_user_token(refresh_token, user_obj, device_info, "user", db)
return JSONResponse({
"access_token": access_token,
"refresh_token": refresh_token,
"user_info": encoded_info
})
저장된 유저 정보를 꺼내올 때는, 마찬가지로 Preferences에서 꺼내오면 됩니다. 이후 유저 정보를 파싱하는 과정은 백엔드에서 인코딩 한 반대로 진행하시면 됩니다. 저는 간단하게 base64로 인코딩하기 때문에(유저 정보에 id값을 제외하면 개인 정보를 포함시키지 않습니다) 아래와 같이 디코딩 해줬습니다.
const getAuth = async () => {
const { value } = await Preferences.get({ key: "auth" });
if (!value) return null;
try {
return JSON.parse(value);
} catch {
return null;
}
};
export const decodeUserInfo = async (): Promise<UserInfo | null> => {
const stored = await getAuth();
if (!stored) return null;
try {
const binary = atob(stored.user_info);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
const decoded = new TextDecoder("utf-8").decode(bytes);
return JSON.parse(decoded);
} catch (e) {
console.error("user_info decode failed", e);
return null;
}
};
마지막으로, 로그아웃 하는 경우에도 웹과 네이티브를 분기처리 해줬습니다.
const nativeLogoutMutation = usePost("api/auth/native_logout");
const handleLogout = async () => {
// 애플 SDK 자체는 쿠키에 넣어서 여기서는 분기처리 안하고 둘 다 진행
const sdk = await decodeUserInfo();
if (sdk) {
await nativeLogoutMutation.mutateAsync();
await Preferences.clear();
} else {
logoutMutation.mutate();
}
navigate("/");
window.location.reload();
};
Preferences.clear()를 통해서만 처리할 수 있지만, 추후 유저의 로그인 여부로 FCM을 이용한 푸시 알림을 보낼 예정이여서 백엔드에 로그아웃 여부를 보내줍니다.
네이티브 인증 구조에 대해...
이 글에서 사용한 인증 방식은 access / refresh token을 엄격하게 분리하거나, Secure Storage를 사용하는 구조는 아닙니다. 일반적인 보안 가이드라인과는 다를 수 있지만, 현재 서비스의 성격과 운영 단계에서는 감당 가능한 선택이라고 판단했습니다. 어차피 네이티브 앱에서 인증 정보가 유출될 정도의 상황이라면, 완벽하게 방어하는 것은 현실적으로 어렵고, 이 글에서는 “털리지 않는 구조”보다는 “복잡하지 않은 구조”를 우선했습니다. 추후 결제나 계정 보호가 더 중요해지는 시점이 오면, access / refresh token 분리나 인증 구조 변경을 고려할 예정입니다.
이번 글에서는 Capacitor 커뮤니티 플러그인을 통해서 Android의 구글, 카카오, 그리고 애플 로그인을 진행해봤습니다. iOS의 경우에도 깃허브에 잘 나와있으니 따라서 진행해보시는걸 추천드립니다. (info.plist, AppDelegate.swift 몇 줄만 수정하면 됩니다.) 다음 글에서는 기존 쿠키로 사용자를 인증하는 방식에 Bearer Header를 통한 네이티브 인증 구현 방식에 대해 알아보도록 하겠습니다.
[capacitor] 인증 로직 수정하기
지난 글에서는 Capacitor 커뮤니티 플러그인을 통해서 Android의 구글, 카카오, 그리고 애플 로그인을 진행했습니다. 이번 글에서는 기존에 쿠키로 사용자를 인증하는 방식에서 Bearer Header을 통한 인
gnaaak.tistory.com
언제나처럼 ㅡ 시작은 삽질이지만, 끝은 지식입니다.
'기능 > 앱' 카테고리의 다른 글
| [capacitor] 인증 로직 수정하기 (0) | 2025.12.31 |
|---|---|
| [capacitor] Safe Area 적용하기 (0) | 2025.12.31 |
| [capacitor] 앱 패키징 시작하기 (1) | 2025.12.30 |
| [capacitor] 앱 패키징 (0) | 2025.11.11 |
