Flutter로 간단한 로그인 앱을 만들어보겠습니다.
로그인 앱은 기본적인 레이아웃 설계와 폼 처리를 배우기에 좋은 예제입니다.
이번 포스팅에서는 작성된 코드와 함께 쓰인 Flutter의 주요 개념을 알아보겠습니다.
1. 프로젝트 구조와 주요 파일
- main.dart: 앱의 진입점이며, 테마와 라우트를 정의합니다.
- LoginPage: 로그인 화면.
- HomePage: 로그인 후 이동할 메인 화면.
- CustomForm: 로그인 폼 컴포넌트.
- CustomTextFormField: 재사용 가능한 텍스트 필드 위젯.
- Logo: 로고를 보여주는 컴포넌트.
- size.dart: 여백과 크기 관련 상수를 정의한 파일.
2. main.dart
앱의 진입점에서 MaterialApp 위젯을 사용해 테마와 라우트를 설정합니다.
import 'package:flutter/material.dart';
import 'package:flutter_login_app/pages/home_page.dart';
import 'package:flutter_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,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
),
minimumSize: const Size(400, 60),
),
),
),
initialRoute: '/login', // 앱 시작 시 기본 화면 설정
routes: {
'/login': (context) => const LoginPage(),
'/home': (context) => const HomePage(),
},
);
}
}
주요 개념:
- initialRoute: 앱 시작 시 기본으로 표시할 화면을 설정합니다.
- routes: 화면 간 네비게이션을 위한 라우트를 정의합니다.
3. size.dart
UI에서 자주 사용하는 여백이나 크기를 상수로 정의해두면 코드의 유지보수가 용이해집니다.
const double smallGap = 5.0;
const double mediumGap = 10.0;
const double largeGap = 20.0;
const double xlargeGap = 100.0;
4. LoginPage
로그인 화면은 로고와 로그인 폼을 포함합니다.
CustomForm 컴포넌트를 사용해 이메일과 비밀번호 입력, 로그인 버튼을 배치합니다.
import 'package:flutter/material.dart';
import '../components/custom_form.dart';
import '../components/logo.dart';
import '../size.dart';
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
SizedBox(height: xlargeGap),
const Logo('Login'),
SizedBox(height: largeGap),
CustomForm(),
],
),
);
}
}
5. CustomForm
CustomForm은 Form 위젯과 함께 유효성 검사를 처리합니다.
GlobalKey를 사용해 폼의 상태를 관리하며, 입력값이 유효한 경우 홈 화면으로 이동합니다.
import 'package:flutter/material.dart';
import '../components/custom_text_field.dart';
import '../size.dart';
class CustomForm extends StatelessWidget {
final _formKey = GlobalKey<FormState>();
CustomForm({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
CustomTextFormField("Email"),
SizedBox(height: mediumGap),
CustomTextFormField("Password"),
SizedBox(height: largeGap),
TextButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
Navigator.pushNamed(context, "/home");
}
},
child: const Text("Login"),
),
],
),
);
}
}
주요 개념:
- GlobalKey: Form의 상태를 관리하기 위해 사용.
- TextButton: 클릭 시 Navigator를 통해 화면 이동.
6. CustomTextFormField
이 위젯은 이메일과 비밀번호 입력 필드를 재사용할 수 있도록 설계되었습니다.
입력값이 비어 있을 경우 유효성 검사 메시지를 표시하며, 비밀번호 입력 필드는 obscureText로 숨깁니다.
import 'package:flutter/material.dart';
import '../size.dart';
class CustomTextFormField extends StatelessWidget {
final String text;
const CustomTextFormField(this.text, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(text),
SizedBox(height: smallGap),
TextFormField(
validator: (value) => value!.isEmpty ? "Please enter some text" : null,
obscureText: text == "Password" ? true : false,
decoration: InputDecoration(
hintText: "Enter $text",
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
],
);
}
}
주요 개념:
- TextFormField: 텍스트 입력 필드로 유효성 검사를 처리.
- obscureText: 입력값을 숨기는 옵션으로 비밀번호 필드에 활용.
7. HomePage
로그인 후 이동하는 화면으로, 로고와 간단한 버튼을 배치했습니다.
import 'package:flutter/material.dart';
import '../components/logo.dart';
import '../size.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
SizedBox(height: xlargeGap),
const Logo('Care Soft'),
SizedBox(height: xlargeGap),
TextButton(
onPressed: () {},
child: const Text('Get Started'),
)
],
),
);
}
}
8. Logo: 로고 위젯
로고와 제목을 표시하는 재사용 가능한 컴포넌트입니다.
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class Logo extends StatelessWidget {
const Logo(this.title, {super.key});
final String title;
@override
Widget build(BuildContext context) {
return Column(
children: [
SvgPicture.asset(
'assets/logo.svg',
height: 70,
width: 70,
),
Text(
title,
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
),
),
],
);
}
}
- 이번 로그인 앱은 컴포넌트 기반 설계와 유효성 검사를 활용해 UI를 구성했습니다.
- 재사용 가능한 컴포넌트를 설계하고, 상수 관리를 통해 유지보수성을 높였습니다.
- Flutter의 주요 개념인 Navigator, GlobalKey, TextFormField, obscureText 등을 학습할 수 있었습니다.