블로그 내의 게시물은 PC 버전에 최적화 되어 있습니다.
오늘은 플러터에서 자동 로그인와 Splash Screen을 구현해보려고 합니다. 자동 로그인은 앱 개발을 접해봤다면 한 번쯤은 구현해봤을 기능이라고 생각합니다. SharedPreference를 활용해서 구현하는데 오늘은 조금 다른 방식으로 접근했습니다. 바로 BLoC를 통해서 구현하는데요, BLoC를 사용하면 자동 로그인과 Splash Screen을 하나의 로직으로 처리할 수 있습니다. 한번 시작해보죠.
바로 BLoC로 구현하지 않고 기존의 방식으로 구현하고 BLoC로 리펙토링하는 방식으로 접근합니다. 기존 방식으로 구현하면서 발생하는 문제와 이 문제를 어떤 식으로 해결하는지가 중요하니까요.
자동 로그인
앞서 말씀드린 것처럼 SharedPreference를 사용하면 됩니다.
shared_preferences | Flutter Package
Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android.
pub.dev
혹은 보안에 좀 더 신경 쓰고 싶다면 SecureSharedPreference를 사용하면 됩니다.
secure_shared_preferences | Flutter Package
Simple to use yet powerful package to encypt shared preferences in android and UserDefaults in iOS.
pub.dev
구현은 간단합니다. 사용자 인증(로그인)에 성공하면 SharedPreference에 특정 값(토큰)을 저장하고 앱을 실행했을 때 처음 로드되는 클래스(페이지)에서 didChangeDependencies를 오버라이딩해서 SharedPreference에 저장된 값을 불러옵니다. 존재하지 않으면 로그인 페이지로 이동하고 존재하는 경우 메인 페이지로 이동합니다.
말로 보니까 복잡해 보이네요. 코드로 간단하게 구현해보도록 하죠. 실제 로그인 페이지처럼 TextField는 쓰지 않을 것입니다. FloatingActionButton으로 로그인을 대신합니다.
Main 클래스입니다.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _isAuth = false;
@override
void didChangeDependencies() async {
// SharedPreference에 저장된 토큰이 있는지를 확인
final token = await SharedPreferManager().getSharedPreference();
if (token != "Not Auth") {
_isAuth = true;
}
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _isAuth ? const HomeScreen() : const LoginScreen(),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await SharedPreferManager()
.setSharedPreference("codokodo is authenticate");
},
child: const Icon(Icons.login),
),
);
}
}
플러터가 기본 제공해주는 스켈레톤 코드 위에서 작업했습니다. 앞서 말씀드린 것처럼 SharedPreference에 저장된 토큰이 있는지를 확인합니다. 토큰이 저장되어 있으면 _isAuth를 true로 변경합니다. 이후 위젯을 렌더링할 때 _isAuth의 값에 따라 다른 페이지를 보여줍니다. 또한 TextField로 구현하지 않고 FloatingActionButton을 클릭하면 SharedPreference에 특정한 문자열(토큰이라고 가정)을 저장하도록 구현했습니다.
Home Screen과 Login Screen는 각각 사용자 인증 이후에 보여지는 화면과 이전에 보여지는 화면입니다. 코드는 아래와 같습니다.
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
"사용자 인증에 성공했습니다.",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
);
}
}
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
"로그인을 진행해주세요.",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
);
}
}
별도로 정의한 SharedPreferManager의 코드는 다음과 같습니다. Singleton으로 정의하였고 getter, setter에 대한 로직만 가지고 있습니다. getter를 보면 key로 전달한 auth의 value가 존재하지 않으면 Not Auth를 리턴합니다.
import 'package:shared_preferences/shared_preferences.dart';
class SharedPreferManager {
static final SharedPreferManager _instance = SharedPreferManager._internal();
final Future<SharedPreferences> _manager = SharedPreferences.getInstance();
SharedPreferManager._internal() {}
factory SharedPreferManager() {
return _instance;
}
Future<void> setSharedPreference(String token) async {
final manager = await _manager;
manager.setString("auth", token);
}
Future<String> getSharedPreference() async {
final manager = await _manager;
return manager.getString("auth") ?? "Not Auth";
}
}
구현이 끝났습니다. 실제로 빌드를 해보면 아래와 같이 앱을 처음 실행했을 때에는 로그인 페이지(LoginScreen)로 이동하지만 FloatingActionButton을 클릭하고 앱을 재실행하면 사용자 인증에 성공했다는 메인 페이지(HomeScreen)으로 이동합니다.
문제점
간단하게 구현을 마쳤습니다. 하지만 뭔가 어색합니다. 사용자 인증에 성공하면 앱을 재실행하지 않아도 메인 페이지로 이동해야 합니다. 하지만 지금은 앱을 재실행해야지만 메인 페이지로 이동이 가능합니다. 물론 간단하게 해결할 수 있습니다. didChangeDependencies에 Navigator 코드를 추가하여 Home Screen으로 이동하도록 구현하면 됩니다. 로그아웃 역시 같은 방식으로 Home Screen에서 Login Screen으로 이동하는 Navigator 코드를 추가하면 정상적으로 로그인, 로그아웃 기능을 구현할 수 있습니다. 하지만 이렇게 Navigator로 구현하게 되면 확장성에서 문제가 발생합니다. (주관적인 의견입니다.)
가령 Splash Screen을 추가한다고 가정해볼까요? 앱을 실행하면 먼저 Splash Screen부터 보여지는데
[Splash Screen] SharedPreference 확인 & 토큰이 저장되어 있지 않음 -> Navigator로 Push -> [Login Screen] 사용자 인증 성공 -> Navigator로 Push -> [Home Screen]
벌써 두 번의 Push가 발생합니다. 쉽고 빠르게 구현할 수 있지만 각 페이지가 종속적으로 구현된다는 문제가 발생합니다. 이를 BLoC으로 해결하려고 합니다. (지금까지 서론인가요?) BLoC를 적용하면 하나의 로직으로 두 개의 기능을 구현할 수 있어 하나의 클래스에서 모두 관리하는게 가능해집니다.
우선 BLoC에 대한 이해가 필요합니다. 저도 인턴을 진행하면서 BLoC에 대해 배우고 있는 단계라 아직 많이 부족합니다. 디자인 패턴이나 상태 관리에 대한 지식이 있으시면 이해가 빠르실 겁니다. 간단하게 설명하자면 UI에 Data를 보여주는 Presention Layer와 Data를 가공하는 Business Logic을 별도로 분리하는 디자인 패턴으로 MVVM과 상당히 유사합니다.
BLoC는 Event, State, BLoC로 구성됩니다. 3개의 클래스로 구분된다고 이해하시는게 좋겠네요. 각각에 대해서는 다음 게시글로 찾아오겠습니다. 하단에 관련 자료를 첨부했습니다.
flutter_bloc | Flutter Package
Flutter Widgets that make it easy to implement the BLoC (Business Logic Component) design pattern. Built to be used with the bloc state management package.
pub.dev
Flutter bloc for beginners
What is flutter bloc?
medium.com
Flutter: BlocBuilder vs BlocConsumer vs BlocListener
You can find the Spanish version here 👇
ppantaleon.medium.com
https://booiljung.github.io/technical_articles/flutter/state_management/architecture_your_flutter_project_using_bloc_pattern.html
Top BLOC 패턴을 사용한 Flutter 프로젝트 아키텍쳐 설계 원문: Architect your Flutter project using BLOC pattern 여러분 안녕하세요! 저는 Flutter에 관한 새로운 글을 가지고 왔습니다. 이번에는 “Flutter 프로젝
booiljung.github.io
'💻 개발 > Flutter' 카테고리의 다른 글
[Flutter] ModalBottomSheet가 키보드에 의해 가려지는 현상 (0) | 2022.08.09 |
---|---|
[Flutter / Dart] What is Equatable? (0) | 2022.08.06 |
[Flutter / Dart] What is Singleton? (0) | 2022.07.21 |
[Flutter] SingleChildScrollView, ListView, ListView.bulider (0) | 2021.08.18 |
4. Column, Row, Expanded (0) | 2021.08.18 |