[Flutter] SNS로그인 뽀개기: 카카오, 네이버, 구글, 애플

KoEunseo·2025년 5월 5일

flutter

목록 보기
46/50

SNS 로그인은 정말 떼려야 뗄 수 없는 부분 중 하나다. 회원가입이 세상 어려웠던 어릴적과 달리 이제 회원가입 정도야 누워서 떡먹기임. 그렇지만 성가신 부분임에는 틀림이 없다. 회원가입이 어렵지는 않지만 귀찮아서... 하려다 말고 취소하는 일도 나는 빈번하다. 그래서 "우리 서비스"에 대한 "접근성"을 높이기 위한 방편으로 SNS 로그인은 그냥 필수 기능이 되었다.

나는 회사에서 앱을 두개를 만들었고 SNS 로그인도 당연히 구현했다. 우리 앱에서는 이메일 로그인 외에 카카오 로그인, 네이버 로그인, 구글 로그인, 애플 로그인을 제공한다.

각 플랫폼에서 제공하는 문서를 참고하고, pub dev를 참고하고, 기술블로그들을 참고해서 개발을 했다. 한번 작업하면 다시 들여다볼 일이 없을 거 같아서 그냥 넘어갔었는데, 급하게 앱을 하나 더 만들게 되면서 그냥 하나 정리를 해놔야겠다는 생각을 했다.

아, 참고로 처음에는 웹뷰를 경유해서 로그인하도록 했으나 SDK를 사용하는 방식으로 수정했다. SDK 로그인을 성공한 후에 로그인 시 백에 토큰(혹은 id token)을 전달하도록 했다.


사용한 SDK 버전 및 패키지


1. 각 플랫폼 페이지에서 준비할 것들

1.1 카카오 로그인 설정

카카오 개발자 콘솔에서 설정합니다.

1.1.1 기본 설정

  1. 애플리케이션 생성

    • 카카오 개발자 콘솔에 접속
    • 내 애플리케이션 > 애플리케이션 추가하기
    • 앱 이름, 사업자명 입력
  2. 카카오 로그인 활성화

    • 제품 설정 > 카카오 로그인 > 활성화 설정: ON
  3. Redirect URI 추가

    • 제품 설정 > 카카오 로그인 > Redirect URI
    • kakao{YOUR_NATIVE_APP_KEY}://oauth 형식으로 추가

1.1.2 플랫폼 추가

안드로이드 플랫폼 추가:

  • 플랫폼 > 플랫폼 추가 > Android
  • 패키지명 입력 (예: kr.co.{회사명})
  • 키해시 입력
    • Tip! 키해시는 앱 실행해서 콘솔에 찍어보는 게 가장 정확하고 빠르다.
    • Debug용과 Release용을 각각 등록해야 함

iOS 플랫폼 추가:

  • 플랫폼 > 플랫폼 추가 > iOS
  • 번들 ID 입력 (예: kr.co.{회사명})

1.2 네이버 로그인 설정

네이버 개발자 센터에서 설정합니다.

  1. 애플리케이션 생성

    • 네이버 개발자 센터 접속
    • Application > 애플리케이션 등록
  2. 기본 정보 입력

    • 어플 이름
    • 카테고리 선택
    • 사용 API: 로그인 오픈 API 서비스 선택
    • 제공 정보 선택 (이메일, 이름, 프로필 사진 등)
  3. 로그인 오픈 API 서비스 환경 설정

    안드로이드 추가:

    • 다운로드 URL 입력
    • 앱 패키지 네임 입력 (예: kr.co.{회사명})

    iOS 추가:

    • 다운로드 URL 입력
    • URL Scheme 입력 (예: naverlogin)
  4. 콜백 URL 작성

    • 예: naverlogin://oauth

1.3 구글 로그인 설정

구글 로그인은 Google Cloud Platform과 Firebase Console 두 곳에서 설정해야 합니다.

1.3.1 Google Cloud Platform 설정

  1. 프로젝트 생성

  2. 사용자 인증 정보 설정

    • API 및 서비스 > 사용자 인증 정보
    • API 키 생성 (iOS용, Android용 각각 생성)
  3. OAuth 2.0 클라이언트 ID 생성

    • 사용자 인증 정보 > OAuth 2.0 클라이언트 ID 만들기

    iOS 클라이언트 ID:

    • 애플리케이션 유형: iOS
    • 번들 ID 입력

    Android 클라이언트 ID:

    • 애플리케이션 유형: Android
    • 패키지 이름 입력
    • SHA-1 인증서 지문 입력
    • Tip! Debug용과 Release용을 각각 생성하는 것을 추천함
    • SHA-1을 등록할 때 디버깅용, 릴리즈용을 각각 기입한다.

1.3.2 Firebase Console 설정

  1. 프로젝트 생성

    • Firebase Console 접속
    • 프로젝트 생성 (Google Cloud Platform 프로젝트와 연결)
  2. Android 앱 추가

    • 프로젝트 설정 > 앱 추가 > Android
    • 패키지 이름 입력
    • 디지털 지문 추가 (Debug용, Release용 둘 다)
    • google-services.json 다운로드 후 android/app/ 폴더에 추가
  3. iOS 앱 추가

    • 프로젝트 설정 > 앱 추가 > iOS
    • 번들 ID 입력
    • 앱스토어 ID, 팀 ID 추가
    • GoogleService-Info.plist 다운로드 후 ios/Runner/ 폴더에 추가

1.4 애플 로그인 설정

Apple Developer에서 설정합니다.

  1. Certificate - 인증서 생성

    • Certificates, Identifiers & Profiles > Certificates
    • + 버튼 클릭
    • iOS Distribution 타입 생성
    • Distribution Managed 타입 생성
  2. Identifiers - 번들 ID 생성

    • Certificates, Identifiers & Profiles > Identifiers
    • + 버튼 클릭
    • App IDs 선택
    • 번들 ID 입력
    • Sign In with Apple 체크
    • Enable as a primary App ID 체크
  3. Profiles - 프로비저닝 프로파일 생성

    • Certificates, Identifiers & Profiles > Profiles
    • + 버튼 클릭
    • Development 프로비저닝 프로파일 생성
    • App Store 프로비저닝 프로파일 생성
  4. Keys - 키 생성

    • Certificates, Identifiers & Profiles > Keys
    • + 버튼 클릭
    • Key Name 입력
    • Sign In with Apple 체크
    • Configure 클릭하여 App ID 선택
    • 키 다운로드 (한 번만 다운로드 가능하므로 주의!)

2. Native Code 설정

2.1 Android 설정

android/app/src/main/AndroidManifest.xml에 다음 내용을 추가합니다.

<!-- 카카오 로그인 -->
<activity 
    android:name="com.kakao.sdk.flutter.AuthCodeCustomTabsActivity"
    android:exported="true">
    <intent-filter android:label="flutter_web_auth">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- "kakao{YOUR_NATIVE_APP_KEY}://oauth" 형식의 앱 실행 스킴 설정 -->
        <data android:scheme="kakao{YOUR_NATIVE_APP_KEY}" android:host="oauth"/>
    </intent-filter>
</activity>

<!-- 애플 로그인 -->
<activity
    android:name="com.aboutyou.dart_packages.sign_in_with_apple.SignInWithAppleCallback"
    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:scheme="signinwithapple" />
        <data android:path="callback" />
    </intent-filter>
</activity>

2.2 iOS 설정

ios/Runner/Info.plist에 다음 내용을 추가합니다.

<!-- URL 스킴 설정 -->
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>{앱이름}</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>{앱이름}</string>
            <string>com.googleusercontent.apps.{구글제공}</string>
            <string>naverlogin</string>
        </array>
    </dict>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>kakao{YOUR_NATIVE_APP_KEY}</string>
        </array>
    </dict>
</array>

<!-- 네이버 로그인 설정 -->
<key>naverConsumerKey</key>
<string>{네이버키}</string>
<key>naverConsumerSecret</key>
<string>{네이버시크릿}</string>
<key>naverServiceAppName</key>
<string>{앱이름}</string>
<key>naverServiceAppUrlScheme</key>
<string>naverlogin</string>

<!-- 앱 쿼리 스킴 (다른 앱 호출을 위한 설정) -->
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>naversearchapp</string>
    <string>naversearchthirdlogin</string>
    <string>kakaokompassauth</string>
</array>

3. Flutter Code 구현

3.1 카카오 로그인 서비스

import 'package:kakao_flutter_sdk_user/kakao_flutter_sdk_user.dart';

class KakaoLoginService {
  void init(String nativeAppKey) {
    KakaoSdk.init(nativeAppKey: nativeAppKey);
  }
  
  Future<String> login() async {
    OAuthToken token;
    
    try {
      // 카카오톡 앱 설치 여부 확인
      final isInstalled = await isKakaoTalkInstalled();
      
      if (isInstalled) {
        // 카카오톡 앱으로 로그인
        token = await UserApi.instance.loginWithKakaoTalk();
      } else {
        // 웹으로 로그인
        token = await UserApi.instance.loginWithKakaoAccount();
      }
      
      return token.accessToken;
    } on KakaoException catch (e) {
      debugPrint('카카오 로그인 실패: ${e.toString()}');
      rethrow;
    }
  }
}

3.2 네이버 로그인 서비스

import 'package:flutter_naver_login/flutter_naver_login.dart';

class NaverLoginService {
  Future<String> login() async {
    try {
      final result = await FlutterNaverLogin.logIn();
      
      if (result.status == NaverLoginStatus.loggedIn) {
        return result.accessToken.accessToken;
      }
      
      throw Exception('네이버 로그인 실패: ${result.status}');
    } catch (e) {
      debugPrint('네이버 로그인 실패: $e');
      rethrow;
    }
  }
}

3.3 구글 로그인 서비스

import 'package:google_sign_in/google_sign_in.dart';

class GoogleLoginService {
  final GoogleSignIn _googleSignIn = GoogleSignIn(
    scopes: ['email', 'profile'],
  );

  Future<String> signIn() async {
    try {
      final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
      
      if (googleUser == null) {
        throw Exception('구글 로그인 취소');
      }
      
      final GoogleSignInAuthentication? googleAuth = 
          await googleUser.authentication;
      
      if (googleAuth?.idToken == null) {
        throw Exception('구글 ID 토큰을 가져올 수 없습니다');
      }
      
      return googleAuth!.idToken!;
    } catch (e) {
      debugPrint('구글 로그인 실패: $e');
      rethrow;
    }
  }
}

3.4 애플 로그인 서비스

import 'package:sign_in_with_apple/sign_in_with_apple.dart';

class AppleLoginService {
  Future<String> login() async {
    try {
      final credential = await SignInWithApple.getAppleIDCredential(
        scopes: [
          AppleIDAuthorizationScopes.email,
          AppleIDAuthorizationScopes.fullName,
        ],
      );
      
      if (credential.identityToken == null) {
        throw Exception('애플 ID 토큰을 가져올 수 없습니다');
      }
      
      return credential.identityToken!;
    } on SignInWithAppleAuthorizationException catch (e) {
      debugPrint('애플 로그인 실패: ${e.code} - ${e.message}');
      rethrow;
    } catch (e) {
      debugPrint('애플 로그인 실패: $e');
      rethrow;
    }
  }
}

3.5 Main.dart에서 초기화

카카오 같은 경우 runApp 하기 전에 초기화가 필요합니다.

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 카카오 SDK 초기화
  KakaoSdk.init(nativeAppKey: 'YOUR_NATIVE_APP_KEY');
  
  runApp(MyApp());
}

4. SNS 로그인 Provider 구현

Enum을 만들어서 버튼에 따라 주입받도록 했고, 그럼 해당 값에 따라 switch문을 활용해 필요한 작업을 하도록 만들었다. snsLoginSDK는 SDK에서 뱉는 토큰을 얻기 위함이고, 이 토큰은 우리 서버로 enum값과 함께 토큰을 전달한다.

4.1 LoginType Enum 정의

enum LoginType {
  kakao,
  naver,
  google,
  apple,
  email,
}

4.2 SNS 로그인 Provider


class Login extends _$Login {
  
  LoginState build() => LoginState.init();

  Future<dynamic> snsLogin(LoginType sns) async {
    try {
      // 1. SDK를 통해 토큰 획득
      final token = await snsLoginSDK(sns: sns);
      
      if (token == null) {
        throw Exception('토큰을 가져올 수 없습니다');
      }
      
      // 2. 서버에 토큰 전달하여 로그인
      return await snsLoginApi(sns: sns, token: token);
    } catch (e, stackTrace) {
      await ref.read(crashlyticsServiceProvider).recordError(
        e,
        stackTrace,
        reason: 'SNS 로그인 실패',
        information: ['로그인 타입: $sns'],
      );
      rethrow;
    }
  }
  
  /// SDK를 통해 각 플랫폼의 토큰 획득
  Future<String?> snsLoginSDK({
    required LoginType sns,
  }) async {
    switch (sns) {
      case LoginType.kakao:
        return await ref.read(kakaoLoginServiceProvider).login();
      case LoginType.naver:
        return await ref.read(naverLoginServiceProvider).login();
      case LoginType.google:
        return await ref.read(googleLoginServiceProvider).signIn();
      case LoginType.apple:
        return await ref.read(appleLoginServiceProvider).login();
      default:
        return null;
    }
  }
  
  /// 서버에 토큰 전달하여 로그인
  Future<dynamic> snsLoginApi({
    required LoginType sns,
    required String token,
  }) async {
    try {
      final url = AuthApi.snsLoginApi(sns);

      // 구글은 id_token, 나머지는 access_token 사용
      final data = sns == LoginType.google
          ? {"id_token": token}
          : {"access_token": token};

      final res = await _dio.post(
        url,
        data: data,
      );

      if (res.statusCode != 200) {
        throw Exception('서버 로그인 실패: ${res.statusCode}');
      }

      return res.data['data']['tokens'];
    } on DioException catch (e) {
      debugPrint('SNS 로그인 API 실패: ${e.response?.data}');
      throw Exception(e.response?.data['error']['message'] ?? '로그인 실패');
    } catch (e, stackTrace) {
      await ref.read(crashlyticsServiceProvider).recordError(
        e,
        stackTrace,
        reason: 'SNS 로그인 API 실패',
        information: [
          '로그인 타입: $sns',
          '토큰: ${token.substring(0, 20)}...',
        ],
      );
      rethrow;
    }
  }
}

4.3 사용 예시

// UI에서 사용
ElevatedButton(
  onPressed: () async {
    try {
      final loginNotifier = ref.read(loginProvider.notifier);
      final tokens = await loginNotifier.snsLogin(LoginType.kakao);
      
      // 토큰 저장 및 로그인 처리
      await _saveTokens(tokens);
      context.go('/home');
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('로그인 실패: $e')),
      );
    }
  },
  child: Text('카카오 로그인'),
)

5. 주의사항 및 트러블슈팅

5.1 ProGuard 설정 (Android)

테스트 버전으로는 잘 되다가 릴리즈 버전으로 빌드했을 때 갑자기 로그인이 안될 수 있다. 특히 안드로이드에서! 암/복호화가 되고 하면서 문제가 생기는 것 같다.

이때는 android/app/proguard-rules.pro에서 해결을 위한 코드를 추가하면 된다.

# SNS 로그인 관련 클래스 유지
-keep class com.kakao.sdk.**.model.* { <fields>; }
-keep public class com.nhn.android.naverlogin.** { public protected *; }
-keep public class com.navercorp.nid.** { public *; }
-keep class com.google.googlesignin.** { <fields>; }
-keep class * extends com.google.gson.TypeAdapter

# Retrofit 관련 (사용하지 않더라도 에러 발생 가능)
-keepattributes Signature
-keepattributes Exceptions
-keep class retrofit2.** { *; }
-keep class okhttp3.** { *; }
-keep class okio.** { *; }

5.2 키해시 확인 방법

카카오 로그인 설정 시 키해시가 필요한데, 앱 실행해서 콘솔에 찍어보는 게 가장 정확하고 빠르다.

// main.dart에서 키해시 출력
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  if (Platform.isAndroid) {
    // Android 키해시 출력
    final keyHash = await _getKeyHash();
    debugPrint('Key Hash: $keyHash');
  }
  
  runApp(MyApp());
}

Future<String> _getKeyHash() async {
  // 키해시 가져오는 로직
  // 실제 구현은 플랫폼별로 다름
}

5.3 iOS Capabilities 설정

애플 로그인을 사용하려면 Xcode에서 Capabilities를 설정해야 합니다.

  1. Xcode에서 프로젝트 열기
  2. Runner 타겟 선택
  3. Signing & Capabilities 탭
  4. + Capability 클릭
  5. "Sign In with Apple" 추가

5.4 네이버 로그인 콜백 URL

네이버 로그인 설정 시 콜백 URL을 정확히 입력해야 합니다. Info.plist에 설정한 URL Scheme과 일치해야 합니다.

예: naverlogin://oauth

5.5 구글 로그인 SHA-1 인증서 지문

구글 로그인 설정 시 Debug용과 Release용 SHA-1을 각각 등록해야 합니다.

Debug용 SHA-1 확인:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

Release용 SHA-1 확인:

keytool -list -v -keystore {your-release-keystore} -alias {your-alias}

5.6 애플 로그인 테스트

애플 로그인은 실제 기기에서만 테스트할 수 있습니다. 시뮬레이터에서는 작동하지 않습니다.


6. 실제로 겪은 문제들

문제 1: 릴리즈 빌드에서 카카오 로그인 실패

증상: Debug 빌드에서는 잘 되는데 Release 빌드에서 카카오 로그인이 실패

원인: ProGuard가 카카오 SDK 클래스를 난독화하면서 문제 발생

해결: proguard-rules.pro에 카카오 SDK 관련 클래스 유지 규칙 추가

문제 2: 네이버 로그인 콜백이 작동하지 않음

증상: 네이버 로그인 후 앱으로 돌아오지 않음

원인: Info.plist의 URL Scheme과 네이버 개발자 센터의 콜백 URL이 일치하지 않음

해결: 두 곳의 URL Scheme을 정확히 일치시킴

문제 3: 구글 로그인 시 "DEVELOPER_ERROR" 발생

증상: 구글 로그인 시 DEVELOPER_ERROR 발생

원인: SHA-1 인증서 지문이 Google Cloud Console에 등록되지 않았거나 잘못 등록됨

해결: Debug용과 Release용 SHA-1을 각각 확인하고 등록

문제 4: 애플 로그인이 시뮬레이터에서 작동하지 않음

증상: 시뮬레이터에서 애플 로그인 버튼을 눌러도 반응이 없음

원인: 애플 로그인은 실제 기기에서만 작동함

해결: 실제 iOS 기기에서 테스트


7. 마무리하며

SNS 로그인을 구현하면서 가장 많이 느낀 점은 각 플랫폼별 설정의 복잡성이었습니다. 특히 처음 설정할 때는 여러 곳에서 설정해야 해서 헷갈릴 수 있지만, 한 번 설정해두면 다시 들여다볼 일이 거의 없습니다.

특히:

  • ✅ 각 플랫폼 개발자 콘솔에서 정확한 설정 필요
  • ✅ Native 코드 설정 (AndroidManifest.xml, Info.plist)
  • ✅ ProGuard 설정으로 릴리즈 빌드 문제 해결
  • ✅ Debug용과 Release용 설정을 각각 관리
  • ✅ 실제 기기에서 테스트 (특히 애플 로그인)

다음에는 소셜 로그인 연동 해제 기능도 추가해볼 예정입니다.


참고 자료

profile
주니어 플러터 개발자의 고군분투기

0개의 댓글