서론
플러터 프로젝트를 진행하다보면 미리 구현된 다양한 패키지를 가져다 쓰는 것을 당연하게 여기게 됩니다. 패키지마다 나름의 구현 방식이 있겠지만, 몇몇 패키지들은 공통적으로 main 함수에서, 그리고 runApp 함수의 호출 전에, WidgetsFlutterBinding.ensureInitialized 함수의 호출이 필수적인 경우가 있습니다. 지금까지는 그저, 플러터 엔진의 어떤 중요한 부분을 '확실하게 초기화한다'라는 의미가 있겠거니 하고 필요하다니까 쓴 경향이 없지 않지만, 문득 '도대체 이게 뭔데?' 라는 의문이 들어 찾아보게 되었습니다.
WidgetsFlutterBinding.ensureInitialized 함수의 역할
이 함수의 기능은 예상 했듯이, 플러터 엔진의 주요한 기능을 초기화하는 것이 맞습니다. 그리고, WidgetsFlutterBinding 이라는 클래스 이름에서 알 수 있듯이, 어떠한 바인딩을 수행합니다.
runApp 함수가 실행되면 플러터 앱의 저수준 엔진과 고수준 프레임워크 간의 연결, 즉 바인딩이 수행됩니다. 하지만, 프로그래머는 runApp 호출 전에 이 바인딩이 미리 완료된 상태가 필요할 수 있습니다. 따라서 플러터는 이 바인딩 초기화 과정을 runApp 이전에도 수행할 수 있도록 만들었는데, WigdetsFlutterBinding.ensureInitialized 함수의 존재 이유가 그것입니다.
예를 들면, 앱의 설정값을 runApp 호출 전에 가져와 초기화를 하는 경우가 있습니다. SharedPreferences 기능을 사용한다고 할 때의 코드는 다음과 같습니다.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
final settings = prefs.get('settings');
AppInitializer.initialize(settings);
runApp(const MyApp());
}
SharedPreferences는 기기의 파일 시스템 접근을 위한 바인딩이 필요하기에, 위와 같이 WidgetsFlutterBinding.ensureInitialized 함수의 호출이 우선적으로 필요합니다.
바인딩
그렇다면 플러터 엔진과 프레임워크 사이의 바인딩이란 구체적으로 무엇을 의미하는 것일까요?
바인딩이란, 플러터의 저수준 엔진의 기능과 고수준 프레임워크의 API 사이의 연결을 의미합니다. 그 연결이란 구체적으로, C++로 구현된 플러터 엔진에서 발생한 이벤트에 대한 콜백함수를 등록하거나, 플러터 엔진이 프레임워크 측의 특정 설정 값을 수정 및 조회 할 수 있는 setter와 getter를 등록하는 것입니다. 그 등록은 MethodChannel과 MessageChannel들을 통해 이루어지는데, SystemChannel이라는 클래스에 플러터 엔진과의 통신을 위한 해당 채널들이 등록되어 있습니다.
실제로, 다양한 바인딩 중 하나인 ServicesBinding의 초기화 함수에는 아래와 같은 부분이 있습니다.
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
_handleLifecycleMessage
함수는 우리가 흔히 사용하던 WidgetsBindingObserver.didChangeAppLifecycleState 함수의 호출을 담당합니다. 또한, 초기화 과정 중에 registerServiceExtension 이라는 콜백 등록 함수가 여러 차례 호출되는데, 아래는 WidgetsBinding 클래스 초기화 함수 내부의 그 호출들 중 한 부분으로, 위젯 트리 구조를 비롯한 다양한 디버깅 정보들을 프레임워크 측에서 받아볼 수 있는 콜백 함수를 등록하는 부분입니다.
registerServiceExtension(
name: WidgetsServiceExtensions.debugDumpApp.name,
callback: (Map<String, String> parameters) async {
final String data = _debugDumpAppString();
return <String, Object>{
'data': data,
};
},
);
바인딩에는 여러가지가 있습니다. WigdetsFlutterBinding 클래스의 정의를 보면 어떤 바인딩이 수행되는 지 알 수 있습니다. SharedPreferences는 아래에 설명한 ServicesBinding이 필요합니다.
class WidgetsFlutterBinding extends BindingBase
with GestureBinding, SchedulerBinding, ServicesBinding,
PaintingBinding, SemanticsBinding, RendererBinding,
WidgetsBinding {
// ...
}
- GestureBinding: 제스처 인식과 관련된 이벤트 처리를 위한 바인딩입니다. 터치 이벤트, 드래그 이벤트, 스케일 이벤트 등의 제스처 이벤트를 처리하는 기능을 제공합니다.
- SchedulerBinding: 스케줄링 작업을 처리하기 위한 바인딩입니다. 애니메이션 프레임, 터치 이벤트 처리, 렌더링 작업 등의 스케줄링을 수행할 수 있습니다. (ex. 애니메이션 프레임 업데이트, 레이아웃 변경 스케줄링 등)
- ServicesBinding: 플랫폼 서비스를 이용하기 위한 바인딩입니다. 플랫폼 채널을 사용하여 네트워크, 위치, 센서, 파일 시스템 등의 기능을 활용할 수 있습니다.
- PaintingBinding: 이미지, 색상, 스타일 등 그래픽 관련 작업을 처리하기 위한 바인딩입니다. 이 바인딩을 통해 그래픽 렌더링 라이브러리인 Skia를 사용하여 그래픽 작업을 수행할 수 있습니다. 정확히는, Skia에게 전달할 그래픽 명령을 준비합니다. (ex. 이미지 리소스 관리, 색상, 그라데이션, 텍스트 스타일 등의 그래픽 속성 설정, 그래픽 객체 생성 등)
- SemanticsBinding: 앱의 접근성과 관련된 작업을 처리하기 위한 바인딩입니다. 화면 판독기, 대체 입력 장치 등 보조 기술에 대한 접근성 기능을 지원할 수 있습니다.
- RendererBinding: 그래픽 렌더링 작업을 처리하기 위한 바인딩입니다. 위젯 트리를 렌더링 트리로 변환하고, 실제 화면에 그리는 작업을 수행할 수 있습니다. 렌더링 파이프라인 관리와 레이아웃 계산, 페인팅 순서 결정 등의 작업을 담당하며, 이 과정에서 Skia가 실제로 화면에 그리는 작업을 수행합니다.
- WidgetsBinding: 플러터의 위젯 시스템과 연결해주는 바인딩입니다. 이 바인딩은 위젯 트리를 구성하고, 위젯 간의 상태 관리와 생명주기를 처리하는 기능을 제공합니다. addObserver 함수를 통해 WidgetsBindingObserver를 구현한 인스턴스를 등록하면 앱의 생명주기 관련 이벤트를 받을 수 있습니다.