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

Written by 코드팩토리 JC

1월 15, 2024

Flutter 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 {
  @override
  _FormScreenState createState() => _FormScreenState();
}

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

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

Form 위젯 사용하기

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

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

  final formKey = GlobalKey<FormState>();

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

TextFormField 위젯 생성 함수

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

  renderTextFormField({
    @required String label,
    @required FormFieldSetter onSaved,
    @required 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,
        ),
      ],
    );
  }
Dart

TextFormField 의 onSaved, validator 파라미터

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

  @override
  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;
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
Dart

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

Flutter Form

패딩 추가하기

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

  renderTextFormField({
    @required String label,
    @required FormFieldSetter onSaved,
    @required 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),
      ],
    );
  }
Dart
Flutter Form

폼 저장버튼 생성

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

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

폼 저장 후 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,
        ),
      ),
    );
  }
Dart

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

Form 저장 완료

Validator 파라미터 작성하기

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

  @override
  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(),
            ],
          ),
        ),
      ),
    );
  }
Dart

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

Form

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

Form

onSaved 파라미터 작성하기

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

  final formKey = GlobalKey<FormState>();

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

  @override
  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(),
            ],
          ),
        ),
      ),
    );
  }
Dart

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

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

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

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,
        ),
      ),
    );
  }
Dart

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

Form 완료

마지막 꿀팁

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

관련 포스트

플러터에서의 Immutable Programming: copyWith 함수 마스터하기!

플러터에서의 Immutable Programming: copyWith 함수 마스터하기!

서론 불변 프로그래밍: 현대 개발의 핵심 현대 소프트웨어 개발에서 불변 프로그래밍(Immutable Programming)의 중요성은 간과할 수 없는 요소입니다. 플러터(Flutter)에서도 마찬가지로 불변 프로그래밍 개념이 매우 중요하며, copyWith 함수는 이러한 불변성을 유지하는 데 핵심적인 역할을 합니다. 이 글에서는 플러터를 배우기 시작하는 개발자들에게 불변 프로그래밍의 중요성을 강조하고, copyWith 함수의 역할과 사용 방법에 대해 설명 해보겠습니다!...

ChatGPT가 이야기하는 2024년 개발자 로드맵

ChatGPT가 이야기하는 2024년 개발자 로드맵

서론 개발자의 여정을 시작하며 안녕하세요, 미래의 개발자 여러분! 오늘부터 시작하는 여러분의 개발 여정에 함께할 수 있어서 기쁩니다. 2023년은 기술이 매우 빠르게 변화하는 해였으며, 이러한 변화 속에서 개발자가 되기 위한 길은 더욱 다채롭고 흥미로워졌습니다. 이 로드맵은 초보자인 여러분이 개발의 세계에 첫발을 내딛는 데 필요한 기초부터 시작해, 점차 심화 단계로 나아가는 길을 안내해 드릴 것입니다. 백엔드 개발 이 글은 단순히 기술을 배우는 것 이상의 의미를 가집니다....

Flutter Freezed 플러그인! Entity Code Generation은 이거 하나로 끝!

Flutter Freezed 플러그인! Entity Code Generation은 이거 하나로 끝!

https://youtu.be/i5p6wXLAX7I 서론 Flutter 는 Code Generation 기능이 상당히 많이 활성화되어 있어요. 흔히들 많이 사용하는 json_serializable 라이브러리도 있고 retrofit 및 chopper 라이브러리도 있습니다. 오늘 알려드릴 freezed 또한 데이터 클래스에 편의 기능들을 제공해주는 code generation 라이브러리입니다. Freezed vs Json Serializable Code Generation 이라는...