Form 으로 손쉽게 여러개의 텍스트필드 상태관리하기!

Form 으로 손쉽게 여러개의 텍스트필드 상태관리하기!

서론

Flutter 에서 기본적으로 TextField 입력을 받으려면 기본적으로 TextEditingController 를 사용해야 합니다. TextField 가 하나면 괜찮지만 여러개가 되면 될수록 컨트롤러 관리가 굉장히 어려워지죠. 하지만 TextFormField 라는 TextField 의 살짝 변형된 위젯을 이용하면 쉽게 validation 과 값을 받아올 수 있습니다.

이번 시간엔 Form 을 사용해서 여러개의 TextFormField 를 관리하는 방법에 대해 알아보겠습니다.

기본 레이아웃

일단 기본적인 레이아웃을 생성 해보겠습니다. 아래 코드를 참조해주세요.

import 'package:codefactory_youtube_flutter_tutorial/Layouts/DefaultAppbarLayout.dart';
import 'package:flutter/material.dart';

class FormScreen extends StatefulWidget {
  
  _FormScreenState createState() => _FormScreenState();
}

class _FormScreenState extends State<FormScreen> {
  
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Column(
        children: [ 
          // 여기에 폼을 작성할거예요!
        ],
      ),
    );
  }
}

DefaultAppbarLayout 은 제가 제작한 기본 레이아웃 위젯입니다. 그냥 Scaffold 를 래퍼로 사용하는 위젯이라 보면 되겠습니다. 자세한 코드는 제 레포지토리를 참고 해주세요!

Form 위젯 사용하기

Columnchildren 파라미터에 폼을 작성해보도록 하겠습니다.

Form 이라는 위젯은 child 파라미터와 Key 파라미터를 받습니다. child 에는 TextFormField 들을 넣어주면되고 key 에는 GlobalKey 를 넣어주면 됩니다. 이 key 는 나중에 폼 내부의 TextFormField 값들을 저장하고 validation 을 진행하는데 사용됩니다.

  final formKey = GlobalKey<FormState>();

  
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Form(
        key: this.formKey,
        child: Column(
          children: [ 
            // 여기에 TextFormField 들을 입력할거예요!
          ],
        ),
      ),
    );
  }

TextFormField 위젯 생성 함수

TextFormField 를 생성하는 함수를 따로 만들어서 텍스트필드를 생성해보도록 할게요. TextFormField 에서 기본적으로 제공해주는 label 파라미터도 있지만 저는 개인적으로 직접 Text 위젯으로 label 을 제작하는걸 좋아합니다.

  renderTextFormField({
     String label,
     FormFieldSetter onSaved,
     FormFieldValidator validator,
  }) {
    assert(onSaved != null);
    assert(validator != null);

    return Column(
      children: [
        Row(
          children: [
            Text(
              label,
              style: TextStyle(
                fontSize: 12.0,
                fontWeight: FontWeight.w700,
              ),
            ),
          ],
        ),
        TextFormField(
          onSaved: onSaved,
          validator: validator,
        ),
      ],
    );
  }

TextFormField 의 onSaved, validator 파라미터

보시다시피 TextFormFieldonSavedvalidator 파라미터를 받습니다. onSaved 의 시그니처는 FormFieldSetter 라는 typedefvalidator 의 시그니처는 FormFieldValidator 입니다. 둘 다 String 값을 받고 있고 validatorString 의 리턴값 또한 받습니다. 리턴되는 String 은 에러메세지로 사용되게 됩니다.

  
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Form(
        key: this.formKey,
        child: Padding(
          padding: EdgeInsets.all(16.0),
          child: Column(
            children: [
              renderTextFormField(
                label: '이름',
                onSaved: (val) {},
                validator: (val) {
                  return null;
                },
              ),
              renderTextFormField(
                label: '이메일',
                onSaved: (val) {},
                validator: (val) {
                  return null;
                },
              ),
              renderTextFormField(
                label: '비밀번호',
                onSaved: (val) {},
                validator: (val) {
                  return null;
                },
              ),
              renderTextFormField(
                label: '주소',
                onSaved: (val) {},
                validator: (val) {
                  return null;
                },
              ),
              renderTextFormField(
                label: '닉네임',
                onSaved: (val) {},
                validator: (val) {
                  return null;
                },
              ),
            ],
          ),
        ),
      ),
    );
  }

위 코드를 작성하면 아래와 같은 화면을 볼 수 있습니다.

Flutter Form

패딩 추가하기

텍스트 필드간의 간격을 넣기 위해서 renderTextFormField 의 맨 아래에 Container 를 추가해주겠습니다.

  renderTextFormField({
     String label,
     FormFieldSetter onSaved,
     FormFieldValidator validator,
  }) {
    assert(onSaved != null);
    assert(validator != null);

    return Column(
      children: [
        Row(
          children: [
            Text(
              label,
              style: TextStyle(
                fontSize: 12.0,
                fontWeight: FontWeight.w700,
              ),
            ),
          ],
        ),
        TextFormField(
          onSaved: onSaved,
          validator: validator,
        ),
        Container(height: 16.0),
      ],
    );
  }

Flutter Form

폼 저장버튼 생성

이제는 폼 입력을 받을 버튼을 제작 해보겠습니다.

  renderButton() {
    return RaisedButton(
      color: Colors.blue,
      onPressed: () {},
      child: Text(
        '저장하기!',
        style: TextStyle(
          color: Colors.white,
        ),
      ),
    );
  }

폼 저장 후 Snackbar 보여주기

모든 버튼은 onPressed 파라미터를 통해 클릭했을때 액션을 받을 수 있죠. 저희는 이 onPressed 파라미터를 이용해서 폼을 검증하고 저장 해보도록 하겠습니다.

  renderButton() {
    return RaisedButton(
      color: Colors.blue,
      onPressed: () async {
        if(this.formKey.currentState.validate()){
          // validation 이 성공하면 true 가 리턴돼요!
          Get.snackbar(
            '저장완료!',
            '폼 저장이 완료되었습니다!',
            backgroundColor: Colors.white,
          );
        }

      },
      child: Text(
        '저장하기!',
        style: TextStyle(
          color: Colors.white,
        ),
      ),
    );
  }

formKey.currentState.validate() 함수를 실행하면 Form 내부에 있는 TextFormField 들의 validation 결과에 따라 성공이면 true 를 리턴해주고 실패하면 false 를 리턴해줍니다. 일단은 성공하면 스낵바로 '저장이 됐습니다!' 라는 메세지를 보여주도록 할게요.

Form 저장 완료

Validator 파라미터 작성하기

위 스크린샷과 같이 저장하기 버튼을 누르면 이제 스넥바가 떠서 저장이 완료되었다는 걸 알려준다는걸 알 수 있어요. 하지만 벌써 문제점이 보이죠? 텍스트필드에 아무것도 입력을 하지 않아도 저장이됩니다. 저희가 모든 텍스트 필드의 validation 파라미터를 return null 로 저장해서 그래요. 그럼 이번엔 텍스트필드별로 적절한 에러 메세지를 작성 해보도록 하겠습니다.

  
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Form(
        key: this.formKey,
        child: Padding(
          padding: EdgeInsets.all(16.0),
          child: Column(
            children: [
              renderTextFormField(
                label: '이름',
                onSaved: (val) {},
                validator: (val) {
                  if(val.length < 1) {
                    return '이름은 필수사항입니다.';
                  }

                  if(val.length < 2) {
                    return '이름은 두글자 이상 입력 해주셔야합니다.';
                  }

                  return null;
                },
              ),
              renderTextFormField(
                label: '이메일',
                onSaved: (val) {
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '이메일은 필수사항입니다.';
                  }

                  if(!RegExp(
                      r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
                      .hasMatch(val)){
                    return '잘못된 이메일 형식입니다.';
                  }

                  return null;
                },
              ),
              renderTextFormField(
                label: '비밀번호',
                onSaved: (val) {},
                validator: (val) {
                  if(val.length < 1) {
                    return '비밀번호는 필수사항입니다.';
                  }

                  if(val.length < 8){
                    return '8자 이상 입력해주세요!';
                  }
                  return null;
                },
              ),
              renderTextFormField(
                label: '주소',
                onSaved: (val) {},
                validator: (val) {
                  if(val.length < 1) {
                    return '주소는 필수사항입니다.';
                  }
                  return null;
                },
              ),
              renderTextFormField(
                label: '닉네임',
                onSaved: (val) {},
                validator: (val) {
                  if(val.length < 1) {
                    return '닉네임은 필수사항입니다.';
                  }
                  if(val.length < 8) {
                    return '닉네임은 8자 이상 입력해주세요!';
                  }
                  return null;
                },
              ),
              renderButton(),
            ],
          ),
        ),
      ),
    );
  }

빌드 함수를 위와 같이 변경해서 validator 를 모두 입력한 다음에 저장하기 버튼을 다시 눌러보겠습니다.

Form

이제 입력된 값이 없는 텍스트 필드에서는 에러가 나는걸 볼 수 있습니다. 그럼 validation 조건에 부합하게 텍스트필드에 값을 집어넣고 저장하기 버튼을 눌러볼게요.

Form

onSaved 파라미터 작성하기

validation 조건을 만족 시키면 문제없이 스넥바가 뜨는걸 볼 수 있습니다. 하지만 실제 저장을 했을때 저희가 텍스트필드의 값을 받아볼 수 있는 방법이 현재는 없습니다. 그럼 값을 받아보기 위해 onSaved 파라미터에서 저장시 위젯의 변수에 값을 저장하는 기능을 작성해보겠습니다.

  final formKey = GlobalKey<FormState>();

  String name = '';
  String email = '';
  String password = '';
  String address = '';
  String nickname = '';

  
  Widget build(BuildContext context) {
    return DefaultAppbarLayout(
      child: Form(
        key: this.formKey,
        child: Padding(
          padding: EdgeInsets.all(16.0),
          child: Column(
            children: [
              renderTextFormField(
                label: '이름',
                onSaved: (val) {
                  setState(() {
                    this.name = val;
                  });
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '이름은 필수사항입니다.';
                  }

                  if(val.length < 2) {
                    return '이름은 두글자 이상 입력 해주셔야합니다.';
                  }

                  return null;
                },
              ),
              renderTextFormField(
                label: '이메일',
                onSaved: (val) {
                  setState(() {
                    this.email = val;
                  });
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '이메일은 필수사항입니다.';
                  }

                  if(!RegExp(
                      r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
                      .hasMatch(val)){
                    return '잘못된 이메일 형식입니다.';
                  }

                  return null;
                },
              ),
              renderTextFormField(
                label: '비밀번호',
                onSaved: (val) {
                  setState(() {
                    this.password = val;
                  });
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '비밀번호는 필수사항입니다.';
                  }

                  if(val.length < 8){
                    return '8자 이상 입력해주세요!';
                  }
                  return null;
                },
              ),
              renderTextFormField(
                label: '주소',
                onSaved: (val) {
                  setState(() {
                    this.address = val;
                  });
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '주소는 필수사항입니다.';
                  }
                  return null;
                },
              ),
              renderTextFormField(
                label: '닉네임',
                onSaved: (val) {
                  setState(() {
                    this.nickname = val;
                  });
                },
                validator: (val) {
                  if(val.length < 1) {
                    return '닉네임은 필수사항입니다.';
                  }
                  if(val.length < 8) {
                    return '닉네임은 8자 이상 입력해주세요!';
                  }
                  return null;
                },
              ),
              renderButton(),
            ],
          ),
        ),
      ),
    );
  }

값을 보여주는 위젯 작성하기

이렇게 작성하고 실제 저장시 값을 조회해볼 수 있도록 함수를 하나 더 작성해보도록 하겠습니다.

  renderValues(){
    return Column(
      children: [
        Text(
          'name: $name'
        ),
        Text(
          'email: $email'
        ),
        Text(
          'password: $password',
        ),
        Text(
          'address: $address',
        ),
        Text(
          'nickname: $nickname',
        ),
      ],
    );
  }

Form 의 Save 함수

renderValues 함수를 build 함수에 넣었는데 저장을 눌러도 안타깝게도 값이 보이지 않습니다. 왜그럴까요? 그 이유는 저희가 폼의 validate 함수만 실행하고 save 함수를 실행하지 않았기 때문이예요. 버튼의 onPressed 콜백을 조금 변경 해볼게요.

  renderButton() {
    return RaisedButton(
      color: Colors.blue,
      onPressed: () async {
        if (this.formKey.currentState.validate()) {
          // validation 이 성공하면 true 가 리턴돼요!
          
          // validation 이 성공하면 폼 저장하기
          this.formKey.currentState.save();
          
          Get.snackbar(
            '저장완료!',
            '폼 저장이 완료되었습니다!',
            backgroundColor: Colors.white,
          );
        }
      },
      child: Text(
        '저장하기!',
        style: TextStyle(
          color: Colors.white,
        ),
      ),
    );
  }

이제는 값을 저장하면 아래와같이 저장한 값이 화면에 보이고 스넥바까지 정상적으로 뜨는걸 볼 수 있습니다.

Form 완료

마지막 꿀팁

TextFieldonChanged 파라미터를 받아서 상태관리를 하는것보다 훨씬 편하죠? 여기에서 한가지 유용한 팁을 드린다면 TextFormFieldautovalidateModeAutovalidateMode.always 로 지정해보세요! 그럼 저장하기 버튼을 누르기 전에 각 TextFormField 가 자동으로 validation 을 진행하는걸 볼 수 있습니다.

©Code Factory All Rights Reserved