버튼과 입력 — ElevatedButton, TextField, Form

사용자 인터랙션의 기본은 버튼과 텍스트 입력입니다. Flutter의 Material Design 버튼 종류와 TextField, Form 위젯 사용법을 정리해보겠습니다.


버튼 종류

ElevatedButton (강조 버튼)

DART
ElevatedButton(
  onPressed: () {
    print('버튼 클릭!');
  },
  child: const Text('확인'),
)

// 비활성화
ElevatedButton(
  onPressed: null,  // null이면 비활성화
  child: const Text('비활성화'),
)

// 아이콘 포함
ElevatedButton.icon(
  onPressed: () {},
  icon: const Icon(Icons.send),
  label: const Text('전송'),
)

버튼 스타일링

DART
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.blue,
    foregroundColor: Colors.white,
    padding: const EdgeInsets.symmetric(
      horizontal: 32,
      vertical: 16,
    ),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
    elevation: 4,
    textStyle: const TextStyle(fontSize: 16),
  ),
  child: const Text('커스텀 버튼'),
)

다른 버튼 종류들

DART
// TextButton — 텍스트만 있는 가벼운 버튼
TextButton(
  onPressed: () {},
  child: const Text('텍스트 버튼'),
)

// OutlinedButton — 테두리만 있는 버튼
OutlinedButton(
  onPressed: () {},
  child: const Text('아웃라인 버튼'),
)

// IconButton — 아이콘만 있는 버튼
IconButton(
  onPressed: () {},
  icon: const Icon(Icons.favorite),
)

// FloatingActionButton — 플로팅 액션 버튼
FloatingActionButton(
  onPressed: () {},
  child: const Icon(Icons.add),
)

// FilledButton — Material 3 채워진 버튼
FilledButton(
  onPressed: () {},
  child: const Text('채워진 버튼'),
)
버튼용도
ElevatedButton주요 액션 (저장, 확인)
TextButton부수적 액션 (취소, 더보기)
OutlinedButton중간 강조
FilledButtonMaterial 3 기본 버튼
IconButton아이콘 액션 (좋아요, 공유)
FAB화면의 가장 중요한 액션 1개

TextField — 텍스트 입력

기본 사용

DART
class SearchBar extends StatefulWidget {
  const SearchBar({super.key});

  @override
  State<SearchBar> createState() => _SearchBarState();
}

class _SearchBarState extends State<SearchBar> {
  // 텍스트 제어를 위한 컨트롤러
  final _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();  // 반드시 해제!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      decoration: InputDecoration(
        labelText: '검색어 입력',
        hintText: '검색할 내용을 입력하세요',
        prefixIcon: const Icon(Icons.search),
        suffixIcon: IconButton(
          icon: const Icon(Icons.clear),
          onPressed: () => _controller.clear(),
        ),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
      onChanged: (value) {
        print('입력 중: $value');
      },
      onSubmitted: (value) {
        print('검색 실행: $value');
      },
    );
  }
}

TextField 주요 속성

DART
TextField(
  controller: _controller,
  keyboardType: TextInputType.emailAddress,  // 키보드 타입
  textInputAction: TextInputAction.next,     // 키보드 엔터키 동작
  obscureText: true,                          // 비밀번호 숨김
  maxLines: 5,                                // 여러 줄 입력
  maxLength: 100,                             // 최대 글자 수
  enabled: true,                              // 활성화 여부
  autofocus: true,                            // 자동 포커스
  readOnly: false,                            // 읽기 전용
  onTap: () {},                               // 탭 이벤트
)

InputDecoration 커스터마이징

DART
InputDecoration(
  labelText: '이메일',           // 상단 라벨
  hintText: 'email@example.com', // 플레이스홀더
  helperText: '유효한 이메일을 입력하세요',  // 하단 도움말
  errorText: '이메일 형식이 아닙니다',       // 에러 메시지
  counterText: '',                           // 글자 수 카운터 숨김
  filled: true,
  fillColor: Colors.grey.shade100,
  border: OutlineInputBorder(
    borderRadius: BorderRadius.circular(8),
  ),
  focusedBorder: OutlineInputBorder(
    borderRadius: BorderRadius.circular(8),
    borderSide: const BorderSide(color: Colors.blue, width: 2),
  ),
  errorBorder: OutlineInputBorder(
    borderRadius: BorderRadius.circular(8),
    borderSide: const BorderSide(color: Colors.red),
  ),
)

Form — 폼 관리

여러 입력 필드를 하나의 Form으로 묶어서 검증, 저장, 초기화를 한 번에 할 수 있습니다.

DART
class LoginForm extends StatefulWidget {
  const LoginForm({super.key});

  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _submit() {
    // 폼 검증
    if (_formKey.currentState!.validate()) {
      // 검증 통과 시 처리
      final email = _emailController.text;
      final password = _passwordController.text;
      print('로그인: $email');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          // 이메일 입력
          TextFormField(
            controller: _emailController,
            decoration: const InputDecoration(
              labelText: '이메일',
              prefixIcon: Icon(Icons.email),
            ),
            keyboardType: TextInputType.emailAddress,
            validator: (value) {
              if (value == null || value.isEmpty) {
                return '이메일을 입력하세요';
              }
              if (!value.contains('@')) {
                return '유효한 이메일을 입력하세요';
              }
              return null;  // 검증 통과
            },
          ),
          const SizedBox(height: 16),
          // 비밀번호 입력
          TextFormField(
            controller: _passwordController,
            decoration: const InputDecoration(
              labelText: '비밀번호',
              prefixIcon: Icon(Icons.lock),
            ),
            obscureText: true,
            validator: (value) {
              if (value == null || value.length < 6) {
                return '비밀번호는 6자 이상이어야 합니다';
              }
              return null;
            },
          ),
          const SizedBox(height: 24),
          // 제출 버튼
          SizedBox(
            width: double.infinity,
            child: ElevatedButton(
              onPressed: _submit,
              child: const Text('로그인'),
            ),
          ),
        ],
      ),
    );
  }
}

면접 포인트: GlobalKey<FormState>로 Form의 상태에 접근하고, validate()로 모든 필드를 한 번에 검증할 수 있습니다. TextFormField의 validator가 null을 반환하면 검증 통과입니다.


FocusNode — 포커스 관리

DART
class FocusExample extends StatefulWidget {
  const FocusExample({super.key});

  @override
  State<FocusExample> createState() => _FocusExampleState();
}

class _FocusExampleState extends State<FocusExample> {
  final _emailFocus = FocusNode();
  final _passwordFocus = FocusNode();

  @override
  void dispose() {
    _emailFocus.dispose();
    _passwordFocus.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          focusNode: _emailFocus,
          textInputAction: TextInputAction.next,
          // 엔터 누르면 다음 필드로 이동
          onSubmitted: (_) {
            FocusScope.of(context).requestFocus(_passwordFocus);
          },
        ),
        TextField(
          focusNode: _passwordFocus,
          textInputAction: TextInputAction.done,
          onSubmitted: (_) {
            // 키보드 숨기기
            FocusScope.of(context).unfocus();
          },
        ),
      ],
    );
  }
}

정리

  • Material Design 버튼은 용도에 맞게 ElevatedButton, TextButton, OutlinedButton 등을 선택하세요
  • TextField에는 반드시 TextEditingController를 사용하고, dispose()에서 해제하세요
  • 여러 입력 필드는 Form + TextFormField로 묶어 검증하는 것이 깔끔합니다
  • FocusNode로 입력 필드 간 포커스 이동을 관리할 수 있습니다
  • validator가 null을 반환하면 검증 통과, String을 반환하면 에러 메시지입니다
댓글 로딩 중...