Flutter 애플리케이션 개발의 핵심은 '상태(State)'에 따라 UI를 동적으로 변경하는 것입니다. 사용자의 상호작용, 네트워크 응답, 시스템 이벤트 등 다양한 요인에 의해 앱의 상태는 끊임없이 변화하며, 뛰어난 사용자 경험을 제공하기 위해서는 이러한 변화를 UI에 즉각적이고 자연스럽게 반영해야 합니다. 단순히 버튼을 눌렀을 때 색상이 변하는 것부터, 로그인 상태에 따라 전혀 다른 페이지를 보여주는 복잡한 시나리오까지, 조건부 UI 렌더링은 Flutter 개발의 근간을 이룹니다.
많은 개발자, 특히 Flutter에 입문하는 분들은 특정 조건에서 위젯을 보여주거나 숨기는 방법에 대해 고민합니다. 가장 직관적인 방법은 무엇일까요? 성능에 가장 유리한 선택은 무엇일까요? 위젯의 상태를 유지하면서 숨길 수는 없을까요? 이 글에서는 이러한 질문에 대한 명쾌한 해답을 제시합니다. 단순한 if
문 사용법부터 Visibility
, Offstage
위젯의 깊이 있는 활용법, 그리고 상황에 맞는 최적의 기술을 선택하는 기준까지, 실무에서 마주할 수 있는 다양한 시나리오에 대응할 수 있는 모든 방법을 상세히 다룹니다.
1. 가장 직관적이고 강력한 방법: Collection `if`
Dart 2.3 버전부터 도입된 'Collection `if`'는 Flutter 위젯 트리 내에서 조건부로 위젯을 포함하거나 제외하는 가장 현대적이고 가독성 높은 방법입니다. 마치 일반적인 Dart 코드에서 if
문을 사용하듯, 위젯 리스트(children
) 안에서 직접 조건을 검사할 수 있습니다.

위젯 리스트 내에서 직접 if문을 사용하여 조건부 렌더링을 구현하는 모습
이 방식의 가장 큰 장점은 '조건이 거짓(false)일 때 위젯이 위젯 트리에 아예 포함되지 않는다'는 점입니다. 이는 단순히 화면에 보이지 않는 것을 넘어, 해당 위젯의 생성(instantiation), 레이아웃(layout), 페인팅(painting) 과정이 모두 생략됨을 의미합니다. 따라서 불필요한 리소스 소모를 막아 성능상 이점을 가집니다.
기본 사용법
Column
, Row
, ListView
, Stack
등 children
프로퍼티를 가지는 대부분의 위젯에서 사용할 수 있습니다. 다음은 로그인 상태를 나타내는 _isLoggedIn
이라는 불리언(boolean) 변수를 사용하여 프로필 아이콘과 로그인 버튼을 조건부로 보여주는 예제입니다.
bool _isLoggedIn = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Dynamic UI with Collection if'),
actions: [
// 로그인 상태일 때만 프로필 아이콘을 보여줌
if (_isLoggedIn)
IconButton(
icon: Icon(Icons.account_circle),
onPressed: () {
// 프로필 화면으로 이동
},
),
// 로그아웃 상태일 때만 로그인 버튼을 보여줌
if (!_isLoggedIn)
TextButton(
child: Text('Login', style: TextStyle(color: Colors.white)),
onPressed: () {
// 로그인 로직 처리
},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Welcome to our App!'),
SizedBox(height: 20),
// 특정 조건(예: 프리미엄 사용자)에 따라 추가 기능 버튼을 보여줌
if (_user.isPremium)
ElevatedButton(
child: Text('Access Premium Features'),
onPressed: () {},
),
// if-else도 사용 가능
if (_isLoggedIn)
Text('Hello, ${_user.name}!')
else
Text('Please log in to continue.'),
],
),
),
);
}
장점과 단점
- 장점:
- 가독성: 코드가 직관적이고 이해하기 쉽습니다. 위젯 트리의 구조를 해치지 않으면서 조건부 로직을 명확하게 표현할 수 있습니다.
- 성능: 조건이 거짓일 때 위젯이 아예 빌드되지 않으므로, 특히 복잡하고 무거운 위젯을 조건부로 렌더링할 때 성능상 유리합니다.
- 간결함: 삼항 연산자나 별도의 함수를 사용하는 것보다 코드가 간결해집니다.
- 단점:
- 상태 소실: 위젯이 트리에서 완전히 제거되었다가 다시 추가되는 방식이므로, 만약 해당 위젯이 자체적인 상태(e.g.,
StatefulWidget
의State
객체)를 가지고 있었다면 그 상태는 소실됩니다. 예를 들어,TextFormField
에 입력된 값이나ListView
의 스크롤 위치 등이 초기화될 수 있습니다.
- 상태 소실: 위젯이 트리에서 완전히 제거되었다가 다시 추가되는 방식이므로, 만약 해당 위젯이 자체적인 상태(e.g.,
따라서, Collection if
는 위젯의 상태 보존이 중요하지 않거나, 일회성으로 보여지는 정보(예: 알림, 배너) 또는 상태가 없는(stateless) 위젯들을 조건부로 렌더링할 때 가장 이상적인 선택입니다.
2. 전통적인 강자: 삼항 연산자 (Ternary Operator)
삼항 연산자(condition ? expr1 : expr2
)는 Collection if
가 등장하기 전부터 Flutter에서 조건부 UI를 구현하는 데 널리 사용된 고전적인 방법입니다. 하나의 위젯 자리에 두 가지 다른 위젯 중 하나를 선택적으로 배치할 때 매우 유용합니다.
기본 사용법
삼항 연산자는 위젯이 단일 자식(child
)을 가지는 경우나, 위젯 리스트 내에서 특정 위치의 위젯을 교체할 때 효과적입니다.
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
// 로딩 상태에 따라 프로그레스 바 또는 컨텐츠를 보여줌
child: _isLoading
? CircularProgressIndicator()
: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_isLoading = !_isLoading;
});
},
// 로딩 상태에 따라 아이콘을 변경
child: Icon(_isLoading ? Icons.pause : Icons.play_arrow),
),
);
}
'아무것도 보여주지 않기'의 함정: `SizedBox.shrink()`
삼항 연산자의 특징은 expr1
과 expr2
자리에 반드시 위젯이 와야 한다는 점입니다. 만약 조건이 거짓일 때 아무것도 보여주고 싶지 않다면 어떻게 해야 할까요? null
을 반환하면 에러가 발생합니다. 이 때 사용하는 대표적인 패턴이 바로 SizedBox.shrink()
또는 Container()
입니다.
SizedBox.shrink()
: 가로, 세로 크기가 0인 상수(const) 위젯입니다. 공간을 전혀 차지하지 않으며, 빌드 및 레이아웃 비용이 거의 없어 '빈 공간'을 표현하는 데 가장 효율적입니다.Container()
: 빈 생성자로 호출하면 크기가 0인 위젯이 되지만,const
가 아니므로SizedBox.shrink()
에 비해 미세하게 비효율적일 수 있습니다. 특별한 이유가 없다면SizedBox.shrink()
사용을 권장합니다.
bool _showBanner = true;
// ... 위젯 트리 내부 ...
Column(
children: [
// 배너를 보여줘야 할 때만 광고 위젯을 표시하고, 아닐 때는 빈 공간으로 대체
_showBanner
? MyAdBannerWidget()
: SizedBox.shrink(),
// 다른 컨텐츠들...
Text('Main Content'),
],
)
이 패턴은 유용하지만, 코드가 길어지면 가독성이 떨어질 수 있습니다. 이런 경우에는 Collection `if`가 더 나은 대안이 될 수 있습니다. (if (_showBanner) MyAdBannerWidget()
)
장점과 단점
- 장점:
- 간결함: 두 개의 위젯을 간단히 교체하는 상황에서는 매우 간결하고 명확합니다.
- 보편성: Dart 언어의 기본 기능이므로 어떤 위젯의 어떤 프로퍼티에도 적용할 수 있습니다.
- 단점:
- 가독성 저하: 조건이 복잡해지거나 중첩된 삼항 연산자를 사용하면 코드를 이해하기 매우 어려워집니다.
- 유연성 부족: 여러 위젯을 한 번에 제어하거나, '아무것도 보여주지 않는' 케이스를 처리하기 위해
SizedBox.shrink()
같은 추가적인 코드가 필요합니다.
3. 상태와 공간을 제어하는 마법사: `Visibility` 위젯
Visibility
위젯은 이름 그대로 자식(child) 위젯의 '보여짐' 상태를 제어하는 데 특화된 위젯입니다. Collection `if`나 삼항 연산자와의 가장 결정적인 차이점은 위젯을 숨길 때 위젯 트리에서 제거하지 않고, 단지 렌더링만 하지 않을 수 있다는 점입니다. 더 나아가, 숨겨진 위젯이 차지하던 공간을 유지할지, 상태를 보존할지 등을 세밀하게 제어할 수 있는 강력한 옵션들을 제공합니다.
기본 사용법과 핵심 프로퍼티
가장 중요한 프로퍼티는 visible
입니다. 이 값이 true
이면 자식 위젯이 보이고, false
이면 보이지 않습니다.
bool _isVisible = true;
// ...
Visibility(
visible: _isVisible,
child: Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text('This is a conditionally visible card.'),
),
),
)
// ...
Visibility
의 진정한 힘은 visible: false
일 때의 동작을 제어하는 프로퍼티들에서 나옵니다.
maintainState
(기본값:false
):true
로 설정하면, 위젯이 보이지 않더라도 위젯의State
객체를 포함한 하위 트리가 메모리에 유지됩니다.TextFormField
의 입력 내용, 스크롤 위치, 애니메이션 상태 등이 보존됩니다. Collection `if`의 가장 큰 단점이었던 '상태 소실' 문제를 해결하는 핵심 옵션입니다.maintainSize
(기본값:false
):true
로 설정하면, 위젯이 보이지 않더라도 원래 차지하던 공간을 그대로 유지합니다. 레이아웃이 변경되는 것을 원치 않을 때 유용합니다. (내부적으로는 투명한SizedBox
로 대체되는 것과 유사하게 동작합니다.)maintainAnimation
(기본값:false
):true
로 설정하면, 보이지 않는 동안에도 애니메이션이 계속 실행됩니다.maintainSemantics
(기본값:false
): 스크린 리더와 같은 접근성 도구를 위해 위젯의 시맨틱 정보를 유지할지 결정합니다.maintainInteractivity
(기본값:false
): 보이지 않는 위젯이 터치와 같은 사용자 입력을 계속 받을 수 있게 할지 결정합니다. (매우 드물게 사용됩니다.)replacement
(기본값:SizedBox.shrink()
):visible: false
일 때 보여줄 대체 위젯을 지정합니다. 기본적으로는 공간을 차지하지 않는SizedBox.shrink()
가 사용됩니다. 만약 공간을 유지하고 싶다면maintainSize: true
를 사용하거나,replacement
에 동일한 크기의 다른 위젯(e.g.,Container(width: 100, height: 100)
)을 지정할 수 있습니다.
실용적인 예제: 상태 보존
사용자가 입력하던 폼 필드를 잠시 숨겼다가 다시 보여줄 때, 입력 내용이 그대로 남아있어야 합니다. 이럴 때 Visibility
와 maintainState: true
가 완벽한 해결책이 됩니다.
bool _showAdvancedOptions = false;
final _textController = TextEditingController();
// ...
Column(
children: [
CheckboxListTile(
title: Text('Show Advanced Options'),
value: _showAdvancedOptions,
onChanged: (value) {
setState(() {
_showAdvancedOptions = value!;
});
},
),
Visibility(
visible: _showAdvancedOptions,
maintainState: true, // 이 옵션 덕분에 숨겨져도 컨트롤러와 입력값이 유지됨
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextFormField(
controller: _textController,
decoration: InputDecoration(
labelText: 'Enter advanced configuration',
),
),
),
),
],
)
위 예제에서 체크박스를 해제하여 TextFormField
를 숨겼다가 다시 체크해서 나타나게 해도, 이전에 입력했던 텍스트가 그대로 남아있는 것을 확인할 수 있습니다. 만약 Collection `if`를 사용했다면 TextFormField
가 매번 새로 생성되어 입력 내용이 사라졌을 것입니다.
4. 성능 최적화의 숨은 강자: `Offstage` 위젯
Offstage
는 Visibility
와 유사한 목적을 가지지만, 동작 방식에 미묘하지만 중요한 차이가 있습니다. Offstage
는 자식 위젯을 화면에 그리지(paint) 않고 히트 테스트(hit testing) 대상에서 제외하지만, 항상 레이아웃을 계산하고 공간을 차지하며 상태를 유지합니다.
Offstage(offstage: true)
는 사실상 Visibility(visible: false, maintainState: true, maintainSize: true, maintainAnimation: true)
와 거의 동일하게 동작합니다. Offstage
는 이 모든 'maintain' 옵션이 항상 true
인, 더 단순화된 버전이라고 생각할 수 있습니다.
`Offstage`는 언제 사용할까?
Offstage
의 주된 용도는 여러 화면이나 복잡한 위젯 그룹을 전환할 때, 현재 보이지 않는 위젯들의 상태를 완벽하게 보존하고 싶을 때입니다. 예를 들어, TabBarView
나 PageView
처럼 여러 페이지를 스와이프하며 전환하는 UI를 직접 구현한다고 상상해 봅시다.
사용자가 A 탭에서 B 탭으로 이동했을 때, A 탭의 스크롤 위치나 상태를 그대로 유지했다가 다시 돌아왔을 때 복원해주고 싶을 것입니다. 이 때 Stack
위젯과 Offstage
를 조합하면 매우 효과적으로 구현할 수 있습니다.
int _currentIndex = 0;
// ...
IndexedStack(
index: _currentIndex,
children: [
Page1(), // index 0
Page2(), // index 1
Page3(), // index 2
],
)
// 위 IndexedStack은 내부적으로 다음과 같이 동작합니다.
// (실제 코드는 더 복잡하지만 개념적으로는 이와 같습니다.)
Stack(
children: [
Offstage(
offstage: _currentIndex != 0, // 현재 인덱스가 0이 아니면 숨김
child: Page1(),
),
Offstage(
offstage: _currentIndex != 1, // 현재 인덱스가 1이 아니면 숨김
child: Page2(),
),
Offstage(
offstage: _currentIndex != 2, // 현재 인덱스가 2이 아니면 숨김
child: Page3(),
),
],
)
Flutter의 IndexedStack
위젯이 바로 이 Offstage
를 활용하여 여러 자식 중 하나만 보여주면서 나머지 자식들의 상태는 그대로 유지하는 대표적인 예입니다. 페인팅 비용은 절약하면서도 상태는 보존해야 하는 복잡한 UI 전환 시나리오에서 Offstage
는 최고의 성능과 UX를 제공하는 선택지가 될 수 있습니다.
5. 로직 분리를 위한 선택: 빌더 함수/메서드
UI를 조건부로 구성하는 로직이 복잡해지면 build
메서드가 비대해지고 가독성이 떨어지기 시작합니다. 이럴 때는 조건부 위젯을 반환하는 별도의 함수나 메서드를 만드는 것이 좋은 해결책이 될 수 있습니다.
enum AuthStatus { unknown, authenticated, unauthenticated }
AuthStatus _status = AuthStatus.unknown;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _buildContent(),
),
);
}
// 인증 상태에 따라 다른 위젯을 반환하는 빌더 메서드
Widget _buildContent() {
switch (_status) {
case AuthStatus.authenticated:
return Text('Welcome, User!');
case AuthStatus.unauthenticated:
return ElevatedButton(child: Text('Login'), onPressed: () {});
case AuthStatus.unknown:
default:
return CircularProgressIndicator();
}
}
장점과 단점
- 장점:
- 관심사 분리(Separation of Concerns): 복잡한 UI 로직을
build
메서드에서 분리하여 코드의 구조를 명확하게 하고 유지보수성을 높입니다. - 재사용성: 여러 곳에서 비슷한 조건부 UI가 필요할 때 함수를 재사용할 수 있습니다. (물론 별도의 위젯 클래스로 만드는 것이 더 나은 경우가 많습니다.)
- 관심사 분리(Separation of Concerns): 복잡한 UI 로직을
- 단점:
- 성능 함정: 빌더 메서드를
StatelessWidget
안에서 잘못 사용하면, 상위 위젯이 리빌드될 때마다 해당 메서드가 불필요하게 계속 호출될 수 있습니다.build
메서드 내에서 로직을 수행하는 것과 성능상 큰 차이는 없지만, 구조를 설계할 때 이 점을 인지하고 있어야 합니다. - 과용 금지: 아주 간단한 조건부 로직을 위해 별도의 메서드를 만드는 것은 오히려 코드를 파편화하고 추적을 어렵게 만들 수 있습니다.
- 성능 함정: 빌더 메서드를
최종 정리: 어떤 방법을 언제 사용해야 할까?
지금까지 살펴본 다양한 방법들은 각각의 장단점과 적합한 사용처가 있습니다. 프로젝트의 요구사항과 상황에 맞는 최적의 도구를 선택하는 것이 중요합니다. 아래 표는 각 방법의 핵심적인 특징을 요약하고 선택을 돕기 위한 가이드입니다.
방법 | 핵심 특징 | 상태 보존 여부 | 공간 차지 여부 | 추천 사용 사례 |
---|---|---|---|---|
Collection `if` | 조건이 거짓일 때 위젯을 트리에서 완전히 제거. 가독성 최상. | 아니오 (X) | 아니오 (X) | - 상태 보존이 필요 없는 위젯 숨기기/보이기 - 리스트에 조건부로 아이템 추가/제거 - 가장 일반적이고 권장되는 방법 |
삼항 연산자 | 두 위젯 중 하나를 교체. 숨기려면 SizedBox.shrink() 필요. |
아니오 (X) | 아니오 (X) (SizedBox.shrink() 사용 시) |
- 두 개의 다른 위젯을 간단히 전환할 때 - 아이콘, 텍스트 등 작은 부분을 동적으로 바꿀 때 |
`Visibility` | 위젯을 트리에 유지하되 렌더링만 제어. 상태/공간 유지 옵션 제공. | 옵션 (O) (maintainState: true ) |
옵션 (O) (maintainSize: true ) |
- TextFormField , ListView 등 상태 유지가 중요한 위젯을 숨길 때- 위젯을 숨겨도 레이아웃이 변하지 않게 하고 싶을 때 |
`Offstage` | 위젯을 트리에 유지하고, 항상 상태와 공간을 보존함. 페인팅만 생략. | 예 (O) | 예 (O) | - IndexedStack , TabBarView 처럼 여러 페이지/뷰 전환 시 상태를 완벽히 보존해야 할 때- 성능 최적화가 중요한 복잡한 UI 전환 |
빌더 메서드 | 복잡한 조건부 로직을 함수로 분리. | 반환하는 위젯에 따라 다름 | 반환하는 위젯에 따라 다름 | - `build` 메서드가 너무 복잡해질 때 - 여러 `switch-case`나 복잡한 `if-else` 로직으로 UI를 구성할 때 |
결론
Flutter에서 조건에 따라 UI를 동적으로 보여주는 것은 단순히 화면을 그리는 것을 넘어, 앱의 성능, 상태 관리, 사용자 경험에 직접적인 영향을 미치는 중요한 기술입니다. 어떤 상황에서는 위젯을 완전히 새로 그리는 것이 효율적이고, 어떤 상황에서는 비용을 조금 더 지불하더라도 상태를 유지하는 것이 올바른 선택입니다.
이제 여러분은 단순한 정보 표시에 가장 적합하고 가독성 좋은 Collection `if`, 두 위젯의 간단한 교체에 유용한 삼항 연산자, 상태와 공간 유지에 대한 세밀한 제어가 필요할 때 빛을 발하는 `Visibility` 위젯, 그리고 복잡한 뷰 전환 시 성능과 상태 보존을 모두 잡는 `Offstage` 위젯의 차이점을 명확히 이해하게 되었습니다.
이러한 도구들의 특성을 정확히 파악하고 적재적소에 활용하는 능력은 여러분의 Flutter 개발 역량을 한 단계 끌어올려 줄 것입니다. 이제 여러분의 코드에서 마주치는 모든 '조건부 렌더링' 시나리오에 자신감을 가지고 최적의 해결책을 적용해 보시기 바랍니다.
0 개의 댓글:
Post a Comment