[Flutter] 블로그 만들기 - 로그인 기능 구현 (상태관리 및 예외처리)

2025. 2. 9. 11:13·Flutter/App
728x90

이번 글에서는 Flutter에서 로그인 기능 구현을 다룹니다.
특히, **상태 관리(Riverpod)**를 활용하여 로그인 상태를 유지하고, **예외 처리(ExceptionHandler)**를 적용하여 보다 안전한 로그인 프로세스를 구축합니다.


1. 로그인 기능 설계

로그인 요청을 보낼 때 어떤 변수가 필요할까?
뷰모델(SessionGVM)에서 세션 유저(SessionUser)를 관리하며, 다음과 같은 기능을 수행합니다.

 

📌 뷰모델의 역할

  1. 세션관리 : 현재 로그인된 사용자 정보(ID, username, accessToken, 로그인 여부)를 저장
  2. 로그인 처리: 서버에 로그인 요청을 보내고, 성공 시 JWT 토큰을 저장하고 상태를 유지
  3. 예외 처리: 네트워크 오류, 서버 에러 등을 처리하여 사용자에게 알림.

2. 상태 관리(ViewModel) 구현

**SessionGVM(Global ViewModel)**은 앱 전역에서 로그인 상태를 관리하는 역할을 합니다.

 

📌 SessionGVM 클래스

import 'package:class_f_story/_core/utils/exception_handler.dart';
import 'package:class_f_story/_core/utils/my_http.dart';
import 'package:class_f_story/data/model/session_user.dart';
import 'package:class_f_story/data/repository/user_repository.dart';
import 'package:class_f_story/main.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 뷰 모델 --> 화면에 있는 데이터를 가지고 와서 사용한다.
// 로그인 요청을 할 때 어떤 변수를 필요할까?

// 뷰모델에 속성 --> 세션 유저가 된다.
// 뷰모델에 행위 --> 로그인 행위, 로그아웃 행위, 자동 로그인 행위
class SessionGVM extends Notifier<SessionUser> {
  // 뷰 모델에서 컨텍스트를 사용하는 방안
  final mContext = navigatorkey.currentContext!;
  UserRepository userRepository = UserRepository();

  // 상태를 초기화 해주어야 한다. (선언형 UI 이기 때문에)
  @override
  SessionUser build() {
    // 추상화 변수 state(외부 에서 접근하는 변수명 )
    // state ==> SessionUser() 객체가 된다.
    return SessionUser(
      id: null,
      username: null,
      accessToken: null,
      isLogin: false,
    );
  }

 

📌 설명

  • SessionGVM은 현재 로그인 상태를 전역적으로 관리하는 역할을 합니다.
  • build() 메서드에서 초기 상태를 설정합니다.
    • 로그인 여부(isLogin): 기본값 false
    • 사용자 정보(id, username, accessToken): null

3. 로그인 요청 처리

로그인 버튼을 눌렀을 때 서버로 요청을 보내고, 상태를 갱신하는 기능입니다.

 

📌 로그인 메서드 (login)

  // 로그인 행위
  // 화면에서 뷰 모델에게 로그인 요청 위임
  Future<void> login(String username, String password) async {
    // 서버측으로 던질 데이터를 구축해야 한다.
    try {
      // 요청 HTTP 메세지 body
      final body = {
        'username': username,
        'password': password,
      };

      final (responseBody, accessToken) =
          await userRepository.findByUsernameAndPassword(body);
      // responseBody['success'] -- true --> false
      // 서버측에서 통신은 성공 했으나 내부 오류 판단
      if (!responseBody['success']) {
        ExceptionHandler.handleException(
            responseBody['errorMessage'], StackTrace.current);
        return; // 실행의 제어권 반납
      }

      // 1. JWT 토큰을 안전한 금고에 보관 처리 //
      // I/O 시간이 많이 걸리기 때문에 비동기 처리
      await secureStorage.write(key: 'accessToken', value: accessToken);

      // 2. 뷰 모델에 상태 갱신
      // 깊은 복사 처리
      Map<String, dynamic> resData = responseBody['response'];
      state = SessionUser(
          id: resData['id'],
          username: resData['username'],
          accessToken: accessToken,
          isLogin: true);

      // 3. Dio 헤더에 JWT 토큰 설정(객체 상태값 추가)
      dio.options.headers['Authorization'] = accessToken;

      // 화면 이동 처리 pushNamed -> pushNamed -> pushNamed
      // Navigator stack 메모리에 계속 쌓인다..
      // Navigator.pushNamed(mContext, '/post/list');
      // 이전에 쌓여 있던 stack(화면) 다 파괴 하면서 이동 처리
      Navigator.popAndPushNamed(mContext, '/post/list');
      // 모든 예외처리가 설정 된다.
    } catch (e, stackTrace) {
      // IP 주소가 잘못, 서버가 종료 되어 있을 때, 서버 연결 시간 초과
      ExceptionHandler.handleException('서버 연결 실패', stackTrace);
    }
  }

 

📌 설명

 

  • 서버로 username, password를 포함한 로그인 요청을 보냅니다.
  • 로그인 성공 시 JWT 토큰을 안전한 저장소(secureStorage)에 저장하고 Dio 헤더에 추가합니다.
  • 로그인 후 기존 화면을 제거하고 (Navigator.popAndPushNamed) 새 페이지로 이동합니다.

4. 로그인 상태 관리 (Provider)

Riverpod을 활용하여 SessionGVM을 전역적으로 관리합니다.

 

📌 Provider 선언

// 창고 관리자 선언 (창고 - 뷰모델), 창고 어떤 관리해라 지정 !!
final sessionProvider = NotifierProvider<SessionGVM, SessionUser>(
  () => SessionGVM(),
);

 

📌  설명

  • **NotifierProvider**를 사용하여 SessionGVM을 관리합니다.
  • UI에서 sessionProvider를 사용하면 로그인 상태 변경 사항을 자동으로 반영할 수 있습니다.

5. 로그인 시 예외 처리

로그인 실패 또는 네트워크 오류 발생 시 사용자에게 알림을 제공합니다.

 

📌 예외 처리 클래스

import 'package:class_f_story/main.dart';
import 'package:flutter/material.dart';
import 'logger.dart';

class ExceptionHandler {
  static void handleException(dynamic exception, StackTrace stackTrace) {
    logger.e('Exception : $exception');
    logger.e('StackTrace : $stackTrace');

    // 간혹 비동기 작업시 currentContext 사라질 수 있다.
    final mContext = navigatorkey.currentContext;
    if (mContext == null) return;

    // 시스템 키보드가 있다면 내려 주자
    FocusScope.of(mContext).unfocus();

    ScaffoldMessenger.of(mContext).showSnackBar(
      SnackBar(
        content: Text(
          exception.toString(),
        ),
      ),
    );
  }
}

 

📌 설명

  • 로그 출력: logger.e()를 사용하여 오류 내용을 콘솔에 기록.
  • 키보드 처리: 예외 발생 시 활성화된 입력 필드를 비활성화하여 키보드를 숨김.
  • 오류 메시지 출력: ScaffoldMessenger를 사용하여 사용자에게 스낵바 형태로 오류 메시지 표시.

6. 로그인 UI (LoginBody) 구현

로그인 UI는 사용자가 아이디, 비밀번호를 입력하고, 로그인 버튼을 눌러 로그인 요청을 보낼 수 있도록 구성됩니다.

📌 로그인 화면 구조

  • CustomLogo → 앱 로고 표시
  • CustomAuthTextFormField → 아이디 및 비밀번호 입력 필드
  • CustomElevatedButton → 로그인 버튼
  • CustomTextButton → 회원가입 버튼

📌 LoginBody 코드

import 'package:class_f_story/_core/constants/size.dart';
import 'package:class_f_story/data/gvm/session_gvm.dart';
import 'package:class_f_story/ui/widgets/custom_auth_text_form_field.dart';
import 'package:class_f_story/ui/widgets/custom_elevated_button.dart';
import 'package:class_f_story/ui/widgets/custom_logo.dart';
import 'package:class_f_story/ui/widgets/custom_text_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 로그인 화면 UI를 담당하는 위젯
// ConsumerWidget을 사용하여 Provider에서 데이터 상태를 읽어올 수 있음
class LoginBody extends ConsumerWidget {
  // 사용자 입력을 받을 컨트롤러 생성
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();

  LoginBody({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 창고 자체에 접근하는 코드 --> 뷰모델을 가지고 옴
    SessionGVM gvm = ref.read(sessionProvider.notifier);

    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: ListView(
        children: [
          // 앱 로고 표시
          CustomLogo('f-story'),

          // 사용자명 입력 필드
          CustomAuthTextFormField(
            text: 'Username',
            controller: _usernameController,
          ),
          const SizedBox(height: largeGap),

          // 비밀번호 입력 필드
          CustomAuthTextFormField(
            text: 'Password',
            controller: _passwordController,
          ),
          const SizedBox(height: largeGap),

          // 로그인 버튼
          CustomElevatedButton(
            text: '로그인',
            click: () {
              // 로그인 요청
              gvm.login(
                _usernameController.text.trim(),
                _passwordController.text.trim(),
              );
            },
          ),

          // 회원가입 페이지 이동 버튼
          CustomTextButton(
            text: '회원가입 페이지로 이동',
            click: () {
              Navigator.pushNamed(context, '/join');
            },
          ),
        ],
      ),
    );
  }
}

📌 코드 설명

1. ConsumerWidget 사용

 
class LoginBody extends ConsumerWidget {
  • ConsumerWidget을 사용하면 Riverpod의 상태 관리 기능을 활용하여 SessionGVM의 상태를 읽을 수 있습니다.

2. 뷰모델(SessionGVM) 가져오기

SessionGVM gvm = ref.read(sessionProvider.notifier);
  • sessionProvider.notifier를 사용하여 로그인 상태를 관리하는 뷰모델(SessionGVM)을 가져옵니다.
  • 이를 통해 UI에서 gvm.login()을 호출하여 로그인 요청을 보낼 수 있습니다.

3. 입력 필드 구현

CustomAuthTextFormField(
  text: 'Username',
  controller: _usernameController,
),
  • 사용자명과 비밀번호 입력을 위해 **커스텀 입력 필드(CustomAuthTextFormField)**를 사용합니다.
  • controller를 설정하여 사용자가 입력한 값을 읽을 수 있도록 합니다.

4. 로그인 버튼 클릭 시 로그인 요청

CustomElevatedButton(
  text: '로그인',
  click: () {
    gvm.login(
      _usernameController.text.trim(),
      _passwordController.text.trim(),
    );
  },
),
  • 사용자가 로그인 버튼을 클릭하면 gvm.login()을 호출하여 로그인 요청을 보냅니다.
  • trim()을 사용하여 공백을 제거한 후 서버로 전송합니다.

5. 회원가입 페이지로 이동 버튼

CustomTextButton(
  text: '회원가입 페이지로 이동',
  click: () {
    Navigator.pushNamed(context, '/join');
  },
),
  • CustomTextButton을 사용하여 회원가입 페이지로 이동할 수 있도록 네비게이션 처리를 합니다.

📌 전체 로그인 흐름

  1. 사용자가 Username, Password 입력 필드에 값을 입력합니다.
  2. 로그인 버튼 클릭 시, gvm.login()을 호출하여 로그인 요청을 보냅니다.
  3. SessionGVM이 서버로 로그인 요청을 전송하고, 성공 시 JWT 토큰을 저장하고 로그인 상태를 변경합니다.
  4. 로그인 성공 후, /post/list 페이지로 이동합니다.
  5. 로그인 실패 시, **ExceptionHandler**를 통해 오류 메시지를 표시합니다.

✅ 마무리

  1. 로그인 화면 UI(LoginBody) 구성
    • CustomAuthTextFormField, CustomElevatedButton, CustomTextButton 사용.
    • ConsumerWidget을 사용하여 뷰모델과 연결.
  2. 뷰모델(SessionGVM)과의 연결
    • sessionProvider.notifier를 통해 로그인 요청을 gvm.login()으로 위임.
  3. 로그인 버튼 클릭 시 실행 흐름
    • 서버 요청 → 로그인 성공 시 JWT 토큰 저장 및 상태 변경 → 페이지 이동.
    • 로그인 실패 시 예외 처리(ExceptionHandler) 실행.

다음 글에서는 회원가입 UI와 기능을 구현해보겠습니다! 🚀

 


서버통신과 로그인 기능구현 이전 글이 궁금하다면??

 

2025.02.08 - [Flutter/App] - "[Flutter] 블로그 만들기 - 서버 통신의 모든 것 (Dio 활용)

 

 

'Flutter > App' 카테고리의 다른 글

[Flutter] 블로그 만들기 -자동 로그인 기능 구현 및 UI  (0) 2025.02.10
[Flutter] 블로그 만들기 - 회원가입 기능 구현 및 UI  (1) 2025.02.09
"[Flutter] 블로그 만들기 - 서버 통신의 모든 것 (Dio 활용)  (0) 2025.02.09
[Flutter] 블로그 만들기 - 로그인 UI 구성  (0) 2025.02.08
[Flutter] 블로그 만들기 - 기본 프로젝트 설정  (1) 2025.02.08
'Flutter/App' 카테고리의 다른 글
  • [Flutter] 블로그 만들기 -자동 로그인 기능 구현 및 UI
  • [Flutter] 블로그 만들기 - 회원가입 기능 구현 및 UI
  • "[Flutter] 블로그 만들기 - 서버 통신의 모든 것 (Dio 활용)
  • [Flutter] 블로그 만들기 - 로그인 UI 구성
공돌이 출신 개발자
공돌이 출신 개발자
공돌이 출신 개발자입니다
  • 공돌이 출신 개발자
    공돌이 출신 개발자
    공돌이 출신 개발자
  • 전체
    오늘
    어제
    • 분류 전체보기 (124)
      • Database (0)
        • SQL (0)
        • 1일 1쿼리 (9)
      • Flutter (40)
        • Dart 언어 (18)
        • App (22)
      • Git (0)
      • Http 기초 지식 (14)
      • HTML5 & CSS3 (0)
      • Java (33)
      • JSP (0)
      • JavaScript (0)
      • Linux (0)
      • MSA (0)
      • Project (0)
      • React (0)
      • Spring (19)
      • 설치 메뉴얼 (1)
      • [Flutter] 프로젝트 (눈길) (8)
        • 작업일지 (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    앱 개발
    spring boot
    블로그 만들기
    코딩
    SQL
    플러터
    프로그래밍
    HTTP
    Android
    개발
    Java
    안드로이드 앱 개발
    객체지향
    객체
    클래스
    android studio
    dart
    앱개발
    jsp
    SQLD
    안드로이드
    회원가입
    flutter
    데이터
    공부
    JAVA 기초
    프로젝트
    1일1쿼리
    로그인
    메서드
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
공돌이 출신 개발자
[Flutter] 블로그 만들기 - 로그인 기능 구현 (상태관리 및 예외처리)
상단으로

티스토리툴바