
Firebase SDK를 사용하는 페이지는 해당 기능에 집중하기 위해 UI를 직접 작성하지 않고 실습 자료의 페이지를 가져다 사용하도록 하겠다. 사용하기에 앞서 어떻게 구성되어 있는지 살펴본 후 필요한 코드를 추가한다.
로그인 페이지의 로고는 Flexible 로 감싸져 있다. 이 녀석은 Expanded 처럼 공간을 확장하거나 축소할 수 있지만 child의 크기를 억지로 잡아늘리지 않고 자기 크기를 유지할 수 있도록 하는 녀석이다. 이것을 사용하여 다음과 같이 키보드가 올라와 공간이 좁아지면 로고 크기가 작아지도록 구현할 수 있다.
기본 상태 키보드 올라온 상태
로그인을 위한 email 입력창과 password 입력창은 TextFormField 를 통해 작성되어 있다. 이것들은 TextEditingController 를 사용하여 개별적으로 제어할 수도 있고 Form 으로 묶어서 관리할 수도 있다.
두 방식의 차이
TextEditingControllerForm장점 입력창의 세부 제어 및 실시간 상태 감지 가능 다중 입력창의 유효성 검사 및 일괄 데이터 수집 가능 단점 보일러플레이트 코드가 많고 일괄 처리 불편 실시간 제어 한계 적합한 상황 검색창, 실시간 글자수 제한, OTP 인증번호 입력 등 회원가입, 로그인, 글쓰기, 설문조사 등 데이터 제출 폼
TextEditingController사용 시 유효성 검사 로직도 일일이 작성해야 하고 종료 시dispose()를 해야 하는 등 작성해야 할 게 많지만 더 많은 기능을 사용할 수 있다.
인간 개발자는Form을 사용하는 게 합리적인 상황도 AI와 함께 하면TextEditingController방식도 괜찮을 것 같기도 하고. 아직까지는 trade-off 관점에서 상황에 맞게 사용하는 것 같다.
Form 으로 사용자 입력 제어하기Form 을 사용할 때는 GlobalKey<FormState> 가 필요하다. LoginPage class의 property로 다음과 같은 key를 추가한다.
lib/login_page.dart>LoginPagefinal GlobalKey<FormState> _formKey = GlobalKey<FormState>();
TextFormField 를 를 포함한 Column 째로 Form 으로 감싸고 key 에 방금 만든 key를 전달한다.
lib/login_page.dart>LoginPage// 앞 부분 생략 return Scaffold( body: SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // TextFormField를 비롯한 widget들 ], ), ), ), ), ); // 뒷 부분 생략
이제 _formKey 를 통해 입력창에 입력된 텍스트를 한 번에 검증하거나 입력된 값을 일괄적으로 변수에 저장하는 등의 작업을 수행할 수 있다.
검증 코드는 TextFormField 의 validator property에 String? Function(String?) callback function으로 작성하며 검증 오류 메시지를 return한다. 오류가 없다면 null 을 반환한다.
값을 변수에 저장하는 코드는 TextFormField 의 onSaved property에 void Function(String?) callback function으로 작성한다.
위 두 개의 callback function은 모두 입력된 텍스트를 parameter로 받는다.
lib/login_page.dart>LoginPage> email 입력창TextFormField( style: TextStyle(color: Colors.black), decoration: InputDecoration( labelText: 'Email', labelStyle: TextStyle(color: Colors.black, fontWeight: FontWeight.w500,), filled: true, fillColor: Colors.grey.shade200, hintText: 'example@example.com', border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), prefixIcon: Icon(Icons.email_outlined), ), onSaved: (value) { _email = value; }, validator: (value) { if (value == null || value.isEmpty) { return '텍스트를 입력하세요.'; } return null; }, ),
lib/login_page.dart>LoginPage> password 입력창TextFormField( style: TextStyle(color: Colors.black), decoration: InputDecoration( labelText: 'Password', labelStyle: TextStyle(color: Colors.black, fontWeight: FontWeight.w500,), filled: true, fillColor: Colors.grey.shade200, prefixIcon: Icon(Icons.lock_outline, color: Colors.black), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), ), obscureText: true, onSaved: (value) { _password = value; }, validator: (value) { if (value == null || value.isEmpty) { return '텍스트를 입력하세요.'; } if (value.length < 6) { return '6자리 이상 입력하세요.'; } return null; }, ),
사용자 입력을 돕는 요소들
property description detail labelText무엇을 입력하는 곳인지 알려주는 이름표 입력창이 비어있을 때는 입력칸 내부에 표시되다가, 입력창을 클릭하거나 글자를 입력하면 입력창 위쪽으로 작게 이동 hintText어떤 형식으로 입력해야 하는지 예시 입력창 내부 배경에 연한 회색으로 표시되며, 사용자가 글자를 입력하기 시작하면 사라짐 helperText입력창 아래쪽에 표시되는 부연 설명이나 주의 사항 하단에 작고 연한 글씨로 노출되다가 에러 텍스트 발생 시 사라짐 errorText유효성 검사에 실패했을 때 나타나는 경고 메시지 입력창 아래쪽에 붉은색 글씨로 표시되며, 동시에 입력창의 테두리와 라벨 등도 함께 붉은색으로 변경되어 시각적 경고
로그인 버튼을 누르면 유효성 검사를 수행 후 property에 값을 저장하도록 onPressed 에 callback function을 작성한다.
lib/login_page.dart>LoginPage> 로그인 버튼onPressed: () { if (_formKey.currentState?.validate() ?? false) { _formKey.currentState?.save(); } },
회원가입 페이지도 실습 자료에서 가져오는데, 이 녀석은 아직 우리의 Router에 들어 있지 않으므로 거기에도 추가해 주어야 한다.
lib/workout_router.dart// 앞 부분 생략 StatefulShellBranch( navigatorKey: _settingsNavigatorKey, routes: [ GoRoute( path: '/settings', builder: (context, state) => SettingsPage(), routes: [ GoRoute( path: 'login', builder: (context, state) => LoginPage(), routes: [ GoRoute( path: 'registration', builder: (context, state) => RegistrationPage(), ), ], ), ] ), ], ), // 뒷 부분 생략
lib/login_page.dart 에서 [Sign up] 버튼을 누르면 '/settings/login/registration' 으로 이동하도록 router를 작성한다.
lib/login_page.dart>LoginPage> 회원가입 버튼onPressed: () { context.go('/settings/login/registration'); },
Form 으로 사용자 입력 제어하기로그인 페이지에서의 코드와 크게 다르지 않다. 특이사항은 '비밀번호 확인' 기능 정도? 이 녀석은 controller를 사용하여 비밀번호 입력창에 들어온 텍스트와 일치하는지 확인해야 한다. 이를 위해 다음과 같은 controller를 추가한다.
lib/registration_page.dart>RegistrationPagefinal TextEditingController _passwordController = TextEditingController();
그리고 비밀번호 입력창에 이 controller를 연결한 뒤, 비밀번호 확인 입력창의 검증 코드에 이를 활용하는 코드를 작성한다.
lib/registration_page.dart>RegistrationPage> 비밀번호 입력창 및 비밀번호 확인 입력창TextFormField( controller: _passwordController, decoration: InputDecoration( labelText: '비밀번호', labelStyle: textTheme.headlineSmall, hintText: '비밀번호를 입력하세요', helperText: '*비밀번호는 6자 이상 입력해주세요.', helperStyle: textTheme.bodySmall?.copyWith( color: colorScheme.primary, ), border: UnderlineInputBorder(), floatingLabelBehavior: FloatingLabelBehavior.always, ), obscureText: true, onSaved: (value) { _password = value; }, validator: (value) { if (value == null || value.isEmpty) { return '텍스트를 입력하세요.'; } if (value.length < 6) { return '6자리 이상 입력하세요.'; } return null; }, ), SizedBox(height: 16), TextFormField( obscureText: true, decoration: InputDecoration( labelText: '비밀번호확인', labelStyle: textTheme.headlineSmall, hintText: '비밀번호를 한번 더 입력하세요', border: UnderlineInputBorder(), floatingLabelBehavior: FloatingLabelBehavior.always, ), validator: (value){ if(value==null || value.isEmpty){ return '비밀번호를 한번 더 입력하세요.'; }else if(value != _passwordController.text){ return '비밀번호가 일치하지 않습니다.'; } return null; }, ),
Firebase console에서 생성한 프로젝트 콘솔에 들어가서 좌측 메뉴에서 [보안 > Authentiation] 선택하고 [시작하기] 눌러 [로그인 방법]에서 [이메일/패스워드]를 추가한다.
이메일/패스워드 방식은 보안 상의 이슈가 있을 수 있다는 알림이 뜨지만 우리는 실습용이기에 이대로 진행한다.
Firebase Auth를 사용하기 위해서는 pubspec.yaml 에 등록한 뒤 [pub get] 해주어야 한다.
pubspec.yamldependencies: flutter: sdk: flutter cupertino_icons: ^1.0.8 audioplayers: ^6.8.1 flex_color_scheme: ^8.4.0 intl: ^0.20.3 go_router: ^17.4.0 shared_preferences: ^2.5.5 firebase_core: ^4.13.0 firebase_auth: ^6.5.7
우리는 Backend에 해당하는 Firebase와의 소통을 하는 녀석을 별도로 작성할 것이다. Service라고 칭하며 다음과 같은 구조로 구현된다.

Backend 기능별로 하나의 Service를 만들 건데 일단은 Firebase Auth를 다루는 Service를 작성하도록 하겠다.
lib/firebase_auth_service.dartimport 'package:firebase_auth/firebase_auth.dart'; class FirebaseAuthService { final FirebaseAuth _auth; FirebaseAuthService() : _auth = FirebaseAuth.instance { _auth.setLanguageCode('kr'); } Future<void> signUpWithEmail() async {} Future<void> signInWithEmail() async {} Future<void> resetPassword() async {} Future<void> deleteAccount() async {} Future<void> signOut() async {} Future<void> updateProfile() async {} }
이 중에서 먼저 회원가입 기능을 작성하여 버튼에 연결해 보자. Backend와 직접 소통할 경우 암호화, 통신 등을 고려해 주어야 하지만 SDK를 통해 소통하므로 많은 것들이 내부적으로 처리된다. 다만, 네트워크 이슈 등이 있을 수 있기 때문에 try-catch 를 통해 예외처리를 해주는 게 좋다.
일관성 있는 예외처리 규칙은 유지보수를 좋게 한다. 에러 코드에 따른 개별적인 예외처리를 상세하게 해주는 게 좋지만 여기서는 간단하게 하고 넘어가겠다. 에러 코드에 대한 명세는 Backend 개발자가 작성할 의무가 있는데, SDK를 사용할 경우 SDK 문서에서 이를 확인할 수 있다.
lib/firebase_auth_service.dartFuture<void> signUpWithEmail({ required String email, required String password, String? name, }) async { String? errorMessage; try { await _auth.createUserWithEmailAndPassword( email: email, password: password, ); await _auth.currentUser?.updateDisplayName(name); await _auth.currentUser?.sendEmailVerification(); } on FirebaseAuthException catch (authError) { switch (authError.code) { case 'weak-password': errorMessage='패스워드가 취약합니다.'; case 'email-already-in-use': errorMessage='이미 사용 중인 이메일입니다.'; default: errorMessage=authError.message; } } catch(e) { errorMessage = '회원가입 에러: $e'; } if (errorMessage != null) { throw Exception(errorMessage); } }
이제 RegistrationPage 의 회원가입 버튼에서 입력창의 내용을 property에 저장한 후 회원가입을 수행하도록 작성할 수 있다.
lib/registration_page.dartimport 'package:flutter/material.dart'; import 'firebase_auth_service.dart'; class RegistrationPage extends StatelessWidget { RegistrationPage({super.key}); final _formKey = GlobalKey<FormState>(); String? _name; String? _email; String? _password; final TextEditingController _passwordController = TextEditingController(); final FirebaseAuthService _auth = FirebaseAuthService(); //생략 }
lib/registration_page.dart>RegistrationPage> 회원가입 버튼onPressed: () { if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); _auth.signUpWithEmail( email: _email!, password: _password!, name: _name, ); } },