지난 백엔드 편에 이어, 이번에는 Flutter 앱 클라이언트에서 소셜 로그인을 붙이는 과정을 정리한다. 백엔드에서 아무리 잘 받아줘도, 클라이언트에서 토큰을 못 던져주면 말짱 도루묵이니까.
일단 결과물부터 보여드리면,


화면은 이런식으로 만들었고, 카톡 로그인 성공 시, 데이터 베이스에는 오른쪽과 같이 암호화된 임시 계정이 저장되었다.
왜 이 글을 쓰게 되었는지
소셜 로그인 라이브러리 kakao_flutter_sdk랑 sign_in_with_apple 가져다 쓰면 끝 아닌가? 싶었다. 그런데 막상 해보니 “카카오톡 앱이 없으면 어떡하지?”, “애플 로그인은 왜 정보가 처음에만 넘어오지?” 같은 디테일한 문제들이 튀어나왔다. 특히 카카오 로그인의 경우 ‘앱 로그인’과 ‘웹 로그인’ 분기 처리가 UX에 중요한 영향을 미치더라.
0. 시작 전 준비물 (Dependencies)
라이브러리는 아래에 항목들을 활용했다. pubspec.yaml에 이 녀석들을 모셔왔다.
dependencies:
kakao_flutter_sdk: ^1.9.0 # 카카오 로그인
sign_in_with_apple: ^6.1.0 # 애플 로그인
get: ^4.6.6 # 상태 관리 & 라우팅
1. 설정
라이브러리만 추가하면 끝날 줄 알았는데, 네이티브 설정이라는 복병이 있었다.
Android: 키 해시(Key Hash)
카카오 개발자 콘솔에 ‘키 해시’를 등록해야 하는데, 이게 디버그용이랑 릴리즈용이 다르다. 처음에 디버그 키만 등록하고 배포했다가 “로그인이 안 돼요” 소리를 들을 뻔했다. 터미널에서 명령어 쳐서 나오는 그 알 수 없는 문자열들… 꼭 Debug / Release 모두 등록해야 한다.
iOS: Capabilities 켜주기
Xcode에서 Runner 타켓 설정에 들어가 Signing & Capabilities 탭을 열어야 한다. Sign in with Apple을 추가해 주지 않으면, 시뮬레이터에서는 되는데 실기기 빌드에서 에러가 빵빵 터진다. 이런 건 에러 로그도 불친절해서 모르면 하루 종일 삽질하기 딱 좋다.
2. 카카오 로그인(Implementation)
설정을 마쳤으니 이제 코드를 볼 차례. 카카오 로그인의 핵심은 **”카카오톡이 설치된 폰에서는 앱으로 인증하고, 아니면 웹으로 띄운다”**는 흐름이다.
A. SDK 초기화 (main.dart)
일단 앱 시작할 때 네이티브 앱 키를 꽂아줘야 한다.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 카카오 SDK 초기화
KakaoSdk.init(
nativeAppKey: 'YOUR_NATIVE_APP_KEY', // 9689... 이건 내꺼니까 비밀
);
runApp(const MyApp());
}
B. 로그인 분기 처리 (login_controller.dart)
여기가 제일 공들인 부분이다. UserApi.instance.loginWithKakaoTalk()만 믿고 있다가 카톡 없는 시뮬레이터에서 에러 터지는 거 보고 식겁했다.
// 카카오 로그인 로직 (요약)
bool isInstalled = await isKakaoTalkInstalled();
if (isInstalled) {
try {
// 1. 카톡 앱으로 시도
token = await UserApi.instance.loginWithKakaoTalk();
} catch (error) {
// 사용자가 '뒤로가기'로 취소한 게 아니라면 웹으로 재시도 (방어 코드)
// CANCELED가 아닌 다른 에러라면 카톡 앱 실행에 문제가 생긴 것
if (error is PlatformException && error.code != 'CANCELED') {
token = await UserApi.instance.loginWithKakaoAccount();
}
}
} else {
// 2. 앱 없으면 바로 웹으로
token = await UserApi.instance.loginWithKakaoAccount();
}
이렇게 해두면 사용자가 어떤 환경이든 자연스럽게 로그인 창을 만날 수 있다.
C. 백엔드 전송 및 상태 관리 (GetX)
백엔드 로그인 API 호출 후, 그냥 끝내는 게 아니다. 회원가입이 덜 된 상태(약관 동의 안 함, 닉네임 미설정 등)를 처리해야 하기 때문이다.
// ... params로 백엔드 호출 후 결과(retVal) 받음
if (retVal == true) {
final userInfo = GlobalService.to.userInfo;
// 상태가 PENDING(가입 대기)이거나 필수 정보가 없으면
if (userInfo['status'] == 'PENDING' || !hasBasicInfo) {
// 프로필 설정 화면으로 납치
Get.offAllNamed(Routes.SOCIAL_PROFILE_SETUP);
} else {
// 쿨하게 메인으로 입장
Get.offAllNamed(Routes.MAIN_TAB);
}
}
여기서 GlobalService는 앱 전체에서 사용자 정보를 들고 있는 싱글톤 서비스다. 로그인 직후에 바로 MAIN으로 보내지 않고, **정보 완성도(Completeness)**를 체크해서 이동시키는 게 UX의 핵심이다.
2. 애플 로그인: 반쪽짜리 성공
애플 로그인은… 솔직히 말하면 아직 백엔드 연동은 TODO 상태다. 일단 클라이언트에서 토큰 받아오는 것까지만 구현했다.
Scopes 설정의 중요성
애플은 이메일이랑 이름을 가져오려면 요청할 때 scopes를 명시해야 한다.
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
받아온 정보 확인
로그인이 성공하면 credential 안에 온갖 정보가 들어오는데, 여기서 명심할 점 (지난 글에서도 강조했지만). 이메일과 이름은 최초 로그인 시에만 들어온다. 그래서 이걸 받자마자 서버로 보내야 하는데…
// 백엔드 연동 파라미터 (준비만 해둠)
var params = {
'appleUserId': credential.userIdentifier, // 애플 고유 ID (sub)
'identityToken': credential.identityToken, // 서버 검증용 JWT
'email': credential.email, // 첫 로그인 아니면 null 일 수 있음!
// ...
};
// TODO: 백엔드 API 구현하면 주석 풀기
// final retVal = await HomeApi.to.reqAppleLogin(params);
Get.log('[임시] 애플 로그인 성공 - 백엔드야 힘내줘');
일단 로그에는 잘 찍히니, 백엔드 로직만 완성되면 바로 붙일 수 있다.
정리 및 소감
- 카카오: SDK가 너무 잘 되어 있어서 ‘앱/웹 분기’만 신경 쓰면 스무스하다.
- 애플: UI는 심플한데 정책(Scope, 최초 1회 정보 제공)이 까다롭다.
이제 클라이언트랑 서버 기본 골격은 잡혔으니, 다음에는 이 로그인 정보를 바탕으로 사용자 프로필 설정(테니스 구력, NTRP 등)을 저장하는 흐름을 다듬어야겠다. 소셜 로그인 하나 붙이는데도 생각보다 챙길 게 많다. 역시 개발은 만만한 게 없다.