[Flutter] BuildContext
그동안 Flutter의 GetX에 익숙했는데, GetX에서는 BuildContext 없이도 객체를 불러올 수 있었다(GetX 말고 Provider나 BloC같은 다른 상태관리 패키지에서는 ViewModel을 얻기 위해 BuildContext가 필요하다고 한다).
그래서인지 BuildContext에 대한 개념이 빈약했던 거 같아, 이번에 이 개념을 자세히 정리하려고 한다.
Flutter 아키텍처
출처 : 공식문서
Flutter 아키텍처는 Embedder, Engine, Framework 순으로 점점 더 추상화된다.
Embedder 계층: 각 플랫폼 별 기능들(통신, 사용자입력 등)을 플랫폼별로 구동시킬 수 있도록 하는 레이어다.
Engine 계층: Flutter의 저수준 기능들을 제공한다. 엔진이 C++로 작성되어 있다. dart.ui를 통해 사용할 수 있다.
Framework 계층: Flutter의 고수준 기능들을 제공한다.
(보충 필요)
이 중 Framework 계층은 개발자가 Dart 언어로 Flutter와 상호작용하는 부분인데, 이 안에 Widget Layer가 포함되어 있다.
Widget Layer는 화면의 구성을 추상화하는 계층이다. Text, Container, Column, Row, Scaffold 등 extends Widget으로 정의된 위젯들뿐만 아니라, StatelessWidget, StatefulWidget, InheritedWidget, PreferredSizeWidget 등 위젯 구조 자체를 정의하거나 전달하는 컴포넌트들도 이 계층에 포함된다.
출처: 공식문서
그리고 Flutter에는 3개의 Tree가 있는데, Widget tree, Element tree, RenderObject tree다.
Widget tree는 위젯들을 계층적으로 구조화한 것이다.
Element tree는 위젯트리에 어떤 요소가 있는지 확인하고, 이 요소들 간의 관계를 관리한다.
RenderObject tree는 위젯을 실제 화면에 그린다.
사실 왜 이렇게 3개로 나눈 건지 잘 모르겠다, 그리고 Element가 뭔지도 감이 잘 안 온다.
서치해본 결과로는, 불필요한 렌더링(만약 어떤 요소에서 문제가 발생했을 때, 위젯 트리의 경우 각 위젯들이 immutable하다보니 해당 위젯 자체를 갈아엎고 다시 만들어야 한다. 반면, 엘리먼트랑 렌더 트리에서는 그 요소 전체를 갈아엎을 필요 없이 필요한 부분만 렌더링하는 게 가능하다.)을 줄이고 성능을 높이기 위해 이런 방식을 사용했다고 한다…
build()와 BuildContext
StatelessWidget과 StatefulWidget(정확히는 이 안의 State)은 build() 메소드를 가지고 있다. 이 메소드는 Widget을 반환하고, BuildContext를 인자로 받는다.
여기서 BuildContext는 사실 추상 클래스다. 실체는 Element 클래스로 구현한다고 한다.
1
2
3
4
5
6
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text('Hello'),
);
}
위 코드에서 build() 메소드가 실행되려면 BuildContext가 필요하다. 이때 전달되는 context는 바로 부모 위젯이 생성했던 Element 객체다.
아랫줄에서 Scaffold가 리턴되는데, Flutter는 이 Scaffold를 위젯 트리에 연결해야 하고, 이때 부모의 BuildContext(=context)를 활용해 연결한다.
그리고 Scaffold 위젯은 이 시점에 자신의 고유한 BuildContext를 갖게되는 것이다.
그래서, 모든 위젯은 자신만의 BuildContext를 가진다고 이해하되, 정확히는 Flutter가 먼저 해당 Widget에 대한 Element를 만들어서 위젯 트리에 연결하는 시점에 BuildContext가 부여되는 거라고 보면 될 듯하다.
🔎 정리
StatelessWidget과StatefulWidget(의State)은build()메소드를 갖는다.- 이
build()는BuildContext를 인자로 받는다. BuildContext는 추상 클래스고, 실제로는Element의 참조다.모든 위젯은
build()를 통해 리턴되는 시점에Element가 만들어지고, 부모의context를 통해 트리에 연결된다. 그리고 고유한BuildContext를 갖게 된다.
Builder 위젯
Builder 위젯 내부의 builder: (context) {} 콜백에 전달되는 context는 완전히 새로운 context이다. 즉, 부모와 구분되는 독립적인 BuildContext가 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Demo")),
body: ElevatedButton(
onPressed: () {
// ❌ 에러 발생 가능
Scaffold.of(context).showSnackBar(
SnackBar(content: Text("안녕하세요!")),
);
},
child: Text("스낵바 보여줘"),
),
);
}
}
Scaffold.of(context)를 하면, context보다 상위에 있는 가장 가까운 Scaffold를 위로 탐색한다. 이 코드에서는, Scaffold.of(context)가 현재 build()가 리턴하는 Scaffold의 BuilderContext를 가리키길 의도했으나, .of()안에 들어간 context가 이미 Scaffold보다 상위에 있는 StatelessWidget의 context이기 때문에 에러가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Demo")),
body: Builder(
builder: (innerContext) => Center(
child: ElevatedButton(
onPressed: () {
Scaffold.of(innerContext).showSnackBar(
SnackBar(content: Text("안녕하세요!")),
);
},
child: Text("스낵바 보여줘"),
),
),
),
);
}
}
의도대로 동작하려면, .of() 안에 들어가는 BuilderContext가 실제로 Scaffold의 하위에 있으면 되기 때문에, Builder로 innerContext를 만들어서 이걸 .of()에 넣어준다(약간 미끼 뿌리는 느낌ㅋ).
여담
GetX에서는 BuildContext 정보가 없어도 뷰모델을 불러올 수 있어서 편리하다. 하지만 이게 불편해질 때도 있다고 한다.
Flutter 인기 아키텍처 라이브러리 3종 비교 분석 - GetX vs BLoC vs Provider라는 글에 아래와 같은 예시가 있었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final tab1 = TabViewModel('tab1');
Get.put(tab1)
final tab2 = TabViewModel('tab2'); // 동일한 클래스로 다른 인스턴스를 생성
Get.put(tab2)
// tab1을 받을지 tab2를 받을지 알 수 없음
TabViewModel tab = Get.find()
...
// 이 문제는 GetX에서 태그를 지정하는 방식을 사용해야 해결할 수 있음
// 태그 지정해야 함
Get.put(TabViewModel(), tag: 'tab1');
Get.put(TabViewModel(), tag: 'tab2');
// 뷰모델 사용, 태그를 알아야 가져올 수 있음
TabViewModel tab1 = Get.find(tag: 'tab1');
TabViewModel tab2 = Get.find(tag: 'tab2');
// ← 뷰모델을 사용하는 자식 위젯에게 태그 값을 매번 전달해야 해서 불편, BuildContext를 사용하는 게 낫지 않을까?
이렇게 되면 자식 위젯에게 태그 값을 계속 전달해야 하는 구조가 되어, 규모가 커질수록 오히려 BuildContext 기반 구조보다 불편해질 수도 있다.
그런데 동일한 컨트롤러 클래스로 다른 인스턴스를 생성해야만 하는 상황이 과연 자주 있을까?
개발 경험이 부족해서 그런지 적절한 사례가 떠오르지 않는다. 챗지피티한테 물어봐도 전부 굉장히 특수한 상황뿐이라… 일단 여기서 마무리하기로 했다.
참고자료
Flutter architectural overview
Flutter 인기 아키텍처 라이브러리 3종 비교 분석 - GetX vs BLoC vs Provider