[Flutter #10] Flutter 로그인 화면 만들기 (Navigator·Form·TextFormField·Theme·SVG·size)

도경원's avatar
Sep 28, 2025
[Flutter #10] Flutter 로그인 화면 만들기 (Navigator·Form·TextFormField·Theme·SVG·size)

1. size.dart로 여백/사이즈 관리하기

왜? ListView 같은 스크롤 위젯은 세로 길이가 사실상 무한대라서 Expanded나 Spacer를 쓸 수 없다. 이런 경우엔 SizedBox 로 높이를 직접 지정해 주자.
어떻게? 무작정 숫자를 박기보다 size.dart 파일로 상수화하여 일관성 있게 관리!
예시:
// size.dart const double small_gap = 8.0; const double medium_gap = 16.0; const double large_gap = 24.0;
스크린 곳곳에서:
SizedBox(height: small_gap), SizedBox(height: medium_gap), SizedBox(height: large_gap),

2. Navigator로 화면 이동 (push/pop) & Named Routes

  • Navigator = 스택(Stack) 기반 화면 관리자
    • push: 화면 위에 올리기
    • pop: 현재 화면 걷어내기(이전 화면으로 돌아감)
// 예: 일반 push Navigator.push( context, MaterialPageRoute(builder: (_) => const HomePage()), ); // 예: pop (현재 화면 제거 → 이전 화면 노출) Navigator.pop(context);

Named Routes(명명된 경로)

  • 경로를 문자열로 관리(예: "/login", "/home").
  • 라우트 맵을 미리 등록해 두면 깔끔하게 네비게이션 가능.
MaterialApp( initialRoute: "/login", routes: { "/login": (context) => const LoginPage(), "/home": (context) => const HomePage(), }, );
// 이동 Navigator.pushNamed(context, "/home"); // 혹은 현재 화면 닫고 이전으로 Navigator.pop(context);

3) Form 위젯으로 입력 묶고, 한 번에 유효성 검사

  • Form = 입력 위젯(TextFormField 등)을 묶는 컨테이너
  • 여러 필드를 한 번에 유효성 검사하거나 전송할 때 사용
  • GlobalKey<FormState>로 상태를 잡고, 버튼에서 validate() 호출
class CustomForm extends StatelessWidget { CustomForm({super.key}); final _formKey = GlobalKey<FormState>(); // 1) 글로벌 키 @override Widget build(BuildContext context) { return Form( key: _formKey, // 2) Form에 연결 child: Column( children: [ CustomTextFormField("Email"), const SizedBox(height: 16), CustomTextFormField("Password"), const SizedBox(height: 24), TextButton( onPressed: () { // 3) 버튼에서 검증 실행 → 각 필드의 validator 동작 if (_formKey.currentState!.validate()) { // true일 때 다음 화면 이동 or 로그인 요청 Navigator.pushNamed(context, "/home"); } }, child: const Text("Login"), ), ], ), ); } }

4) TextFormField: 키보드 인셋·스크롤·유효성 검사

  • 키보드가 올라오면(인셋 영역) 화면 그릴 수 없는 높이가 생겨 Overflow 위험
  • 해결: 상위에 스크롤을 달아주자(예: SingleChildScrollView or ListView)
  • TextFormFieldvalidator로 유효성 검사 가능
class CustomTextFormField extends StatelessWidget { final String text; const CustomTextFormField(this.text); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(text), SizedBox(height: small_gap), TextFormField( // 1. 느낌표는 null이 절대 아니다 라고 컴파일러에게 알려주는 것 validator: (value) => value!.isEmpty ? "Please enter some text" : null, // 값이 없으면 Please enter some text 경고 화면 표시 obscureText: // 2. 해당 TextFormField가 비밀번호 입력 양삭이면 **** 처리 해주기 text == "Password" ? true : false, decoration: InputDecoration( hintText: "Enter $text", enabledBorder: OutlineInputBorder( // 3. 기본 TextFormField 디자인 borderRadius: BorderRadius.circular(20), ), focusedBorder: OutlineInputBorder( // 4. 손가락 터치시 TextFormField 디자인 borderRadius: BorderRadius.circular(20), ), errorBorder: OutlineInputBorder( // 5. 에러발생시 TextFormField 디자인 borderRadius: BorderRadius.circular(20), ), focusedErrorBorder: OutlineInputBorder( // 6. 에러가 발생 후 손가락을 터치했을 때 TextFormField 디자인 borderRadius: BorderRadius.circular(20), ), ), ), ], ); } }
Tip
즉시 에러를 보고 싶다면 autovalidateMode: AutovalidateMode.onUserInteraction 를 추가하면 입력/포커스 시 바로 validator가 동작한다.
예:
TextFormField( autovalidateMode: AutovalidateMode.onUserInteraction, validator: (value) => (value == null || value.trim().isEmpty) ? "Please enter some text" : null, ... )

5) SVG 위젯 사용하기

SVG(Vector) = 확대/축소해도 깨지지 않는 벡터 이미지
  • Flutter에선 flutter_svg 패키지의 SvgPicture 위젯 사용
  • pubspec.yaml에 의존성 추가 후, 에셋 경로 등록하여 호출
dependencies: flutter_svg: ^2.0.0
import 'package:flutter_svg/flutter_svg.dart'; SvgPicture.asset( 'assets/images/logo.svg', width: 120, height: 120, )

6) 앱 전체 디자인을 Theme로 통일하기 (TextButton 예시)

  • 반복되는 버튼 디자인은 개별 위젯 대신 Theme에 정의 → 앱 전역 일관성 + 생산성 UP
  • 최신 Flutter(Material 3)에서는 TextButton.styleFromprimary가 제거되고, foregroundColor 를 사용
import 'package:flutter/material.dart'; import 'package:login_app/pages/home_page.dart'; import 'package:login_app/pages/login_page.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( // 테마 설정 theme: ThemeData( textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( backgroundColor: Colors.black, foregroundColor: Colors.white, // ← primary 대신 foregroundColor shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), minimumSize: const Size(400, 60), ), ), ), debugShowCheckedModeBanner: false, initialRoute: "/login", routes: { "/login": (context) => const LoginPage(), "/home": (context) => const HomePage(), }, ); } }

Share article

Gyeongwon's blog