왜 세션 상태 관리가 필요한가?
실제 앱에서는 이런 요구사항들이 있습니다:
- 사용자가 앱을 껐다 켜도 로그인 상태 유지
- 여러 화면에서 로그인한 사용자 정보 공유
- 로그인 상태에 따라 다른 화면 표시
- 로그아웃시 모든 사용자 데이터 정리
- 토큰 만료시 자동 처리
이런 복잡한 로직을 체계적으로 관리하기 위해 전용 상태 관리 클래스를 만듭니다.
핵심 개념들
- 세션(Session): 사용자가 앱에 로그인한 상태를 유지하는 것
- 자동 로그인: 앱을 다시 켤 때, 이전 로그인 정보로 자동으로 로그인되는 기능
- 토큰 관리: 서버에서 받은 인증 토큰을 저장하고 관리하는 것
- 전역 상태 관리: 로그인 상태를 앱의 모든 화면에서 사용할 수 있게 관리
session_notifier 에 로그인 기능을 완성해 보자 (추가 개발 로직들 있음)
문제점 고민해보기 —> 리팩토링
- 책임 혼재 : UI 로직 + 비즈니스 로직 + 검증 로직 중복
- 테스트 어려움 : UI 와 비즈니스 로직이 동시에 존재해서 관리 어려움
- 재사용성 저하 : 다른 화면에서 로그인 로직 재사용 어려움
import 'package:flutter/material.dart'
show ScaffoldMessenger, Text, SnackBar, Navigator;
import 'package:flutter_blog/_core/utils/my_http.dart';
import 'package:flutter_blog/data/models/repository/user_repository.dart';
import 'package:flutter_blog/data/models/user.dart';
import 'package:flutter_blog/main.dart';
import 'package:flutter_blog/providers/form/join_form_notifier.dart';
import 'package:flutter_blog/providers/form/login_form_notifier.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logger/logger.dart';
// 세션이라는 데이터를 구조화 해보자
// 창고 데이터 구상하기
class SessionModel {
User? user;
bool? isLogin;
SessionModel({this.user, this.isLogin = false});
@override
String toString() {
return 'SessionModel{user: $user, isLogin: $isLogin}';
}
}
// 창고 매뉴얼
class SessionNotifier extends Notifier<SessionModel> {
// context 가 없는 환경에서 네비게시션과 알림 처리를 위한 전역 키이다.
final mContext = navigatorKey.currentContext!;
@override
SessionModel build() {
return SessionModel();
}
// 로그인 로직 설계
Future<void> login(String username, String password) async {
// 유효성 검증 - 사용자가 입력한 username, password
bool isValid = ref.read(loginFormProvider.notifier).validate();
if (isValid == false) {
ScaffoldMessenger.of(mContext)
.showSnackBar(SnackBar(content: Text("유호성 검사 실패")));
return;
}
Map<String, dynamic> body =
await UserRepository().login(username, password);
if (body["success"] == false) {
ScaffoldMessenger.of(mContext)
.showSnackBar(SnackBar(content: Text("로그인 실패")));
return;
}
// 서버에서 받은 사용자 정보를 앱에서 사용할 수 있는 형태로 변환
User user = User.fromMap(body["response"]);
// 로컬 저장소에 JWT 토큰을 저장해 두자.
// Shared... 보안에 강화된 녀석 // yml 라이브러리 선언 됨
await secureStorage.write(key: "accessToken", value: user.accessToken);
state = SessionModel(user: user, isLogin: true);
// 로그인 성공 이후에 서버측에 통신 요청할 때마다 JWT 토큰을 주입해야 된다
dio.options.headers['Authorization'] = user.accessToken;
Navigator.pushNamed(mContext, "/post/list");
}
// 로그 아웃 기능
// 자동 로그인 기능
// 회원 가입 로직 추가하기
Future<void> join(String username, String email, String password) async {
Logger()
.d("username : ${username}, email: ${email}, password : ${password}");
// 한번 더 유효성 검사
bool isValid = ref.read(joinProvider.notifier).validate();
if (isValid == false) {
ScaffoldMessenger.of(mContext)
.showSnackBar(SnackBar(content: Text("유호성 검사 실패")));
return;
}
Map<String, dynamic> body =
await UserRepository().join(username, email, password);
if (body["success"] == false) {
ScaffoldMessenger.of(mContext)
.showSnackBar(SnackBar(content: Text(body["errorMessage"])));
return;
}
//회원 가입 후 로그인 페이지로 이동 처리 (자동 로그인)
Navigator.pushNamed(mContext, "/login");
}
} // end of SessionNotifier
// 실제 창고 개설
final sessionProvider =
NotifierProvider<SessionNotifier, SessionModel>(() => SessionNotifier());
LoginForm 에 로그인 기능 완성해보기
import 'package:flutter/material.dart';
import 'package:flutter_blog/providers/form/login_form_notifier.dart';
import 'package:flutter_blog/providers/global/session_notifier.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../../_core/constants/size.dart';
import '../../../../widgets/custom_auth_text_form_field.dart';
import '../../../../widgets/custom_elavated_button.dart';
import '../../../../widgets/custom_text_button.dart';
class LoginForm extends ConsumerWidget {
const LoginForm({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
LoginModel loginModel = ref.watch(loginFormProvider);
LoginFormNotifier loginFormNotifier = ref.read(loginFormProvider.notifier);
return Form(
child: Column(
children: [
CustomAuthTextFormField(
title: "Username",
errorText: loginModel.usernameError,
onChanged: (value) {
loginFormNotifier.username(value);
},
),
const SizedBox(height: mediumGap),
CustomAuthTextFormField(
title: "Password",
errorText: loginModel.passwordError,
onChanged: (value) {
loginFormNotifier.password(value);
},
obscureText: true,
),
const SizedBox(height: largeGap),
CustomElevatedButton(
text: "로그인",
click: () {
// 검증
bool isValid = loginFormNotifier.validate();
if (isValid) {
// 전역 상태로 관리되는 세션 프로바이더에게 로그인 요청
ref.read(sessionProvider.notifier).login(
loginModel.username,
loginModel.password,
);
// 서버로 로그인 통신 요청
// 서버로 로그인 요청 성공 한다면 페이지 이동 ...
// Navigator.popAndPushNamed(context, "/post/list");
} else {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("유효성 검사 실패")));
}
},
),
CustomTextButton(
text: "회원가입 페이지로 이동",
click: () {
Navigator.pushNamed(context, "/join");
},
),
],
),
);
}
}
JoinForm 에 기능 완성해보기
import 'package:flutter/material.dart';
import 'package:flutter_blog/data/models/repository/user_repository.dart';
import 'package:flutter_blog/providers/form/join_form_notifier.dart';
import 'package:flutter_blog/providers/global/session_notifier.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../../_core/constants/size.dart';
import '../../../../widgets/custom_auth_text_form_field.dart';
import '../../../../widgets/custom_elavated_button.dart';
import '../../../../widgets/custom_text_button.dart';
//
class JoinForm extends ConsumerWidget {
const JoinForm({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 상태 값 감시
JoinModel joinModel = ref.watch(joinProvider);
JoinFormNotifier formNotifier = ref.read(joinProvider.notifier);
return Form(
child: Column(
children: [
CustomAuthTextFormField(
title: "Username",
errorText: joinModel.usernameError,
onChanged: (value) {
// 입력값 변경시 상태 업데이트 + 실시간 검증
print("value : ${value}");
formNotifier.username(value);
},
),
const SizedBox(height: mediumGap),
CustomAuthTextFormField(
title: "Email",
errorText: joinModel.emailError,
onChanged: (value) {
formNotifier.email(value);
},
),
const SizedBox(height: mediumGap),
CustomAuthTextFormField(
title: "Password",
obscureText: true,
errorText: joinModel.passwordError,
onChanged: (value) {
formNotifier.password(value);
},
),
const SizedBox(height: largeGap),
CustomElevatedButton(
text: "회원가입",
click: () {
// 최종 검증
bool isValid = formNotifier.validate();
if (isValid) {
ref.read(sessionProvider.notifier).join(
joinModel.username,
joinModel.email,
joinModel.password,
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("유효성 검사 실패입니다")),
);
}
},
),
CustomTextButton(
text: "로그인 페이지로 이동",
click: () {
Navigator.pushNamed(context, "/login");
},
),
],
),
);
}
}
리팩토링 코드
import 'package:flutter/material.dart'
show ScaffoldMessenger, Text, SnackBar, Navigator;
import 'package:flutter_blog/_core/utils/my_http.dart';
import 'package:flutter_blog/data/models/repository/user_repository.dart';
import 'package:flutter_blog/data/models/user.dart';
import 'package:flutter_blog/main.dart';
import 'package:flutter_blog/providers/form/join_form_notifier.dart';
import 'package:flutter_blog/providers/form/login_form_notifier.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logger/logger.dart';
// 세션이라는 데이터를 구조화 해보자
// 창고 데이터 구상하기
class SessionModel {
User? user;
bool? isLogin;
SessionModel({this.user, this.isLogin = false});
@override
String toString() {
return 'SessionModel{user: $user, isLogin: $isLogin}';
}
}
// 창고 매뉴얼
class SessionNotifier extends Notifier<SessionModel> {
@override
SessionModel build() {
return SessionModel();
}
// 로그인 로직 설계 - 비즈니스 로직에 집중 !
Future<Map<String, dynamic>> login(String username, String password) async {
try {
Map<String, dynamic> body =
await UserRepository().login(username, password);
if (body["success"] == false) {
return body; // 서버에서 내려준 에러 정보를 그대로 반환 --> UI 측으로.
}
// 서버에서 받은 사용자 정보를 앱에서 사용할 수 있는 형태로 변환
User user = User.fromMap(body["response"]);
// 로컬 저장소에 JWT 토큰을 저장해 두자.
// Shared... 보안에 강화된 녀석 // yml 라이브러리 선언 됨
await secureStorage.write(key: "accessToken", value: user.accessToken);
state = SessionModel(user: user, isLogin: true);
// 로그인 성공 이후에 서버측에 통신 요청할 때마다 JWT 토큰을 주입해야 된다
dio.options.headers['Authorization'] = user.accessToken;
return {"success": true}; // 로그인 성공 정보 반환
} catch (e) {
// 네트 워크 장애
return {"success": false, "errorMessage": "네트워크 오류 발생했습니다"};
}
}
// 로그 아웃 기능
Future<void> logout() async {
try {
await secureStorage.delete(key: "accessToken");
state = SessionModel();
dio.options.headers['Authorization'] = "";
Logger().d("로그아웃 종료");
} catch (e) {
Logger().d("로그아웃 중 오류 발생");
state = SessionModel();
dio.options.headers['Authorization'] = "";
}
}
// 자동 로그인 기능
Future<bool> autoLogin() async {
try {
// 1단계 : 저장소에서 인증 토큰 확인
String? accessToken = await secureStorage.read(key: "accessToken");
if (accessToken == null) {
return false;
}
// 2단계 : UserRepository()에게 자동 로그인 요청
Map<String, dynamic> body = await UserRepository().autoLogin(accessToken);
// 3단계 : 서버에서 받은 사용자 정보를 창고에 다시 넣어야 한다
User user = User.fromMap(body["response"]);
user.accessToken = accessToken;
// 4단계 : 창고 데이터에 로그인된 상태로 업데이트 처리
state = SessionModel(user: user, isLogin: true);
// 5단계 : 이 후 모든 HTTP 요청에 인증 열쇠 자동 부여 하기
dio.options.headers["Authorization"] = user.accessToken;
Logger().d("자동 로그인 성공");
return true;
} catch (e) {
Logger().d("자동 로그인 중 오류 발생 : ${e}");
return false;
}
}
// 회원 가입 로직 추가하기
Future<Map<String, dynamic>> join(
String username, String email, String password) async {
Logger()
.d("username : ${username}, email: ${email}, password : ${password}");
Map<String, dynamic> body =
await UserRepository().join(username, email, password);
if (body["success"] == false) {
return {"success": false, "errorMessage": body["errorMessage"]};
} else {
return {"success": true};
}
}
} // end of SessionNotifier
// 실제 창고 개설
final sessionProvider =
NotifierProvider<SessionNotifier, SessionModel>(() => SessionNotifier());'Flutter' 카테고리의 다른 글
| 블로그 프로젝트 - 게시글 목록 구현 하기 (0) | 2025.09.04 |
|---|---|
| 블로그 프로젝트 - 로그아웃 기능 연결 및 UI 수정 (0) | 2025.09.04 |
| 블로그 프로젝트 - 로그인 폼 상태 관리 (1) | 2025.09.01 |
| 블로그 프로젝트 - 회원가입 폼 상태 관리 (1) | 2025.08.22 |
| 블로그 프로젝트 - main.dart 코드 리뷰 (0) | 2025.08.22 |