Flutter, 모바일 앱 개발의 새로운 표준
Flutter 모바일 앱 개발 프레임워크가 날로 인기를 얻으면서 많은 기업들이 프로젝트에 이를 도입하고 있습니다.
수많은 개발자들이 위젯(Widget)이라는 간결한 구조만으로 픽셀 단위까지 완벽한 UI를 개발할 수 있다는 점에 매력을 느낍니다. 저 역시 UI 개발의 단순함과 Dart 프로그래밍 언어를 활용한 기능 로직 구현 능력 때문에 Flutter야말로 모바일 앱 개발의 미래라고 생각합니다.
이 튜토리얼에서는 간단한 팁 계산기 앱을 직접 만들어 보며 Flutter 프레임워크의 기본기를 익혀 보겠습니다. Stateful 및 Stateless 위젯 클래스 같은 표준 코딩 패턴은 물론, Flutter 앱 개발 과정에서 가장 자주 사용하게 될 핵심 위젯들도 함께 다룹니다.
전체적인 흐름은 스타터 Flutter 프로젝트 설정으로 시작해, 전체 UI 구현과 기본 기능 추가 순으로 진행됩니다.
그럼 시작해 볼까요?
Flutter 프로젝트 설정 방법
새로운 Flutter 프로젝트를 생성하려면 먼저 시스템에 Flutter SDK가 설치되어 있어야 합니다. 간편하고 빠른 설치를 원하신다면 공식 Flutter 문서를 참고하세요.
Android 플랫폼용 앱을 개발하는 경우 Android Studio와 Android SDK도 함께 필요하다는 점을 잊지 마세요.
문서를 따라 모든 환경 설정이 끝났다면, 터미널에서 아래의 Flutter 명령어를 실행합니다:
flutter create tipCalculator
이 명령어는 스타터 Flutter 프로젝트를 자동으로 다운로드하고 설정해 줍니다. 이제 Visual Studio Code IDE에서 프로젝트를 열 수 있습니다.
디바이스 시뮬레이터 또는 실제 스마트폰이 연결되어 있다면 다음 명령어만으로 앱을 실행할 수 있습니다:
flutter run
또는 키보드에서 F5 키를 누르면 VSCode에 메뉴 옵션이 나타나는데, 여기서 앱을 실행할 디바이스를 선택하면 됩니다.
이 명령어가 정상적으로 작동하려면 반드시 확장자가 .dart인 파일 내부에 있어야 합니다.
위 명령어 또는 F5 키로 빌드 후 실행하면 에뮬레이터/실제 기기에서 다음과 같은 스타터 템플릿을 확인할 수 있습니다:

이제 Flutter 앱이 정상적으로 구동되고 있을 겁니다.
메인 프로젝트 파일인 main.dart에서 어떤 일이 일어나고 있는지 조금 더 자세히 살펴보겠습니다.
main.dart 파일에는 두 개의 클래스 객체가 있습니다. 하나는 Stateful 위젯을 상속하고, 다른 하나는 Stateless 위젯을 상속합니다. 그렇다면 이것이 무엇을 의미할까요?
- Stateful 위젯: 앱의 상태(state)를 담고 있는 클래스입니다. 상태는 변경될 수 있으며, 상태가 변하면 해당 클래스의 위젯이 다시 렌더링됩니다. 동적인 상태 변화를 처리하는 역할을 합니다.
- Stateless 위젯: 상태를 담고 있지 않은 클래스입니다. 변하지 않는 위젯 뷰를 나타내며, 동적인 상태 변화와는 무관합니다.
main.dart 파일에는 runApp 메서드 안에서 MyApp 클래스를 호출하여 기기에서 Flutter 앱 실행을 트리거하는 main() 함수도 포함되어 있습니다.
팁 계산기 UI 만들기
UI 구현을 시작하기 위해 먼저 MyHomePageState 클래스에 기본으로 들어 있는 코드를 모두 지워야 합니다.
정리한 후에는 build 함수 안에서 간단한 Scaffold 위젯을 반환하도록 합니다.
Scaffold 위젯은 appBar와 body 속성을 추가할 수 있는 기반을 제공합니다. 우선 간단한 앱 바(app bar)를 추가해 보겠습니다. 전체 구현은 아래 코드 스니펫에서 확인할 수 있습니다:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title:Text('Tip Calculator', style: TextStyle(color: Colors.black87),),
),
body: Container()
);
}
}
AppBar 위젯에 title 속성을 넣어 앱 바를 추가한 뒤 빌드하고 실행하면, 에뮬레이터 화면에서 다음 결과를 확인할 수 있습니다:

Dart 파일을 저장할 때 Flutter의 핫 리로딩(hot reloading) 기능이 작동합니다. 프로젝트의 Dart 파일에 변경 사항을 저장할 때마다 에뮬레이터에 자동으로 반영됩니다.
1단계: 앱 바 디자인하기
이제 AppBar 위젯이 제공하는 다양한 속성들을 활용해 앱 바를 꾸며 보겠습니다. 수정된 코드는 아래 스니펫과 같습니다:
appBar: AppBar(
title: Text('Tip Calculator', style: TextStyle(color: Colors.black87),),
centerTitle: true,
elevation: 0.0,
backgroundColor: Colors.white70,
),
빌드 후 실행하면 에뮬레이터 화면에서 다음 결과를 얻을 수 있습니다:

여기서 사용한 기본적인 AppBar 위젯 속성들은 다음과 같습니다:
elevation: z-index처럼 앱 바의 그림자 효과를 조절할 수 있습니다,centerTitle: 제목을 가운데 정렬합니다,- 그리고 배경색을 흰색으로 변경했습니다.
2단계: Scaffold 본문(Body) 디자인하기
지금까지는 body 속성에 빈 Container 위젯만 존재했습니다. 이제 아래 코드 스니펫과 같이 Container 위젯에 몇 가지 속성과 자식 위젯을 추가해 보겠습니다:
body: Container(
color: Colors.white70,
padding: const EdgeInsets.all(16.0),
child: Center(
child: Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
],
),
),
),
빌드 후 실행하면 에뮬레이터 화면에서 다음 결과를 확인할 수 있습니다:

위 스크린샷에서 볼 수 있듯이 본문 배경색을 흰색으로 변경했습니다. 또한 padding을 적용하고, 본문 전체 UI를 중앙에 배치해 주는 Center 위젯을 자식 위젯으로 추가했습니다.
Center 위젯 안에는 텍스트 필드로 구성된 간단한 폼을 만들기 위한 Form 위젯(자식 중 하나)이 있습니다.
그리고 가장 중요한 것은 Form의 자식 위젯인 Column 위젯입니다. Column 위젯은 children 배열 속성을 제공하므로, 이곳에 원하는 만큼 위젯을 통합하면 화면에 세로로 나열됩니다.
3단계: 상수와 변수 정의하기
폼 요소(텍스트 필드 포함)를 구현하기 전에, 입력 필드의 값을 처리할 수 있도록 몇 가지 상수를 먼저 정의해야 합니다.
필요한 상수와 변수는 아래 코드 스니펫에서 확인할 수 있습니다:
// 기본 금액(bill amount)
static const defaultBillAmount = 0.0;
// 기본 팁 비율(tip percentage)
static const defaultTipPercentage = 15;
// 금액 입력값의 변화를 추적하는 TextEditingController
final _billAmountController =
TextEditingController(text: defaultBillAmount.toString());
// 팁 비율 입력값의 변화를 추적하는 TextEditingController
final _tipPercentageController =
TextEditingController(text: defaultTipPercentage.toString());
// 계산된 최신 금액 값을 저장
double _billAmount = defaultBillAmount;
// 계산된 최신 팁 비율 값을 저장
int _tipPercentage = defaultTipPercentage;
위 코드에서 TextEditingController 메서드를 사용하고 있는데, 이 컨트롤러 덕분에 나중에 TextFormField 위젯의 텍스트 입력을 처리할 수 있으며, 기본값으로 초기화됩니다.
4단계: 입력 폼 필드 추가하기
이제 TextFormField 위젯을 사용해 두 개의 입력 폼 필드를 추가하겠습니다.
이 위젯을 사용할 때는 앞서 정의한 컨트롤러 변수를 controller 속성에 반드시 할당해야 합니다. 전체 구현 코드는 아래 스니펫을 참고하세요:
body: Container(
color: Colors.white70,
padding: const EdgeInsets.all(16.0),
child: Center(
child: Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
key: Key("billAmount"),
controller: _billAmountController,
keyboardType: TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
hintText: 'Enter the Bill Amount',
labelText: 'Bill Amount',
labelStyle: TextStyle(
fontSize: 25,
letterSpacing: 1,
fontWeight: FontWeight.bold
),
fillColor: Colors.white,
border: new OutlineInputBorder(
borderRadius: new BorderRadius.circular(20.0),
),
),
),
SizedBox(height: 25,),
TextFormField(
key: Key("tipPercentage"),
controller: _tipPercentageController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: 'Enter the Tip Percentage',
labelText: 'Tip Percentage',
labelStyle: TextStyle(
fontSize: 25,
letterSpacing: 1,
fontWeight: FontWeight.bold
),
fillColor: Colors.white,
border: new OutlineInputBorder(
borderRadius: new BorderRadius.circular(20.0),
),
),
),
],
),
),
),
),
여기서 keyboardType 속성을 지정했는데, 이를 통해 사용자가 입력 필드를 탭했을 때 필요한 유형의 키보드가 표시됩니다.
또한 decoration 속성을 활용하면 InputDecoration 위젯으로 입력 필드를 스타일링할 수 있습니다.
InputDecoration 위젯에는 플레이스홀더 텍스트와 입력 필드 상단의 라벨을 표시해 주는 여러 속성이 있습니다. border 속성을 적용해 둥근 외곽선 테두리도 표시했습니다.
빌드 후 실행하면 에뮬레이터 화면에서 다음 결과를 얻을 수 있습니다:

5단계: 이벤트 리스너와 함수 추가하기
사용자가 금액이나 비율을 입력하는 즉시 팁 금액을 계산해야 하므로, 텍스트 입력 필드의 변경 사항을 감지(listen)할 필요가 있습니다.
이를 위해 addListener 메서드를 사용해 컨트롤러에 이벤트 리스너를 등록합니다.
입력 필드에 변화가 생기는 즉시 금액과 팁 비율을 업데이트하는 특정 함수도 트리거해야 합니다.
이를 위해 setState 메서드와 함께 필요한 함수들을 사용합니다. setState는 변화가 발생하는 즉시 전체 UI를 다시 렌더링해 줍니다.
setState메서드는build메서드의 재실행을 트리거한다는 점에 유의하세요.
전체 구현 코드는 아래 스니펫에서 확인할 수 있습니다:
@override
void initState() {
super.initState();
_billAmountController.addListener(_onBillAmountChanged);
_tipPercentageController.addListener(_onTipAmountChanged);
}
_onBillAmountChanged() {
setState(() {
_billAmount = double.tryParse(_billAmountController.text) ?? 0.0;
});
}
_onTipAmountChanged() {
setState(() {
_tipPercentage = int.tryParse(_tipPercentageController.text) ?? 0;
});
}
여기서 initState 메서드도 함께 사용했습니다. 이 메서드는 앱에서 해당 화면에 진입하자마자 실행되므로, 화면에 들어오는 즉시 이벤트 리스너가 등록됩니다.
6단계: 금액 표시 섹션 추가하기
다시 UI 위젯으로 돌아와서, 입력 필드 바로 아래 Column 위젯 안에 금액 표시 섹션을 추가하겠습니다.
여기서는 위젯 사이에 필요한 간격을 제공할 수 있는 SizedBox 위젯도 함께 사용합니다. 금액 표시 섹션의 구현 코드는 아래 스니펫과 같습니다:
),
SizedBox(height: 20,),
Container(
margin: EdgeInsets.all(15),
padding: EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(
Radius.circular(15),
),
border: Border.all(color: Colors.white),
boxShadow: [
BoxShadow(
color: Colors.black12,
offset: Offset(2, 2),
spreadRadius: 2,
blurRadius: 1,
),
],
),
child: Column(
children: [
Text("Tip Amount"),
Text("Total Amount")
],
),
),
여기서는 필요한 스타일 데코레이션이 적용된 Container를 사용했습니다. child 속성 안에는 세로로 나열된 두 개의 Text 위젯을 담은 또 다른 Column 위젯이 있습니다.
빌드 후 실행하면 에뮬레이터 화면에서 다음 결과를 확인할 수 있습니다:

7단계: 금액을 표시하는 별도의 Stateless 위젯 만들기
팁 금액과 총액을 약간의 스타일과 함께 표시하고 싶습니다. 이 위젯은 상태를 포함하지 않고, Stateful 위젯에서 전달받은 값에 의존하게 됩니다.
AmountText라는 Stateless 위젯 클래스의 전체 구현 코드는 아래 스니펫에서 확인할 수 있습니다:
class AmountText extends StatelessWidget {
final String text;
const AmountText(
this.text, {
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8),
child: Text(text.toUpperCase(),
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueAccent, fontSize: 20)),
);
}
}
여기서는 생성자 클래스를 통해 실제로 표시할 텍스트 값을 전달받습니다. 이 클래스의 build 메서드는 간단한 padding이 적용된 Container 위젯과 그 자식 위젯인 Text 위젯을 반환합니다.
AmountText 위젯이 준비되었으니, 이제 Stateful 위젯 안에서 호출할 수 있습니다.
앞서 정의한 Column 위젯 안의 단순 Text 위젯들에 이 위젯을 추가할 차례입니다. Text 위젯을 AmountText 위젯으로 교체하고 필요한 텍스트 값만 전달하면 됩니다.
구현 코드는 아래 스니펫을 참고하세요:
Container(
margin: EdgeInsets.all(15),
padding: EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(
Radius.circular(15),
),
border: Border.all(color: Colors.white),
boxShadow: [
BoxShadow(
color: Colors.black12,
offset: Offset(2, 2),
spreadRadius: 2,
blurRadius: 1,
),
],
),
child: Column(
children: [
AmountText(
'Tip Amount: ${_getTipAmount()}',
key: Key('tipAmount'),
),
AmountText(
'Total Amount: ${_getTotalAmount()}',
key: Key('totalAmount'),
),
],
),
),
여기서 AmountText 위젯 안에 함수를 전달했습니다. 이 함수는 각각의 팁 금액과 총액 값을 반환하며, 아래 코드 스니펫에서 확인할 수 있습니다:
_getTipAmount() => _billAmount * _tipPercentage / 100;
_getTotalAmount() => _billAmount + _getTipAmount();
마지막으로, 화면을 벗어날 때 컨트롤러를 종료해 주어야 합니다. 이를 위해 내장된 dispose 함수를 사용합니다. 이 함수는 현재 화면을 떠날 때 실행됩니다.
이 메서드 안에서 컨트롤러들의 dispose 메서드를 호출해 입력 텍스트 컨트롤러를 종료해야 합니다. 이렇게 하면 컨트롤러가 입력 필드의 변화 감지를 멈추게 됩니다.
dispose 함수는 아래 코드 스니펫에서 확인할 수 있습니다:
@override
void dispose() {
// 메모리 누수를 방지하기 위해, 위젯이 메모리에서 해제될 때
// 사용한 TextEditingController를 모두 dispose 합니다.
_billAmountController.dispose();
_tipPercentageController.dispose();
super.dispose();
}
빌드 후 실행하면 팁 계산기 구현의 최종 결과물을 확인할 수 있으며, 아래 데모에서 그 모습을 볼 수 있습니다:

입력 필드의 값을 변경하면 두 금액의 값도 함께 변하는 것을 확인할 수 있습니다.
이것으로 튜토리얼을 마칩니다. 여러분은 Flutter 프레임워크와 Dart를 활용해 간단한 팁 계산기를 성공적으로 구현했습니다.
다음 단계는 무엇일까요?
이 튜토리얼의 핵심 목표는 간단한 팁 계산기를 만들어 보면서 Flutter 앱 개발 프레임워크의 기본 코딩 패턴을 익히는 것이었습니다.
탐구할 만한 흥미로운 위젯과 추가 기능은 아직 많이 남아 있습니다. 기능 컴포넌트는 그대로 유지하면서 앱의 UI만 자유롭게 바꿔볼 수도 있습니다.
전체적으로 Flutter는 위젯 패턴 덕분에 복잡한 UI 디자인을 한층 단순하게 만들어 줍니다. 위젯의 child 속성을 활용해 위젯들을 쌓기만 하면 훌륭한 UI를 손쉽게 만들 수 있습니다.
다음 단계로는 Flutter의 네비게이션 메커니즘을 활용해 여러 화면 간 이동을 구현해 볼 수 있습니다. Flutter는 커스텀 드로어(drawer) 메뉴나 하단 탭(bottom tab) 추가도 간편하게 지원합니다.
이것은 Flutter 개발의 시작점일 뿐입니다. 겉으로 보이는 것보다 훨씬 많은 가능성이 있습니다. 계속 탐구하고 코딩하면 됩니다.
전체 프로젝트의 데모는 Codepen에서 확인할 수 있습니다.
이미 출시된 다른 Flutter 앱들에서 영감을 얻어보는 것도 좋습니다.
Happy Coding!