2025. 4. 7. 18:07ㆍProject Log/학부 졸업프로젝트
Kakao OAuth 로그인 구현
Android/iOS 호환되는 Flutter 앱에서 '카카오 로그인' 기능을 구현하였다. 전체 코드를 다 넣기보다는 주요 코드 위주로 구현 내용을 정리하려고 한다. (어르신 목록 페이지에 출력된 사용자 및 토큰 정보는 플러터 저장소의 정상 작동 여부를 테스트하는 용도이다.)
Kakao Developer 앱 등록
Kakao Developer 사이트(https://developers.kakao.com)에 들어가서 애플리케이션을 등록한다.
[카카오 로그인] 탭 > 카카오 로그인 활성화 설정 2개를 ON 한다.
[동의항목] 탭 > 수집하려는 정보 동의 설정을 할 수 있다. 일부 정보는 바로 동의로 전환할 수 있고, 나머지는 사업자 등록을 해야 가능하다.
[앱 설정] - [플랫폼] 탭 > Android 플랫폼 수정, iOS 플랫폼 수정을 진행한다.
android/app/build.gradle 파일의 defaultConfig의 applicationId 항목에 있는 값을 패키지명에 등록한다.
프로젝트의 루트 경로에서 아래 커맨드를 실행하여 디버그 키 해시와 릴리즈 키 해시를 얻는다.
// 디버그용
keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore -storepass android -keypass android | openssl sha1 -binary | openssl base64
// 릴리스용
keytool -exportcert -alias upload -keystore ~/dev/flutter/upload-keystore.jks | openssl sha1 -binary | openssl base64
ios/Runner/Runner.xcodeproj/project.pbxproj 파일의 PRODUCT_BUNDLE_IDENTIFIER 항목에 있는 값을 번들 ID에 등록한다. 이를 등록해야 정상적으로 앱에서 로그인을 진행할 수 있으므로, 매우 중요한 부분이다.
[앱 키] 탭 > 앱에서 OAuth를 구현하려면 네이티브 앱 키를 복사하여 사용한다.
플러터 패키지 설치
flutter pub add 커맨드로 아래 패키지들을 설치하였다.
dependencies:
flutter:
sdk: flutter
(...중략...)
smooth_page_indicator: ^1.2.1
flutter_secure_storage: ^9.2.4
kakao_flutter_sdk_user: ^1.9.7+3
kakao_flutter_sdk: ^1.9.7+3
flutter_dotenv: ^5.2.1
모바일 OS별 설정 파일 수정
Android와 iOS 설정 파일을 수정한다.
AndroidManifest.xml
카카오 인증 관련 액티비티와 인텐트 필터를 추가한다. 위에서 복사했던 실제 '네이티브 앱 키'를 입력해야 한다.
<activity
android:name="com.kakao.sdk.flutter.AuthCodeCustomTabsActivity">
<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" />
<data android:scheme="kakao{NativeAppKey 입력}" android:host="oauth" />
</intent-filter>
</activity>
Info.plist
CFBundleURLTypes와 LSApplicationQueriesSchemes 설정을 추가한다. 마찬가지로 실제 '네이티브 앱 키'를 입력해야 한다.
<!--Kakao oauth login-->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>kakao{NativeAppKey 입력}</string>
</array>
</dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>kakaokompassauth</string>
<string>kakaolink</string>
<string>kakaotalk</string>
</array>
ios/Podfile
카카오에서 요구하는 iOS 플랫폼 최소 버전이 존재한다. 기존 주석을 해제하고, 13.0 버전으로 설정했다.
이 부분을 명시하지 않으면, 에러가 발생한다.
platform :ios, '13.0'
.env에 Kakao Native App Key 저장
Native App Key는 .env에 따로 저장하였다. 따옴표 필요없이 그대로 실제 네이티브 앱 키를 입력하면 된다.
KAKAO_NATIVE_APP_KEY={NativeAppKey 입력}
.gitignore에 .env 파일을 등록한다.
/assets/.env
OAuth 로그인 코드 구현
main.dart
- dotenv로 .env 파일의 KAKAO_NATIVE_APP_KEY 값을 로드한다.
- 이것으로 KakaoSdk의 네이티브 앱 키값을 설정한다.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: "assets/.env");
FlutterNativeSplash.remove;
KakaoSdk.init(
nativeAppKey: dotenv.env['KAKAO_NATIVE_APP_KEY'] ?? '',
);
runApp(const MyApp());
}
login_screen.dart
- FlutterSecureStorage를 변수에 저장한다.
- 카카오 로그인 가이드 텍스트를 저장한다.
class _LoginScreenState extends State<LoginScreen> {
final PageController _pageController = PageController();
final storage = FlutterSecureStorage();
final List<String> guideTexts = [
"1. 카카오 로그인 버튼을 누른 후\n카카오 계정으로 로그인하세요.",
"2. 이전에 등록한 카카오 아이디로\n간편하게 로그인 할 수 있어요.",
"3. 다른 카카오계정으로 로그인을\n선택하고 새로운 계정 로그인도 가능합니다.",
];
- autoLogin(자동 로그인) 변수를 false로 설정한다.
- 유저 인스턴스에 있는 액세스 토큰, 리프레시 토큰, 유저 아이디, 닉네임, 그리고 자동 로그인 변수를 storage에 저장한다.
- 로그인에 성공하면 /homeElderlyList 화면으로 이동한다.
bool _autoLogin = false;
Future<void> _loginWithKakao() async {
try {
OAuthToken token;
if (await isKakaoTalkInstalled()) {
token = await UserApi.instance.loginWithKakaoTalk();
} else {
token = await UserApi.instance.loginWithKakaoAccount();
}
final user = await UserApi.instance.me();
await storage.write(key: 'accessToken', value: token.accessToken);
await storage.write(key: 'refreshToken', value: token.refreshToken);
await storage.write(key: 'userId', value: user.id.toString());
await storage.write(key: 'nickname', value: user.kakaoAccount?.profile?.nickname ?? 'unknown');
await storage.write(key: 'autoLogin', value: _autoLogin.toString());
if (context.mounted) context.go('/homeElderlyList');
} catch (e) {
print('로그인 실패: $e');
}
}
- autoLogin 변수에 저장된 값이 true이고, accessToken이 null이 아니면, 바로 /homeElderlyList 화면으로 이동한다.
@override
void initState() {
super.initState();
_checkAutoLogin();
}
Future<void> _checkAutoLogin() async {
final autoLogin = await storage.read(key: 'autoLogin');
if (autoLogin == 'true') {
final accessToken = await storage.read(key: 'accessToken');
if (accessToken != null) {
context.go('/homeElderlyList');
}
}
}
- 체크박스의 체크 여부에 따라 autoLogin 값을 바꾼다.
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Checkbox(
value: _autoLogin,
onChanged: (value) {
setState(() {
_autoLogin = value!;
});
},
activeColor: Colors.amber,
),
const Text(
"자동 로그인은 네모를 클릭해주세요.",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
- 카카오 로그인 버튼을 누르면 _loginWithKakao 함수가 실행된다.
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.amber,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
),
onPressed: _loginWithKakao,
child: const Text(
"카카오 로그인",
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
router_without_animation.dart
- initialLocation은 /login 화면으로 설정한다.
final GoRouter appRouter = GoRouter(
initialLocation: '/login',
routes: [
GoRoute(
path: '/login',
pageBuilder: (context, state) => NoTransitionPage(child: LoginScreen()),
),
ShellRoute(
builder: (context, state, child) {
return MainScreen(child: child);
},
routes: [
GoRoute(
path: '/homeElderlyList',
pageBuilder: (context, state) => NoTransitionPage(child: HomeElderlyListScreen()),
routes: [ (...중략...)
home_elderly_list.dart
- loadUserInfo 함수로 storage에 저장된 유저 아이디, 닉네임, 액세스 토큰, 리프레시 토큰 정보를 불러온다.
class _HomeElderlyListScreenState extends State<HomeElderlyListScreen> {
final storage = FlutterSecureStorage();
String? userId, nickname, accessToken, refreshToken;
@override
void initState() {
super.initState();
_loadUserInfo();
}
Future<void> _loadUserInfo() async {
userId = await storage.read(key: 'userId');
nickname = await storage.read(key: 'nickname');
accessToken = await storage.read(key: 'accessToken');
refreshToken = await storage.read(key: 'refreshToken');
setState(() {});
}
- 테스트용으로 어르신 목록 화면에 사용자 정보를 출력해 본다.
Text(
'User ID: $userId\nNickname: $nickname\nAccess Token: $accessToken\nRefresh Token: $refreshToken',
style: const TextStyle(fontSize: 12, color: Colors.black),
),
logout_cancel_screen.dart
- 로그아웃 버튼을 클릭하면 logout 함수가 실행된다.
_buildSettingOption(
context,
title: '계정 로그아웃 (나가기)',
imagePath: 'assets/setting_icon/logout.png',
onTap: () {
_logout(context);
},
),
- logout 함수는 storage에 저장된 사용자 정보를 모두 지우고, /login 화면으로 이동시킨다.
Future<void> _logout(BuildContext context) async {
final storage = FlutterSecureStorage();
await storage.deleteAll();
if (context.mounted) {
context.go('/login');
}
}
구현 소감
우선 카카오에서 OAuth 구현 방법을 친절하게 제공하고 있어서 나름 빠르게 만들어볼 수 있었다.
하지만, 자동 로그인 및 로그아웃 로직에 부족한 부분이 없는지 다시 한번 검토해 보면 좋을 것 같다.