ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SWeetMe Project - 회원가입, 로그인 기능
    프로젝트 2024. 3. 18. 22:15
    반응형

     

    SWeetMe Project에서 회원 기능을 구현하기 위해 Firebase Authentication API를 활용하였습니다.

     

    Firebase Authentication은 사용자 인증과 로그인 기능을 지원하는게 대표적인데요. 이외에도 익명 로그인, 이메일 인증, 전화번호 인증 등도 지원하고 있으며, 소셜 로그인을 통한 인증도 지원합니다.

     

    해당 API를 활용하면 회원가입, 로그인, 로그아웃 등을 간편하게 구현할 수 있으며, Firebase의 강력한 기능 중 하나인 Firestore 데이터 베이스를 사용할 경우 보안 규칙의 기준이 되는 key인 uid를 해당 API를 통해 생성 및 관리할 수 있습니다.

    추가적으로 카카오나 네이버 등의 소셜 로그인은 OAuth2(Open Authorization 2.0) 프로토콜을 더해서 구현할 수 있습니다. OAuth2를 사용하는 프로젝트의 경우 따로 게시글을 작성하겠습니다.


    void main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await Firebase.initializeApp(
          options: DefaultFirebaseOptions.currentPlatform); // firebase 초기화 기본 코드
      runApp(MainApp());
    }
     

    플러터 앱은 프로젝트의 lib 폴더 안의 main.dart 파일의 main 메소드를 시작점으로 사용합니다. 해당 block에서 WidgetsFlutterBinding, 즉 플러터의 위젯 렌더링 및 이벤트 처리와 같은 UI 작업을 처리하기 위한 기본 작업, Firebase 앱 초기화 등의 작업을 진행합니다. 이외의 서버 통신 등의 비동기 작업의 경우 해당 작업을 진행한 뒤에 runApp을 실행합니다. runApp에는 가장 처음 띄울 위젯을 전달합니다.

    enum Status { uninitialized, authenticated, authenticating, unauthenticated }
    
    class UserProvider extends ChangeNotifier {
      final FirebaseAuth _auth = FirebaseAuth.instance; // 파이어베이스 Auth 객체 인스턴스
      Status _status; // 현재 사용자 상태
      User? _user; // 사용자의 정보 담고 있는 객체
    
      FirebaseAuth get auth => _auth;
      Status get status => _status;
      String get uid => _user?.email ?? '';
    
      UserProvider()
          : _user = null,
            _status = Status.unauthenticated {
        _auth.authStateChanges().listen(_onStateChanged);
      }
    
     Future<void> _onStateChanged(User? user) async {
        _user = user;
        _status = _user != null ? Status.authenticated : Status.unauthenticated;
      }
     
    // 초기 화면 구성
    class MainApp extends StatelessWidget {
      final userProvider = UserProvider();
    
      @override
      Widget build(BuildContext context) {
        return ChangeNotifierProvider(
          create: (context) => userProvider,
          child: MaterialApp(
            debugShowCheckedModeBanner: false,
            theme: ThemeData(
              useMaterial3: true,
              colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
            ),
            initialRoute: '/',
            routes: {
              "/": (context) => SafeArea(child: AuthWrapper()),
              "/LoginView": (context) => SafeArea(child: LoginView()),
              "/HomeView": (context) => SafeArea(child: HomeView()),
              "/SignUpView": (context) => SafeArea(child: SignUpView()),
              "/PasswordResetView": (context) =>
                  SafeArea(child: PasswordResetView()),
              "/PostReadView": (context) => SafeArea(child: PostReadView()),
              "/PostUploadView": (context) => SafeArea(child: PostUploadView()),
              "/ComparisonView": (context) => SafeArea(child: ComparisonView()),
            },
          ),
        );
      }
    }
     

    일반적으로 플러터에서 최상위 위젯으로 사용할 수 있는 것은 MaterialApp, CupertinoApp, WidgetsApp 등이 있습니다.

    MaterialApp은 앱의 기본적인 구성요소를 제공하고, 네비게이션, 테마, 언어 설정 등을 관리할 수 있습니다.

    CupertinoApp은 iOS앱을 만들 때 사용되는 위젯으로, iOS의 디자인 언어와 기능을 제공합니다.

    WidgetsApp은 위 2개의 공통 요소를 추상화한 위젯입니다. 즉 플랫폼 중립적인 위젯입니다.

    반드시 위 3가지 중 하나를 사용하여 앱을 감싸줘야합니다.

     

    user 정보를 저장하고, Provider로 전달할 수 있도록 UserProvider class(ChangeNotifier 클래스를 상속받아서 Provider로 전달할 수 있음)를 만들어 사용하기로 했습니다. 전역적으로 사용할 예정이기에 ChangeNotifierProvider 위젯으로 최상위 위젯인 MaterialApp을 감싸줍니다. 결과적으로 해당 프로바이더가 하위 위젯에서 사용될 수 있도록 context에 제공됩니다. 해당 프로바이더를 사용할 경우 아래 처럼 Provider.of<UserProvider>(context)를 통해 사용하면 됩니다.

    class AuthWrapper extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final userProvider = Provider.of<UserProvider>(context);
        return StreamBuilder(
          stream: userProvider.auth.authStateChanges(),
          builder: (BuildContext context, AsyncSnapshot<User?> snapshot) {
            if (snapshot.data == null) {
              return LoginView();
            } else {
              return HomeView();
            }
          },
        );
      }
    }
    
     

    본 프로젝트에서 서비스 이용자는 필수적으로 회원가입을 해야합니다. 그래서 로그인 상태에 따라 띄우는 위젯을 다르게 구성했습니다.

    Firebase Auth의 authStateChanges stream을 구독하면 auth 상태가 변경될 때 마다 user가 stream으로 들어옵니다.

    로그인할 때는 user가 null이 아니므로 HomeView를 띄우고,

    로그아웃해서 user가 null이 되면 LoginView를 띄우도록 구현했습니다.

     


    LoginView에 대해서 설명하기 전에, 플러터에서 가장 중요한 Widget에 대해서 설명하겠습니다.

    플러터에서 Widget은 크게 2개로 나뉩니다. 상태가 존재하지 않는 Stateless Widget, 상태가 존재하는 Stateful Widget.

    그리고 각 위젯의 생명 주기(life cycle)은 아래와 같습니다. 프로바이더 값을 가져오거나, 비동기 작업을 진행하는 등, 위젯의 생명주기를 이해하지 못한 상태에서 코딩하면 여러 오류가 발생할 수 있으니 주의해야합니다.

     
     
    출처:  https://velog.io/@dnflekf2748/Flutter-%EC%9C%84%EC%A0%AF%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0
     

    1. 상태 생성: createState()

    @override
    State<MyApp> createState() => _MyAppState();
     

    Stateful 위젯은 반드시 createState() 함수를 호출해야 하고 해당 함수에서는 State 클래스를 반환해야합니다.

    실제 프로젝트 진행하면서 따로 건들건 없습니다.

     

    2. 상태 초기화: initState()

    @override
    void initState() {
      super.initState();
      initFunction();
      asyncFunction().then((_){});
      WidgetsBinding.instance
          .addPostFrameCallback((_) async => await initAsync());
    }
    
    Future<void> initAsync() async {
      ...
      setState(() => _isInitComplete = true);
    }
    
    @override
    Widget build(BuildContext context) {
      if (!_isInitComplete) return CustomProgressIndicator();
      ...
    }
     

    위젯을 초기화 할 때 처음 한 번만 호출되는 함수입니다. 주로 초기 데이터 불러오기, 컨트롤러 초기화 등 작업(initFunction)을 진행합니다.

    비동기 함수를 직접적으로 실행할 수 없기때문에 await 대신에 then과 빈 익명함수를 사용하면 await 효과를 낼 수 있습니다.

     

    다만 저는 사용자 경험을 위해서 WidgetsBinding.instance.addPostFrameCallback 함수를 사용했습니다. 해당 함수는 위젯 build 직후에 실행할 콜백함수를 예약하는 함수입니다. 초반에 _isInitComplete를 false로 설정해두고, 여러 작업이 진행하는 동안에는 로딩바를 띄우고, 작업이 끝나면 위젯을 rebuild하는 형태로 구현했습니다.

     

    3. 의존성 변경: didChangeDependencies()

    해당 함수는 initState 다음에 바로 호출됩니다. 위젯이 의존하는 데이터의 객체가 호출될 때마다 추가적으로 호출되며, 공식문서에서는 상속한 위젯이 업데이트 될 경우, 또는 네트워크 호출(API 호출 등)이 필요한 경우 유용하다고 합니다.

    저는 따로 사용하진 않았습니다...

     

    4. 위젯 렌더링: build()

    반드시 위젯을 return해야하며, 가장 자주 실행되는 함수입니다. init->didChange->build 순서로 실행되고, 이후에는 상태 변경, 위젯 업데이트 등의 이벤트가 발생할 경우 다시 실행됩니다. 사용자에게 표시할 인터페이스를 꾸미면 됩니다.

     

    5. 위젯 업데이트: didUpdateWidget(Widget oldWidget)

    부모 위젯이 변경되어서 현재 위젯을 재 구성해야 하는 경우 사용됩니다. 새로운 위젯과 이전 위젯의 차이점을 처리하는 로직을 구현할 수 있습니다. 실제로 사용해본적은 없습니다.

     

    6. 상태 변경: setState()

    위 함수는 개발자가 가장 자주 호출할 함수입니다. 데이터가 변경되었음을 프레임워크에 알리는 함수이며, 현재 context의 위젯을 다시 빌드합니다. 해당 함수도 initState와 마찬가지로 비동기 callback을 사용할 수 없습니다.

     

    7. 위젯 폐기: dispose()

    State 객체를 영구적으로 폐기할 때 호출하는 함수입니다. 해당 위젯에서 네트워크 통신, 스트림 구독을 하고 있는 경우 dispose()함수가 호출되면 모두 중지됩니다.

     

    8. mount

    모든 위젯은 this.mounted attribute가 존재하고 해당 속성이 true가 되었을 때 setState를 사용할 수 있습니다. createState가 state 클래스를 생성하면 BuildContext가 state에 할당되고, 이 때 this.mounted가 true로 설정됩니다. 최종적으로 state를 영구적으로 폐기할 때 this.mounted가 false로 설정되고, 이는 해당 state를 재사용할 수 없다는 뜻입니다.

     

    class LoginView extends StatefulWidget {
      @override
      State<LoginView> createState() => _LoginViewState();
    }
    
    class _LoginViewState extends State<LoginView> {
      final TextEditingController _emailController = TextEditingController();
      final TextEditingController _passwordController = TextEditingController();
      String msg = '';
      bool _isInitComplete = false;
    
      @override
      void initState() {
        super.initState();
        WidgetsBinding.instance
            .addPostFrameCallback((_) async => await initLoginView());
      }
    
      Future<void> initLoginView() async {
        setState(() => _isInitComplete = true);
      }
    
      @override
      void dispose() {
        _emailController.dispose();
        _passwordController.dispose();
        super.dispose();
      }
    }
    
     

    위는 LoginView 위젯의 기반 코드입니다. 전역적으로 email, password를 입력받아야하기때문에 TextEditingController를 생성해줬습니다. 또한 추후 소셜 로그인 기능을 추가하는 등, 확장성을 위해서 기본적인 함수들을 작성해두었습니다.

     

    구체적인 코드는 https://github.com/WooyoungJun/plow_project/tree/master/lib/PageView/BeforeAuth 를 참고해주세요.

    CustomTextField 위젯을 컴포넌트로 만들어서 프로젝트 전반적으로 재사용했습니다. 해당 컴포넌트는 따로 포스팅하겠습니다.

     

    아래는 UserProvider 클래스 내부에 정의한 회원 가입, 로그인, 로그아웃 코드입니다.

    문자열을 return하도록 해서, 에러 시 해당 메세지를 입력창 아래에 띄우도록 구현했습니다.

    Future<String> signUp(
          {required String email, required String password}) async {
        try {
          await _auth.createUserWithEmailAndPassword(
              email: email, password: password);
          CustomToast.showToast('Login 성공');
          return '성공';
        } on FirebaseAuthException catch (err) {
          return err.message!;
        } catch (err) {
          return err.toString();
        }
      }
    
      Future<String> signIn(
          {required String email, required String password}) async {
        try {
          await _auth.signInWithEmailAndPassword(email: email, password: password);
          CustomToast.showToast('Login 성공');
          return '성공';
        } on FirebaseAuthException catch (err) {
          return err.message!;
        } catch (err) {
          return err.toString();
        }
      }
    
      Future<void> signOut() async {
        try {
          await _auth.signOut();
          CustomToast.showToast('Sign Out 성공');
        } catch (err) {
          CustomToast.showToast('Sign Out 에러: $err');
          print(err);
        }
      }
     

    로그인 하게 되면 맨 처음 authStateChanges를 구독해놨으므로 user가 스트림으로 넘어가게되고, StreamBuilder에 의해서 HomeView 위젯을 띄우게 됩니다.

     

    아래는 최종 구현한 모습입니다. 각 컴포넌트에 대해 모두 설명하면 너무 길어질 거 같아서 나중에 포스팅할 기회가 생긴다면 포스팅하도록 하고, 지금은 생략하도록 하겠습니다.

     

    읽어주셔서 감사합니다. 질문 및 문의사항 있으시면 댓글 남겨주시고, 잘못된 내용 있으면 알려주세요!

     

    반응형
Designed by Tistory.