개발노트

49. [Flutter] Firebase Authentication (이메일/비밀번호) 인증 구현하기 본문

앱 개발/Flutter

49. [Flutter] Firebase Authentication (이메일/비밀번호) 인증 구현하기

mroh1226 2023. 12. 1. 13:36
반응형

Flutter 앱에 Firebase Authentication 이메일/비밀번호 로그인 인증 추가하기

- 주요 기능들.

  • User 생성하기 (이메일, 패스워드)
    • Future<UserCredentialPlatform> createUserWithEmailAndPassword(String email, String password)
  • User Sign-In 기능 (로그인)
    • Future<UserCredential> signInWithEmailAndPassword({required String email, required String password})
  • User Sign-Out 기능 (로그아웃)
    • Future<void> signOut()

 

위와 같이 크게 3가지 기능을 구현하고, MVVM 패턴에 적용할 수 있도록 예시를 작성해보겠습니다.


우선 아래 포스팅으로 Firebase를 연동하고, Authenication 기능을 추가하여 Flutter 프로젝트 환경을 만들어줍니다.

 

1. Flutter 앱에 Firebase 연동하는방법: https://mroh1226.tistory.com/153

 

3. [Firebase] Firebase의 기능을 Flutter App에 추가하기

Firebase의 플러그인 을 이용한다면 손쉽게 구현할 수 있는 기능들이 많습니다. Firebase 공식문서 링크: https://firebase.google.com/docs/flutter/setup?authuser=0&hl=ko&platform=ios Flutter 앱에 Firebase 추가 의견 보내

mroh1226.tistory.com

2. Firebase Authenication 기능 추가하기: https://mroh1226.tistory.com/155

 

4. [Firebase] Authentication (로그인 인증) 기능 추가하기

Firebase와 연동된 앱에 사용자가 로그인하면, 로그인 정보를 인증하는 기능을 추가해보겠습니다. 1. 아래 Console 링크에 접속 > 프로젝트로 이동 링크: https://console.firebase.google.com/ 로그인 - Google 계

mroh1226.tistory.com

3. main 함수에 Firebase 추기화 해주기

main.dart
void main() async {
  ...

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  
  ...
 }

구현할 기능, 화면 모습

(좌측) Firebase Console&nbsp; /&nbsp; (우측) Flutter App

  • 로그인, 로그아웃, 회원가입 3가지 기능을 가진 버튼과 이메일, 비밀번호를 입력받을 수 있는 텍스트 필드를 구현
  • 이메일/비밀번호 방식을 통해 회원가입 버튼을 클릭, User Authenication을 추가해줍니다.
  • 추가된 계정은 Firebase Console에 바로 추가됩니다.
  • 로그인으로 Sign In, 로그아웃으로 Sign Out 할 수 있습니다.

예시 설명.

이 소스 코드는 Flutter를 사용하여 Firebase Authentication을 통한 이메일 및 비밀번호 인증을 처리하는 앱의 일부입니다. 아래에 각 클래스와 주요 소스 코드에 대한 설명을 제공합니다.

 

 

1. AuthRepository 클래스:

  • Firebase의 Authentication을 다루기 위한 클래스입니다.
  • get user 메서드: 현재 로그인된 사용자를 가져옵니다.
  • get isLoggedIn 메서드: 사용자가 로그인되어 있는지 여부를 확인합니다.
  • authStateChange 메서드: 사용자 상태의 변화를 스트림으로 반환합니다.
  • createUser, userSignOut, userSignIn 메서드: 사용자 생성, 로그아웃, 로그인 기능을 제공합니다.

2. Providers:

  • authRepoProvider: **AuthRepository**의 인스턴스를 생성하는 Provider입니다.
  • authStreamProvider: **AuthRepository**의 **authStateChange**를 사용하여 사용자 인증 상태 변화를 관찰하는 Provider입니다.
AuthRepository (Repository)
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

////Firebase///////////////////////////////////////////////////////////////////////
class AuthRepository {
  final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
  //Firebase의 Authentication을 다룰 수 있도록 FirebaseAuth 인스턴스를 생성함
  User? get user => _firebaseAuth.currentUser; //get 정의하는 방법 중 하나
  bool get isLoggedIn => user != null;
  //_firebaseAuth.currentUser: Firebase에 currentUser가 있는지? 유무로 로그인 되었는지 확인함
  // bool isLoggedIn() {
  //   return (_firebaseAuth.currentUser != null);
  // }
  Stream<User?> authStateChange() => _firebaseAuth.authStateChanges();

  Future<void> createUser(String email, String password) async {
    await _firebaseAuth.createUserWithEmailAndPassword(
        email: email, password: password);
    //.createUserWithEmailAndPassword(): 유저 이메일/비밀번호로 로그인 인증을 생성함
  }

  Future<void> userSignOut() async {
    await _firebaseAuth.signOut();
    //.signOut() 로그아웃하는 메서드
  }

  Future<void> userSignIn(String email, String password) async {
    await _firebaseAuth.signInWithEmailAndPassword(
        email: email, password: password);
    //.signInWithEmailAndPassword: firebase 이메일,비밀번호로 로그인
  }
}

final authRepoProvider = Provider((ref) => AuthRepository());
//AuthRepository() 를 expose 하기 위해 만들어진 Provider
final authStreamProvider =
    StreamProvider((ref) => ref.read(authRepoProvider).authStateChange());
//AuthRepository()의 _firebaseAuth.authStateChanges() 로
//sign-in 이나 sign-out의 실시간 변화를 expose하기 위해 만들어진 Provider

 

3. LoginViewModel 클래스:

  • **AsyncNotifier<void>**를 확장한 클래스로, 사용자 로그인 및 관련 작업을 처리합니다.
  • logIn, logOut, createUsers, getEmail 메서드: 로그인, 로그아웃, 사용자 생성 및 이메일 가져오기 기능을 제공합니다.
LoginViewModel (ViewModel)
class LoginViewModel extends AsyncNotifier<void> {
  late final AuthRepository _authRepository;

  @override
  FutureOr<void> build() async {
    _authRepository = ref.read(authRepoProvider);
    //authRepoProvider로 AuthRepository()가 가진 것들을 _authRepository에 넣음
  }

  Future<void> logIn(
      String email, String password, BuildContext context) async {
    state = const AsyncValue.loading();
    //로딩 상태로 들어감
    state = await AsyncValue.guard(
        //guard: 에러발생과 데이터 처리결과 상태를 상황에 맞게 state에 넣어줌
        () async => await _authRepository.userSignIn(email, password));
    if (state.hasError) {
      final snackBar = SnackBar(
          content: Text(
              (state.error as FirebaseException).message ?? "알수없는 오류입니다."));
      ScaffoldMessenger.of(context).showSnackBar(snackBar);
    } else {
      return;
    }
  }

  Future<void> logOut() async {
    await ref.read(authRepoProvider).userSignOut();
  }

  Future<void> createUsers(String email, String password) async {
    state = const AsyncValue.loading();
    await ref.read(authRepoProvider).createUser(email, password);
    state = const AsyncValue.data(null);
  }

  String getEmail() {
    String email = _authRepository.user?.email ?? "로그인 해주세요.";
    return email;
  }
}

final userStateProvider = StateProvider((ref) => {});
final userProvider = AsyncNotifierProvider<LoginViewModel, void>(
  () => LoginViewModel(),
);

 

 

4. LoginPage 클래스:

  • **ConsumerStatefulWidget**를 상속한 클래스로, 상태를 사용하여 UI를 업데이트합니다.
  • _onTapLogin, _onTapLogout, _onTapCreateUser 메서드: 버튼 클릭 시 수행되는 로그인, 로그아웃, 사용자 생성 동작을 처리합니다.
  • build 메서드: UI를 구성하고, 사용자가 로그인되어 있는지 여부에 따라 다른 아이콘 및 텍스트를 표시합니다.

5. 위젯 및 UI:

  • **TextFormField**를 사용하여 이메일 및 비밀번호를 입력 받습니다.
  • **CupertinoButton**을 사용하여 로그인, 로그아웃, 사용자 생성 기능을 수행하는 버튼을 생성합니다.
  • 사용자가 로그인되어 있으면 초록색 아이콘을, 그렇지 않으면 기본 사용자 아이콘을 표시합니다.
LoginPage (View)
class LoginPage extends ConsumerStatefulWidget {
  const LoginPage({super.key});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _LoginPageState();
}

class _LoginPageState extends ConsumerState<LoginPage> {
  final TextEditingController _emailTextEditingController =
      TextEditingController();
  final TextEditingController _passwordTextEditingController =
      TextEditingController();

  Future<void> _onTapLogin() async {
    if (_emailTextEditingController.text.isEmpty ||
        _passwordTextEditingController.text.isEmpty) return;

    ref.read(userProvider.notifier).logIn(_emailTextEditingController.text,
        _passwordTextEditingController.text, context);
    setState(() {});
  }

  void _onTapLogout() {
    ref.read(userProvider.notifier).logOut();
    setState(() {});
  }

  void _onTapCreateUser() {
    ref.read(userProvider.notifier).createUsers(
        _emailTextEditingController.text, _passwordTextEditingController.text);
    setState(() {});
  }

  @override
  void initState() {
    _emailTextEditingController.addListener(() {});
    _passwordTextEditingController.addListener(() {});
    super.initState();
  }

  @override
  void dispose() {
    _emailTextEditingController.dispose();
    _passwordTextEditingController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Form(
            child: Center(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ref.watch(authRepoProvider).isLoggedIn
              ? Icon(
                  Icons.gpp_good_sharp,
                  size: 100,
                  color: Colors.green.shade200,
                )
              : const Icon(
                  Icons.supervised_user_circle,
                  color: Colors.white,
                  size: 100,
                ),
          ref.watch(userProvider).isLoading
              ? const CircularProgressIndicator()
              : Text(
                  ref.read(userProvider.notifier).getEmail(),
                  style: TextStyle(
                      color: Colors.amber.shade600,
                      fontSize: 30,
                      fontWeight: FontWeight.bold),
                ),
          TextFormField(
            textAlign: TextAlign.center,
            controller: _emailTextEditingController,
            keyboardType: TextInputType.emailAddress,
            validator: (value) {
              if (value != null || value != "") {
                return "check Your Email";
              } else {
                return null;
              }
            },
          ),
          TextFormField(
            textAlign: TextAlign.center,
            controller: _passwordTextEditingController,
            keyboardType: TextInputType.number,
            validator: (value) {
              if (value != null || value != "") {
                return "It is Not Safe";
              } else {
                return null;
              }
            },
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: CupertinoButton(
                  disabledColor: Colors.grey,
                  color: Colors.green.shade500,
                  onPressed: _onTapLogin,
                  child: const Text("로그인"),
                ),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
                child: CupertinoButton(
                  disabledColor: Colors.grey,
                  color: Colors.red.shade500,
                  onPressed: _onTapLogout,
                  child: const Text("로그아웃"),
                ),
              ),
            ],
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: CupertinoButton(
              disabledColor: Colors.grey,
              color: Colors.blue.shade300,
              onPressed: _onTapCreateUser,
              child: const Text("회원으로 가입하기"),
            ),
          ),
        ],
      ),
    )));
  }
}

 


전체소스.

import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

////Firebase///////////////////////////////////////////////////////////////////////
class AuthRepository {
  final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
  //Firebase의 Authentication을 다룰 수 있도록 FirebaseAuth 인스턴스를 생성함
  User? get user => _firebaseAuth.currentUser; //get 정의하는 방법 중 하나
  bool get isLoggedIn => user != null;
  //_firebaseAuth.currentUser: Firebase에 currentUser가 있는지? 유무로 로그인 되었는지 확인함
  // bool isLoggedIn() {
  //   return (_firebaseAuth.currentUser != null);
  // }
  Stream<User?> authStateChange() => _firebaseAuth.authStateChanges();

  Future<void> createUser(String email, String password) async {
    await _firebaseAuth.createUserWithEmailAndPassword(
        email: email, password: password);
    //.createUserWithEmailAndPassword(): 유저 이메일/비밀번호로 로그인 인증을 생성함
  }

  Future<void> userSignOut() async {
    await _firebaseAuth.signOut();
    //.signOut() 로그아웃하는 메서드
  }

  Future<void> userSignIn(String email, String password) async {
    await _firebaseAuth.signInWithEmailAndPassword(
        email: email, password: password);
    //.signInWithEmailAndPassword: firebase 이메일,비밀번호로 로그인
  }
}

final authRepoProvider = Provider((ref) => AuthRepository());
//AuthRepository() 를 expose 하기 위해 만들어진 Provider
final authStreamProvider =
    StreamProvider((ref) => ref.read(authRepoProvider).authStateChange());
//AuthRepository()의 _firebaseAuth.authStateChanges() 로
//sign-in 이나 sign-out의 실시간 변화를 expose하기 위해 만들어진 Provider

////ViewModel/////////////////////////////////////////////////////////////////////////
class LoginViewModel extends AsyncNotifier<void> {
  late final AuthRepository _authRepository;

  @override
  FutureOr<void> build() async {
    _authRepository = ref.read(authRepoProvider);
    //authRepoProvider로 AuthRepository()가 가진 것들을 _authRepository에 넣음
  }

  Future<void> logIn(
      String email, String password, BuildContext context) async {
    state = const AsyncValue.loading();
    //로딩 상태로 들어감
    state = await AsyncValue.guard(
        //guard: 에러발생과 데이터 처리결과 상태를 상황에 맞게 state에 넣어줌
        () async => await _authRepository.userSignIn(email, password));
    if (state.hasError) {
      final snackBar = SnackBar(
          content: Text(
              (state.error as FirebaseException).message ?? "알수없는 오류입니다."));
      ScaffoldMessenger.of(context).showSnackBar(snackBar);
    } else {
      return;
    }
  }

  Future<void> logOut() async {
    await ref.read(authRepoProvider).userSignOut();
  }

  Future<void> createUsers(String email, String password) async {
    state = const AsyncValue.loading();
    await ref.read(authRepoProvider).createUser(email, password);
    state = const AsyncValue.data(null);
  }

  String getEmail() {
    String email = _authRepository.user?.email ?? "로그인 해주세요.";
    return email;
  }
}

final userStateProvider = StateProvider((ref) => {});
final userProvider = AsyncNotifierProvider<LoginViewModel, void>(
  () => LoginViewModel(),
);

////View////////////////////////////////////////////////////////////////////
class LoginPage extends ConsumerStatefulWidget {
  const LoginPage({super.key});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _LoginPageState();
}

class _LoginPageState extends ConsumerState<LoginPage> {
  final TextEditingController _emailTextEditingController =
      TextEditingController();
  final TextEditingController _passwordTextEditingController =
      TextEditingController();

  Future<void> _onTapLogin() async {
    if (_emailTextEditingController.text.isEmpty ||
        _passwordTextEditingController.text.isEmpty) return;

    ref.read(userProvider.notifier).logIn(_emailTextEditingController.text,
        _passwordTextEditingController.text, context);
    setState(() {});
  }

  void _onTapLogout() {
    ref.read(userProvider.notifier).logOut();
    setState(() {});
  }

  void _onTapCreateUser() {
    ref.read(userProvider.notifier).createUsers(
        _emailTextEditingController.text, _passwordTextEditingController.text);
    setState(() {});
  }

  @override
  void initState() {
    _emailTextEditingController.addListener(() {});
    _passwordTextEditingController.addListener(() {});
    super.initState();
  }

  @override
  void dispose() {
    _emailTextEditingController.dispose();
    _passwordTextEditingController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Form(
            child: Center(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ref.watch(authRepoProvider).isLoggedIn
              ? Icon(
                  Icons.gpp_good_sharp,
                  size: 100,
                  color: Colors.green.shade200,
                )
              : const Icon(
                  Icons.supervised_user_circle,
                  color: Colors.white,
                  size: 100,
                ),
          ref.watch(userProvider).isLoading
              ? const CircularProgressIndicator()
              : Text(
                  ref.read(userProvider.notifier).getEmail(),
                  style: TextStyle(
                      color: Colors.amber.shade600,
                      fontSize: 30,
                      fontWeight: FontWeight.bold),
                ),
          TextFormField(
            textAlign: TextAlign.center,
            controller: _emailTextEditingController,
            keyboardType: TextInputType.emailAddress,
            validator: (value) {
              if (value != null || value != "") {
                return "check Your Email";
              } else {
                return null;
              }
            },
          ),
          TextFormField(
            textAlign: TextAlign.center,
            controller: _passwordTextEditingController,
            keyboardType: TextInputType.number,
            validator: (value) {
              if (value != null || value != "") {
                return "It is Not Safe";
              } else {
                return null;
              }
            },
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: CupertinoButton(
                  disabledColor: Colors.grey,
                  color: Colors.green.shade500,
                  onPressed: _onTapLogin,
                  child: const Text("로그인"),
                ),
              ),
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
                child: CupertinoButton(
                  disabledColor: Colors.grey,
                  color: Colors.red.shade500,
                  onPressed: _onTapLogout,
                  child: const Text("로그아웃"),
                ),
              ),
            ],
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: CupertinoButton(
              disabledColor: Colors.grey,
              color: Colors.blue.shade300,
              onPressed: _onTapCreateUser,
              child: const Text("회원으로 가입하기"),
            ),
          ),
        ],
      ),
    )));
  }
}

 

 

 

 

반응형
Comments