위젯(Widget)과 상태(State)의 모든 것: 앱 개발의 첫 단추
최근 바이브 코딩을 해보면서 예전 공부했던 플러터를 복습해야 한다는 생각이 많이 들어서 차근차근 다시 글을 만들려고 합니다. 바이브 코딩을 하면서 막혔던 게 바로 코드를 모를 때라서 다트와 플러터에 대한 기본은 확실히 하고 가야 할 것 같았습니다. 앱 개발이 막막하게 느껴질 수도 있지만, 플러터의 핵심 철학인 '위젯'만 제대로 이해하면 누구나 나만의 앱을 만들 수 있습니다. 오늘은 플러터 개발의 '관문'이라 불리는 위젯과 상태(State)에 대해 상세히 알아보겠습니다.
1. Widget(위젯)이란 무엇인가?
플러터에서 가장 자주 듣게 될 말은 바로 "모든 것이 위젯이다(Everything is a Widget)"입니다.
🧱 개념 비유: 레고(LEGO) 블록
레고 성을 만들기 위해 작은 브릭들을 하나하나 쌓아 올리듯, 플러터 앱 역시 작은 '위젯'들을 조립하여 완성됩니다. 눈에 보이는 버튼, 텍스트뿐만 아니라 화면의 중앙 정렬(Center)이나 간격(Padding) 같은 보이지 않는 설정조차도 모두 위젯입니다.
왜 모든 것을 위젯으로 만들었을까요?
- 재사용성: 한 번 잘 만든 버튼 위젯을 다른 화면에서도 그대로 가져다 쓸 수 있습니다.
- 유지보수: 특정 부품만 갈아 끼우듯 UI를 쉽게 수정할 수 있어 개발 속도가 비약적으로 향상됩니다.
2. Stateless vs Stateful Widget (핵심!)
위젯은 성격에 따라 두 가지로 나뉩니다. 이 차이를 아는 것이 플러터 개발의 절반이라고 해도 과언이 아닙니다.
① Stateless Widget
상태가 없는 위젯: 한 번 그려지면 스스로 변하지 않는 정적인 위젯입니다.
- 예: 로고 이미지, 단순 안내 텍스트, 배경색
- 특징: 데이터 변화가 없는 화면 요소에 사용되어 성능을 최적화합니다.
② Stateful Widget
상태가 있는 위젯: 사용자 상호작용이나 데이터 변화에 따라 화면을 '다시 그려야 할 때' 사용합니다.
- 예: 카운터 숫자, 로그인 입력창, 좋아요 버튼
- 특징:
setState()함수를 통해 플러터에게 화면 갱신을 요청할 수 있습니다.
3. Widget Tree(위젯 트리)란?
위젯들은 서로 부모-자식 관계를 맺으며 거대한 나무(Tree) 구조를 형성합니다.
[그림] 위젯들의 계층 구조 - 위젯 트리(Widget Tree)
예를 들어, Scaffold(뼈대)라는 부모 위젯 안에 AppBar(상단바)와 Body(본문)가 자식으로 들어가고, 그 안에 다시 Column, Text 등이 배치되는 식입니다. 플러터는 이 트리를 실시간으로 감시하며 변화가 있는 부분만 효율적으로 업데이트합니다.
4. 실습 코드로 익히기
바이브코딩 실습 과정에서 다룬 카운터 앱 코드를 통해 실제 작동 원리를 살펴봅시다.
import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); // 1. 앱의 전체적인 설정 (변하지 않음) class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: const CounterPage(), ); } } // 2. 상태에 따라 변화하므로 StatefulWidget 사용 class CounterPage extends StatefulWidget { const CounterPage({super.key}); @override State<CounterPage> createState() => _CounterPageState(); } class _CounterPageState extends State<CounterPage> { int _count = 0; // 앱의 상태(State) void _increment() { setState(() { // 화면 갱신 명령 _count++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('기본기 복습: 카운터 앱')), body: Center( child: Text('$_count', style: TextStyle(fontSize: 40)), ), floatingActionButton: FloatingActionButton( onPressed: _increment, child: const Icon(Icons.add), ), ); } }
💡 퀴즈 (Check-point)
Q. 여러분이 '로그인 화면'을 만든다고 가정해 봅시다. 아이디를 입력하는 칸(TextField)은 Stateless일까요, Stateful일까요?
(정답: Stateful입니다. 사용자가 글자를 입력할 때마다 화면에 표시되는 텍스트가 실시간으로 변해야 하기 때문이죠!)