Flutter로 Flutter 소개 발표 자료 만들기

Flutter 소개 자료

B612 동아리에서 모바일 세션으로 참여하고 있는데, 각 세션은 정기 모임 활동에서 세션 발표를 해야 한다. 모바일 세션의 첫 발표는 내가 맡았다. Flutter에 대해서 소개하는 임무를 맡았다. 나름 모바일 세션이고 Flutter를 소개하는 자리인데 심심하게 발표를 시작하고 싶지 않았다. 그래서 Flutter를 소개하는 발표 자료를 Flutter로 만들기로 했다.

발표 자료 기획하기

우선 발표 자료부터 기획해 보자. 다행히 이는 이미 준비가 되어 있다. 지난 2주 동안 정기 모임 과제로 WIL을 작성했기에, 이를 바탕으로 구성하면 된다.

지난 WIL에서의 내용을 압축하면 다음과 같다.

  1. Flutter 소개
    1. Cross Platform Framework
      • iOS, Android, Web, MacOS, Windows
    2. Flutter의 아키텍처 구조
      • Framework, Engine, Embedder 계층
      • SKIA와 Impeller 엔진
    3. Flutter의 장점
      • Hot Reload & Hot Restart
      • Dart
      • Open Source with a Google Support
  2. Flutter 개발 환경 세팅
  3. Dart 언어 소개
    1. JS를 대체하기 위해서 2011년 Google이 공개한 OOP 기반 모던 언어
    2. Everything is an Object
    3. 100% Sound Null Safety
    4. AOT & JIT 컴파일러 지원

허락된 발표 시간은 10분 이내이다. 따라서 우선순위에 맞춰서 발표 내용을 선정해야 한다.

 

우선 2번 Flutter 개발 환경 세팅은 자동으로 탈락. Flutter를 소개하는데 Flutter 개발 환경 세팅을 설명할 이유는 없다.

 

1번 Flutter 소개에서, 사실 Web에 관해서 조금 이야기를 더 하고 싶었다. 그러나 Flutter는 어디까지나 모바일 개발에 관심을 갖는 사람들이 많이 사용하는 프레임워크이기에, Web에 관한 이야기는 줄여야 한다. 그러나 발표 자료 자체가 Web 혹은 MacOS 데스크톱 어플리케이션에서 구동이 되므로, 자연스럽게 Flutter를 이용해서 웹 어플리케이션 뿐만 아니라 데스크톱 어플리케이션을 동시에 개발할 수 있다는 것을 자연스럽게 보여줄 수 있다.

 

Flutter의 아키텍처 구조는 간략하게만 설명하고, SKIA와 Impeller 엔진 역시 간략하게 소개하고 넘어갈 것이다.

 

Flutter의 장점은 조금 집중해서 소개할 필요가 있다. Flutter를 처음 보는 사람들에게 소개하는 자리인 만큼 왜 B612가 Flutter를 모바일 세션의 공식 기술 스택으로 채택했는지 보여주어야 하기 때문이다.

 

Flutter의 가장 큰 장점은 Hot Reload 및 Hot Restart에 있다. Hot Reload가 없다면 위젯을 한 픽셀 옮기는 것, 색을 조금 바꾸는 변화도 다시 컴파일을 해서 시뮬레이터/에뮬레이터 등에 밀어 넣어야 했을 것이다. 때문에 DX(개발 경험)이 저하될 수밖에 없다. Flutter는 Dart 언어의 JIT 컴파일 기술을 이용해서 Hot Reload를 구현하여, 보다 최적화된 개발 경험을 선사한다.

 

또한 Dart 언어 역시 Flutter의 가장 큰 특징이자 장점인 동시에 단점이다. Dart 언어는 Flutter 프레임워크를 제외하고는 거의 사용되지 않는 언어이다. 그래서 Flutter의 진입 장벽을 높이는 1차 관문이기도 한데, 반면에 Dart 언어가 주는 강력한 편의성과 개발 경험은 Flutter의 장점으로 다가오기도 한다.

 

3번 Dart 언어의 소개에서 Dart 언어에 대한 이야기를 이어간다. Dart 언어는 모든 것이 객체로 이루어져 있다는 특징, 그리고 100% Sound Null Safety를 지원, AOT & JIT 컴파일이 Dart 언어의 특징이다.

 

100% Sound Null Safety 역시 Dart의 특징이다. 가장 중요한 특징이다. Flutter는 이 Null Safety 때문에 그 기반 언어로 Dart를 채택했다 해도 과언이 아닐 것이다.

발표 자료 만들기

사실 멋들어지게 준비하려고 했는데, 사실 준비할 시간이 그리 많지 않았다. 발표일이 화요일인데, 전주 목요일 날에 미리 틀만 조금 만들어두고 월요일 날 하루 전에 부랴부랴 만들었다. 그래서 내가 생각했던 많은 내용이 들어가 지는 못 했고, 굉장히 허접한 결과물이 나와서 아쉽다.

const 지정하기

colors.dart

물론 Material 3을 사용한다면, 구글이 정의한 적당한 상수를 사용해서 쉽게 색을 구성할 수 있다. Flutter의 핵심을 보여주는 발표인 만큼 Material 3의 색 그레이딩 기능을 사용해 볼까 하긴 했는데, 정작 내가 써본 적이 없으므로 그냥 colors.dart에 색을 지정해 주기로 했다. const/colors.dart에 색을 지정해 두었다.

/// const/colors.dart
import 'package:flutter/material.dart';

// 주 색상
const kPrimaryColor = Color(0xFF91C8E4);

// 배경 색상
const kBackgroundColor = Color(0xFFF6F4EB);

// 글자 색상
const kBodyTextColor = Color(0xFF868686);

// 텍스트 필드 배경 색상
const kInputBackgroundColor = Color.fromARGB(255, 247, 247, 244);

// 텍스트 필드 테두리 색상
const kInputBorderColor = Color(0xFFF3F2F2);

한 가지 꿀팁은, ColorHunt라는 사이트에서 적당히 좋은 컬러 팔레트를 확인해 볼 수 있다.

text_styles.dart

텍스트 스타일도 미리 지정해 줄 것이다. 본문 텍스트 스타일, 제목 텍스트 스타일 등의 자주 사용되는 텍스트 스타일은 미리 지정해 주는 게 좋다.

/// 본문 텍스트 스타일
const bodyTextStyle = TextStyle(
  fontSize: 16.0,
  fontWeight: FontWeight.w400,
  color: kBodyTextColor,
);

/// h1 텍스트 스타일
const bigTitleTextStyle = TextStyle(
  fontSize: 34.0,
  fontWeight: FontWeight.w700,
  color: kBodyTextColor,
);

/// 제목 텍스트 스타일
const titleTextStyle = TextStyle(
  fontSize: 24.0,
  fontWeight: FontWeight.w700,
  color: kBodyTextColor,
);

/// 부제목 텍스트 스타일
const subtitleTextStyle = TextStyle(
  fontSize: 18.0,
  fontWeight: FontWeight.w500,
  color: kBodyTextColor,
);

/// 작은 텍스트 스타일 (인용 등)
const smallTextStyle = TextStyle(
  fontSize: 14.0,
  fontWeight: FontWeight.w300,
  color: kBodyTextColor,
);

뼈대 잡기

DefaultLayout

한 번 쓰고 말 자료이기에 리팩토링이 필요하지도 않고, 굳이 코드의 구조를 체계적으로 가져갈 필요가 없긴 한데, 그럼에도 불구하고 어느 정도 공통적으로 쓰이는 요소들은 따로 빼놓으려고 했다. 어차피 프론트 단에서의 작업은 완성된 이후에도, Padding 넣고 위치 조절하고 이러한 자잘한 작업들이 계속되기 때문이다.

 

먼저 DefaultLayout을 만들어주었다. DefaultLayout은 코드팩토리 님 강의에서 배운 팁인데, 초반에 별 의미가 없더라도 나중에 모든 페이지에 들어가는 공통적인 요소가 추가될 때가 오면 DefaultLayout을 만든 게 도움이 될 거라고 했다.

 

그리고 아니나 다를까 바로 효력이 발휘되었는데, 화면을 전환할 때 방법을 화면의 하단에 FloatingButton으로 전환하려고 했기에, DefaultLayout에 이 FloatingButton을 집어넣으니 정말 간편하게 문제가 해결되었다.

class DefaultLayout extends StatelessWidget {
  final Widget child;
  final Color? backgroundColor;
  final String? title;
  final Widget? bottomNavigationBar;
  final Widget? nextPageFloatingActionButton;
  final Widget? prevPageFloatingActionButton;

  const DefaultLayout({
    Key? key,
    required this.child,
    this.backgroundColor,
    this.title,
    this.bottomNavigationBar,
    this.nextPageFloatingActionButton,
    this.prevPageFloatingActionButton,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: backgroundColor ?? kBackgroundColor,
      appBar: renderAppBar(),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Stack(children: [
          child,
          if (prevPageFloatingActionButton != null)
            Align(
              alignment: Alignment.bottomLeft,
              child: prevPageFloatingActionButton!,
            ),
          if (nextPageFloatingActionButton != null)
            Align(
              alignment: Alignment.bottomRight,
              child: nextPageFloatingActionButton!,
            ),
        ]),
      ),
      bottomNavigationBar: bottomNavigationBar,
    );
  }

    // some methods ...
}

주목해야 할 것은 FloatingActionButton 부분이다. 저게 엄밀히 말하자면 FloatingActionButton은 아니고, 그냥 일반적인 Stateless 위젯인데 화면 하단부에 Floating 형태로 떠있기에 FloatingActionButton이라고 부르기로 했다.

메인페이지
메인페이지

저게 무슨 역할을 하냐면, 화면을 이동시키는 버튼 역할을 할 것이다. 위 화면에서 우측 하단부에 버튼이 하나 있는데, 해당 버튼을 DefaultLayout에 넣어줄 것이다.

child: Stack(children: [
  child,
  if (prevPageFloatingActionButton != null)
    Align(
      alignment: Alignment.bottomLeft,
      child: prevPageFloatingActionButton!,
    ),
  if (nextPageFloatingActionButton != null)
    Align(
      alignment: Alignment.bottomRight,
      child: nextPageFloatingActionButton!,
    ),
]),

Stack 위젯을 이용하면, 위젯 위에 또 다른 위젯을 올릴 수 있다. 이때 Align 위젯을 이용해서 bottomLeft와 bottomRight 위치에 각각 prevPageFloatingActionButtonnextPageFloatingActionButton를 넣어준다.

TitleBodyLayout

TitleBodyLayoutTitleBodyLayout
TitleBodyLayout

페이지를 몇 개 만들다 보니깐, 아래 페이지들의 형태가 계속 반복되는 것을 확인했다.

화면 속 두 페이지 모두, 제목과 본문 형태가 반복된다. 따라서 이 부분도 별도의 레이아웃으로 만들어놓으면 편할 것이다.

class TitleBodyLayout extends StatelessWidget {
  final String title;
  final String? subTitle;
  final Widget? nextPageFloatingActionButton;
  final Widget? prevPageFloatingActionButton;
  final Widget child;

  /// image asset
  final Image? image;

  const TitleBodyLayout({
    Key? key,
    required this.title,
    this.subTitle,
    this.nextPageFloatingActionButton,
    this.prevPageFloatingActionButton,
    required this.child,
    this.image,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DefaultLayout(
      nextPageFloatingActionButton: nextPageFloatingActionButton,
      prevPageFloatingActionButton: prevPageFloatingActionButton,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          _buildTitle(),
          const SizedBox(height: 20),
          child,
        ],
      ),
    );
  }

  Widget _buildTitle() {
    // body는 생략
    }
}

Route 설정하기

Flutter에서 페이지를 이동하는 방법은 무수히 많은데, 그중에서 GoRouter를 이용할 것이다.

 

원래 생각했던 것은 페이지 번호를 1, 2, … 이런 식으로 만들어놓고서, GoRouter에서 pathParameters로 번호를 넘기거나 하는 것을 생각했는데, 어떻게 만들다 보니깐 그냥 각 페이지 순서를 하드코딩해서 넘기는 형식으로 만들게 되었다. 그게 조금 아쉽긴 하다.

final routerProvider = Provider<GoRouter>((ref) {
  return GoRouter(
    routes: _routes,
    initialLocation: '/',
    debugLogDiagnostics: true,
  );
});

List<GoRoute> _routes = [
  GoRoute(
    path: '/',
    builder: (_, __) => const HomePage(),
    name: HomePage.routeName,
  ),
  GoRoute(
    path: '/introduce',
    name: ExplainMePage.routeName,
    builder: (_, __) => const ExplainMePage(),
  ),
  GoRoute(
    path: '/why_flutter',
    name: WhyFlutterPage.routeName,
    builder: (context, state) => const WhyFlutterPage(),
  ),
    /// ... page 구성 계속 진행
]

위와 같이 path에다가 계속해서 새로운 페이지를 추가해 나가면서 페이지 라우트를 설정했다.

 

goRouter에는 페이지 라우트 별칭을 따로 설정해 줄 수 있다. path는 실제 /path 형태의 라우트 주소이고, 별도의 이름을 지정해 주는 것도 가능하다. 그렇게 된다면, path가 아닌 이 라우트 네임으로 페이지를 이동하는 것도 가능하다.

 

페이지 이름은 무조건 고유한 이름이어야 한다. 다른 이름과 겹치지 않게, 모든 페이지에 아래와 같이 페이지 이름을 지정해 주었다.

class FlutterStructurePage extends StatelessWidget {
  static String get routeName => 'flutterStructurePage';

    // ...
}
class WhyFlutterPage extends StatelessWidget {
  static String get routeName => 'whyFlutterPage';

    // ...
}

이런 식으로 static get 을 이용해서 routeName을 모두 지정해 준다.

자주 쓰이는 Component 정리하기

페이지를 만들다가, 한 두 번 자주 쓰이는 컴포넌트들은 따로 빼두는 게 좋다. 여기서도 페이지 한 두 개 만들어보니깐, 몇몇 컴포넌트는 공용으로 빼두는 게 좋을 것 같은 컴포넌트들이 보였다.

CustomFloatingActionButton

class CustomFloatingActionButton extends StatelessWidget {
  final String routeName;
  final IconData icon;
  final Color? backgroundColor;

  const CustomFloatingActionButton({
    super.key,
    this.backgroundColor,
    required this.routeName,
    required this.icon,
  });

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: () => context.goNamed(routeName),
      elevation: 0,
      backgroundColor: backgroundColor ?? kBackgroundColor,
      child: Icon(icon),
    );
  }
}

우선 가장 먼저, 위에서 소개한 페이지 화면 이동을 위한 버튼이다. ‘Custom’ FloatingActionButton인데 정작 상속받는 건 FloatingActionButton은 아니다.

 

routeName 을 주면, 해당 페이지로 go router를 이용해서 이동한다. 이때 goRouter에서는 context.go가 있고 context.goNamed 가 있는데, 전자는 파라미터 안에 path가 들어가고, 후자는 path가 아닌, 위에서 등록한 이름을 넣어주어야 한다. 주의하자.

class NextPageFloatingButton extends StatelessWidget {
  final Color? backgroundColor;
  final String routeName;

  /// gorouter 이름

  const NextPageFloatingButton({
    Key? key,
    this.backgroundColor,
    required this.routeName,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomFloatingActionButton(
      routeName: routeName,
      icon: Icons.arrow_forward_ios,
      backgroundColor: backgroundColor,
    );
  }
}
class PrevPageFloatingButton extends StatelessWidget {
  final String routeName;

  const PrevPageFloatingButton({
    Key? key,
    required this.routeName,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomFloatingActionButton(
      routeName: routeName,
      icon: Icons.arrow_back_ios,
      backgroundColor: kBackgroundColor,
    );
  }
}

CustomFloatingActionButton을 이용해서 각각 페이지 버튼을 만들 수 있다. 적절히 추상화시킨다면 더 깔끔하게 만들 수 있을 것 같기는 한데, 일단은 이렇게 그냥 만들어두었다.

CustomTextField

/// [title]: 텍스트 위젯의 제목 (없으면 표시하지 않음)
/// [text]: 텍스트 위젯의 내용
/// [bodyColor]: 텍스트 위젯의 색상 (없으면 기본 색상)
class CustomTextField extends StatelessWidget {
  final String? title;
  final String text;
  final Color? bodyColor;
  final double? height;
  final double? width;

  const CustomTextField({
    Key? key,
    this.title,
    required this.text,
    this.bodyColor,
    this.height,
    this.width,
  }) : super(key: key);

이건 뭐냐면, 본문이다. 간단히 텍스트 위젯 두 개를 Column으로 이어 붙이는 식으로 만들었다.

 

참고로 이름을 잘못 지었다. Flutter에서 보통 TextField라 하면 텍스트를 입력받는 필드를 의미한다. 그런데 이건 그냥 텍스트를 보여주는 공간이다. 아는 데 마땅한 이름이 생각이 안 나서 결국 이렇게 지었다.

Page 만들기

이제 개별 페이지를 만들면 된다.

여기서부터는 말 그대로 직접 구현의 영역이므로 코드를 보이는 건 의미가 없다. 다만 위에서 정의한 레이아웃이나 컴포넌트들이 어떻게 쓰이는 지만 보자면,

class FlutterStructurePage extends StatelessWidget {
  static String get routeName => 'flutterStructurePage';

  const FlutterStructurePage({super.key});

  @override
  Widget build(BuildContext context) {
    return TitleBodyLayout(
      title: 'Flutter Structure',
      subTitle: 'Flutter의 구조 알아보기',
      prevPageFloatingActionButton: PrevPageFloatingButton(
        routeName: CrossPlatformPage.routeName,
      ),
      nextPageFloatingActionButton: NextPageFloatingButton(
        routeName: FlutterEnginePage.routeName,
      ),
      child: // ...

이런 식으로 사용할 수 있다.

맨 위에 static String get routeName은 goRouter에서 페이지 이름을 지정하기 위한 것으로, 일반 스트링으로 직접 페이지 이름을 적어놓는 것보다 실수의 위험을 방지할 수 있어 저렇게 짜는 게 권장된다고 한다.

결과물

랜딩 페이지자기소개
첫 페이지와 자기소개
Why Flutter
Why Flutter Slide

이 슬라이드에서는, 왜 Flutter를 사용해야 하는지, 그 장점을 담았다. Dart 언어, Google이 지원하는 100% Open Source, 그리고 Hot Reload와 Hot Restart를 소개하였다.

Flutter Structure Slide
Flutter Structure Slide

이 슬라이드에서는 Flutter의 구조에 대해서 소개하였다. Framework, Engine, Embedder 계층으로 이루어진 Flutter의 구조에 대해서 설명했다.

Cross Platform FrameworkCross Platform Framework Comparison
Cross Platform Slide

크로스 플랫폼 앱 개발과 네이티브 앱 개발의 차이점에 대해서 소개하였다.

 

Cross Platform Framework의 삼대장 Xamarine, Flutter, React Native에 대해서 소개하였다.

사실 Xamarine은 이미 시장에서 사장되었기에 사실상 2대장이다.

 

SKIA와 Impeller 엔진
SKIA와 Impeller 엔진

Flutter의 핵심 엔진인 SKIA와 Impeller에 대해서 소개하였다.

 

사실, 이 부분은 생략하는 게 더 낫지 않았을까 싶다. 그보다는 Flutter의 코드 스타일 등, 좀 더 Flutter의 매력을 가까이서 드러낼 수 있는 주제를 픽하는 게 낫지 않았을까 생각한다.

 

Dart 언어의 특징
Dart 언어의 특징

Dart 언어에 대해서 소개하였다. Dart 언어는 모든 것이 객체로 이루어져있다는 점, 100% Sound Null Safety, 그리고 객체지향과 함수형 프로그래밍, final과 const 등의 키워드에 대해서 이야기했다.

 

이때 모든 것이 객체로 이루어져 있다고 할 때, Dart의 특징은 int, double, bool과 같은 Primitive 타입 역시도 전부 객체로 되어 있다는 것이다. 예컨대 int, double은 모두 num이라는 클래스를 상속받는 final class들이다. num은 다시 Comparable을 extends 한다. 객체지향 언어인 Java, C++조차 int 등의 Primitive 타입은 그냥 Primitive 하게 선언되어 있는데, Reference 타입은 물론이고 Primitive 타입조차 객체인 Dart의 특징은 한 번 짚고 넘어갈만하다.

 

사실 무언가를 더 많이 넣으려고 했었다. 예를 들어서 Flutter가 Web 상에서 CanvasKit을 이용해서 고성능의 연산을 수행하는 것을 보이기 위해, Evolving Flutter’s supports for the web에 등장한 데모를 추가하려고 했었다. 저거는 Flutter github에 가면 있다. 다만 시간이 없어서 시도도 못 했다.

 

결국 만든 건 그냥 흔히 볼 수 있는 단순한 UI여서 아쉬울 따름이다. 근데 PPT로 만드는 것보다는 의미가 있다고 느낀다. 굉장히 허접하지만 만든 것에 의의를 두기로 했다.

 

최종 결과물은 여기에서 확인할 수 있다.