Widget — 개념, 유형 및 Flutter 구성

저자: IT Sectr 게시일: 2026-06-30 읽는 시간: 10 분

Widget은 Flutter 프레임워크의 핵심 개념으로, 사용자 인터페이스 요소의 구성을 설명합니다. 버튼부터 복잡한 애니메이션까지 모든 시각적 구성 요소가 Widget입니다. UI가 별도의 XML 파일로 설명되거나 명령적으로 그려지는 다른 프레임워크와 달리, Flutter는 Widget 컴포지션(합성)을 통해 인터페이스를 구축합니다. 즉, 작은 분할 불가능한 요소들을 계층적 트리로 결합합니다. Flutter 문서(2025)에 따르면, Flutter SDK 라이브러리에는 Material Design, Cupertino 및 사용자 정의 스타일을 포괄하는 260개 이상의 내장 Widget이 포함되어 있습니다.

핵심 사항

  • Widget — Flutter에서 UI의 기본 빌딩 블록으로, 요소의 구성을 설명합니다.
  • 컴포지션 — UI는 상속이 아닌 Widget을 서로 중첩하여 구축됩니다.
  • StatelessWidget — 렌더링 후 변경되지 않는 위젯(텍스트, 아이콘, 패딩).
  • StatefulWidget — 변경 가능한 상태를 가진 위젯(양식, 애니메이션, 데이터 목록).
  • Element tree — Flutter는 Widget, Element, RenderObject의 세 가지 트리를 유지합니다.

Flutter에서 Widget이란

Flutter에서 Widget은 사용자 인터페이스의 일부에 대한 불변(immutable) 설명입니다. 각 Widget에는 구성 속성(크기, 색상, 위치, 텍스트, 이벤트 핸들러, 자식 Widget)이 포함됩니다. Widget 자체는 직접 렌더링되지 않습니다. 이는 Flutter Engine이 RenderObject(화면의 실제 그래픽 객체)를 생성하는 기반이 되는 청사진(blueprint)입니다.

Flutter의 철학은 “Everything is a Widget”입니다. 즉, 보이는 요소(Text, Image, Button)뿐만 아니라 구조적 블록(Padding, Center, Column, Stack), 동작 블록(GestureDetector, AnimatedBuilder), 심지어 애플리케이션 자체(MaterialApp, CupertinoApp)도 Widget입니다. 이 접근 방식은 모든 화면 요소를 단순한 중첩을 통해 다른 요소와 결합할 수 있는 통일성을 보장합니다.

Google I/O 2024 — Flutter Widgets Deep Dive에 따르면, 평균적인 Flutter 애플리케이션에는 특정 시점에 200~1500개의 Widget이 포함됩니다. 이 양에도 불구하고 Flutter는 C++ Skia/Impeller 엔진 수준의 최적화 덕분에 저가형 기기에서도 60FPS를 유지합니다. Widget은 가벼운 객체(각 40~80바이트)이므로 생성이 성능 병목 현상이 되지 않습니다.

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Text('안녕하세요, Flutter!'),
        ),
      ),
    );
  }
}

Flutter의 세 가지 트리: Widget, Element 및 RenderObject

Widget이 어떻게 작동하는지 이해하려면 세 개의 상호 연결된 트리로 구성된 Flutter 아키텍처를 이해해야 합니다. 첫 번째는 Widget 트리로, UI 구성을 설명합니다. 이는 가벼운 트리로, 매 프레임마다 완전히 재구성될 수 있습니다(가비지 컬렉터가 이전 Widget을 제거하고 새 Widget을 생성합니다). Widget은 불변(immutable)입니다. 텍스트 색상이 변경되면 새 색상으로 새 Text Widget이 생성되고 이전 것은 폐기됩니다.

두 번째 트리는 Element 트리로, Widget과 RenderObject 사이의 연결 고리입니다. Element에는 Widget(구성)과 RenderObject(렌더링)에 대한 참조가 포함됩니다. Widget이 변경되면 Flutter는 새 Widget을 이전 Element와 비교하여 결정합니다. 기존 RenderObject를 업데이트(Widget이 동일한 유형인 경우)하거나 새로 생성(Widget 유형이 변경된 경우)합니다. 이 프로세스를 Reconciliation이라고 하며 React Virtual DOM과 유사합니다.

세 번째 트리는 RenderObject 트리로, 화면의 실제 렌더링을 담당합니다. RenderObject에는 구체적인 크기, 위치 및 페인트 메서드가 포함됩니다. Flutter Engine(C++ Skia 또는 Impeller)은 RenderObject 트리를 순회하며 각 노드를 렌더링합니다. RenderObject 트리가 가장 무거운 트리이므로 Flutter는 동일한 유형의 Widget으로 전환할 때 RenderObject를 재사용하여 변경을 최소화합니다.

트리목적불변?수명 주기
WidgetUI 구성(청사진)모든 빌드 시 재생성
ElementWidget ↔ RenderObject 연결아니요위젯이 트리에 있는 동안 존재
RenderObject렌더링 및 레이아웃아니요무거움, 가능하면 재사용

StatelessWidget vs StatefulWidget

Flutter는 Widget을 두 가지 기본 유형으로 나눕니다. StatelessWidgetStatefulWidget입니다. StatelessWidget은 변경 가능한 상태를 포함하지 않는 위젯입니다. StatelessWidget의 모양은 생성자에 의해 완전히 결정되며 렌더링 후 변경할 수 없습니다. 예: Text, Icon, Divider, Padding. StatelessWidget의 모든 속성은 생성자에서 final로 선언되며 읽기 전용입니다.

StatefulWidget은 변경 가능한 상태를 가진 위젯입니다. 이는 두 개의 클래스로 구성됩니다. Widget 자체(StatelessWidget과 같은 불변 구성)와 State(변경 가능한 상태)입니다. Widget과 State를 분리하는 것은 Flutter의 핵심 아키텍처 결정입니다. Widget은 매 빌드마다 재생성되지만 State 객체는 트리에서 위젯의 전체 수명 주기 동안 지속되며 상태를 유지합니다.

setState()가 호출되면 Flutter는 State를 “더티”로 표시하고 다음 프레임에서 build() 메서드를 호출하여 하위 트리를 재구성합니다. 중요: setState()는 Widget 자체를 재생성하지 않습니다. 기존 State에서 build() 호출만 트리거합니다. 즉, StatefulWidget은 자식 요소의 키(Key)가 안정적으로 유지되는 한 자식 Widget의 상태를 잃지 않고 UI를 업데이트할 수 있습니다.

dart
// StatelessWidget — 모양이 절대 변하지 않음
class GreetingWidget extends StatelessWidget {
  const GreetingWidget({super.key, required this.name});
  final String name;

  @override
  Widget build(BuildContext context) {
    return Text('안녕하세요, $name님');
  }
}

// StatefulWidget — 변경 가능한 상태를 가진 카운터
class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('카운트: $_count'),
        ElevatedButton(
          onPressed: () => setState(() => _count++),
          child: const Text('증가'),
        ),
      ],
    );
  }
}

Widget 유형: 레이아웃, 페인팅, 인터랙티브

Flutter의 모든 Widget은 기능적 목적에 따라 세 가지 주요 범주로 나눌 수 있습니다. 레이아웃 Widget — 화면에서 자식 요소의 배치를 담당합니다. Row와 Column은 자식을 일렬로 배치하고, Stack은 서로 위에 겹쳐 놓으며, Expanded와 Flexible은 사용 가능한 공간을 분배합니다. 레이아웃 Widget은 자체 시각적 표현이 없으며 자식 위젯의 위치와 크기를 관리합니다.

페인팅 Widget — 시각적 스타일링을 담당합니다. Container는 장식(색상, 그라데이션, 그림자, 테두리)을 레이아웃 속성과 결합합니다. Padding은 간격을 추가하고, DecoratedBox는 배경을 그리고, Transform은 변환(회전, 크기 조정)을 적용합니다. 페인팅 Widget은 시각적 스타일의 구성 요소이며 원하는 모양을 얻기 위해 레이아웃 Widget과 함께 자주 사용됩니다.

인터랙티브 Widget — 사용자 입력을 처리합니다. GestureDetector는 제스처(탭, 스와이프, 핀치)를 감지하고, InkWell은 Material 물결 효과를 추가하며, TextField는 텍스트 입력을 받고, Slider와 Switch는 표준 제어 요소를 제공합니다. 인터랙티브 Widget은 생성자에 전달되거나 상태 공급자를 통해 처리되는 콜백 함수를 통해 이벤트를 발생시킵니다.

범주Widget 예시목적
레이아웃Row, Column, Stack, Expanded, Flexible, Align자식 요소의 위치 지정 및 크기 조정
페인팅Container, Padding, DecoratedBox, RotatedBox색상, 배경, 테두리, 그림자, 변환
인터랙티브GestureDetector, InkWell, TextField, Slider터치, 입력, 제스처 처리
플랫폼MaterialApp, CupertinoApp, Theme, MediaQuery플랫폼 통합, 테마, 적응
비동기FutureBuilder, StreamBuilder, ValueListenableBuilder비동기 데이터로부터 반응형 업데이트

Flutter Widget of the Week(Google, 2025)에 따르면, Flutter 커뮤니티는 거의 모든 인터페이스를 구축하기 위해 레이아웃 + 페인팅 + 인터랙티브 Widget의 조합을 적극적으로 사용합니다. 예를 들어, 버튼: InkWell(인터랙티브) + Container(페인팅) + Text(정적) + Padding(레이아웃). 이러한 모듈성을 통해 코드 중복 없이 다양한 컨텍스트에서 표준 블록을 재사용할 수 있습니다.

Widget 컴포지션 및 BuildContext

Widget 컴포지션은 일부 Widget을 다른 Widget 안에 중첩하여 UI를 구축하는 프로세스입니다. 자식 클래스가 부모의 동작을 상속받는 고전적인 상속(extends)과 달리, Flutter는 집계를 사용합니다. 각 Widget은 child 매개변수(하나의 경우) 또는 children 매개변수(여러 개의 경우)를 통해 다른 Widget을 포함합니다. 이 접근 방식은 더 큰 유연성과 재사용성을 제공합니다.

BuildContext는 Widget 다음으로 두 번째로 중요한 개념입니다. BuildContext는 요소 트리에서 Widget 위치의 설명자입니다. BuildContext를 통해 Widget은 조상 위젯(Theme.of(context), MediaQuery.of(context), Navigator.of(context))에 액세스할 수 있습니다. BuildContext는 build() 메서드에 전달되며 부모 및 자식 요소와의 상호 작용에 사용됩니다. 각 Widget은 정확히 하나의 BuildContext를 가지며, 이는 트리에서 해당 위치를 고유하게 식별합니다.

Flutter Architectural Overview(Google, 2025)에 따르면, BuildContext는 InheritedWidget의 기초입니다. InheritedWidget은 생성자를 통한 명시적 전달 없이 트리 아래로 데이터를 전달할 수 있는 메커니즘입니다. Theme, MediaQuery, Navigator 및 Provider는 내부적으로 InheritedWidget을 사용합니다. 깊게 중첩된 모든 Widget은 BuildContext.dependOnInheritedWidgetOfExactType을 통해 조상 데이터에 액세스할 수 있으며, 이는 BuildContext를 Flutter의 반응형 아키텍처의 핵심으로 만듭니다.

dart
// 중첩을 통한 Widget 컴포지션
Scaffold(
  appBar: AppBar(title: const Text('내 앱')),
  body: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const Text('Flutter에 오신 것을 환영합니다',
          style: TextStyle(fontSize: 24)),
        const SizedBox(height: 16),
        ElevatedButton(
          onPressed: () { /* nav */ },
          child: const Text('시작하기'),
        ),
      ],
    ),
  ),
)

// BuildContext를 통한 테마 접근
Text(
  '스타일 지정 텍스트',
  style: Theme.of(context).textTheme.headlineMedium,
)

Widget 관련 일반적인 실수

첫 번째이자 가장 흔한 실수는 StatelessWidget으로 충분한 곳에 StatefulWidget을 사용하는 것입니다. 많은 초보 Flutter 개발자는 상태가 외부 공급자(Provider, Riverpod, BLoC)에 저장되어 있더라도 모든 위젯에 StatefulWidget을 만듭니다. 이는 과도하며 성능을 저하시킵니다. 규칙: 상태가 외부에서 관리되거나 위젯에 자체 변경 가능한 상태가 없는 경우 StatelessWidget을 사용하십시오.

두 번째 실수는 const 생성자 없이 build 메서드 내에서 Widget을 생성하는 것입니다. const 없이 생성된 각 Widget은 매 빌드마다 재할당됩니다. build() 내에서 const 생성자로 Widget을 생성하면 Flutter가 동일한 인스턴스를 재사용하여 가비지 컬렉터의 부하를 줄일 수 있습니다. 가능한 모든 곳에 const를 추가하십시오. 특히 Text, Icon, SizedBox, Padding 및 기타 상태 비저장 Widget에 대해 추가하십시오.

세 번째 실수는 키(Key)의 잘못된 사용입니다. Flutter는 트리를 재구성할 때 Widget을 식별하기 위해 Key를 사용합니다. Widget 목록이 Key 없이 재구성되면 Flutter가 요소 순서를 혼동하여 잘못된 애니메이션 또는 상태 손실이 발생할 수 있습니다. 목록의 요소에는 항상 Key(예: ValueKey 또는 ObjectKey)를 추가하십시오. 특히 동적 데이터로 ListView.builder를 사용할 때 중요합니다.

자주 묻는 질문

StatelessWidget과 StatefulWidget의 차이점은 무엇인가요?

StatelessWidget은 변경 가능한 상태가 없는 위젯으로, 모양이 생성자에 의해 완전히 결정됩니다. StatefulWidget은 변경 가능한 상태가 있는 위젯으로, 별도의 State 객체에 저장되며 위젯 자체를 재생성하지 않고 setState()를 통해 업데이트할 수 있습니다. 가능한 경우 StatelessWidget을 사용하고, 로컬 상태가 필요한 경우 StatefulWidget을 사용하십시오.

Widget을 불변(immutable)이라고 부르는 이유는 무엇인가요?

Widget의 불변성은 성능을 위한 Flutter의 아키텍처 결정입니다. Widget이 가변적이라면 Flutter는 매 빌드마다 이전 구성과 새 구성을 안전하게 비교할 수 없습니다. 불변성을 통해 Flutter는 Widget이 변경되었는지(== 연산자를 통해) 빠르게 확인하고 기존 RenderObject를 재사용하여 값비싼 렌더링 작업을 최소화할 수 있습니다.

BuildContext란 무엇이며 왜 필요한가요?

BuildContext는 요소 트리에서 Widget 위치의 설명자입니다. 이를 통해 Widget은 조상 위젯(Theme, MediaQuery, Navigator) 및 InheritedWidget에 액세스할 수 있습니다. BuildContext는 탐색(Navigator.of(context)), SnackBar 표시 및 Provider와의 상호 작용에도 사용됩니다. 각 Widget은 build() 메서드를 통해 BuildContext를 받아 자손에게 전달합니다.

레이아웃을 위해 Row, Column, Stack 중에서 어떻게 선택하나요?

요소의 수평 배치에는 Row, 수직 배치에는 Column, 요소를 서로 겹치게 하려면 Stack을 사용하십시오. Row와 Column은 flexbox 원리로 작동합니다. 자식은 mainAxisSize, mainAxisAlignment 및 crossAxisAlignment에 따라 공간을 차지합니다. Stack은 가장자리 또는 중앙을 기준으로 정확한 위치 지정을 위해 위치 지정된 자식을 사용합니다.

Flutter는 수천 개의 Widget으로 60FPS를 어떻게 유지하나요?

Flutter는 세 가지 메커니즘을 통해 높은 성능을 달성합니다. (1) Widget은 가볍습니다 — 가벼운 불변 객체(40~80바이트)로, 생성이 GC에 부담을 주지 않습니다. (2) RenderObject 재사용 — 동일한 유형의 Widget으로 전환하면 RenderObject가 재사용되어 값비싼 재생성을 피합니다. (3) Skia/Impeller 엔진 — 다시 그리기 경계(repaint boundaries)를 통해 드로우 콜을 최소화한 C++ 수준의 렌더링.

요약

  • Widget — Flutter에서 UI의 불변 구성, 모양과 동작을 설명하는 기본 빌딩 블록.
  • 세 가지 트리 — Flutter는 최적의 렌더링을 위해 Widget 트리(구성), Element 트리(연결), RenderObject 트리(렌더링)를 사용합니다.
  • Stateless vs Stateful — StatelessWidget은 상태 없음, StatefulWidget은 변경 가능한 State와 UI 업데이트를 위한 setState() 메서드 있음.
  • 컴포지션 — UI는 상속 없이 child/children을 통한 Widget 중첩으로 구축되어 유연성과 재사용성을 제공합니다.
  • BuildContext — Theme, Navigator, MediaQuery 및 InheritedWidget에 액세스하기 위한 트리 내 Widget 위치 설명자.
  • 성능 — Widget은 매 빌드마다 재생성(60FPS)되지만 유형이 일치하면 RenderObject가 재사용됩니다.
  • 범주 — Widget은 레이아웃(Row, Column, Stack), 페인팅(Container, Padding), 인터랙티브(GestureDetector, TextField), 플랫폼(MaterialApp, Theme)으로 나뉩니다.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기