기술 리뷰

플러터 위젯: Stateless와 Stateful, 무엇을 선택해야 할까요?

강코의 코딩 일기 2026. 7. 29. 12:23
반응형

Flutter widget, Stateless Stateful, Widget tree

플러터,위젯,StatelessWidget,StatefulWidget,위젯트리,모바일개발,UI개발,프론트엔드

안녕하세요, 주니어 개발자 여러분! 플러터 개발, 시작은 했는데 막상 코드를 보면 헷갈리는 부분이 많으시죠? 특히 위젯(Widget)이 그런데요.

플러터는 모든 것이 위젯이라는 말은 귀에 못이 박히도록 들었지만, 막상 StatelessWidgetStatefulWidget 중에 뭘 써야 할지, 그리고 내가 만든 위젯들이 화면에 어떻게 그려지는지 그 과정이 명확하게 이해되지 않을 때가 많거든요. 대충 알 것 같으면서도, 왜 이렇게 만들었는지, 언제 뭘 써야 할지 감이 잘 안 올 때가 있으실 거예요.

이 글에서는 플러터 위젯에 대한 흔한 오해들을 하나씩 풀어보고, Stateless 위젯, Stateful 위젯, 그리고 위젯 트리(Widget Tree)의 진짜 의미와 동작 원리를 파헤쳐 볼 거예요. 이 기초 개념들을 탄탄하게 다지면, 더 효율적이고 견고한 플러터 앱을 만들 수 있는 눈을 가지게 될 거라고 확신합니다!

플러터 위젯의 이해: Stateless, Stateful 위젯부터 위젯 트리까지 기초 다지기 - programming, html, css, javascript, php, website development, code, html code, computer code, coding, digital, computer programming, pc, www, cyberspace, programmer, web development, computer, technology, developer, computer programmer, internet, ide, lines of code, hacker, hacking, gray computer, gray technology, gray laptop, gray website, gray internet, gray digital, gray web, gray code, gray coding, gray programming, programming, programming, programming, javascript, code, code, code, coding, coding, coding, coding, coding, digital, web development, computer, computer, computer, technology, technology, technology, developer, internet, hacker, hacker, hacker, hacking

Image by Boskampi on Pixabay

"Stateless 위젯은 그냥 '정적인' UI고, Stateful 위젯은 '움직이는' UI다?"

이건 플러터 입문자들이 가장 많이 하는 오해 중 하나인데요, 사실은 조금 더 깊은 의미가 있답니다.

Stateless 위젯: 불변하는 '청사진'

StatelessWidget은 말 그대로 '상태가 없는(Stateless)' 위젯이에요. 여기서 '상태가 없다'는 건 위젯이 한 번 그려지면 스스로는 그 모습을 바꿀 수 없다는 뜻입니다. 외부에서 새로운 데이터가 주어지거나 부모 위젯이 재빌드될 때만 다시 그려질 수 있죠.

쉽게 비유하자면, Stateless 위젯은 한 장의 포스터와 같아요. 포스터 내용은 항상 고정되어 있죠? 만약 포스터 내용을 바꾸고 싶다면, 아예 새로운 포스터를 만들어서 바꿔 붙여야 하는 것과 비슷합니다. 내부적으로 변할 수 있는 데이터(mutable state)를 가지지 않으며, 모든 속성은 final로 선언됩니다.

Stateful 위젯: 내부 상태를 관리하는 '동작하는 장치'

반면에 StatefulWidget은 '상태를 가지는(Stateful)' 위젯이에요. 이 위젯은 자신만의 내부 상태(internal state)를 가질 수 있고, 그 상태가 변하면 위젯 스스로 자신의 UI를 업데이트할 수 있습니다. setState() 메서드를 호출해서요.

이건 마치 TV와 비슷합니다. TV는 계속 같은 자리에 있지만, 화면에 나오는 내용은 리모컨을 누르거나 채널을 돌리면 계속 바뀌죠? Stateful 위젯은 State라는 별도의 객체를 통해 이 내부 상태를 관리하며, 상태가 변경될 때마다 build() 메서드를 다시 호출하여 UI를 업데이트합니다.

// Stateless 위젯 예시: 변하지 않는 텍스트를 보여줍니다.
class MyStatelessText extends StatelessWidget {
  final String message; // final로 선언되어 불변합니다.

  const MyStatelessText({Key? key, required this.message}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(message); // 받은 메시지를 그대로 출력
  }
}

// Stateful 위젯 예시: 버튼을 누르면 숫자가 증가합니다.
class MyStatefulCounter extends StatefulWidget {
  const MyStatefulCounter({Key? key}) : super(key: key);

  @override
  State<MyStatefulCounter> createState() => _MyStatefulCounterState();
}

class _MyStatefulCounterState extends State<MyStatefulCounter> {
  int _counter = 0; // 내부 상태 (mutable state)

  void _incrementCounter() {
    setState(() { // 상태를 변경하고 UI 업데이트를 요청합니다.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('현재 카운트: $_counter'),
        ElevatedButton(
          onPressed: _incrementCounter,
          child: const Text('증가'),
        ),
      ],
    );
  }
}

결론적으로, Stateless 위젯은 불변하는 데이터를 기반으로 UI를 그리는 역할을 하고, Stateful 위젯은 내부의 가변하는 상태를 관리하며 그에 따라 UI를 변경하는 역할을 한다고 이해하시면 정확합니다.

특징 StatelessWidget StatefulWidget
상태 관리 내부 상태 없음 (외부에서 주입받은 데이터로 UI 구성) State 객체를 통해 내부 상태 변경 및 관리
재빌드 조건 부모 위젯 변경, 상위 InheritedWidget 변경 시 setState() 호출, 부모 위젯 변경, 상위 InheritedWidget 변경 시
생명 주기 단순 (build()만) 복잡 (initState(), didUpdateWidget(), dispose() 등)
주요 용도 고정된 텍스트, 아이콘, 이미지, 레이아웃 등 사용자 입력, 애니메이션, 데이터 변경에 따른 UI 업데이트

"Stateful 위젯이 더 강력하니까 그냥 다 Stateful로 만들면 되는 거 아니야?"

음, 이건 매우 위험한 생각입니다! '강력하다'는 것은 '만능이다'와는 다르거든요.

물론 Stateful 위젯은 내부 상태를 관리할 수 있다는 점에서 훨씬 유연하고 다양한 기능을 구현할 수 있게 해줍니다. 하지만 이 유연성에는 복잡성성능 오버헤드가 뒤따르게 됩니다.

  • 불필요한 복잡성 증가: Stateful 위젯은 State 클래스를 별도로 가져야 하고, initState(), dispose() 같은 생명주기 메서드들을 관리해야 합니다. 만약 변경될 상태가 전혀 없는 위젯을 Stateful로 만들면, 불필요한 코드만 늘어나고 가독성도 떨어지겠죠?
  • 성능 저하 가능성: setState()가 호출되면 해당 위젯과 그 자식 위젯들의 build() 메서드가 다시 실행될 수 있습니다. 만약 앱의 대부분을 Stateful 위젯으로 구성하고 자주 setState()를 호출한다면, 불필요한 재빌드가 빈번하게 발생하여 앱의 성능이 저하될 수 있어요. 플러터는 똑똑하게 최소한의 변경만 하려고 하지만, 애초에 변경될 필요가 없는 위젯을 Stateful로 만드는 건 비효율적입니다.
  • 테스트 및 유지보수의 어려움: 상태를 가지는 위젯은 상태가 없는 위젯보다 테스트하기가 더 까다롭습니다. 가능한 한 작고 단순한 위젯으로 분리하고, 상태 관리는 꼭 필요한 곳에서만 하는 것이 유지보수에 훨씬 유리합니다.

핵심은 '필요에 따라' 사용하는 겁니다. 대부분의 UI 요소는 사실상 변경될 내부 상태가 없는 경우가 많아요. 예를 들어, 단순히 텍스트를 표시하거나 아이콘을 보여주는 위젯은 Stateless 위젯으로 충분합니다. Stateless 위젯을 기본으로 생각하고, 정말 내부 상태 변화가 필요하고 그 상태를 위젯 자신이 관리해야 할 때만 Stateful 위젯을 사용하세요. 이것이 플러터 개발의 좋은 관행이자 성능을 지키는 비결 중 하나입니다.

"위젯 트리는 그냥 UI 계층 구조를 보여주는 그림일 뿐이야."

이 오해도 굉장히 흔한데요, 위젯 트리(Widget Tree)는 단순히 UI의 계층 구조를 나타내는 것을 넘어 플러터의 렌더링 시스템과 성능 최적화의 핵심적인 부분입니다.

플러터는 실제로 세 가지 종류의 트리를 사용해서 화면에 UI를 그리는데요, 이 세 트리가 서로 유기적으로 연결되어 동작합니다.

  1. Widget Tree (위젯 트리):
    • 우리가 Dart 코드로 작성하는 UI의 '설계도' 또는 '청사진'입니다.
    • 모든 위젯은 불변(immutable)합니다. 한 번 생성되면 변경할 수 없어요.
    • UI의 논리적인 구조를 정의합니다. 예를 들어, Center 위젯 안에 Column이 있고 그 안에 TextButton이 있는 식이죠.
    • setState()가 호출되면 플러터는 새로운 위젯 트리를 생성합니다.
  2. Element Tree (엘리먼트 트리):
    • 위젯 트리를 기반으로 생성되는 '실제 UI 요소들의 인스턴스' 트리입니다.
    • 위젯과 렌더 오브젝트 사이의 다리 역할을 합니다.
    • 엘리먼트는 가변(mutable)하며, 위젯의 구성 정보를 가지고 렌더 오브젝트를 관리합니다.
    • 가장 중요한 점은, 엘리먼트 트리는 위젯 트리가 재빌드될 때 최소한의 변경만을 시도한다는 거예요. 플러터는 새로운 위젯 트리의 위젯들과 기존 엘리먼트 트리의 엘리먼트들을 비교해서, 실제로 변경이 필요한 부분만 업데이트합니다. 이것이 플러터 성능의 핵심입니다!
  3. Render Tree (렌더 트리):
    • 엘리먼트 트리를 기반으로 생성되는 '화면에 실제로 그려질 픽셀'들의 트리입니다.
    • 각 렌더 오브젝트는 화면에 어떻게 그려질지(크기, 위치, 색상, 모양 등)에 대한 정보를 가지고 있습니다.
    • 엘리먼트 트리가 변경되면, 렌더 트리가 업데이트되고 최종적으로 GPU를 통해 화면에 픽셀로 그려집니다.

이해를 돕기 위해 비유를 들어볼게요.

  • 위젯 트리는 건축가가 그린 건물 설계도입니다.
  • 엘리먼트 트리는 설계도에 따라 건물을 짓고 관리하는 건설 현장 관리자입니다. 설계도가 바뀌면 관리자는 변경된 부분만 파악해서 기존 건물의 최소 부분만 수정하려 하겠죠.
  • 렌더 트리는 실제로 완성된 건물 그 자체입니다.

setState()가 호출되어 UI가 업데이트될 때, 플러터는 새로운 위젯 트리를 만들고, 이 새로운 설계도를 기존의 엘리먼트 트리와 비교합니다. 이때 플러터는 runtimeTypekey를 사용해서 위젯이 같은지 다른지를 판단해요. 만약 위젯이 같다면 기존 엘리먼트를 재활용하고 속성만 업데이트합니다. 다르다면 기존 엘리먼트를 제거하고 새로운 엘리먼트를 생성하죠. 이 비교 과정을 통해 불필요한 재빌드를 최소화하고 효율적으로 UI를 업데이트하는 것입니다.

플러터 위젯의 이해: Stateless, Stateful 위젯부터 위젯 트리까지 기초 다지기 - technology, computer, code, javascript, developer, programming, programmer, jquery, css, html, website, technology, technology, computer, code, code, code, code, code, javascript, javascript, javascript, developer, programming, programming, programming, programming, programmer, html, website, website, website

Image by Pexels on Pixabay

"부모 위젯이 재빌드되면 자식 위젯도 무조건 다 재빌드되는 거 아니야?"

이것도 잘못된 오해입니다! 플러터는 생각보다 훨씬 더 똑똑하게 작동합니다. 앞서 설명드렸듯이, 플러터는 엘리먼트 트리를 통해 불필요한 재빌드를 최소화하려고 노력해요. 특히 const 키워드Key 속성은 이 최적화에 결정적인 역할을 합니다.

const 키워드의 마법

만약 위젯이 const로 선언되면, 플러터는 이 위젯이 불변(immutable)하다는 것을 미리 알게 됩니다. 즉, 한 번 생성되면 그 내용이 절대로 변하지 않는다는 뜻이죠. 따라서 부모 위젯이 재빌드되더라도, 플러터는 const로 선언된 자식 위젯을 이전 위젯과 비교할 필요 없이 '아, 얘는 똑같겠네' 하고 건너뛸 수 있습니다.

class MyParentWidget extends StatefulWidget {
  const MyParentWidget({Key? key}) : super(key: key);

  @override
  State<MyParentWidget> createState() => _MyParentWidgetState();
}

class _MyParentWidgetState extends State<MyParentWidget> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('MyParentWidget build() 호출됨');
    return Column(
      children: [
        Text('부모 카운트: $_count'),
        const Text('나는 const 위젯이야!'), // 이 위젯은 부모가 재빌드되어도 다시 비교되지 않고 재활용됩니다.
        ElevatedButton(
          onPressed: _increment,
          child: const Text('부모 카운트 증가'),
        ),
      ],
    );
  }
}

위 코드에서 const Text('나는 const 위젯이야!')_count가 변하여 부모 위젯이 재빌드되더라도 실제로 다시 빌드되지 않습니다. 콘솔 출력 결과에서 MyParentWidget build() 호출됨만 반복될 뿐, const Text의 빌드는 일어나지 않음을 확인할 수 있습니다. 이는 엄청난 성능 최적화 효과를 가져오죠!

Key 속성의 중요성

Key는 플러터가 위젯 트리를 비교할 때 위젯의 '정체성'을 부여하는 중요한 도구입니다. 특히 동적으로 생성되거나 순서가 변경될 수 있는 리스트 형태의 위젯들에서 그 진가를 발휘하죠.

  • 플러터는 위젯을 비교할 때 runtimeTypekey를 함께 사용합니다. 만약 두 위젯의 runtimeTypekey가 모두 같다면, 플러터는 같은 엘리먼트를 재활용하고 업데이트합니다.
  • 예시: 리스트에서 아이템의 순서가 바뀌거나 추가/삭제될 때 Key가 없다면, 플러터는 어떤 아이템이 바뀌었는지 정확히 알기 어렵습니다. 그래서 모든 아이템을 새로 빌드하거나 비효율적으로 업데이트할 수 있죠. 하지만 각 아이템에 고유한 Key를 부여하면, 플러터는 Key를 기준으로 아이템의 정체성을 파악하고 필요한 부분만 효율적으로 업데이트할 수 있게 됩니다.
// Key를 사용하지 않은 리스트 (비효율적)
ListView(
  children: [
    Text('Item A'),
    Text('Item B'),
    Text('Item C'),
  ],
);

// Key를 사용한 리스트 (효율적)
ListView(
  children: [
    Text('Item A', key: ValueKey('A')),
    Text('Item B', key: ValueKey('B')),
    Text('Item C', key: ValueKey('C')),
  ],
);

ValueKey, ObjectKey, UniqueKey, GlobalKey 등 다양한 종류의 Key가 있으니, 상황에 맞게 사용하시면 됩니다.

결론적으로, 가능한 한 많은 위젯에 const를 붙이고, 동적인 리스트에는 반드시 Key를 부여하여 플러터의 재빌드 최적화 기능을 최대한 활용하세요. 이는 앱의 성능을 크게 향상시키는 중요한 팁입니다.

플러터 위젯의 이해: Stateless, Stateful 위젯부터 위젯 트리까지 기초 다지기 - code, coding, computer, data, developing, development, ethernet, html, programmer, programming, screen, software, technology, work, code, code, coding, coding, coding, coding, coding, computer, computer, computer, computer, data, programming, programming, programming, software, software, technology, technology, technology, technology

Image by Pexels on Pixabay

"Stateful 위젯의 setState는 그냥 아무 데서나 막 불러도 되는 거 아니야?"

setState()Stateful 위젯의 핵심 기능이지만, '아무 데서나' 막 부르는 것은 지양해야 합니다. setState()는 해당 위젯의 build() 메서드를 다시 호출하도록 예약하는 함수인데요, 이 과정이 마냥 가볍지만은 않거든요.

  • 불필요한 재빌드: setState()는 비동기적으로 스케줄링되어 다음 프레임에 위젯을 재빌드합니다. 만약 상태가 실제로 변하지 않았는데도 setState()를 호출하거나, 너무 작은 단위의 위젯에서 자주 호출하면 불필요한 렌더링 작업이 발생하여 앱의 성능을 저하시킬 수 있습니다.
  • 위젯 트리 분리 및 상태 관리 솔루션: 복잡한 앱에서는 setState()만으로 상태를 관리하는 것이 어려워지고, 상태 변경 로직이 위젯 코드와 뒤섞여 가독성과 유지보수성이 떨어집니다. 이럴 때는 Provider, Riverpod, Bloc, GetX와 같은 상태 관리 솔루션을 사용하여 상태를 위젯으로부터 분리하고, 필요한 위젯만 선택적으로 업데이트하는 것이 훨씬 효율적입니다.
  • mounted 속성 확인: setState()를 호출할 때 가장 흔하게 발생하는 버그 중 하나가 위젯이 이미 위젯 트리에서 제거된 후에 setState()를 호출하는 경우입니다. 이는 에러를 발생시키므로, 비동기 작업 후 setState()를 호출하기 전에는 항상 if (mounted)를 통해 위젯이 여전히 트리에 존재하는지 확인하는 습관을 들이는 것이 좋습니다.
// 비동기 작업 후 setState 호출 시 mounted 확인 예시
Future<void> _fetchData() async {
  // 데이터 로딩 시작 (UI 업데이트)
  setState(() {
    // 로딩 상태 변경
  });

  await Future.delayed(const Duration(seconds: 2)); // 데이터 로딩 시뮬레이션

  if (mounted) { // 위젯이 여전히 트리에 있는지 확인
    setState(() {
      // 로딩 완료 후 데이터 업데이트 (UI 업데이트)
    });
  }
}

setState()는 꼭 필요한 최소한의 범위에서, 그리고 위젯이 mounted 상태일 때만 호출하는 것이 바람직합니다. 무분별한 setState() 호출은 앱의 성능과 안정성을 해칠 수 있으니 주의해야 해요.

마무리하며: 기초가 탄탄해야 흔들리지 않습니다

자, 이제 플러터 위젯에 대한 몇 가지 흔한 오해들을 풀어보면서, Stateless 위젯Stateful 위젯의 본질적인 차이, 그리고 위젯 트리가 실제 UI 렌더링 과정에서 어떤 역할을 하는지 좀 더 명확하게 이해하게 되셨을 거예요.

핵심을 다시 한번 요약하자면 이렇습니다.

  • Stateless 위젯은 불변하는 '청사진'으로, 외부 데이터에 의존하여 UI를 그립니다.
  • Stateful 위젯은 내부 상태를 관리하며 스스로 UI를 업데이트할 수 있는 '동작하는 장치'입니다.
  • Stateless를 기본으로 생각하고, 정말 내부 상태가 필요할 때만 Stateful을 사용하세요.
  • 위젯 트리는 단순히 UI 계층이 아니라 엘리먼트 트리, 렌더 트리와 함께 플러터의 효율적인 렌더링을 담당하는 핵심 구조입니다.
  • const 키워드Key 속성을 적극 활용하여 불필요한 위젯 재빌드를 막고 앱의 성능을 최적화하세요.
  • setState()는 신중하게, 최소한의 범위에서, 그리고 mounted 상태를 확인하며 호출하는 것이 중요합니다.

이 기초 개념들을 제대로 이해하고 적용하는 것이야말로, 복잡한 UI와 동적인 기능을 가진 플러터 앱을 만들 때 마주하는 수많은 문제들을 해결하고, 더 견고하고 성능 좋은 앱을 개발하는 데 큰 도움이 될 겁니다.

이 글이 플러터 위젯에 대한 여러분의 이해를 한 단계 더 끌어올리는 데 도움이 되었기를 바랍니다. 혹시 또 다른 궁금증이나 플러터 위젯 관련 경험이 있으시다면 댓글로 공유해 주세요! 함께 성장하는 개발 커뮤니티를 만들어가요!

📌 함께 읽으면 좋은 글

  • [기술 리뷰] 쿼리 100개에서 2개로 줄여 API 응답 속도 5배 향상시킨 ORM N+1 해결 실전 노하우
  • [커리어 취업] 해외 테크 기업 취업, FAANG과 스타트업 사이의 진실은?
  • [개발 책 리뷰] 개발팀과 소통이 어려웠던 PM이 '엘리펀트 인 더 룸'에서 찾은 답 3가지

이 글이 도움이 되셨다면 공감(♥)댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.

반응형