[Flutter] Flutter로 Firebase를 활용한 회원가입 화면 구현하기: CreateAccountScreen 클래스

StudipU·2024년 3월 2일
0

이번 글에서는 FlutterFirebase를 결합하여 회원가입 화면을 구현하는 방법에 대해 자세히 알아보겠습니다. 함께 사용하면 강력한 기능과 사용자 관리 기능을 쉽게 구현할 수 있습니다.

CreateAccountScreen 클래스 소개 ✨

CreateAccountScreen 클래스는 진중문고 앱에 로그인하기 위한 계정을 생성하는 회원가입창입니다. 상단에는 진중문고 앱의 로고를 삽입하였고, 그 아래에는 회원가입에 필요한 정보들을 입력받는 TextFormField들과 회원가입 버튼으로 구성하였습니다.

주요 기능 및 코드 분석 🎭

1. Controller와 FocusNode 선언

회원가입 입력필드에 입력된 값을 저장하기 위한 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();
  
  //...
  
}

2. 회원가입 정보 입력 폼

아래 코드를 보면 회원가입 정보를 FormTextFormField로 회원가입 정보 입력을 받고 있습니다. 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().validateEmailCheckValidate().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하면 아래와 같이 경고문구가 나오게 됩니다.

3. 회원가입 버튼

입력필드에 입력을 마친 후 회원가입 버튼을 누르게 되면, 입력받은 e-mail과 password 정보는 FirebaseAuth.instance.createUserWithEmailAndPassword()를 활용하여 계정을 생성하며, 회원가입창의 다른 추가정보들은 Firestoreusers 컬렉션에 직전에 만들어진 계정 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,
        ),
      ),
    ),
  ),
)
profile
컴공 대학생이 군대에서 작성하는 앱 개발 블로그

0개의 댓글