Flutter 소셜 로그인 (카카오/애플) 개발기

지난 백엔드 편에 이어, 이번에는 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 등)을 저장하는 흐름을 다듬어야겠다. 소셜 로그인 하나 붙이는데도 생각보다 챙길 게 많다. 역시 개발은 만만한 게 없다.

댓글 남기기