[Flutter] 2. 플러터의 위젯

건전한건전지·2022년 8월 23일
0

Flutter Architecture

목록 보기
3/4

이 글을 쓰는 이유..

플러터 개발자로 면접을 보다보면 거의 10번 중 8번은 이런 질문을 하는 것 같다.

플러터의 렌더링 과정이나 트리의 구성을 설명해주세요.

대충은 알고있다. 트리는 3개가 있고 레이아웃은 렌더 트리에서 어쩌구.. 어디선가 주워듣고 단편적으로 찾아본 내용으로 최대한 말은 해본다.
그러면 돌아오는 대답은 '어느정도 알고 계시네요'다.
어느정도 안다는 말은 플러터 개발자 길을 선택한 나에겐 적지않은 충격이었다.
적어도 자신이 다루는 것에 대해서는 잘 알고있어야 한다고 생각하기 때문이다.
코드 짜는데 신나서 문서를 등한시 했으니..

그래서 각잡고 플러터의 아키텍처에 관해 공부해보려한다.


지난 포스트에서는 플러터의 구조에 대해 알아보았다.
이번엔 '플러터는 모든 것이 위젯이다'의 위젯에 대해 공부하려한다.


플러터의 위젯

플러터에서 위젯은 매우 중요한 요소다.
아마 개발하면서 제일 많이 사용하는 요소일 것이다.
위젯은 컴포지션을 베이스로 계층 구조로 구성되어있다. 컴포지션에 대해서는 잠시 후에 알아보자.
각각의 위젯은 그 부모의 내부에 중첩되고, 따라서 부모로부터 컨텍스트를 전달받을 수 있다.
이러한 계층 구조는 루트 위젯(일반적으로 MaterialApp or CupertinoApp)까지 전달된다.

예시를 보자!

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('My Home Page'),
        ),
        body: Center(
          child: Builder(
            builder: (context) {
              return Column(
                children: [
                  const Text('Hello World'),
                  const SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () {
                      print('Click!');
                    },
                    child: const Text('A button'),
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

플러터의 위젯 구조를 설명하기에 더 없이 좋은 예시다.

MaterialApp Class는 자식으로 Scaffold Class를 받고 Scaffold Class는 자식으로 Center Class를 받고... ElevatedButton Class는 자식으로 Text Class를 받는다.

플러터는 이벤트(사용자 입력, 반응 등)의 응답으로 계층 구조에 있는 위젯을 다른 위젯으로 교체함으로서
사용자 인터페이스를 업데이트한다.

플러터는 운영체제가 제공하는 UI에 대한 자체 구현을 가지고 있다.
스위치를 예로들면, 운영체제에서 제공하는 IOS 스위치Android 스위치를 구현한 플러터 스위치 위젯을 제공한다.

이렇게 자체 구현함으로서 얻는 이점은 뭘까?

  • 무한 확장성을 제공한다. 개발자가 원하는 임의의 위젯을 만들 수 있고, 운영체제에서 제공하는 확장 포인트에 제한되지 않는다.
  • 플러터가 화면을 한번에 그림으로써, 플랫폼 코드와 플러터 코드를 오가며 발생하는 병목 현상을 방지할 수 있다.
  • 운영체제의 종속성을 벗어 날 수 있다. 즉, 운영체제가 바뀌어도 어플리케이션은 똑같이 보인다.

컴포지션

위젯은 일반적으로 많은 소형 단일 목적 위젯들로 구성되어있다.
클래스 계층은 한 가지 일을 수행하는 작고, 구성 가능한 위젯에 초점을 맞춰 조합의 수를 최대화하기 위해 의도적으로 가능한 얕고 광범위하게 되어있다.
핵심 기능들은 추상적이고, 패딩과 정렬같은 기본 기능들도 코어에 내장되어 있지 않고 별도의 구성요소로 구현되어 있다.
예를 들어, 위젯을 중앙정렬하기위해 Align 속성을 선언하는 대신 Center 위젯으로 감싸면 된다.

패딩, 정렬, 행, 열 등 레이아웃 위젯들은 시각적인 표현을 가지고 있지않지만, 다른 위젯의 레이아웃을 컨트롤하는 목적을 수행한다. 플러터는 이러한 구성방식을 활용하는 유틸리티 위젯도 포함되어있다.
예를 들어, 흔히 사용되는 Container 위젯은 레이아웃, 페인팅, 위치, 크기를 담당하는 위젯들로 구성되어 있다. 따라서, Container를 서브클래스로 커스터마이즈 이펙트를 만드는 대신, Container의 컴포지션들로 새로운 Container를 만들 수 있다.

즉, 컴포지션은 위젯을 구성하는 기능 하나 하나의 또다른 위젯이라 보면 될 것 같다.


빌딩 위젯

위젯은 새로운 엘리먼트 트리를 반환하는 build() 함수를 override하여 위젯의 시각적 표현을 결정한다. 이러한 트리는 위젯의 유저 인터페이스 부분을 나타낸다. 예를 들어, 툴바 위젯은 몇몇 텍스트와 버튼을 가진 수평 레이아웃을 반환하는 build 함수를 가지고 있다.
필요에 따라, 프레임워크는 렌더링 가능한 객체에 의해 트리가 구성될 때까지 각 위젯을 반복적으로 요청한다. (리스트나 그리드 같은 위젯을 말하는 것 같다..) 그런다음 프레임워크는 렌더링 가능한 객체들을 렌더링 객체 트리에 연결한다.

위젯의 build 함수는 사이드 이펙트에서 자유롭다.
빌드 요청을 받을때마다, 위젯은 전에 무엇을 반환했든지 상관없이 새로운 위젯 트리를 반환한다. 프레임워크는 렌더 객체 트리에서 어떤 build 매소드가 호출되어야하는지 결정하는 작업을 수행한다.(자세한 내용은 다음에 알아보자.)
렌더링된 프레임에서 플러터는 build 매소드를 호출하여 상태가 변경된 UI만 다시 생성할 수 있다. 따라서, build 매소드는 빠르게 반환되어야하는 것이 중요하고, 무거운 계산 작업은 비동기로 수행되어야 한다.

이러한 자동화된 비교는 고성능의 인터렉티브 앱을 가능하게 한다. 또한, build 함수 디자인은 위젯이 어떻게 구성되어 있는지에 집중하여 코드를 단순하게 한다.

읽어도 완벽하게는 모르겠다..


위젯의 상태(state)

프레임워크는 두가지 중요 클래스인 stateful 그리고 stateless 위젯이 있다.
유저 상호작용이나 다른 요소들로 인해 위젯이 변경된다면 stateful, 그 외 시간이 지나도 요소가 변경되지 않는다면 stateless다.
StatefulWidget은 State라는 분리된 하위 클래스에 상태를 저장한다. StatefulWidget은 build 매소드가 없는 대신 State 객체를 통해 유저 인터페이스를 빌드한다.
State 객체가 변경되면, setState() 매소드를 불러 프레임워크에 State의 build 매소드를 다시 부르라는 신호를 보내야한다.

상태 관리

앱을 만들다보면 많은 상태를 관리하게 된다.
다른 클래스들과 마찬가지로 위젯 역시 생성자로 데이터를 넘겨줄 수 있다.


Widget build(BuildContext context) {
   return ContentWidget(importantState);
}

위와 같이 위젯의 생성자로 데이터를 전달할 수 있다.

하지만 위젯 트리의 깊이가 깊어지면서 데이터의 상속은 매우 복잡해질 것이다.
이를 해결할 수 있는 InheritedWidget이 있다. 이 위젯은 공유된 조상으로부터 데이터를 쉽게 가져올 수 있게 해준다.


위와 같이 공통 조상으로 InheritedWidget을 구현하면 ExamWidget, GradeWidget 등 하위 위젯에서

final studentState = StudentState.of(context);

를 사용하여 간단하게 데이터에 접근할 수 있다.

of(context)란?
현재 위젯 위치를 관리하는 build context를 가져와서, 트리에서 가장 가까운 조상을 반환하는 매소드다.
즉, StudentState.of(context)는 조상 중 가장 가까운 StudentState Widget을 반환한다.

InheritedWidget은 updateShouldNotify() 매소드도 제공하는데, 이는 해당 위젯의 데이터를 사용하는 자식 중 리빌드되어야하는 자식을 결정하고 리빌드한다.

플러터 자기자신도 InheritedWidget을 프레임워크 여러 파트에서 사용하고 있다. 가장 좋은 예가 Theme일 것이다. Theme는 앱의 컬러나 스타일 등의 속성이 있는데 이를 하위 위젯에 일일이 전달해야한다면 엄청난 고역일 것이다.
이 때, InheritedWidget이 빛을 발한다.

Container(
  color: Theme.of(context).secondaryHeaderColor,
  child: Text(
    'Text with a background color',
    style: Theme.of(context).textTheme.headline6,
  ),
);

위와 같이 Theme.of(context)로 Theme data에 접근해 사용하면 된다! Theme 외에도 Navigator, MediaQuery 등도 같은 식으로 데이터에 접근하여 사용한다.
앱이 커져가면서 상태관리는 더욱 힘들어진다. 많은 플러터 앱들이 provider와 같은 상태관리 패키지를 사용하고 있다.
상태관리 패키지에 대해서는 다음에 자세히 알아보자.


마무리

아무래도 위젯은 플러터 개발자들이 제일 많이 접하기에 다소 쉬운 내용이 많을 것이다. 필자 역시 포스트를 작성하면서 전에 공부했던 내용이 많아 공부 난이도가 어렵진 않았다. 그래도 누군가에게는 도움이 되리라 생각하면서 열심히 글을 썼다.(생각보다 오래걸렸다..)

다음 포스트에서는 대망의(?) 플러터 렌더링과 레이아웃에 대해 알아보기로 하자.


원문 : https://docs.flutter.dev/resources/architectural-overview#widgets

profile
Flutter 공부를 위한 블로그입니다.

0개의 댓글