이번 글에서는 Flutter
와 Firebase
를 결합하여 회원가입 화면을 구현하는 방법에 대해 자세히 알아보겠습니다. 함께 사용하면 강력한 기능과 사용자 관리 기능을 쉽게 구현할 수 있습니다.
CreateAccountScreen
클래스는 진중문고 앱에 로그인하기 위한 계정을 생성하는 회원가입창입니다. 상단에는 진중문고 앱의 로고를 삽입하였고, 그 아래에는 회원가입에 필요한 정보들을 입력받는 TextFormField
들과 회원가입 버튼으로 구성하였습니다.
회원가입 입력필드에 입력된 값을 저장하기 위한 controller
와 입력값에 대한 유효성 검사를 위한 FoucusNode
를 먼저 선언하였습니다.
class _CreateAccountScreenState extends State<CreateAccountScreen> {
final TextEditingController _emailController = TextEditingController();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _passwordConfirmController =
TextEditingController();
final TextEditingController _militaryNumberController =
TextEditingController();
final TextEditingController _unitNameController = TextEditingController();
final _formKey = GlobalKey<FormState>();
FocusNode _emailFocus = new FocusNode();
FocusNode _passwordFocus = new FocusNode();
//...
}
아래 코드를 보면 회원가입 정보를 Form
과 TextFormField
로 회원가입 정보 입력을 받고 있습니다. FormKey
는 폼 위젯을 식별하는 데 사용되는 특별한 키입니다. 이 키를 사용하면 폼 위젯 내의 필드들을 유효성 검사하거나 저장할 수 있습니다.
Form(
key: _formKey,
child: Container(
alignment: Alignment.center,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Color(0xA545B0C5),
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 폼 필드들
],
),
),
),
)
아래 코드는 위 //폼 필드들
에 들어가는 TextFormField
들입니다. 모든 입력필드에는 validator
로 입력값의 타당성을 판단하는데, 기본적으로 모든 입력필드에 대해 빈 입력값이 없는지 확인하고, 특히 이메일과 비밀번호는 각각 CheckValidate().validateEmail
과 CheckValidate().validatePassword
로 이메일 양식을 충족하는지와 안전한 비밀번호의 조건을 만족하는지 판단합니다. 이 과정에서 앞서 선언한 focusNode
인 _emailFocus
와 _passwordFocus
를 활용하였습니다.
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
margin: EdgeInsets.only(top: heightRatio * 30),
width: widthRatio * 250,
child: TextFormField(
focusNode: _emailFocus,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: Icon(Icons.email),
labelText: "e-mail",
labelStyle: TextStyle(
color: Color(0xE5001F3F),
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.40,
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xE5001F3F),
),
),
isDense: true,
hintText: '이메일을 입력하세요',
hintStyle: TextStyle(
color: Color(0xFFCCCCCC),
fontSize: 10,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
errorStyle: TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
),
style: TextStyle(
color: Colors.black,
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
controller: _emailController,
validator: (value) => CheckValidate().validateEmail(_emailFocus, value ?? ''),
onSaved: (value) {},
),
),
Container(
margin: EdgeInsets.only(top: heightRatio * 30),
width: widthRatio * 250,
child: TextFormField(
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: Icon(Icons.badge),
labelText: "닉네임",
labelStyle: TextStyle(
color: Color(0xE5001F3F),
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.40,
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xE5001F3F),
),
),
isDense: true,
hintText: '닉네임을 입력하세요',
hintStyle: TextStyle(
color: Color(0xFFCCCCCC),
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
errorStyle: TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
// 다른 속성들 설정
),
style: TextStyle(
color: Colors.black,
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
controller: _usernameController,
validator: (value) {
if (value == null || value.isEmpty) {
return '닉네임을 입력해주세요.';
}
return null;
},
onSaved: (value) {},
),
),
Container(
margin: EdgeInsets.only(top: heightRatio * 30),
width: widthRatio * 250,
child: TextFormField(
focusNode: _passwordFocus,
textInputAction: TextInputAction.next,
obscureText: true,
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock),
labelText: "P/W",
labelStyle: TextStyle(
color: Color(0xE5001F3F),
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.40,
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xE5001F3F),
),
),
isDense: true,
hintText: '비밀번호를 입력하세요',
hintStyle: TextStyle(
color: Color(0xFFCCCCCC),
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
errorStyle: TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
// 다른 속성들 설정
),
style: TextStyle(
color: Colors.black,
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
controller: _passwordController,
validator: (value) => CheckValidate().validatePassword(_passwordFocus, value ?? ''),
onSaved: (value) {},
),
),
Container(
margin: EdgeInsets.only(top: heightRatio * 30),
width: widthRatio * 250,
child: TextFormField(
textInputAction: TextInputAction.next,
obscureText: true,
decoration: InputDecoration(
prefixIcon: Icon(Icons.enhanced_encryption),
labelText: "P/W C",
labelStyle: TextStyle(
color: Color(0xE5001F3F),
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.40,
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xE5001F3F),
),
),
isDense: true,
hintText: '비밀번호를 한번 더 입력하세요',
hintStyle: TextStyle(
color: Color(0xFFCCCCCC),
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
errorStyle: TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
// 다른 속성들 설정
),
style: TextStyle(
color: Colors.black,
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
controller: _passwordConfirmController,
validator: (value) {
if (value == null || value.isEmpty) {
return '비밀번호를 한 번 더 입력해주세요.';
} else if (value != _passwordController.text.toString()) {
return '비밀번호가 일치하지 않습니다.';
}
return null;
},
onSaved: (value) {},
),
),
Container(
margin: EdgeInsets.only(top: heightRatio * 30),
width: widthRatio * 250,
child: TextFormField(
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: Icon(Icons.person),
labelText: "군번",
labelStyle: TextStyle(
color: Color(0xE5001F3F),
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.40,
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xE5001F3F),
),
),
isDense: true,
hintText: '군번을 입력하세요',
hintStyle: TextStyle(
color: Color(0xFFCCCCCC),
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
errorStyle: TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
// 다른 속성들 설정
),
controller: _militaryNumberController,
validator: (value) {
if (value == null || value.isEmpty) {
return '군번을 입력해주세요.';
}
return null;
},
onSaved: (value) {},
),
),
Container(
margin: EdgeInsets.only(top: heightRatio * 30,),
width: widthRatio * 250,
child: TextFormField(
textInputAction: TextInputAction.done,
decoration: InputDecoration(
prefixIcon: Icon(Icons.groups_2),
labelText: "부대명",
labelStyle: TextStyle(
color: Color(0xE5001F3F),
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.40,
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Color(0xE5001F3F),
),
),
isDense: true,
hintText: '부대명을 입력하세요',
hintStyle: TextStyle(
color: Color(0xFFCCCCCC),
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
errorStyle: TextStyle(
color: Colors.red,
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
// 다른 속성들 설정
),
style: TextStyle(
color: Colors.black,
fontSize: 13,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.33,
),
controller: _unitNameController,
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
validator: (value) {
if (value == null || value.isEmpty) {
return '부대명을 입력해주세요.';
}
return null;
},
onSaved: (value) {},
),
),
],
),
validator에 의해 유효성 검사에서 Fail하면 아래와 같이 경고문구가 나오게 됩니다.
입력필드에 입력을 마친 후 회원가입 버튼을 누르게 되면, 입력받은 e-mail과 password 정보는 FirebaseAuth.instance.createUserWithEmailAndPassword()
를 활용하여 계정을 생성하며, 회원가입창의 다른 추가정보들은 Firestore
의 users
컬렉션에 직전에 만들어진 계정 e-mail을 id
로 갖는 문서를 만들어 저장합니다. 아래 사진은 회원가입을 통해 저장된 Firestore
데이터입니다.
Container(
width: widthRatio * 250,
height: heightRatio * 52,
margin: EdgeInsets.symmetric(
vertical: heightRatio * 30,
),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF46B1C6), // 배경색 설정
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0), // 원하는 값으로 조절
),
),
onPressed: () async {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
/// 회원가입 정보를 문자열 형태로 가져오기
String emailValue = _emailController.text.toString();
String usernameValue = _usernameController.text.toString();
String passwordValue = _passwordController.text.toString();
String passwordConfirmValue = _passwordConfirmController.text.toString();
String militartyNumberValue = _militaryNumberController.text.toString();
String unitNameValue = _unitNameController.text.toString();
/// 회원가입 정보로 Account Instance 생성
Account newAccount = Account(
email: emailValue,
username: usernameValue,
password: passwordValue,
passwordConfirm: passwordConfirmValue,
militaryNumber: militartyNumberValue,
unitName: unitNameValue,
);
if (!newAccount.isEmpty()) {
try {
UserCredential userCredential = await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: newAccount.email,
password: newAccount.password,
);
/// FirebaseFirestore에 회원가입 상세정보 저장
FirebaseFirestore _firestore = FirebaseFirestore.instance;
await _firestore.collection("users").doc(newAccount.email).set({
"email": newAccount.email,
"username": newAccount.username,
"militaryNumber": newAccount.militaryNumber,
"unitName": newAccount.unitName,
"bookCount": 0,
"rank": 0,
});
FirebaseAuth.instance.signOut();
Navigator.pop(context);
} on FirebaseAuthException catch (e) {
if (e.code == 'weak-password') {
print('The password provided is too weak.');
} else if (e.code == 'email-already-in-use') {
print('The account already exists for that email.');
}
} catch (e) {
print(e);
}
}
}
},
child: Container(
alignment: Alignment.center,
child: Text(
'가입하기',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontFamily: 'GowunBatang',
fontWeight: FontWeight.w700,
height: 0,
letterSpacing: -0.40,
),
),
),
),
)