수많은 소셜앱은 대부분 사용자가 로그인을 하는 로그인기능을 제공합니다만, 직접 로그인 기능을 제작하는 경우에 그에 따른 개발비용 및 시간이 증가하게 됩니다. 그 때, 이미 충분히 인증된 소셜 서비스의 로그인기능을 사용하여 로그인을 구현할 수 있습니다. 이를 우리는 Oauth라고 합니다.
대한민국에서 사용하는 대부분의 어플리케이션은 카카오로그인 api를 이용해서 로그인할 수 있습니다. 특히, 카카오는 Flutter와 카카오 서비스를 연동할 수 있는 라이브러리를 공식적으로 제공하고 있어요.
이번 글을 통해서 아주 간단하게 겉핥기로 카카오 로그인을 사용해보도록 하겠습니다.
이번에 카카오 로그인 api를 사용하고, 결과를 확인할 UI를 만들었습니다. 우리는 위 UI에서 버튼을 통해 로그인을 실행하고, 그 결과로 프로필 사진과 이름을 가져와보도록 하겠습니다.
현재 이 글에 설명된 모든 내용은 공식 문서에서 더욱 자세하게 확인할 수 있고, 심지어 공식문서에는 어떻게 카카오가 Oauth를 제공하고 있는지도 자세하게 적혀있습니다. 따라서, 공식문서를 토대로 해당 작업을 진행하겠습니다.
가장 먼저 해야하는 일은 프로젝트를 등록하는 일입니다. 그래야 우리가 사용할 수 있는 api 키를 얻을 수 있습니다.
카카오 개발자에서 시작하기 버튼을 누르면 위 버튼이 보이실겁니다. 버튼을 클릭해서 새로운 프로젝트를 추가해주겠습니다.
앱 이름은 표기법이 따로있는 것 같습니다. 허용되는 이름으로 기입하시고 사업자명, 카테고리도 선택해서 저장해줍니다.
이제 여기서 우리가 api 요청을 위해 사용할 수 있는 key를 확인할 수 있습니다. 이 key는 절대 외부인에게 공개하지 마세요. 이제 각각 플랫폼에 맞는 설정을 하도록 하겠습니다.
안드로이드는 키 해시가 필요합니다. 키 해시는 공식문서에서 안드로이드 관련 문서에 명시되어 있으니, Flutter가 아니라 안드로이드 문서를 확인하셔야 합니다.
저는 MacOS를 사용하고 있으니, 본인의 OS에 맞는 명령어를 사용하시면 됩니다. 이 명령어를 터미널에 입력하면 해시값이 등장하는데 그 값을 복사해서 키해시에 넣어줍니다.
이제 안드로이드 프로젝트의 커스텀 URL 스킴을 등록해줍니다.
<!-- 카카오 로그인 커스텀 URL 스킴 설정 -->
<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" 형식의 앱 실행 스킴 설정 -->
<!-- 카카오 로그인 Redirect URI -->
<data android:scheme="kakao[네이티브키]" android:host="oauth"/>
</intent-filter>
</activity>
이 내용을 AndroidManifest.xml에 넣어주시면 됩니다. 네이티브 키는 초기에 나와있던 네이티브 키를 입력해주시면 됩니다. kakao는 생략하시면 안됩니다. 마지막으로 midSdkVersion을 21로 설정합시다.
이전에는 19로 바꾸던 것인데요. 멀티덱스 이슈도 있고 하니 21이 제일 안전합니다.
이제 IOS 설정을 해주도록 하겠습니다. IOS는 기본적으로 패키지이름이 아니라 번들 ID라고 합니다. 그래서 번들 ID를 등록해주어야 하는데요. 이는 Xcode를 이용해서 프로젝트를 여시면, 쉽게 알 수 있습니다. 근데, 보통 본인이 만든 프로젝트의 패키지명을 건들지 않았다면 기본적으로 카멜표기법으로 표시되어있습니다. 이렇게 말이죠.
이제 info.plist 파일에 아래와 같은 내용을 추가해줍니다. 이는 카카오 서비스를 IOS에서 사용하기 위해 설정하는 것입니다.
<key>LSApplicationQueriesSchemes</key>
<array>
<!-- 카카오톡으로 로그인 -->
<string>kakaokompassauth</string>
<!-- 카카오톡 공유 -->
<string>kakaolink</string>
<!-- 카카오톡 채널 -->
<string>kakaoplus</string>
</array>
마지막으로, 커스텀 URL 스킴도 등록하겠습니다. 이제 IOS 설정은 모두 완료하였습니다.
간혹 하이브리드로 웹도 함께 하신다면, 웹에서는 native 키가 아니라 javascript 키를 함께 사용해야 합니다. 그리고 당연히 도메인을 등록해줘야 합니다.
$ flutter run -d Chrome --web-port:8080
위 명령어는 웹에서 실행하면서 8080포트를 이용하여 실행하라는 명령어입니다. 당연하지만 포트를 사용중이면 에러가 발생하겠죠? 사용하지 않는 포트번호를 이용하면 됩니다. 그냥 디버깅하면 랜덤 포트를 사용하는데요. 아래에 내용을 보시면 왜 포트를 고정해야하는지 이해하실것이라 생각합니다.
이것은 로그인을 하기위한 Redirect URI를 등록하는 과정입니다. 당연히, 포트번호를 고정적으로 사용하기 때문에, 그에 맞는 도메인을 적어주어야 디버깅할 수 있겠죠?
void main() {
...
// 웹 환경에서 카카오 로그인을 정상적으로 완료하려면 runApp() 호출 전 아래 메서드 호출 필요
WidgetsFlutterBinding.ensureInitialized();
// runApp() 호출 전 Flutter SDK 초기화
KakaoSdk.init(
nativeAppKey: '${YOUR_NATIVE_APP_KEY}',
javaScriptAppKey: '${YOUR_JAVASCRIPT_APP_KEY}',
);
runApp(MyApp());
...
}
main 메소드에서 카카오 SDK의 대한 초기화코드를 위와같이 수정해줘야 합니다. 만약 모바일만 사용한다고하면 native키만 입력해줘도 됩니다. 그리고 Flutter는 web으로 디버깅하면 URL에 #이 붙습니다. 이를 제거하고 싶다면, url_strategy 라이브러리를 사용할 수 있습니다.
void main() {
WidgetsFlutterBinding.ensureInitialized();
KakaoSdk.init(
nativeAppKey: '${YOUR_NATIVE_APP_KEY}',
javaScriptAppKey: '${YOUR_JAVASCRIPT_APP_KEY}',
);
setPathUrlStrategy(); // 꼭 제일 밑에
runApp(const MyApp());
}
setPathUrlStrate() 메소드를 main에 추가해주시면 되는데, 모든 초기화 코드 가장 하단에 적어주세요. 다만, url_strategy는 실제로 deploy하면 새로고침 같은 기능을 사용하면 404 에러가 발생합니다. 해결법은 있으니까 해결하시면 됩니다만, 모르면 시간만 버릴수도 있으니 주의합시다.
일단은 저희는 Provider를 이용해서 상태관리를 할것입니다. UserController라는 클래스를 생성해서 User의 정보를 관리하겠습니다.
class UserController with ChangeNotifier {
User? _user;
KakaoLoginApi kakaoLoginApi;
User? get user => _user;
UserController({required this.kakaoLoginApi});
}
이 Controller를 통해서 User의 정보를 가져오거나 없앨 수 있습니다. 그리고 Provider를 적용하기 위해서 MaterialApp에 하단의 내용으로 작성해 주셔야합니다.
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => UserController(
kakaoLoginApi: KakaoLoginApi(),
),
child: const MaterialApp(
home: App(),
),
);
}
}
이제 UI에 대해서 코드를 작성하죠. 뭐, 간단하게 프로필과 이름, 카카오 로그인 버튼이니까 어려울 것 없습니다.
import 'package:flutter/material.dart';
import 'package:flutter_kakao_login_with_provider/src/controller/user_controller.dart';
import 'package:provider/provider.dart';
class App extends StatefulWidget {
const App({super.key});
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Flutter with Kakao login"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_profile(),
_nickName(),
_loginButton(),
],
),
));
}
Widget _profile() => Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
shape: BoxShape.circle,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(100),
child:
Consumer<UserController>(builder: (context, controller, child) {
// 사용자의 프로필 사진 가져오기
final String? src = controller.user?.properties?["profile_image"];
if (src != null) {
return Image.network(src, fit: BoxFit.cover);
} else {
return Container(
color: Colors.black,
);
}
}),
),
),
);
Widget _nickName() => Padding(
padding: const EdgeInsets.all(8.0),
child: Consumer<UserController>(builder: (context, controller, child) {
// 사용자의 닉네임 가져오기
final String? name = controller.user?.properties?["nickname"];
if (name != null) {
return Text(name);
} else {
return const Text("로그인이 필요합니다");
}
}),
);
Widget _loginButton() => Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
// 카카오 로그인을 위한 메소드
onTap: context.read<UserController>().kakaoLogin,
child: Image.asset("assets/images/kakao_login_medium_narrow.png")),
);
}
이제 UI가 준비되었으니, 비즈니스 로직을 작성하겠습니다. 이 코드는 공식문서에 작성된 코드를 제가 취향대로 바꿔서 작성한 코드입니다. 되도록 공식문서 코드를 사용하는 것이 예외처리가 쉽습니다.
import 'package:flutter/services.dart';
import 'package:kakao_flutter_sdk_user/kakao_flutter_sdk_user.dart';
class KakaoLoginApi {
signWithKakao() async {
final UserApi api = UserApi.instance;
if (await isKakaoTalkInstalled()) {
try {
api.loginWithKakaoTalk().then((_) {
return api.me();
});
} catch (error) {
print('카카오톡으로 로그인 실패 $error');
// 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
// 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
if (error is PlatformException && error.code == 'CANCELED') {
return;
}
// 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인
try {
await UserApi.instance.loginWithKakaoAccount();
return api.me();
} catch (error) {
print('카카오계정으로 로그인 실패 $error');
}
}
} else {
try {
await UserApi.instance.loginWithKakaoAccount();
return api.me();
} catch (error) {
print('카카오계정으로 로그인 실패 $error');
}
}
}
}
이제 Controller에서 해당 비즈니스로직을 이용하면 됩니다.
class UserController with ChangeNotifier {
User? _user;
KakaoLoginApi kakaoLoginApi;
User? get user => _user;
UserController({required this.kakaoLoginApi});
// 카카오 로그인
void kakaoLogin() async {
kakaoLoginApi.signWithKakao().then((user) {
// 반환된 값이 NULL이 아니라면
// 정보 전달
if (user != null) {
_user = user;
notifyListeners();
}
});
}
}
이제, 모든 준비가 완료되었습니다. 간단하면서 살짝 허접하게 작성하였습니다. 실제였으면 추상 클래스 생성해서 여러 소셜 로그인에 대한 객체 지향 프로그래밍을 하셔야겠지만, 뭐 예를 드는거니까요.
안드로이드 먼저 확인해보겠습니다. 전체 영상으로 보여드리고 싶었는데, 제 계정을 직접사용한거라, 미리 로그인하고 결과만 보여드린점 양해부탁드립니다.!
이제 IOS도 확인하겠습니다.
마지막으로 Web에서도 확인하겠습니다.
만약 직접해보신다면, 계정입력하는 url이 나타나게됩니다.
카카오톡으로 이동했다가 동작없이 바로 돌아오기만하고 LoginWithKakaoTalk에서 user 가 null로 나오는데.. 어떤게 문제일까요?