소셜 로그인
모바일 앱에서 사용자 인증은 핵심 기능 중 하나입니다. 소셜 로그인은 사용자가 기존 소셜 미디어 계정을 사용하여 앱에 로그인할 수 있게 해주므로 편의성을 높이고 가입 과정을 간소화합니다. 이 문서에서는 Flutter 앱에서 주요 소셜 로그인(카카오, 네이버, 애플)을 구현하는 방법을 다룹니다.
소셜 로그인 공통 설정
Section titled “소셜 로그인 공통 설정”각 소셜 로그인을 구현하기 전에 공통적으로 필요한 설정을 살펴보겠습니다.
1. 플랫폼별 환경 설정
Section titled “1. 플랫폼별 환경 설정”Android 설정
Section titled “Android 설정”android/app/src/main/AndroidManifest.xml
파일에 인터넷 권한을 추가합니다:
<manifest ...> <uses-permission android:name="android.permission.INTERNET"/> <!-- 기타 권한 --></manifest>
iOS 설정
Section titled “iOS 설정”ios/Runner/Info.plist
파일에 URL 스킴 처리를 위한 설정을 추가합니다:
<key>CFBundleURLTypes</key><array> <!-- 각 소셜 로그인별 URL 스킴 설정이 여기에 추가됩니다 --></array>
2. 공통 로그인 시스템 구현
Section titled “2. 공통 로그인 시스템 구현”소셜 로그인을 통합적으로 관리하기 위한 인터페이스와 모델을 정의합니다:
// 소셜 로그인 결과 모델@freezedclass SocialLoginResult with _$SocialLoginResult { const factory SocialLoginResult.success({ required String accessToken, required String provider, String? email, String? name, String? profileImage, }) = _SocialLoginResultSuccess;
const factory SocialLoginResult.error({ required String message, required String provider, }) = _SocialLoginResultError;
const factory SocialLoginResult.cancelled({ required String provider, }) = _SocialLoginResultCancelled;}
// 소셜 로그인 인터페이스abstract class SocialLoginProvider { Future<SocialLoginResult> login(); Future<void> logout();}
카카오 로그인 구현
Section titled “카카오 로그인 구현”카카오 로그인을 구현하려면 먼저 Kakao Developers에서 애플리케이션을 등록해야 합니다.
1. 패키지 설치
Section titled “1. 패키지 설치”pubspec.yaml
파일에 카카오 로그인 패키지를 추가합니다:
dependencies: kakao_flutter_sdk_user: ^1.9.7+3
2. 플랫폼별 설정
Section titled “2. 플랫폼별 설정”Android 설정
Section titled “Android 설정”android/app/src/main/AndroidManifest.xml
파일에 카카오 관련 설정 추가:
<manifest ...> <application ...> <activity ...> <!-- ... -->
<!-- 카카오 로그인 커스텀 URL 스킴 설정 --> <intent-filter> <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}" 형식의 스킴 설정 --> <data android:scheme="kakao${YOUR_NATIVE_APP_KEY}" android:host="oauth"/> </intent-filter> </activity> </application></manifest>
android/app/build.gradle
파일의defaultConfig
섹션에 매니페스트 플레이스홀더 추가:
defaultConfig { // ... manifestPlaceholders += [ 'kakaoNativeAppKey': '${YOUR_NATIVE_APP_KEY}' ]}
iOS 설정
Section titled “iOS 설정”ios/Runner/Info.plist
파일에 카카오 설정 추가:
<key>CFBundleURLTypes</key><array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <!-- "kakao${YOUR_NATIVE_APP_KEY}" 형식의 스킴 설정 --> <string>kakao${YOUR_NATIVE_APP_KEY}</string> </array> </dict></array>
<key>LSApplicationQueriesSchemes</key><array> <string>kakaokompassauth</string> <string>kakaolink</string></array>
3. 카카오 로그인 구현
Section titled “3. 카카오 로그인 구현”import 'package:kakao_flutter_sdk_user/kakao_flutter_sdk_user.dart';
class KakaoLoginProvider implements SocialLoginProvider { @override Future<SocialLoginResult> login() async { try { // 카카오톡 설치 여부 확인 if (await isKakaoTalkInstalled()) { try { // 카카오톡으로 로그인 await UserApi.instance.loginWithKakaoTalk(); } catch (error) { // 사용자가 카카오톡 로그인을 취소한 경우 카카오계정으로 로그인 시도 if (error is PlatformException && error.code == 'CANCELED') { return const SocialLoginResult.cancelled(provider: 'kakao'); }
// 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인 await UserApi.instance.loginWithKakaoAccount(); } } else { // 카카오톡이 설치되어 있지 않은 경우, 카카오계정으로 로그인 await UserApi.instance.loginWithKakaoAccount(); }
// 사용자 정보 요청 User user = await UserApi.instance.me();
// 액세스 토큰 가져오기 OAuthToken token = await TokenManagerProvider.instance.manager.getToken();
return SocialLoginResult.success( accessToken: token.accessToken, provider: 'kakao', email: user.kakaoAccount?.email, name: user.kakaoAccount?.profile?.nickname, profileImage: user.kakaoAccount?.profile?.profileImageUrl, ); } catch (error) { return SocialLoginResult.error( message: error.toString(), provider: 'kakao', ); } }
@override Future<void> logout() async { await UserApi.instance.logout(); }}
4. 앱 초기화 설정
Section titled “4. 앱 초기화 설정”앱 시작 시 카카오 SDK를 초기화합니다:
void main() { // 카카오 SDK 초기화 KakaoSdk.init( nativeAppKey: '${YOUR_NATIVE_APP_KEY}', javaScriptAppKey: '${YOUR_JAVASCRIPT_APP_KEY}', // 웹 환경에서 필요한 경우 );
runApp(MyApp());}
네이버 로그인 구현
Section titled “네이버 로그인 구현”네이버 로그인을 구현하려면 먼저 네이버 개발자 센터에서 애플리케이션을 등록해야 합니다.
1. 패키지 설치
Section titled “1. 패키지 설치”pubspec.yaml
파일에 네이버 로그인 패키지를 추가합니다:
dependencies: naver_login_sdk: ^3.0.0
준비중입니다.
애플 로그인 구현
Section titled “애플 로그인 구현”애플 로그인은 iOS 13 이상에서 지원되며, iOS 앱에서는 소셜 로그인 옵션으로 애플 로그인을 제공해야 합니다.
1. 패키지 설치
Section titled “1. 패키지 설치”pubspec.yaml
파일에 애플 로그인 패키지를 추가합니다:
dependencies: sign_in_with_apple: ^5.0.0 crypto: ^3.0.3
2. 플랫폼별 설정
Section titled “2. 플랫폼별 설정”iOS 설정
Section titled “iOS 설정”-
Xcode에서
Runner
프로젝트를 열고Signing & Capabilities
탭에서+ Capability
버튼을 클릭하여Sign in with Apple
기능을 추가합니다. -
ios/Runner/Info.plist
파일에 관련 설정 추가:
<key>CFBundleURLTypes</key><array> <!-- 기존 URL 스킴 설정 --></array>
Android 설정
Section titled “Android 설정”Android에서 애플 로그인을 지원하려면 웹 기반 인증 흐름을 사용해야 합니다:
android/app/src/main/AndroidManifest.xml
파일에 인텐트 필터 추가:
<manifest ...> <application ...> <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="signinwithapple" android:path="callback" /> </intent-filter> </activity> </application></manifest>
- 웹 서비스에 Apple 개발자 계정에서 서비스 ID와 키를 설정해야 합니다.
3. 애플 로그인 구현
Section titled “3. 애플 로그인 구현”import 'dart:convert';import 'dart:math';
import 'package:crypto/crypto.dart';import 'package:sign_in_with_apple/sign_in_with_apple.dart';
class AppleLoginProvider implements SocialLoginProvider { /// Generates a cryptographically secure random nonce, to be included in a /// credential request. String _generateNonce([int length = 32]) { const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._'; final random = Random.secure(); return List.generate(length, (_) => charset[random.nextInt(charset.length)]).join(); }
/// Returns the sha256 hash of [input] in hex notation. String _sha256ofString(String input) { final bytes = utf8.encode(input); final digest = sha256.convert(bytes); return digest.toString(); }
@override Future<SocialLoginResult> login() async { try { // 보안을 위한 nonce 생성 final rawNonce = _generateNonce(); final nonce = _sha256ofString(rawNonce);
// 애플 로그인 요청 final credential = await SignInWithApple.getAppleIDCredential( scopes: [ AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName, ], nonce: nonce, webAuthenticationOptions: WebAuthenticationOptions( clientId: 'your.app.bundle.id.service', redirectUri: Uri.parse( 'https://your-redirect-uri.example.com/callbacks/sign_in_with_apple', ), ), );
// 사용자 이름 생성 (애플은 첫 로그인 후에만 이름 정보 제공) final name = [ credential.givenName, credential.familyName, ].where((name) => name != null && name.isNotEmpty).join(' ');
return SocialLoginResult.success( accessToken: credential.identityToken ?? '', provider: 'apple', email: credential.email, name: name.isNotEmpty ? name : null, profileImage: null, // 애플은 프로필 이미지를 제공하지 않음 ); } catch (error) { if (error is SignInWithAppleAuthorizationException) { if (error.code == AuthorizationErrorCode.canceled) { return const SocialLoginResult.cancelled(provider: 'apple'); } }
return SocialLoginResult.error( message: error.toString(), provider: 'apple', ); } }
@override Future<void> logout() async { // 애플은 클라이언트 측에서 직접 로그아웃 기능을 제공하지 않음 // 필요한 경우 서버 측에서 토큰 무효화 처리 }}
소셜 로그인 통합 관리
Section titled “소셜 로그인 통합 관리”여러 소셜 로그인 방식을 통합적으로 관리하기 위한 서비스를 구현합니다:
class SocialLoginService { final KakaoLoginProvider _kakaoLoginProvider = KakaoLoginProvider(); final NaverLoginProvider _naverLoginProvider = NaverLoginProvider(); final AppleLoginProvider _appleLoginProvider = AppleLoginProvider();
Future<SocialLoginResult> loginWithKakao() async { return await _kakaoLoginProvider.login(); }
Future<SocialLoginResult> loginWithNaver() async { return await _naverLoginProvider.login(); }
Future<SocialLoginResult> loginWithApple() async { return await _appleLoginProvider.login(); }
Future<void> logoutFrom(String provider) async { switch (provider.toLowerCase()) { case 'kakao': await _kakaoLoginProvider.logout(); break; case 'naver': await _naverLoginProvider.logout(); break; case 'apple': // 애플 로그아웃은 서버에서 처리 break; } }
// 모든 소셜 계정 로그아웃 Future<void> logoutAll() async { await _kakaoLoginProvider.logout(); await _naverLoginProvider.logout(); // 애플 로그아웃은 서버에서 처리 }}
Riverpod을 활용한 인증 상태 관리
Section titled “Riverpod을 활용한 인증 상태 관리”Riverpod을 사용하여 소셜 로그인 상태를 관리하는 예제입니다:
// 사용자 상태 모델@freezedclass AuthState with _$AuthState { const factory AuthState.initial() = _Initial; const factory AuthState.loading() = _Loading; const factory AuthState.authenticated({ required String accessToken, required String provider, String? email, String? name, String? profileImage, }) = _Authenticated; const factory AuthState.error(String message) = _Error;}
// 인증 제공자@riverpodclass Auth extends _$Auth { final SocialLoginService _socialLoginService = SocialLoginService();
@override AuthState build() { return const AuthState.initial(); }
Future<void> loginWithKakao() async { state = const AuthState.loading();
final result = await _socialLoginService.loginWithKakao();
state = result.when( success: (accessToken, provider, email, name, profileImage) { return AuthState.authenticated( accessToken: accessToken, provider: provider, email: email, name: name, profileImage: profileImage, ); }, error: (message, provider) { return AuthState.error(message); }, cancelled: (provider) { return const AuthState.initial(); }, ); }
Future<void> loginWithNaver() async { state = const AuthState.loading();
final result = await _socialLoginService.loginWithNaver();
state = result.when( success: (accessToken, provider, email, name, profileImage) { return AuthState.authenticated( accessToken: accessToken, provider: provider, email: email, name: name, profileImage: profileImage, ); }, error: (message, provider) { return AuthState.error(message); }, cancelled: (provider) { return const AuthState.initial(); }, ); }
Future<void> loginWithApple() async { state = const AuthState.loading();
final result = await _socialLoginService.loginWithApple();
state = result.when( success: (accessToken, provider, email, name, profileImage) { return AuthState.authenticated( accessToken: accessToken, provider: provider, email: email, name: name, profileImage: profileImage, ); }, error: (message, provider) { return AuthState.error(message); }, cancelled: (provider) { return const AuthState.initial(); }, ); }
Future<void> logout() async { if (state is _Authenticated) { final provider = (state as _Authenticated).provider; await _socialLoginService.logoutFrom(provider); }
state = const AuthState.initial(); }}
UI 구현 예제
Section titled “UI 구현 예제”소셜 로그인 버튼을 포함한 로그인 화면 예제입니다:
class LoginScreen extends ConsumerWidget { const LoginScreen({super.key});
@override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(authProvider);
return Scaffold( appBar: AppBar(title: const Text('로그인')), body: Center( child: authState.when( initial: () => _buildLoginButtons(ref), loading: () => const CircularProgressIndicator(), authenticated: (token, provider, email, name, profileImage) { return _buildUserInfo(ref, name, email, profileImage); }, error: (message) => Column( mainAxisSize: MainAxisSize.min, children: [ Text('오류: $message', style: const TextStyle(color: Colors.red)), const SizedBox(height: 16), _buildLoginButtons(ref), ], ), ), ), ); }
Widget _buildLoginButtons(WidgetRef ref) { return Column( mainAxisSize: MainAxisSize.min, children: [ // 카카오 로그인 버튼 ElevatedButton( onPressed: () => ref.read(authProvider.notifier).loginWithKakao(), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFEE500), foregroundColor: Colors.black87, minimumSize: const Size(250, 50), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.chat_bubble, color: Colors.black87), SizedBox(width: 8), Text('카카오 로그인'), ], ), ),
const SizedBox(height: 16),
// 네이버 로그인 버튼 ElevatedButton( onPressed: () => ref.read(authProvider.notifier).loginWithNaver(), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF03C75A), foregroundColor: Colors.white, minimumSize: const Size(250, 50), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Text('N', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)), SizedBox(width: 8), Text('네이버 로그인'), ], ), ),
const SizedBox(height: 16),
// 애플 로그인 버튼 if (Platform.isIOS) ElevatedButton( onPressed: () => ref.read(authProvider.notifier).loginWithApple(), style: ElevatedButton.styleFrom( backgroundColor: Colors.black, foregroundColor: Colors.white, minimumSize: const Size(250, 50), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.apple), SizedBox(width: 8), Text('Apple로 로그인'), ], ), ), ], ); }
Widget _buildUserInfo(WidgetRef ref, String? name, String? email, String? profileImage) { return Column( mainAxisSize: MainAxisSize.min, children: [ if (profileImage != null) CircleAvatar( radius: 50, backgroundImage: NetworkImage(profileImage), ), const SizedBox(height: 16), Text('이름: ${name ?? '정보 없음'}', style: const TextStyle(fontSize: 18)), const SizedBox(height: 8), Text('이메일: ${email ?? '정보 없음'}', style: const TextStyle(fontSize: 16)), const SizedBox(height: 24), ElevatedButton( onPressed: () => ref.read(authProvider.notifier).logout(), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('로그아웃'), ), ], ); }}
보안 고려사항
Section titled “보안 고려사항”소셜 로그인 구현 시 고려해야 할 보안 측면:
-
토큰 관리
- 액세스 토큰은 안전하게 저장해야 합니다(flutter_secure_storage 사용 권장).
- 앱 내에서 토큰 유효성 검사 메커니즘 구현.
-
백엔드 인증
- 소셜 로그인 토큰을 백엔드로 전송하여 검증 후 자체 JWT 토큰 발급.
- 서버 측에서 OAuth 토큰 갱신 및 관리.
-
개인정보 처리
- 사용자 정보는 필요한 최소한으로 요청.
- 개인정보 처리방침에 소셜 로그인을 통해 수집되는 정보 명시.
-
사용자 식별
- 동일 사용자가 다른 소셜 계정으로 로그인할 경우 처리 방안 마련.
- 이메일 주소를 기준으로 계정 연동 기능 구현 고려.
Flutter 앱에서 카카오, 네이버, 애플 소셜 로그인을 구현하는 방법을 살펴보았습니다. 각 플랫폼마다 설정 방법이 다르므로 공식 문서를 참조하여 최신 정보를 확인하는 것이 중요합니다. 소셜 로그인은 사용자 경험을 개선하고 가입 과정을 간소화하는 데 큰 도움이 되지만, 보안과 개인정보 보호에도 각별한 주의가 필요합니다.
각 소셜 로그인 패키지는 지속적으로 업데이트되므로, 항상 최신 버전의 패키지와 공식 문서를 확인하십시오.