Flutter Basic Chapter 3 - 상태로 움직이는 UI 만들기

StatefulWidget으로 상태 저장하기

🎯 이번 단계에서 배울 것

  • StatefulWidgetState 클래스 구조 이해하기
  • 위젯이 가진 상태값(counter)을 변수로 선언하는 방법 익히기
  • 버튼 탭 시 상태를 변경하는 로직을 작성하기
  • setState를 호출하지 않으면 UI가 갱신되지 않는다는 문제 인식하기

📂 파일 구조

1
2
수정되는 파일
- lib/main.dart (앱을 StatefulWidget으로 전환)

📝 1단계: StatefulWidget 골격 만들기

전체 코드 (lib/main.dart @ 3.0):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import 'package:flutter/material.dart';

void main() {
runApp(App());
}

class App extends StatefulWidget {
@override
State<App> createState() => _AppState();
}

class _AppState extends State<App> {
int counter = 0;

void onClicked() {
counter = counter + 1;
}

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFFF4EDDB),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Click Count',
style: TextStyle(fontSize: 30),
),
Text(
'$counter',
style: const TextStyle(fontSize: 30),
),
IconButton(
iconSize: 40,
onPressed: onClicked,
icon: const Icon(
Icons.add_box_rounded,
),
),
],
),
),
),
);
}
}

🔍 코드 상세 설명

1. StatefulWidget 선언

1
2
3
4
class App extends StatefulWidget {
@override
State<App> createState() => _AppState();
}
  • StatefulWidget은 화면이 다시 그려질 때 상태를 유지합니다.
  • createState에서 상태 전용 클래스인 _AppState를 반환합니다.

2. 상태 보관 변수를 State 클래스에 선언

1
2
class _AppState extends State<App> {
int counter = 0;
  • _AppStateApp과 짝을 이루는 상태 관리 클래스입니다.
  • counter는 버튼을 누른 횟수를 저장하는 필드입니다.

3. 상태 변경 메서드 작성

1
2
3
void onClicked() {
counter = counter + 1;
}
  • 버튼이 눌렸을 때 호출되며, 숫자를 1 증가시킵니다.
  • 아직 setState를 호출하지 않아 UI는 바로 갱신되지 않습니다.

4. 상태값을 UI에 노출

1
Text('$counter')
  • 문자열 보간을 이용해 현재 카운터 값을 화면에 보여줍니다.

비교 예시:

1
2
3
4
5
6
// StatelessWidget 시절
class App extends StatelessWidget { ... }

// StatefulWidget 전환 후
class App extends StatefulWidget { ... }
class _AppState extends State<App> { ... }

시각화:

1
2
3
4
5
┌───────────────┐
│ Click Count │
│ 0 │ ← counter
│ [ + button ] │
└───────────────┘

📊 동작 흐름

1
2
3
4
1. 사용자가 + 버튼을 누르면 onClicked 실행
2. counter 변수가 1 증가하지만
3. setState가 없어 build()가 다시 호출되지 않음
4. 화면에는 여전히 마지막으로 그린 값이 남음

✅ 체크리스트

  • AppStatefulWidget으로 정의됐는가?
  • _AppState에서 counter 변수를 선언했는가?
  • 버튼이 onClicked를 호출하도록 연결됐는가?
  • 버튼을 눌러도 화면 숫자가 바뀌지 않는 문제를 확인했는가?

💡 연습 과제

  1. counter = counter * 2;로 바꿔보고 어떤 값이 저장되는지 확인하세요 (UI는 여전히 갱신되지 않습니다).
  2. IconButton 대신 ElevatedButton으로 바꾸고 onPressed에 같은 함수를 연결해보세요.
  3. counterdouble로 선언하면 어떤 변화가 생기는지 테스트하세요.

setState로 화면 다시 그리기

🎯 이번 단계에서 배울 것

  • setState의 역할과 사용법 이해하기
  • 상태 변경과 UI 갱신을 하나의 블록으로 묶기
  • Closure 안에서 상태 변수를 안전하게 업데이트하기

📂 파일 구조

1
2
수정되는 파일
- lib/main.dart (setState 적용)

📝 1단계: onClicked에 setState 추가하기

1
2
3
4
5
void onClicked() {
setState(() {
counter = counter + 1;
});
}

🔍 코드 상세 설명

1. setState 호출 시점

  • setState는 상태가 바뀌었다고 Flutter 프레임워크에 알려주는 메서드입니다.
  • 전달한 콜백 실행이 끝나면 Flutter가 build()를 다시 호출합니다.

2. 콜백 안에서 상태 수정

  • counter = counter + 1;을 콜백 안에 넣어 상태 변경 범위를 명확히 합니다.
  • 콜백 밖에서 상태를 바꾸면 프레임워크가 변화를 감지하지 못할 수 있습니다.

3. UI 갱신 확인

  • 이제 버튼을 누를 때마다 Text('$counter')가 최신 값으로 다시 렌더링됩니다.
  • 핫리로드 없이도 실시간으로 숫자가 증가하는 것을 확인할 수 있습니다.

시각화:

1
before: counter = 0 → setState 실행 → rebuild → 화면에 1 표시

📊 동작 흐름

1
2
3
4
1. onClicked → setState 시작
2. counter 값 증가
3. setState 콜백 종료
4. Flutter가 build() 호출, 새 counter 값을 Text에 반영

✅ 체크리스트

  • setState가 onClicked 안에 존재하는가?
  • 버튼 클릭 시 숫자가 즉시 증가하는가?
  • counter 관련 코드는 setState 내부로 이동했는가?

💡 연습 과제

  1. setState(() => counter += 2);처럼 축약 문법을 사용해보세요.
  2. counter가 10 이상이면 0으로 초기화하도록 조건문을 추가해보세요.
  3. setState 호출 전후에 print(counter)를 찍어 값 변화를 콘솔에서 확인하세요.

List와 컬렉션 for로 여러 위젯 만들기

🎯 이번 단계에서 배울 것

  • 리스트 상태(List<int> numbers)를 관리하는 방법 익히기
  • numbers.add(numbers.length)로 연속 증가 값을 생성하기
  • 컬렉션 for 문법을 사용해 Column 안에서 동적으로 위젯 생성하기

📂 파일 구조

1
2
수정되는 파일
- lib/main.dart (카운터 → 리스트 누적 방식으로 전환)

📝 1단계: 리스트 상태로 전환

전체 코드 (lib/main.dart @ 3.2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class _AppState extends State<App> {
List<int> numbers = [];

void onClicked() {
setState(() {
numbers.add(numbers.length);
});
}

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFFF4EDDB),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Click Count',
style: TextStyle(fontSize: 30),
),
for (var n in numbers) Text('$n'),
IconButton(
iconSize: 40,
onPressed: onClicked,
icon: const Icon(
Icons.add_box_rounded,
),
),
],
),
),
),
);
}
}

🔍 코드 상세 설명

1. 리스트 상태 선언

1
List<int> numbers = [];
  • 버튼을 누를 때마다 새 숫자를 리스트에 저장합니다.
  • numbers.length를 추가하여 0,1,2,… 순서로 값이 쌓입니다.

2. setState에서 리스트 갱신

1
numbers.add(numbers.length);
  • 리스트의 길이를 그대로 추가하면 자연스럽게 증가하는 시퀀스를 만들 수 있습니다.
  • setState가 리스트에 새 항목이 추가된 사실을 반영하게 합니다.

3. 컬렉션 for 문법

1
for (var n in numbers) Text('$n')
  • Dart 컬렉션 literal 안에서 for 문법을 사용할 수 있습니다.
  • 리스트 길이에 따라 Text 위젯이 동적으로 늘어납니다.

비교 예시:

1
2
3
4
// Before: 단일 Text('$counter')

// After: 리스트를 순회하며 Text 위젯 여러 개 생성
for (var n in numbers) Text('$n')

시각화:

1
2
3
4
5
Click Count
0
1
2
[ + ]

(버튼을 누를수록 숫자 줄이 계속 추가됩니다.)

📊 동작 흐름

1
2
3
4
1. 버튼 클릭 → setState 실행
2. numbers에 현재 길이 값 추가
3. build()가 다시 호출되어 for 루프가 새로운 숫자를 포함한 Text 리스트 렌더링
4. 화면에 누적된 숫자가 순서대로 표시됨

✅ 체크리스트

  • numbers 리스트가 빈 배열로 초기화되어 있는가?
  • numbers.add(numbers.length) 로직이 setState 안에 있는가?
  • UI에 숫자가 여러 줄로 쌓이는가?

💡 연습 과제

  1. numbers.add(numbers.length * 2);로 바꿔 짝수만 쌓아보세요.
  2. ListView로 교체해 스크롤 가능한 숫자 리스트를 만들어보세요.
  3. 리스트 길이가 5 이상이면 가장 오래된 값을 제거하도록 조건을 추가해보세요.

BuildContext와 Theme 활용

🎯 이번 단계에서 배울 것

  • ThemeDataTextTheme으로 전역 스타일 정의하기
  • 별도 위젯(MyLargeTitle)에서 Theme.of(context)로 상위 테마 가져오기
  • BuildContext가 위젯 트리를 탐색하는 열쇠라는 점 이해하기

📂 파일 구조

1
2
수정되는 파일
- lib/main.dart (Theme 적용 + 커스텀 위젯 분리)

📝 1단계: ThemeData 설정 및 커스텀 위젯 분리

전체 코드 (lib/main.dart @ 3.3):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class _AppState extends State<App> {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
textTheme: const TextTheme(
titleLarge: TextStyle(
color: Colors.red,
),
),
),
home: Scaffold(
backgroundColor: const Color(0xFFF4EDDB),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
MyLargeTitle(),
],
),
),
),
);
}
}

class MyLargeTitle extends StatelessWidget {
const MyLargeTitle({
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Text(
'My Large Title',
style: TextStyle(
fontSize: 30,
color: Theme.of(context).textTheme.titleLarge?.color,
),
);
}
}

🔍 코드 상세 설명

1. MaterialApp에 Theme 적용

1
2
3
4
5
theme: ThemeData(
textTheme: const TextTheme(
titleLarge: TextStyle(color: Colors.red),
),
),
  • ThemeData는 앱 전반에 사용할 색상·폰트 등을 정의합니다.
  • TextTheme.titleLarge를 빨간색으로 지정해 커다란 제목 스타일을 통일합니다.

2. 커스텀 위젯 분리

1
class MyLargeTitle extends StatelessWidget { ... }
  • 제목 전용 위젯을 별도로 만들어 재사용성과 가독성을 높였습니다.
  • children: const [MyLargeTitle()],로 const 생성자를 사용해 불필요한 리빌드를 막습니다.

3. BuildContext로 상위 테마 접근

1
Theme.of(context).textTheme.titleLarge?.color
  • BuildContext는 현재 위젯이 트리에서 어디 위치했는지 알려주는 객체입니다.
  • Theme.of(context)를 통해 가장 가까운 Theme 위젯을 찾아옵니다.
  • ?.는 널 안전 연산자로, titleLarge가 null이어도 에러 없이 처리합니다.

비교 예시:

1
2
3
4
5
// Before: 직접 색상 지정
Text('$counter', style: TextStyle(color: Colors.black))

// After: Theme에서 색상 가져오기
color: Theme.of(context).textTheme.titleLarge?.color

시각화:

1
2
3
4
MaterialApp (ThemeData)
└─ Scaffold
└─ Column
└─ MyLargeTitle → Theme.of(context) → TextTheme.titleLarge

📊 동작 흐름

1
2
3
4
1. MaterialApp이 ThemeData를 자식 위젯에 제공합니다.
2. Column이 MyLargeTitle 위젯을 렌더링합니다.
3. MyLargeTitle.build()가 전달받은 context로 Theme.of(context)를 호출합니다.
4. Theme에서 titleLarge 스타일을 읽어 Text에 반영합니다.

✅ 체크리스트

  • ThemeData가 MaterialApp의 theme 속성에 설정되었는가?
  • MyLargeTitle 위젯이 const 생성자로 추가되었는가?
  • Theme.of(context)로 색상을 읽어오는가?
  • titleLarge 색상을 바꾸면 텍스트 색도 함께 변하는가?

💡 연습 과제

  1. ThemeDataprimaryColorappBarTheme를 추가해 전역 스타일을 더 만들어보세요.
  2. MyLargeTitle 안에서 Theme.of(context).textTheme.headlineMedium 등을 사용해 다른 스타일을 테스트하세요.
  3. MediaQuery.of(context).size를 출력해 BuildContext로 가져올 수 있는 다른 정보도 살펴보세요.

📚 전체 흐름 정리

  • 앱을 StatefulWidget으로 전환해 상태를 저장할 수 있게 했습니다.
  • setState를 도입해 상태 변경이 즉시 UI에 반영되도록 수정했습니다.
  • 숫자를 리스트로 누적하고 컬렉션 for 문법을 써서 동적 위젯 생성을 경험했습니다.
  • BuildContextThemeData를 활용해 상위 위젯의 스타일을 하위 위젯에서 재사용했습니다.

✅ 최종 점검 리스트

  • StatefulWidget과 State 클래스를 구분해서 작성할 수 있는가?
  • setState를 호출해야만 UI가 다시 그려진다는 사실을 이해했는가?
  • 리스트 상태를 관리하고 컬렉션 for 문법으로 위젯을 만들 수 있는가?
  • BuildContext를 통해 Theme 혹은 다른 상위 데이터에 접근할 수 있는가?

출처 : https://nomadcoders.co/flutter-for-beginners