Flutter는 Google이 만든 크로스 플랫폼 앱 개발 프레임워크이다. 일반적으로 앱을 개발하려면 Android는 Kotlin, iOS는 Swift를 따로 써야 한다. 하지만 Flutter를 사용하면 Dart 언어 하나로 Android, iOS, 웹, 데스크톱 앱을 동시에 만들 수 있다.
Flutter의 가장 큰 장점 중 하나는 Hot Reload다. 코드를 수정하고 저장하면 앱이 처음부터 재시작되는 게 아니라 바뀐 부분만 즉시 화면에 반영된다. 덕분에 UI를 조금씩 다듬으면서 빠르게 작업하기에 탁월하다.
한 줄 요약: Dart 하나로 Android + iOS + Web + Desktop, Hot Reload로 빠른 UI 개발.
우리가 작성하는 모든 Dart 코드는 이 폴더 안에 들어간다. main.dart 파일이 앱의 시작점으로, 앱을 실행하면 가장 먼저 이 파일이 실행된다.
화면이 여러 개라면 lib/screen/ 같은 하위 폴더를 만들어 화면별로 파일을 나눠 관리하는 것이 일반적인 패턴이다.
lib/
├── screen/
│ └── home_screen.dart
└── main.dart
프로젝트 전체의 설정 파일이다. 외부 패키지(라이브러리)를 등록하거나, 이미지 같은 에셋(assets)을 등록할 때 사용한다.
dependencies:
flutter:
sdk: flutter
webview_flutter: ^4.4.2 # 외부 패키지 등록
flutter:
assets:
- asset/img/ # 이미지 폴더 등록
⚠️ 주의: pubspec.yaml을 수정하면 반드시
Pub get을 실행해야 변경 사항이 적용된다.
Flutter에서 화면에 보이는 모든 것 — 글자, 버튼, 이미지, 여백 — 은 전부 위젯(Widget) 이다. 작은 위젯들을 조립해서 화면을 만드는 방식이다.
위젯은 트리(tree) 구조로 중첩된다. 위젯 안에 위젯이 들어가고, 그 안에 또 위젯이 들어가는 식이다.
Scaffold(
appBar: AppBar(
title: Text('다연쌤 최고'),
),
body: WebViewWidget(
controller: webViewController,
),
)
위젯은 상태(state)의 유무를 기준으로 두 종류로 나뉜다.
한 번 그려지면 변하지 않는 위젯이다. 데이터가 바뀔 일이 없는 정적인 화면에 사용한다. build() 메서드 하나만 존재하며, 이 메서드가 화면을 그리는 역할을 담당한다.
class HomeScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(...);
}
}
사용자의 조작이나 시간에 따라 화면이 바뀌어야 할 때 사용한다. Flutter가 내부적으로 위젯을 효율적으로 관리하기 위해 클래스 2개로 나뉜다.
HomeScreen — 껍데기(위젯 정의)_HomeScreenState — 실제 내용과 상태 관리class HomeScreen extends StatefulWidget {
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
DateTime firstDay = DateTime.now(); // 상태값
Widget build(BuildContext context) {
return Scaffold(...);
}
}
상태를 변경할 때는 반드시 setState() 안에서 값을 바꿔야 Flutter가 변경을 감지하고 화면을 다시 그린다.
| 구분 | StatelessWidget | StatefulWidget |
|---|---|---|
| 화면 변화 | 없음 (정적) | 있음 (동적) |
| setState() | 불필요 | 필수 |
| 클래스 수 | 1개 | 2개 |
| 사용 예 | 소개 화면, 고정 UI | 카운터, 입력 폼, D-day |
Flutter 앱은 항상 아래 순서로 시작된다.
void main() { // ① 앱의 시작점
runApp( // ② 위젯을 화면에 띄우는 함수
MaterialApp( // ③ 앱 전체를 감싸는 최상위 위젯
home: HomeScreen(), // ④ 첫 화면 지정
),
);
}
main() — Dart 프로그램의 시작점. Java의 public static void main과 동일한 역할.runApp() — 괄호 안에 넣은 위젯을 화면에 표시.MaterialApp — 앱 전체를 감싸는 최상위 위젯. Google Material Design 적용. theme, home 등을 설정.Scaffold — 각 화면의 뼈대. appBar(상단바), body(본문), floatingActionButton(하단 버튼) 제공.전체 구조를 트리로 나타내면 다음과 같다.
main()
└── runApp()
└── MaterialApp // 앱 전체 설정
└── HomeScreen // 첫 화면
└── Scaffold // 화면 뼈대
children 배열에 위젯을 넣으면 위에서 아래로 순서대로 배치된다.
Column(
children: [
Text('U&I'),
Text('우리 처음 만난 날'),
IconButton(...),
Text('D+365'),
],
)
children 배열에 위젯을 넣으면 왼쪽에서 오른쪽으로 나란히 배치된다.
Row(
children: [
Icon(Icons.star),
Text('별점'),
Text('4.5'),
],
)
Column과 Row에는 두 가지 정렬 축이 존재한다.
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [...],
)
자주 쓰는 mainAxisAlignment 값 정리:
| 값 | 설명 |
|---|---|
start | 위(또는 왼쪽)에 몰아서 배치 |
center | 가운데에 모아서 배치 |
spaceBetween | 첫·끝 위젯은 양 끝에 붙이고, 나머지 간격 균등 분배 |
spaceEvenly | 모든 간격(양 끝 포함)을 균등하게 |
spaceAround | 양 끝 간격은 절반, 위젯 사이 간격은 균등 |
Flutter에서는 CSS의 margin이나 padding 대신 SizedBox를 끼워 넣어 간격을 만드는 방식을 많이 사용한다.
// 세로 16px 여백
const SizedBox(height: 16),
// 가로 16px 여백
const SizedBox(width: 16),
요즘 스마트폰에는 상단에 노치(카메라 영역)가 있고, 하단에 홈 인디케이터가 있다. SafeArea로 감싸면 그 영역을 자동으로 피해서 콘텐츠를 배치할 수 있다.
SafeArea(
top: true, // 위쪽 노치 영역 피하기
bottom: false, // 아래쪽은 피하지 않기
child: Column(...),
)
자식 위젯을 부모 영역의 정가운데에 배치한다.
Center(
child: Image.asset('asset/img/j.jpg'),
)
Flutter에서 로컬 이미지를 사용하려면 총 3단계를 거쳐야 한다.
① 폴더 만들기 — 프로젝트 루트에 asset/img/ 폴더를 만들고 이미지 파일을 넣는다.
② pubspec.yaml 등록 — 아래와 같이 경로를 등록한 뒤 반드시 Pub get을 실행한다.
flutter:
assets:
- asset/img/ # 폴더 안 모든 파일 사용 가능
③ 코드에서 사용 — Image.asset()으로 경로를 지정하면 된다.
Image.asset('asset/img/whtjdcks.jpg')
⚠️
pubspec.yaml수정 후 Pub get을 빠뜨리면 이미지가 로드되지 않는다.
텍스트 스타일을 각 Text 위젯마다 직접 적으면, 나중에 폰트 크기를 바꿀 때 전부 찾아서 수정해야 한다. MaterialApp의 theme에 한 번만 정의해두면 main.dart 한 곳만 수정해도 앱 전체에 일괄 반영된다.
MaterialApp(
theme: ThemeData(
textTheme: TextTheme(
displayLarge: TextStyle(
color: Colors.white,
fontSize: 80,
fontWeight: FontWeight.w700,
),
displayMedium: TextStyle(color: Colors.white, fontSize: 50),
bodyLarge: TextStyle(color: Colors.white, fontSize: 30),
bodyMedium: TextStyle(color: Colors.white, fontSize: 20),
),
),
home: HomeScreen(),
)
각 화면에서는 Theme.of(context)로 정의된 스타일을 가져와 사용한다.
final textTheme = Theme.of(context).textTheme;
Text('U&I', style: textTheme.displayLarge),
Text('우리 처음 만난 날', style: textTheme.bodyLarge),
💡
context는 "내가 위젯 트리에서 어디에 있는지"를 알려주는 정보다. 이를 통해 상위에 설정된 Theme 정보를 가져올 수 있다.
한 파일에 모든 코드를 넣으면 길어질수록 읽기가 어려워진다. 화면의 각 부분을 의미 단위로 잘라 별도의 위젯 클래스로 만드는 것이 Flutter의 일반적인 패턴이다.
class _DDay extends StatelessWidget {
final GestureTapCallback onHeartPressed;
final DateTime firstDay;
_DDay({required this.onHeartPressed, required this.firstDay});
Widget build(BuildContext context) { ... }
}
생성자에서 값을 받아 부모 위젯 → 자식 위젯으로 데이터를 전달하는 방식이다. required 키워드를 붙이면 해당 값이 반드시 전달되도록 강제할 수 있다.
정리: 화면을
_DDay,_CoupleImage같은 작은 위젯으로 쪼개면 코드 가독성이 높아지고, 각 위젯을 독립적으로 수정할 수 있어 유지보수에 유리하다.