Flutter 공식 페이지에도 소개되는 내용이지만 한번 더 소화해서 정리해 보자.
Introduction to declarative UI
Explains the difference between a declarative and imperative programming style.
flutter.dev
Imperative vs. Declarative
일단 Imperative 와 Declarative 의 차이부터 알아보자.
Imperative는 명령형 Declarative는 선언형 을 의미한다. 뭔 말이지?
아래 Flutter 공식 사이트에 나오는 예제로 이야기해 보자.
- 배경색을 노란색에서 빨간색으로 변경하고
- ViewB 타입의 b에 있는 ViewC 타입의 c1과 c2를 없애고 대신 ViewC 타입의 c3 를 추가하는 예제이다.

위의 어플리케이션을 Imperative 방식으로 작성하면 아래와 같다. 익숙하지 않은가? 그냥 b의 setColor() 로 색을 바꾸고, cleanChildren() 으로 자식인 c1, c2를 모두 없애고, add() 로 c3를 추가했다.
// Imperative style
b.setColor(red)
b.clearChildren()
ViewC c3 = new ViewC(...)
b.add(c3)
반면 Declarative 방식은 아래와 같다. 그냥 배경이 빨갛고 ViewC인 자식 c3가 있는 ViewB 타입의 b를 새로 만들었다.
// Declarative style
return ViewB(
color: red,
child: ViewC(...),
)
결국 가장 큰 차이는 이렇다.
- Imperative 는 개발자가 UI 요소의 상태를 관리하면서 직접 상태를 변경해주는 방식이고
- Declarative 방식은 특정 UI 요소의 변경이 필요할 때마다 해당 UI 요소를 새로 만들어 내는 방식이다.
사실 후자가 매번 새로 만드니까 로직이 간단해 지지만 불필요하게 느릴것 같은 느낌이 든다. 그럼에도 Flutter는 왜 후자를 선택한걸까?
- 모든 UI 요소를 변경 불가능 (Immutable) 하게 만들어서 개발자가 UI 요소들의 상태를 관리하는 부담을 덜어준다.
- 선언적으로 UI 요소를 표현하므로 마치 안드로이드의 layout xml 을 작성하는 느낌으로 레이아웃 구성을 직관적으로 프로그래밍 가능하다.
- 반면 매번 UI 요소를 새로 구성함으로 인해 우려되는 성능의 문제는 flutter 프레임워크 내부에서 구조적으로 해결해 준다. 추후, 렌더링 과정에 대한 포스팅을 하겠지만, 내부적으로 위젯 트리, 엘리먼트 트리, 렌더 트리라는 세 개의 트리를 거쳐 렌더링이 수행되는데 그 과정에서 위젯 트리를 캐싱하여 재활용 가능한 구조로 만들어져 있다. 따라서 개발자는 매번 UI 요소를 새로 만드는 것 같지만, 실제는 재활용 가능한 캐싱된 UI 요소는 새로만들지 않고 재활용을 하게 된다.
사실 Declarative UI 패러다임은 모던 UI 프레임워크에서 지속적으로 채택되고 있다. 특히 안드로이드도 Jetpack Compose 라는 형태로 Declarative 스타일을 흉내 내기 시작했다.
재미 있는 것은, 개발의 난이도를 낮추고 복잡한 부분은 프레임워크가 담당하는 형태라는 점에서 immediate mode vs. retained mode UI 를 비교하던 시절이 떠오른다. 결국 주류였던 retained mode UI 도 개발자는 변경하고 싶은 UI 요소의 속성만 모두 변경해 두고, 프레임워크가 clipping 영역 계산 및 업데이트를 통해 렌더링을 최적화했었는데.. 디테일은 전혀 다르나 지향점이 비슷한..느낌이다.. ㅎㅎ;;
댓글