Deep Dive/Flutter
[플러터 인 액션] C3 : 플러터의 세계로 (1) 위젯과 상태 관리
nunukim
2025. 2. 20. 16:32
플러터 프로젝트 구조
- 플러터 프로젝트를 만들면 디렉터리가 생성된다.
- 모든 디렉터리를 알 필요는 없으며, 사용하지 않는 디렉터리도 있다.
counter_app
=> android
=> ios
=> lib
=> main.dart // 프로젝트 진입점, main을 포함
=> test // 비즈니스 로직 검증
=> widget_test.dart
=> pubspec.yaml // 모든 다트 프로젝트에 필수, 의존성과 메타데이터 관리
=> pubspec.lock // 편집하면 안 되는 잠금 파일 생성
=> README.md
플러터 앱 내부
- 플러터의 많은 기능은 다트 라이브러리로 이루어져 있다.
- 구글의 머티리얼 디자인 시스템 기본 위젯을 사용할 수 있다.
앱 진입점
- 앱의 맨 윗부분에 main 함수를 선언한다.
- runApp이라는 메서드로 최상위 위젯을 감싸며, 모든 것은 위젯으로 이루어져 있다.
void main() => runApp(MyApp());
플러터의 위젯
- 위젯은 다른 프레임워크의 컴포넌트와 같다.
- 여러 위젯을 사용해 다양한 방법으로 조합하여 더 큰 위젯을 만들어 앱을 완성해야 한다.
- Button, TextField 같은 위젯은 덜 추상적이며 구조적 요소를 정의한다.
- 테마 위젯으로 앱의 색과 폰트를 정의하고 위젯으로 애니메이션을 정의한다.
- 플러터에서는 한 위젯의 스타일로 다른 위젯을 설정한다.
- 패딩을 추가하기 위해 Padding 위젯을 사용해야 한다.
- 모든 UI가 위젯이며, 앱의 경로도 위젯으로 이루어져 있다.
build 메서드
- 모든 위젯은 다른 위젯을 반환하는 build 메서드를 반드시 포함한다.
class RedButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: TextButton(
child: Text("Red Button"),
onPressed: () {},
),
));
}
}
- 최상위 위젯은 가장 상위의 위젯이며, 다른 위젯으로 부터 조립된다.
import 'RedButton.dart';
class Myapp extends StatelessWidget {
// 이 클래스의 슈퍼클래스인 StatelessWidget도 build
// 메서드를 갖지만 이 메서드를 대신 호출해야 한다는 의미로 사용된다.
@override
Widget build(BuildContext context) {
// 앱의 모든 위젯에서 머티리얼 디자인 기능을 이용할 수 있도록 앱을 감싼다.
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: RedButton(),
);
}
}
new, const 생성자
- 플러터에서는 한 위젯의 여러 인스턴스를 만든다.
- 많은 내장 위젯은 일반 생성자와 const 생성자를 모두 제공한다.
- 가능하면 const를 사용하는 것이 좋으며, 둘 다사용하지 않으면 프레임워크가 const로 추론한다.
핫 리로드
- 다트는 AOT 컴파일러이면서 JIT 컴파일러다.
- 컴퓨터로 앱을 개발할 때는 JIT를 사용하며, just in time 컴파일러라고 부른다.
- 실시간으로 코드를 컴파일하고 실행한다.
- 배포할 때는 AOT 컴파일러를 사용한다.
위젯 트리와 형식, State 객체
- 플러터의 대부분의 위젯은 StatelessWidget이나 StatefulWidget의 형식을 가진다.
- 플러터 UI 개발은 수많은 위젯을 조합해 위젯 트리를 완성하는 것을 뜻한다.
- 위젯은 자신의 자식이 또 다른 위젯들을 포함한다고 설명하는 방식으로 트리를 구축한다.
- 모든 위젯이 child 프로퍼티를 갖는 것은 아니며, children, builder 등의 프로퍼티를 갖는 위젯도 있다.
상태를 갖지 않는 위젯
- StatefulWidget은 내부 상태를 추적하며, SatelessWidget은 위젯 생명주기 동안 내부 상태를 가지지 않는다.
class Submitbutton extends StatelessWidget {
// 위젯으로 전달한 모든 데이터를 설정으로 활용한다.
late final String buttonText;
Submitbutton(this.buttonText);
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () { },
child: Text(buttonText)
);
}
}
- 버튼에 대한 텍스트를 다르게 표시할 때, 표시 문자열을 바꾸도록 플러터에 지시할 수 있다.
- 이는 정적이며, 직접 자신을 갱신하는 로직을 포함하지 않는다.
- 부모 위젯의 설정에 따라 정해진 문자열을 보여주며, 자신을 언제 어떻게 리빌드해야 하는지 모른다.
상태를 갖는 위젯
- StatefulWidget 클래스는 Stateless와 다르게 build 메서드를 포함하지 않는다.
- 대신 상태 객체를 포함하며, 모든 상태 객체가 build 메서드를 포함한다.
class MyHomePage extends StatefulWidget {
// 슈퍼클래스의 메서드 createState를 오버라이드
// 모든 StatefulWidget은 State 객체를 반환하는 create 메서드 반드시 정의
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
// TODO: implement build
throw UnimplementedError();
}
}
비공개 클래스
- _MyHomePageState와 같이 언더바로 시작하는 클래스를 비공개 클래스라고 한다.
- 현재 클래스에서만 이를 접근한다는 것을 의미한다.
setState
void _incrementCounter() {
setState((){
_counter++;
});
}
- 이 메서드는 객체의 상태를 변화시키는 기능을 한다.
- Void-Callback 형식의 인수 하나를 가지고 있다.
- 비동기 코드는 실행할 수 없다는 특징이 있다.
initState
- 상태 객체는 위젯이 트리에 마운트되면 호출되는 initState 메서드도 포함한다.
- 위젯의 build 메서드가 실행되면 화면에 위젯을 그리기 전에 String 등을 알맞은 형태로 포맷한다.
class FirstNameText extends StatefulWidget {
final String name;
const FirstNameText({Key? key, required this.name}) : super(key: key);
@override
State<FirstNameText> createState() => _FirstNameTextState();
}
class _FirstNameTextState extends State<FirstNameText> {
late String name;
@override
void initState() {
super.initState();
name = widget.name.toUpperCase();
}
@override
Widget build(BuildContext context) {
return Text(name);
}
}
- initState는 상태 객체를 만들 때 한 번만 호출된다.
- setState를 호출해 플러터가 위젯을 다시 그리도록 할 수 있다.
BuildContext
- 모든 build 메서드는 위젯 트리에서 위젯의 위치를 참조하는 BuildContext 하나를 인수로 받는다.
- 개발자가 관리할 필요는 없으며, 자주 사용할 수 있다.
- 모든 위젯은 자신만의 빌드 콘텍스트를 가지며, 한 위젯이 다양한 테마를 변화하게 만들어 한 트리에 여러 테마를 적용할 수 있다.
- Theme.of
- of 메서드는 트리에서 형식이 같은 가장 가까운 부모를 반환한다.
- 특정 위젯을 정확하게 어떻게 표현할지 결정한다.
- 모달과 라우트 표시에 주로 활용한다.
위젯 활용하기
ElevatedButton
- 머티리얼 디자인에서 제공하는 버튼 중 하나로, 약간 솟아오른 효과를 제공한다.
- FlatButton과 달리 레이아웃에 입체적인 느낌을 준다.
class _MyHomePageState extends State<MyHomePage> {
int _counter = 10;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
// 콜백을 활용해, 부모 위젯에서 상태를 관리한다.
onPressed: _decrementCounter,
child: const Text("Decrement Counter"),
)
],
),
),
);
}
void _decrementCounter() {
// setState는 콜백을 인수로 받으며, 이 콜백은 위젯의 상태를 갱신한다.
setState(() => _counter--);
}
}
상속보다 조합을 선호하는 플러터
- 상속은 ~은(는) ~이다(is a) 관계를 성립한다.
- 조합은 갖고 있다(has a) 관계를 성립한다.
- 조합은 추상 클래스와 비슷하며, 클래스를 만들어 동작을 구현한다.
플러터에서 조합하기
- 플러터는 항상 상속보다 조합으로 재사용할 수 있고 결합되지 않은 위젯을 만든다.
- 대부분의 위젯은 자식 위젯이 누구인지 미리 알 수 없다.
import 'package:flutter/material.dart';
class Panicbutton extends StatelessWidget {
final Widget display;
final VoidCallback onPressed;
// 표현할 위젯과 위젯 설정을 전달한다.
Panicbutton({
required this.display,
required this.onPressed,
});
Widget build(BuildContext context){
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.red,
),
child: display,
// 콜백을 전달하는데, 어떤 기능과도 의존성이 없어 유연하다.
onPressed: onPressed,
);
}
}
- 위 코드에서 조합을 이용했으므로 텍스트는 버튼이다가 아닌, 버튼은 텍스트를 포함한다라고 표현할 수 있다.
- 버튼은 자식이 한 개 있다는 사실만 알 뿐 자식이 무엇인지 알 필요가 없다.