[Flutter 앱] Kakao OAuth 로그인 구현

2025. 4. 7. 18:07Project 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 구현 방법을 친절하게 제공하고 있어서 나름 빠르게 만들어볼 수 있었다.

    하지만, 자동 로그인 및 로그아웃 로직에 부족한 부분이 없는지 다시 한번 검토해 보면 좋을 것 같다.