Dart Programmer 되기 [44]

< Desktop 개발 – Flutter for Desktop >

본 글은 2020년 5월 9일에 https://github.com/flutter/flutter/wiki/Desktop-shells 사이트를 토대로 작성 되었습니다. 아직 기술의 변화가 큰 부분이므로, 해당 사이트를 직접 방문하여 최신의 내용을 확인하기를 권장 합니다.

앞서 Flutter를 통한 모바일 어플리케이션과 Flutter for Web을 통해서, 하나의 Flutter 기반 프로그램이 소스 코드의 수정 없이 스마트폰/테블릿과 Web 브라우저에서 동일하게 동작하는 것을 확인 하였습니다. 같은 방법으로 Desktop용 어플리케이션을 Flutter를 통해서 개발할 수 있습니다. Flutter로 Desktop을 지원하는 기술은 아직 공식 릴리즈 단계는 아니며, 개발이 진행중인 상황입니다. 궁극적으로 macOS, Windows, Linux를 지원하는 것을 목표로 하고 있습니다. Flutter for Desktop은 GitHub의 Flutter 공식 사이트에서 Desktop shells로 설명되고 있습니다. Desktop운영체제 별로 개발 수준에 차이가 있는데 각각의 운영체제별 상황은 다음과 같습니다.

macOS

macOS는 가장 진도가 많이 나간 것으로 되어 있습니다. alpha 버전에 진입한 상태이며, https://flutter.dev/desktop 사이트를 통해서 설치, 프로젝트 생성 및 환경 설정, 프로젝트 실행, App Store를 통한 배포시 주의사항, macOS를 지원하는 Plugin 상황, macOS용 Plugin 제작방법 및 Sample 들이 제공되고 있습니다.

특히 flutter create 명령이 fully support 되는 것으로 되어 있습니다. 따라서 이 글은 macOS에서 Flutter for Desktop을 개발하는 환경을 기반으로 설명 하겠습니다.

Windows

현재 early technical preview 상태 입니다. Win32 버전으로 되어 있으며, UWP (Universal Windows Platform – Windows 10에 처음 도입된 MS사 제품에 대한 Universal[= Cross] Platform) 버전으로의 진화를 계확하고 있다고 합니다. 현재의 API 들에서 많은 변화가 있을 것으로 설명하고 있습니다.

특히 flutter create 명령시에 동작은 하지만, 안정화되지 않았다고 합니다. 따라서, Flutter를 업데이트 한 후에는, windows 디렉토리를 반드시 삭제하고 재생성할 필요가 있는 등의 후속 작업이 필요하다고 합니다.

Linux

현재 early experiment가 가능한 상태 입니다. GLFW(Graphics Library Framework : OpenGL을 위한 오픈소스 멀티 플랫폼 라이브러리) 기반으로 만들어져 있으며, 향후 다른 기술로 변경될 예정입니다. 다양한 플랫폼을 지원하는 기술을 선택하기를 희망하여 기술의 선택을 검토중이라고 하며, GTK+가 현재 후보라고 합니다. 현재 사용한 API은 대부분 변경될 예정이라고 하니, 현재 버전을 사용하는 경우에는 주의가 필요합니다.

특히 flutter create 명령시에 동작은 하지만, 안정화되지 않았다고 합니다. 따라서, Windows와 동일하게, Flutter를 업데이트 한 후에는, linux 디렉토리를 반드시 삭제하고 재생성할 필요가 있는 등의 후속 작업이 필요하다고 합니다.

Plugins

Desktop 운영체제를 지원하는 Plugins의 갯수가 많지도 않지만, Desktop 운영체제별로 구현 수준이 서로 다르기에 플랫폼 독립적인 구동도 기대하기 어렵습니다. 특히 Windows와 Linux의 경우는 아직 시험적인 수준이기에, Plugin을 개발하더라도 pub.dev를 통한 배포는 권장하지 않고 있으니, Desktop을 위한 Plugin을 개발하거나 사용하는 경우는 주의하기 바랍니다.

“Hello, World!” Flutter for Desktop

Flutter for Desktop을 사용하는 방법은 앞서 Flutter for Web에서 경험한 것처럼, 소스 코드 레벨에서의 수정이 아닌 개발 환경에서의 변경으로 가능합니다. 일반 Flutter 프로젝트를 만들고 실행하는 것과 같은 맥락으로 진행하며, 다음과 같이 하여 최초의 Flutter for macOS 프로그램을 만들고 실행해 보겠습니다 ( 참조: https://flutter.dev/desktop ).

첫번째 단계는 Flutter for Desktop 환경으로 전환하기 위해서, 다음 명령을 수행합니다. 이는 Flutter for Desktop이 아직 개발 단계이기에, Flutter channel을 master로 바꾸는 작업과, 필요한 기능들을 platform 안에 설치하는 단계 입니다. 향후 Flutter for Desktop의 공식 릴리즈가 이루어 지면, 없어지거나 달라질 부분 입니다.

flutter channel master
flutter upgrade

두번째 작업은 Flutter for Desktop 환경을 macOS 환경에 맞도록 설정하는 단계로 다음의 명령을 실행합니다. Windows 운영체제는 –enable-windows-desktop 옵션을 주고, Linux 운영체제는 –enable-linux-desktop 옵션을 주면 됩니다. 이 부분은 아마도 공식 릴리즈가 나온 후에도 동일한 작업일 것으로 예상합니다.

flutter config –enable-macos-desktop

세번째 작업은 제대로 동작이 이루어 지는지 확인하는 단계로서, 다음의 세가지 명령을 실행해서 결과를 확인 합니다.

flutter devices
flutter doctor
flutter config

두 가지 명령은 앞서에서 여러번 등장했습니다. 즉 flutter devices는 Flutter를 다양한 플랫폼에서 동작하는 경우, 현재 실행 가능한 플랫폼을 확인하는 명령으로서, 앞서 flutter config –enable-macos-desktop로 인하여, [그림 1]과 같이 macOS가 실행 가능한 플랫폼으로 등록된 것을 볼 수 있습니다.

[그림 1] flutter devices 실행 화면

flutter doctor는 Flutter 개발 및 실행 환경에 대한 점검을 하는 기능으로 여러번 등장 했습니다. [그림 2]와 같이 master 채널로 설정되고, 3개의 디바이스가 연결되었으며, 이슈가 없는 것을 볼 수 있습니다.

[그림 2] flutter doctor 실행 화면

마지막으로 flutter config –enable-macos-desktop의 경우는 macOS용 환경을 활성화 하기 위한 용도 였지만, [그림 3]과 같이 그냥 flutter config 만 실행하면, 사용법을 설명한 후 맨 마지막에 enable-macos-desktop 설정이 true가 된 것을 확인 할 수 있습니다.

[그림 3] flutter config 실행 화면

다음 과정은 일반적인 Flutter 프로젝트를 만들고 실행하는 부분과 동일합니다. 특히 Flutter for Web과 같은 방식으로 구동하게 됩니다.

작업을 희망하는 디렉토리에서 다음 명령을 실행하여 프로젝트를 생성합니다.

flutter create myapp

그리고 myapp 디렉토리로 이동하여 (cd myapp), 다음의 명령으로 실행합니다. 이를 통해서 debug 모드에서 앞서에서 여러번 등장한 Flutter의 Start 어플리케이션이 [그림 4]와 같이 동작하는 것을 볼 수 있습니다. “+” 버튼을 누르면 숫자가 업데이트 되는 동작도 동일합니다. 따라서 소스 코드 레벨에서의 수정 없이 동일한 Start 어플리케이션을 Desktop 환경에서도 구동할 수 있는 것을 보았습니다.

flutter run -d macos

[그림 4] Flutter Start App을 macOS Desktop에서 실행한 모습

만역 debug 용도가 아닌 build 용도라면, 다음의 명령을 수행하면 됩니다.

flutter build macos

Flutter Mobile App을 Desktop App으로 실행하기

이번에는 스마트폰을 위해서 개발한 Flutter 기반 어플리케이션을 macOS Desktop 환경에서 실행해 보겠습니다. 앞서 35번째 글인 “Flutter 활용하기 – Skeleton Program for Future Usage”에서 완성한 main.dart로 [그림 4]에 해당하는 myapp의 main.dart를 갱신 합니다. 그리고 프로그램을 다시 실행하면 [그림 5]와 같이 macOS에서 실행하는 프로그램을 만날 수 있습니다.

[그림 5] Flutter 기반 Mobile Application을 macOS Desktop에서 실행한 모습

만약 “Flutter 활용하기 – Skeleton Program for Future Usage”에서 완성한 프로젝트 폴더를 보관하고 있다면, 단지 폴더안에서 다음의 명령을 실행하는 것으로 macOS Desktop에서의 실행 환경을 마칠 수 있습니다.

flutter create .

마무리

Flutter for Desktop이 아직 공식 릴리즈가 아니고, Desktop 운영체제별로 상황이 판이하지만, 어떤 기술인지와 현재 수준에 대해서 알아보았습니다. 그리고 Flutter로 만든 스마트폰 어플리케이션을 Web 어플리케이션으로 실행한 경우와 거의 동일하게, 다시 macOS Desktop 환경에서 실행해 보았습니다. 결국 하나의 소스 프로그램이 플랫폼과 상관없이 동일한 도구와 함수들로 구현되도록 하는 것이 Flutter의 철학이라는 것을 이해하게 됩니다.

하지만, Flutter를 사용하는 입장에서는 이렇게 간단하게 이야기 할 수 있지만, 이렇게 하기 위한 노력은 용이하지 않으며, Desktop 운영체제별로 꽤 오랜 시간이 지나야 안정적인 환경이 제공될 수 있으리라 생각합니다. 하지만 충분히 가능성이 있는 기술인 만큼, 꾸준한 관심과 학습이 있다면, ONE SOURCE MULTI PLATFORM 이라는 철학을 구현하는 가장 핫한 기술로 나름 의미있는 효율성과 생산성을 보장할 것으로 봅니다.

Creative Commons License (CC BY-NC-ND)

Dart Programmer 되기 [40]

< Web 개발 – Flutter for Web : History & Roadmap >

2019년 5월 7일, Google I/O 행사에서, Flutter 팀은 Flutter 프레임워크의 지향점을 기존 모바일에서 다양한 디바이스들로 확대하겠다는 발표를 했습니다 (참조: https://developers.googleblog.com/2019/05/Flutter-io19.html ). 이의 기술적 방법론으로, Flutter for Web, Flutter for Mobile Devices, Flutter for Desktop, Flutter for Embedded Devices를 발표 하였습니다. 이 중 첫번째 이슈가 당분간 연재하는 글의 주제이며, 두번째에서 네번째 사항은 추후 데스크탑에 대한 내용에서 다루도록 합니다.

Flutter 팀은 Flutter for Web의 초기 버전을 출시하면서, 이 기술의 목적을 명확하게 규정했습니다. 즉, Web에서 Flutter가 지향하는 초기 비전은, HTML에 최적화 된 문서 환경을 대체 할 목적이 아닙니다. 대신 정교한 UI 프레임워크의 장점을 잘 느낄 수 있는 대화식(highly interactive)의 그래픽이 풍부(graphically rich)한 콘텐츠를 제작할 수 있는 좋은 방법으로 제안하고 있습니다.

Web 용 Flutter를 소개하기 위해, Flutter 팀은 New York Times와 함께 데모 프로그램을 제작해서 공개 했습니다. New York Times는 세계적인 뉴스 보도 외에도 크로스 워드 및 기타 퍼즐 게임으로 유명합니다. 열렬한 퍼즐 매니아 들은 당시 사용하고있는 모든 기기에서 게임을 하기를 원했기 때문에, Flutter  개발 팀은 Flutter에 관심을 끌 수 있는 잠재적 인 솔루션으로 매료 되었습니다. 이에 2019년 5월 Google I/O에서 새로 업데이트 된 KENKEN 퍼즐 게임을 공개 하였습니다 (참조: https://www.nytimes.com/games/prototype/kenken#/ ). 이 게임은 Android, iOS, Web, Mac 및 Chrome OS에서 동일한 코드로 실행되도록 만들어 졌습니다.

그리고 2019년 12월에 열린 Flutter Interact 이벤트에서, Flutter 팀은 Web에 대한 Flutter의 개발 수준을 beta-level로 상향 조정한다고 발표 했습니다. 아울러 Android와 iOS에 상응하는 수준으로 Web에 대한 지원을 지속할 것 임을 발표하였습니다. 또한 향후 Desktop 어플리케이션을 위한 최적의 개발 방법으로 발전시켜 나가겠다는 발표도 함께 있었습니다.

Flutter for Web에 대한 로드맵은 GitHub의 Flutter 공식 사이트에도 명시 되어 있습니다 (참조: https://github.com/flutter/flutter/wiki/Roadmap#2019 ). 이곳에 명시된, 2020년의 목표는 “flutter create; flutter run”을 통해서 Flutter로 개발한 어플리케이션이 Web 브라우저, macOS, Windows, Android, Fuchsia 및 iOS에서 실행하도록 하는 것 입니다. 아울러 How-Reload, 플러그인, 테스트 및 릴리스 모드 빌드를 일관되게 지원하게 한다고 합니다. 또한, Material 디자인 위젯 라이브러리가, 이 모든 플랫폼에서 잘 작동 하도록 하는 것을 목표로 한다고 합니다.

참고로 GitHub의 Flutter 공식 페이지의 WiKi를 방문하면, Flutter에 대한 각종 기술적 자료와 함께, Flutter for Web과 같이 시험적으로 만들어 지고 기능(experimental features)을 확인할 수 있으니, 참조하기 바랍니다. 여기에는 기존 iOS/Android App을 Flutter로 변환하기, dart:ffi를 통한 native 코딩, macOS Desktop 어플리케이션을 Flutter로 만드는 법 등 다양한 시도들이 이루어 지고 있는 것을 볼 수 있습니다.

마무리

Flutter for Web은 살펴본 것처럼 공식적으로 알려지기 시작한 기간이 매우 짧은 기술 입니다. 하지만, 빠른 속도로 Web 개발자들의 관심을 모으고 있습니다. 특히 2020년 올해는 기술적인 도약이 매우 기대되는 한해가 될 것으로 보이므로, 공식 홈페이지를 주기적으로 살펴보면서, 어떤 변화가 있을지 꾸준하게 파악하는 것이 필요해 보입니다.

Creative Commons License (CC BY-NC-ND)

Dart Programmer 되기 [41]

< Web 개발 – Flutter for Web : Login UI Example >

지금까지 Flutter for Web를 개발하기 위하여, 기술의 연혁과 도구 설치, 그리고 간단한 예제 프로그램을 실행하고 이해하여 보았습니다. 앞서 dart2js 등에서 본 것처럼, Flutter for Web은 HTML/CSS/JavaScript와 같은 표준 웹 기술을 사용하여 Web 콘텐츠를 개발하도록 지원 함으로써, Dart로 만든 프로그램이 어떠한 Web 브라우저 상에서도 동작하도록 합니다.

Flutter for Web은 Dart 언어로 만들어진 프로그램이 JavaScript로 변환되는 단계에서 더 나아가서, Flutter 프레임워크가 제공하는 다양한 core drawing layer가 기존 Web 브라우저 위에서 동작 할 수 있도록, [그림 1]과 같이, Dart/Flutter Framework를 표준 Web 브라우저 API 상에서 구현하는 작업을 수행하였습니다.

[그림 1] Flutter for Web 기본 개념 (출처: https://flutter.dev/web )

앞서의 글에서, 현재 Flutter for Web이 기존 Web 기술로 만들어진 Web 사이트들을 모두 바꾸려 하기 보다는 인터액티브하고 동적인 콘텐츠를 표현하는 Web 어플리케이션을 목표로 한다고 하였습니다. Flutter for Web 공식 사이트( https://flutter.dev/web )에는 이에 대해서 다음의 세가지 시나리오를 예를 들어서 설명하고 있습니다.

A connected Progressive Web Application built with Flutter : Flutter로 만들어진 기존의 모바일 어플리케이션을 보다 다양한 device들에 적용하거나, 아니면 Web 상에서도 같은 경험을 제공하려는 경우

Embedded Interactive content : 기존 Web 페이지 안에서도 쉽게 운용이 가능한 rich & data-centric 기능을 적용하는 경우로서, 예를 들어 데이터 시각화, 자동차 구성 요소 설정과 같은 온라인 도구, embedded된 형태의 차트 등의 embedded web content를 개발하는 경우

Embedding dynamic content in a Flutter mobile app : 기존 모바일 애플리케이션 내에서 동적 컨텐츠를 업데이트 하기 위해서 사용하는 일반적인 방법은 동적으로 컨텐츠를 로드 할 수 있는 Web view control 이였지만, Flutter는 웹 및 모바일 컨텐츠를 위한 통합 환경을 제공하므로, 컨텐츠를 플랫폼에 따라서 다시 작성하지 않고도 앱에 내장 할 수 있음

따라서, Flutter for Web의 사용에는, 기존의 모든 Web 기술을 대체하겠다는 접근 보다는, 목적에 맞는 사용이 필요합니다. 예를 들어 블로그 기사와 같은, 텍스트 중심의 정적인 컨텐츠를 제공하는 경우는 기존 Web 기반 기술을 활용하고, app-centric한 서비스에서 interactive하고 data-centric한 UI 프레임워크가 동적으로 동작하는 것이 필요한 경우가 Flutter for Web을 도입하기 좋은 환경인 것으로 권장하고 있습니다.

이제, Web 환경을 고려한 Flutter 기반 첫번째 어플리케이션을 만들어 보겠습니다. 주 내용은 Flutter 공식 사이트( https://flutter.dev/docs/get-started/codelab-web )의 “Write your first Flutter app on the web”을 기반으로 진행하고자 합니다. 동작은 단순 합니다. [그림 2]와 같은 로그인 화면을 통해서 사용자로부터 이름과 사용자 아이디를 입력 받은 후, 환영 인사 문구를 화면에 보여주는 정도입니다.

[그림 2] First Flutter app in the web
(출처: https://flutter.dev/docs/get-started/codelab-web )

좀 더 세부적으로 기능을 살펴보면, 화면에는 이름, 성 및 사용자 이름의 세 가지 텍스트 필드가 있습니다. 사용자가 필드를 채우면 진행률 표시 줄이 로그인 영역의 상단을 따라 움직입니다. 세 개의 필드가 모두 채워지면 진행률 표시 줄이 로그인 영역의 전체 너비를 따라 녹색으로 표시되고 가입 버튼이 활성화됩니다. 가입 버튼을 클릭하면 시작 화면이 화면 하단에서 애니메이션으로 표시됩니다.

실행에 앞서서 flutter doctor를 실행해서, 개발 환경에 문제가 없는지 다시 한번 확인하고, flutter devices를 통해서 Chrome과 Web Server가 Flutter for Web의 실행을 위해서 연결되어 있는지 확인 합니다.

Step.0 New Flutter Project with Text Input Fields

Flutter for Web을 경험할 새로운 Flutter 프로젝트를 생성합니다. 이를 위해서 본인이 작업할 디렉토리로 이동하여 flutter create 명령을 실행합니다. 아래는 myapp 이라는 이름으로 Flutter 프로젝트를 생성한 경우 입니다.

flutter create myapp

다음은 Flutter 공식 홈페이지에서 가져온 초기 단계 코드인 darttutorial-41-main-step-0.dart 프로그램으로 myapp 프로젝트의 main.dart( myapp/lib/main.dart ) 프로그램을 대체 합니다.

// Source: https://flutter.dev/docs/get-started/codelab-web
// darttutorial-41-main-step-0.dart

 import 'package:flutter/material.dart';
 void main() => runApp(LoginApp());
 class LoginApp extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
       routes: {
         '/': (context) => LoginScreen(),
       },
     );
   }
 }
 class LoginScreen extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       backgroundColor: Colors.grey[200],
       body: Center(
         child: SizedBox(
           width: 400,
           child: Card(
             child: LoginForm(),
           ),
         ),
       ),
     );
   }
 }
 class LoginForm extends StatefulWidget {
   @override
   _LoginFormState createState() => _LoginFormState();
 }
 class _LoginFormState extends State {
   final _firstNameTextController = TextEditingController();
   final _lastNameTextController = TextEditingController();
   final _usernameTextController = TextEditingController();
 double _formProgress = 0;
 @override
   Widget build(BuildContext context) {
     return Form(
       child: Column(
         mainAxisSize: MainAxisSize.min,
         children: [
           LinearProgressIndicator(value: _formProgress),
           Text('Sign Up', style: Theme
               .of(context)
               .textTheme
               .display1), // display1 changes to headline4 in 1.16
           Padding(
             padding: EdgeInsets.all(8.0),
             child: TextFormField(
               controller: _firstNameTextController,
               decoration: InputDecoration(hintText: 'First name'),
             ),
           ),
           Padding(
             padding: EdgeInsets.all(8.0),
             child: TextFormField(
               controller: _lastNameTextController,
               decoration: InputDecoration(hintText: 'Last name'),
             ),
           ),
           Padding(
             padding: EdgeInsets.all(8.0),
             child: TextFormField(
               controller: _usernameTextController,
               decoration: InputDecoration(hintText: 'Username'),
             ),
           ),
           FlatButton(
             color: Colors.blue,
             textColor: Colors.white,
             onPressed: null,
             child: Text('Sign up'),
           ),
         ],
       ),
     );
   }
 }

그리고 다음의 명령을 통해서 실행해 봅니다. 지금까지의 작업을 잘 따라왔다면, 문제없이 Chrome 브라우저를 통해서 이름과 아이디를 입력하도록 하는 화면이 열렸을 겁니다.

flutter run -d Chrome

Flutter 공식 홈페이지에서는 바로 수정 작업을 진행하지만, 본 글에서는 앞서 main.dart의 내용을 좀 더 살펴 보도록 하겠습니다. 편의상 main.dart의 설명을 용이하게 하기 위하여 줄 번호가 포함된 형태인 [그림 3]과 [그림 4]로 나누어서 이해 하도록 합니다.

[그림 3] main.dart 이해 (1/2)

3번 줄에서 이 프로그램이 Material 디자인을 기반으로 만들어 지는 것을 알 수 있습니다.

5번 줄에서는 main() 실행 시, LoginApp() 객체를 만들어서, 프로그램이 처음 구동하도록 합니다.

7번 줄에서는 LoginApp 클래스의 정의가 StatelessWidget를 확장하여 만들어 지는 것을 볼 수 있습니다. 내부 구성은 앞서 Flutter에서 다뤘던 내용과 동일하게 build()를 override하는 것을 볼 수 있습니다. MaterialApp() 객체를 리턴 하는 것 까지는 이전에 다룬 전형적인 형태인데, 내부를 보면 지금까지 다루지 않았던 routes Property를 설정하는 내용이 나옵니다. 여기서 MaterialApp 클래스의 routes Property에 대해서 이해가 필요합니다.

MaterialApp 클래스의 routes Property는 Map<String, WidgetBuilder> 타입으로 정의되어 있습니다 (출처: https://api.flutter.dev/flutter/material/MaterialApp/routes.html ). String에 해당하는 문자열과 이 문자열에 매핑 되는 Widget을 생성할 Builder 함수를 입력 파라메타로 받는 다는 것 입니다. 일반적인 Web 사이트 접속 시 사용하는 URL(웹 사이트 주소)을 연상하면 쉽게 이해할 수 있습니다. 예를 들어, test.net 이라는 Web 사이트에 접속할 때, Web 브라우저의 주소 창에 다음처럼 입력을 합니다.

http://test.net

이렇게 하면, 통상 test.net 사이트를 동작하고 있는 Web 서버 컴퓨터에서 Web 서버 프로그램의 root 디렉토리의 index.html 화일을 동작하게 됩니다. Root 디렉토리를 기호로 “/”로 나타낸다는 것을 이해한다면, 12번 줄의 의미가 바로 이해될지 모르겠습니다.

12번 줄은 사용자가 root 디렉토리(“/”)로 접속하면 LoginScreen() 객체를 생성하여, 해당 객체의 생성시 수행하는 작업을 진행하라는 의미 입니다. Routes Property가 Map 타입인 것을 상기 한다면, 여러 개의 입력 값에 반응 가능한 복수의 기능을 정의할 수 있는 것을 예측 할 수 있습니다. 따라서, [그림 3]에서는 root 디렉토리(“/”)에 대한 처리만 정의 했지만, 만약 root 하단의 sub1 디렉토리에 대한 요청과 root 하단의 sub2 디렉토리에 대한 동작을 추가로 정의하고 싶은 경우는 다음과 같이 각각에 functionSub1()과 functionSub2()를 수행하도록 정의할 수 있습니다.

routes: {
‘/’: (context) => LoginScreen(),
‘/sub1’: (context) => functionSub1(),                   
‘/sub2’: (context) => functionSub2(),
}

12번 줄에서 최초 root 로의 접근시 실행하는 LoginScreen()은 18번 줄부터 정의되어 있습니다. 이는 StatelessWidget으로 확장하여 만듭니다. 그리고 전형적인 실행 예제처럼 build()를 override하는 것을 볼 수 있습니다.

21번 줄을 보면 Scaffold()를 return하는 것을 볼 수 있는데, 22번 줄을 보면 배경 색깔을 grey[200]으로 정의하는 것과, body()를 포함하도록 합니다.

23번 줄을 보면, body()에 Center()를 생성하는데, 이 안을 보면, LoginForm()을 child로 포함하는 SizedBox()를 하나 포함하는 것을 볼 수 있습니다.

[그림 4] main.dart 이해 (2/2)

다음은 main.dart의 반에 해당하는 내용인 [그림 4]를 설명하도록 하겠습니다.

35번 줄에서는 LoginForm이 StatefulWidget()을 확장해서 만들어 지는 것을 볼 수 있습니다. 그리고 내부 상태로 LoginFormState를 createState()로 생성하는 것을 볼 수 있습니다.

40번 줄부터 LoginFormState()가 State<>의 확장으로 만들어 지는 것을 볼 수 있습니다.

41번 줄에서 43번 줄을 통해서, 3개의 TextController()가 내부 상태로 만들어 집니다. 이들은 TextEditingController()로 만들어 집니다. 여기서 처음 등장한 TextEditingController 클래스에 대해서 좀 더 알아 보겠습니다.

TextEditingController 클래스는 편집 가능한 텍스트 필드에 대한 컨트롤러 입니다(참조: https://api.flutter.dev/flutter/widgets/TextEditingController-class.html ). 이 클래스에서 주요한 property는 value, text 그리고 selection 입니다. 먼저 value Property는 TextEditingController 안에 저장되어 있는 현재의 값입니다. text Property는 사용자가 편집하고 있는 현재 문자열을 의미합니다. selection Property는 현재 선택된 텍스트를 의미합니다.

사용자가 관련 TextEditingController로 텍스트 필드를 수정할 때마다 텍스트 필드는 value Property를 업데이트하고 컨트롤러는 해당 Listeners에게 알립니다. 그러면 Listeners는 text Property 및 selection Property를 읽고 사용자가 입력 한 내용 또는 selection이 어떻게 업데이트 되었는지 알 수 있습니다. 마찬가지로 text Property 또는 selection Property를 수정하면 텍스트 필드에 알리고 적절하게 업데이트 됩니다. TextEditingController를 사용하여 텍스트 필드의 초기 값을 제공 할 수도 있습니다. 이미 text Property가 있는 컨트롤러로 텍스트 필드를 작성하면 텍스트 필드는 해당 텍스트를 초기 값으로 사용합니다. 이 컨트롤러에 추가 된 Listeners 내에서 text Property 또는 selection Property를 설정할 수 있습니다. 두 속성을 모두 변경해야하는 경우 컨트롤러 value Property를 대신 설정해야 합니다. 더 이상 필요하지 않은 경우 TextEditingController의 dispose Method를 실행 합니다. 이를 통해 객체가 사용하는 모든 리소스를 폐기 할 수 있습니다.

48번 줄을 보면, 이렇게 3가지 편집 가능한 텍스트 컨트롤러를 상태 값으로 관리하는 LoginFormState 클래스는 build 함수에서 Form() 객체를 리턴 합니다.

49번 줄을 보면, Form 클래스는 복수의 form field widget들을 묶어서 그룹으로 만든 container를 만듭니다.

50번 줄을 보면, Form 클래스의 구성 요소는 Column 형태로 묶인, LinearProgressIndicator(), Text(), 그리고 3개의 Padding()와 FlatButton()인 것을 알 수 있습니다. Text()는 입력 창의 맨 위에 표시한 “Sign Up” 글자를 나타내며, FlatButton()은 아직 활성화가 안된 (onpressed Property가 null인) “Sign Up” 글자를 내용으로 가지는 버튼입니다.

58번 줄을 포함한 3개의 Padding()은 각각 TextFormField()를 가지고 있는데, 이 안의 controller Property를 보면, 앞서 정의한 StatefulWidget의 상태 값인 TextEditingController()로 설정되어 있는 것을 볼 수 있습니다. Padding Property와 decoration Property는 각각 사용자 화면의 구성과 힌트 문자열을 보여주는 효과를 나타냅니다.

Step.1 Add welcome screen

이제 사용자가 텍스트의 입력을 마치면, 화면의 맨 아래 위치한 “Sign up” 단추를 눌러서 작업을 마치는 기능을 추가 하도록 합니다. 이를 위해서, main.dart 프로그램을 수정합니다. 먼저, 화면 중간에 간단하게 “Welcome!” 이라는 글자를 출력하는 WelcomeScreen 클래스를 위한 아래 코드를 추가 합니다.

class WelcomeScreen extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       body: Center(
         child: Text('Welcome!', style: Theme.of(context).textTheme.display3),
       ),
     );
   }
 }

해당 함수는 앞서의 MaterialApp의 routes Property를 통해서 접근 가능하도록 수정할 계획입니다. 따라서, 아래의 문장을 “/”에 대한 구문 아래 추가 합니다.

'/welcome': (context) => WelcomeScreen(),

이제 “Sign up” 단추를 활성화 하고, 이 단추가 눌렸을 때, 프로그램이 “/welcome” route로 이동하도록 해야 합니다. 이 기능을 위해서, LoginFormState 클래스 내부 method로, 아래의 showWelcomeScreen() 함수를 추가 합니다.

void _showWelcomeScreen() {
   Navigator.of(context).pushNamed('/welcome');
 }

Flutter는 단 하나의 Navigator 객체를 갖습니다. 이 위젯은 스택 내에서 Flutter의 화면 (routes 또는 pages 라고도 함)을 관리합니다. 스택 맨 위에 있는 화면은 현재 표시된 view 입니다. 이 스택에 새로운 스크린을 push 하면, 디스플레이가 새 화면으로 전환됩니다. 이것이 showWelcomeScreen() 함수가 WelcomeScreen을 Navigator의 스택으로 push 하는 이유입니다. 이제, 사용자가 “Sign up” 버튼을 클릭하면, Welcome 화면이 나타납니다. 마찬가지로, Navigator에서 pop()을 호출하면 이전 화면으로 돌아갑니다. Flutter의 navigation 기능은 브라우저의 navigation 기능에 통합되어 있습니다. 이로 인하여, 브라우저의 ‘뒤로가기’ 화살표 버튼을 클릭하면 암시적으로 이전 화면으로 돌아가는 pop()이 실행 됩니다.

이렇게 완성된 프로그램을 darttutorial-41-main-step-1.dart에서 확인할 수 있습니다.

// Source: https://flutter.dev/docs/get-started/codelab-web
// darttutorial-41-main-step-1.dart

 import 'package:flutter/material.dart';
 void main() => runApp(LoginApp());
 class LoginApp extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
       routes: {
         '/': (context) => LoginScreen(),
         '/welcome': (context) => WelcomeScreen(),
       },
     );
   }
 }
 class LoginScreen extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       backgroundColor: Colors.grey[200],
       body: Center(
         child: SizedBox(
           width: 400,
           child: Card(
             child: LoginForm(),
           ),
         ),
       ),
     );
   }
 }
 class LoginForm extends StatefulWidget {
   @override
   _LoginFormState createState() => _LoginFormState();
 }
 class _LoginFormState extends State {
   final _firstNameTextController = TextEditingController();
   final _lastNameTextController = TextEditingController();
   final _usernameTextController = TextEditingController();
 double _formProgress = 0;
 void _showWelcomeScreen() {
     Navigator.of(context).pushNamed('/welcome');
   }
 @override
   Widget build(BuildContext context) {
     return Form(
       child: Column(
         mainAxisSize: MainAxisSize.min,
         children: [
           LinearProgressIndicator(value: _formProgress),
           Text('Sign Up',
               style: Theme.of(context)
                   .textTheme
                   .display1), // display1 changes to headline4 in 1.16
           Padding(
             padding: EdgeInsets.all(8.0),
             child: TextFormField(
               controller: _firstNameTextController,
               decoration: InputDecoration(hintText: 'First name'),
             ),
           ),
           Padding(
             padding: EdgeInsets.all(8.0),
             child: TextFormField(
               controller: _lastNameTextController,
               decoration: InputDecoration(hintText: 'Last name'),
             ),
           ),
           Padding(
             padding: EdgeInsets.all(8.0),
             child: TextFormField(
               controller: _usernameTextController,
               decoration: InputDecoration(hintText: 'Username'),
             ),
           ),
           FlatButton(
             color: Colors.blue,
             textColor: Colors.white,
             onPressed: _showWelcomeScreen,
             child: Text('Sign up'),
           ),
         ],
       ),
     );
   }
 }
 class WelcomeScreen extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       body: Center(
         child: Text('Welcome!', style: Theme.of(context).textTheme.display3),
       ),
     );
   }
 }

Step.2 Enable sign in progress tracking

앞서 [그림 4]의 53번 줄을 보면, LinearProgressIndicator가 있는 것을 볼 수 있습니다. 이는 progress bar라고도 하는데, Material 디자인에 기반해서 작업이 진행 중이라는 것을 사용자에게 보여주는 역할 입니다. 두가지 타입이 존재하는 Determinate와 Indeterminate 입니다. 전자는 0.0 ~ 1.0 사이의 값으로 현재의 진행 상황을 표시합니다. 후자는 값을 갖지 않으며(null value), 단지 진행 중이라는 의미를 전달하기 위해서 사용합니다. (참조: https://api.flutter.dev/flutter/material/LinearProgressIndicator-class.html )

이제 비활성화 되어 있는 LinearProgressIndicator를 활성화 하도록 하겠습니다. 즉, 사용자가 [그림 2]의 각각의 칸을 채워 나가면, LinearProgressIndicator의 색과 채워지는 모양이 바뀌며, 모든 칸에 대해서 사용자가 입력을 하면, 꽉찬 줄의 모양을 보여주도록 수정합니다. 이를 위해서 사용자가 각 칸 별로 글을 썼는지 확인한 후, 칸이 채워지는 만큼의 값을 LinearProgressIndicator의 파라메타로 설정한 formProgress 변수의 값을 0.0 ~ 1.0으로 변경하는 방식으로 줄의 모양이 바뀌도록 합니다. 이를 위해서 가장 먼저 할 일은 3개의 칸에 대해서 빈칸이 아니면, 각 칸별로 1/3의 값을 formProgress에 더하는 함수 입니다. 이 함수는 updateFormProgress() 이며, 다음과 같은 내용을 갑습니다. 이 함수를 LoginFormState 클래스 안에 정의합니다. 함수의 내용은 명확 합니다. 세개의 편집 가능한 칸을 담당하는 컨트롤러의 isNotEmpty Property를 통해서, 빈 칸인지 확인하고, 빈 칸이 아니라면 임시 값인 progress에 1/3의 값을 추가 합니다. 그리고 이 값을 LinearProgressIndicator가 관리하는 formProgress에 전달하여 LinearProgressIndicator의 모양이 바뀌도록 합니다. 이때, setState() Method안에서 formProgress를 설정하는데, setState()는 앞서 Flutter의 내용을 설명할 때, 화면을 업데이트 하도록 유도 한다는 부분을 다시 한번 리마인드 합니다.

void _updateFormProgress() {
   var progress = 0.0;
   var controllers = [
     _firstNameTextController,
     _lastNameTextController,
     _usernameTextController
   ];
 for (var controller in controllers) {
     if (controller.value.text.isNotEmpty) {
       progress += 1 / controllers.length;
     }
   }
 setState(() {
     _formProgress = progress;
   });
 }

다음에 할 일은 updateFormProgress() Method가 칸에 글을 입력하거나 삭제하는 활동이 발생 했을때, 자동으로 호출되도록 하는 것 입니다. 이는 LoginFormState 클래스의 build() 메소드에서 리턴되는 Form() 내의 onChanged Property 값에 updateFormProgress()를 전달하는 것으로 가능합니다. 즉, Form 안의 칸에 값이 입력/삭제 되어 변경이 발생하면, updateFormProgress() 함수가 자동 호출 되도록 하는 것 입니다. 따라서 Form()의 첫번째 줄을 다음과 같이 추가 합니다.

onChanged: () => _updateFormProgress(), 

마지막으로 칸이 모두 채워졌는지 아닌지에 대한 판단을 할 수 있게 되었으므로, 칸이 채워지면 Welcome 화면으로 넘어가지만, 빈칸이 있다면 Welcome 화면으로 넘어가지 않도록 하는 기능을 추가로 작성하도록 하겠습니다. 이는 매우 단순하게, 작업을 마치고 누르는 FlatButton의 onPressed Property를 다음과 같이 변경합니다.

onPressed: _formProgress == 1 ? _showWelcomeScreen : null, 

이는 formProgress가 1이면 showWelcomeScreen Method를 실행하고, 1이 아니면 아무런 동작을 하지 말라는 의미입니다.

이제 Step.2의 모든 작업을 마쳤으므로, “flutter run -d Chrome”으로 수정된 프로그램을 실행하고, 수정된 내용을 확인해 봅니다.

이렇게 수정한 main.dart는 darttutorial-41-main-step-2.dart와 같습니다.

// Source: https://flutter.dev/docs/get-started/codelab-web

import 'package:flutter/material.dart';

void main() => runApp(LoginApp());

class LoginApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        '/': (context) => LoginScreen(),
        '/welcome': (context) => WelcomeScreen(),
      },
    );
  }
}

class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[200],
      body: Center(
        child: SizedBox(
          width: 400,
          child: Card(
            child: LoginForm(),
          ),
        ),
      ),
    );
  }
}

class LoginForm extends StatefulWidget {
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _firstNameTextController = TextEditingController();
  final _lastNameTextController = TextEditingController();
  final _usernameTextController = TextEditingController();

  double _formProgress = 0;

  void _showWelcomeScreen() {
    Navigator.of(context).pushNamed('/welcome');
  }

  void _updateFormProgress() {
    var progress = 0.0;
    var controllers = [
      _firstNameTextController,
      _lastNameTextController,
      _usernameTextController
    ];

    for (var controller in controllers) {
      if (controller.value.text.isNotEmpty) {
        progress += 1 / controllers.length;
      }
    }

    setState(() {
      _formProgress = progress;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      onChanged: () => _updateFormProgress(),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          LinearProgressIndicator(value: _formProgress),
          Text('Sign Up',
              style: Theme.of(context)
                  .textTheme
                  .display1), // display1 changes to headline4 in 1.16
          Padding(
            padding: EdgeInsets.all(8.0),
            child: TextFormField(
              controller: _firstNameTextController,
              decoration: InputDecoration(hintText: 'First name'),
            ),
          ),
          Padding(
            padding: EdgeInsets.all(8.0),
            child: TextFormField(
              controller: _lastNameTextController,
              decoration: InputDecoration(hintText: 'Last name'),
            ),
          ),
          Padding(
            padding: EdgeInsets.all(8.0),
            child: TextFormField(
              controller: _usernameTextController,
              decoration: InputDecoration(hintText: 'Username'),
            ),
          ),
          FlatButton(
            color: Colors.blue,
            textColor: Colors.white,
            onPressed:
                _formProgress == 1 ? _showWelcomeScreen : null, 
            child: Text('Sign up'),
          ),
        ],
      ),
    );
  }
}

class WelcomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Welcome!', style: Theme.of(context).textTheme.display3),
      ),
    );
  }
}

마무리

공식 사이트에는 추가적으로 progress indicator에 애니메이션 효과를 추가하는 예제까지 되어 있습니다. 하지만 개인적으로 애니메이션에는 큰 흥미가 없기에 이 부분은 본 글에서는 다루지 않도록 하겠습니다.

이번 글에서 어떤 부분을 느낄수 있을까요? Web 브라우저 화면을 채우고 있지만, HTML/CSS/JS에 대한 전문 지식이 그다지 필요하지 않은 것을 볼 수 있습니다. 그리고 모바일 어플리케이션을 개발하는 것과 동일하게 개발이 되는 것을 알 수 있습니다. 이렇게 플랫폼이 다르더라도 같은 기술을 사용하여 Web 어플리케이션을 개발하는 것이 가능한 매력을, Dart/Flutter 개발 환경은 제공하고 있습니다.

Creative Commons License (CC BY-NC-ND)

Dart Programmer 되기 [42]

< Web 개발 – Dart for Web >

이 글에서 설명하는 기술은 Flutter를 사용하지 않고, Dart언어의 Core 라이브러리인 dart:html을 사용하는 방법 입니다. 이는 HTML/CSS/JavaScript의 조합에서 JavaScript에 상응하는 역할을 Dart 언어 그리고 dart:html의 코어 라이브러리 기능이 수행 한다고 보면 됩니다. 따라서, 이 글을 이해하기 위해서는 HTML/CSS/JavaScript를 기반으로 Web 어플리케이션을 만드는 기술에 대한 기본적인 이해가 필요합니다.

“Hello, World!” Dart web-only app 이해

앞서 < Web 개발 – “Hello, World!” Dart web-only app >에서 Stagehand가 자동으로 생성한 소스 코드를 살펴보도록 합니다. 먼저 index.html 입니다.

<!DOCTYPE html>

<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="scaffolded-by" content="https://github.com/dart-lang/stagehand">
    <title>dart4web</title>
    <link rel="stylesheet" href="styles.css">
    <link rel="icon" href="favicon.ico">
    <script defer src="main.dart.js"></script>
</head>

<body>

  <div id="output"></div>

</body>
</html>

head 부분은 일반적인 내용으로 채워지고 있습니다. script가 main.dart.js로 설정되어 dart 소스 프로그램이 JavaScript로 변환되어 실행된다는 것을 볼 수 있습니다. 그리고 JavaScript에서 HTML 컨텐츠를 동적으로 다룰때 사용하는 전형적인 형태로서, id를 가진 영역을 선언하였으나, 처음에는 비어 있는 것을 볼 수 있습니다. 여기서 우리가 동적으로 컨텐츠를 업데이트할 id는 “output” 입니다.

그러면, 이 “output”라는 id를 갖는 영역을 업데이트 하는 Dart 코드를 볼 차례입니다. 해당 코드는 main.dart에 정의되어 있습니다.

import 'dart:html';

void main() {
  querySelector('#output').text = 'Your Dart app is running.';
}

가장 먼저 Dart 언어의 HTML 제어 코어 라리브러리인 dart:html을 import하는 것을 볼 수 있습니다. 그리고 main()가 선언되어 있습니다. main()안에는 한줄의 코드가 있습니다. 기능은 html 화인에서 “output”을 id로 갖는 구문을 찾은후(querySelector()), 그 속에 있는 문자열을 “Your Dart app is running.”로 바꾼다는 의미입니다.

따라서 이 프로그램이 수행이 되면, 비어있는 영역에 상기 문자열을 동적으로 삽입하는 것을 볼 수 있습니다. 여기서 알수 있듯이, HTML과 JavaScript가 연결되는 프로그래밍 방식과 거의 동일한 방식으로 HTML과 Dart 소스 프로그램이 연결되어 있는 것을 볼 수 있습니다. 따라서, JavaScript에서 HTML을 제어하는 방식에 상응하는 함수들이 dart:html에서 제공이 되고 있다는 것을 예측할 있습니다.

마지막 확인인 css 화일은 styles.css 로서, 다음의 내용을 갖습니다.

@import url(https://fonts.googleapis.com/css?family=Roboto);

html, body {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  font-family: 'Roboto', sans-serif;
}

#output {
  padding: 20px;
  text-align: center;
}

CSS에 대한 기본 지식이 있다면, 특별히 어렵거나 인상적인 코드는 아니며, 일반적인 형태와 폰트에 대한 부분인 것을 알 수 있습니다.

dart:html을 통한 HTML 제어

dart:html을 사용하여 html로 표시되는 Web 컨텐츠를 동적으로 변경하는 내용에 대해서 조금 더 이해를 해 보겠습니다. 이를 위해서, main.dart의 내용을 다음과 같이 변경 합니다. 이는 Dart의 공식 사이트( https://dart.dev/tutorials/web/get-started )에 게재된 내용을 그대로 사용합니다.

import 'dart:html';

Iterable<String> thingsTodo() sync* {
  var actions = ['Walk', 'Wash', 'Feed'];
  var pets = ['cats', 'dogs'];

  for (var action in actions) {
    for (var pet in pets) {
      if (pet == 'cats' && action != 'Feed') continue;
      yield '$action the $pet';
    }
  }
}

void addTodoItem(String item) {
  print(item);

  var listElement = LIElement();
  listElement.text = item;
  todoList.children.add(listElement);
}

UListElement todoList;

void main() {
  todoList = querySelector('#todolist');
  thingsTodo().forEach(addTodoItem);
}

다음으로 index.html 화일도 body 부분을 변경한 다음의 내용으로 변경 합니다.

<!DOCTYPE html>

<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="scaffolded-by" content="https://github.com/dart-lang/stagehand">
    <title>dart4web</title>
    <link rel="stylesheet" href="styles.css">
    <link rel="icon" href="favicon.ico">
    <script defer src="main.dart.js"></script>
</head>

<body>
    <h2>A Simple To-Do List</h2>
    <p>Things to do:</p>
    <ul id="todolist">
    </ul>
</body>
</html>

먼저 index.html 화일의 body 내용을 살펴보면, 헤더(h2)로 제목을 나타냅니다. 그리고 문장(p)으로 “Things to do:”를 문자열을 화면에 출력합니다. 그리고 id를 “todolist”로 정의한 비어 있는 Unnumbered-List를 생성하는 것을 볼 수 있습니다. 앞서 Hello World 예제처럼 이 부분의 내용을 Dart 언어를 사용하여 동적으로 변경할 것 입니다. 이 프로그램을 webdev serve 명령으로 수행하고, http://localhost:8080으로 접속하면 다음과 같이 Web 컨텐츠가 채워지는 것을 볼 수 있습니다.

이렇게 화면이 채워지게된 이유를 main.dart를 통해서 설명 하겠습니다. main.dart 안에는 main()을 제외하고 두개의 함수가 있습니다.

addTodoItem() 함수는 입력 파라메타도 문자열을 하나 받습니다. 그리고 디버그 용도를 위해서 print() 함수로 화면 출력을 수행합니다. 이 출력은 Web 브라우저를 개발자 모드로 수행해서, JavaScript 프로그램의 출력이 표시되는 console 탭을 선택하면 확인 가능 합니다. 다음의 3줄이 수행해야 하는 동작인데, 먼저 비어있는 List Element를 하나 생성합니다. 그리고, 이 List Element의 text 내용을 입력 파라메타로 받은 내용으로 채우게 됩니다. 이렇게 채워진 문자열은 HTML 화일인 index.html의 body 부분인 todolist id 부분을 채우기 위한 용도로 사용되어, 함수 실행시 마다 List Element를 하나씩 추가 합니다. 이를 통해서 위의 화면 출력과 같이 4개의 Unnumbered List 4개가 나타나도록 합니다.

thingsTodo() 함수가 실질적으로 HTML에 추가되는 4개의 Unnumbered List Element를 만들어 내는 기능을 합니다. 이 함수 안에는 동작을 의미하는 문자열 3개로 이루어진 actions 리스트와 동물을 나타내는 pets 리스트가 있습니다. Nested for 구문을 통해서, 이들을 매칭하게 되며, pets가 dogs인 경우는 3가지 actions에 모두 매핑하여, “$action the $pet”의 문자열을 생성합니다. pets가 cats 인 경우는 오직 actions이 “Feed”인 경우메나 문자열을 생성합니다. 여기서 yield 구문은 return 구문과 유사하게 값을 리턴하는 문법 입니다. 차이점은 return이 한번에 하나의 값을 전달하고 마친다면, yield 구문은 generator 구문으로서, thingsTodo()의 결과 값이 필요한 구문이 실행될때 마다 그때 그때 필요한 결과 값을 만들어서 리턴하는 점에서 다릅니다. 이를 thingsTodo() 함수가 async* 구문의 비동기적인 동작을 하는 부분에서 기인 합니다.

이 두가지 함수를 이해한 상태에서 main() 함수를 보면, 이해가 용이합니다. 즉 main()의 첫번째 줄에서는 index.html에서 정의한 비어있는 영역을 선택하게 되고, 여기에 thingsTodo()가 실행되면서 만들어지는 하나 하나의 문자열을 추가하게 되는 것 입니다.

따라서, HTML/CSS/JavaScript를 혼용하여 Web 콘텐츠를 다룰수 있는 개발자라면, JavaScript 대신 Dart 언어와 dart:html 코어 라이브러리를 사용하여 Web 콘텐츠를 제어하는 것이 가능 합니다.

결론적으로 Dart 언어로 DOM을 사용하려면, JavaScript를 통한 Web 컨텐츠를 다루는 경우와 동일하게, Window, Document, Element 및 Node 에 대해 알아야합니다.

Window 객체는 웹 브라우저의 실제 윈도우를 나타냅니다. 각 Window에는 현재 로드 된 문서를 가리키는 Document 객체가 있습니다. Window 객체에는 IndexedDB (데이터 저장 용), requestAnimationFrame (애니메이션 용) 등과 같은 다양한 API에 대한 접근자가 있습니다. 탭 브라우저에서 각 탭에는 고유한 Window 객체가 있습니다.

Document 객체를 사용하면 문서 내에서 Element 객체를 만들고 조작 할 수 있습니다. Document 자체는 요소이며 조작 할 수 있습니다.

DOM은 Node 트리를 모델링 합니다. 이러한 Node는 종종 Element 이지만 attributes, test, comment 및 기타 DOM 유형일 수도 있습니다. 부모가 없는 루트 노드를 제외하고 DOM의 각 노드에는 하나의 부모가 있으며 많은 자식이 있을 수 있습니다.

마무리

이 글에서는 전통적인 Web 어플리케이션을 만드는 기술인 HTML/CSS/JavaScript의 조합을 사용하는 기술에서 JavaScript 대신 Dart를 활용하는 접근 방법인 Dart for Web 기술에 대해서 알아 보았습니다.

dart:html을 사용한 Dart for Web 기술에 대해서 좀 더 이해하고 싶다면, Dart 공식 사이트에서 “A tour of the core libraries”의 dart:html에 대한 설명인 “dart:html – browser-based apps” 부분( https://dart.dev/guides/libraries/library-tour#darthtml )을 읽고 실행해 보기를 권장합니다.

Creative Commons License (CC BY-NC-ND)

Dart Programmer 되기 [43]

< Web 개발 – AngularDart for Web >

결론부터 이야기 하면, AngularDart는 기존의 Angular 프레임워크를 이해하는 개발자에게 권장하기에 적합한 기술 입니다. Angular를 다뤄보지 않은 경우라면, Flutter 기반으로 개발을 하거나, Angular에 대한 이해를 한후 AngularDart를 다루는 것을 권합니다.

Dart 언어로 Web 어플리케이션을 만드는 방법으로 지금까지 두가지 방법을 설명 하였습니다. 첫째는 Flutter를 사용하는 방법으로, HTML/CSS에 대한 이해 보다는 Flutter 프레임워크로 모바일 프로그래밍 방식과 동일하게 Web 어플리케이션을 만드는 방법입니다. 두번째는 Dart for Web으로 dart:html 코어 라이브러리를 사용하여, HTML/CSS/JavaScript를 사용하는 방식에서 JavaScript 대신 Dart 언어가 사용되는 형태였습니다. 마지막은 Web 개발시 사용되는 Angular 프레임워크를 Dart 언어로 porting한 AnduglarDart 입니다. JavaScript로 개발된 Angular 프레임워크에 대응하는 Dart의 프레임워크라고 보면 됩니다. 공식 홈페이지는 https://angulardart.dev/ 입니다.

AngularDart는 현재 공식 릴리즈는 아니고, alpha 버전 입니다. Google은 Angular에 의존하는 미션 크리티컬 앱이 계속 제대로 작동하는지 확인하기 위해서, 각 버전의 AngularDart (alpha 릴리스 포함)를 철저히 테스트 하고 있다고 공표하고 있습니다. alpha 레이블은 API가 변경될 가능성이 있으며, 앞으로 나올 릴리스 (또는 이후 릴리스)로 인해 코드가 손상 될 수 있음도 경고하고 있으니 참조 바랍니다.

일단 가장 간단한 AngularDart 기반의 프로그램을 돌려보도록 하겠습니다. 이를 위해서, 공식 홈페이지의 Get Started를 방문( https://angulardart.dev/guide/setup )해 봅니다. 공식 버전이 릴리즈 되지 않은 이유인지, 여러 방법이 제시되고 있는데, 여기서는 가장 간단한 방법을 사용해 보고자 합니다.

첫번째로 AngularDart 기반의 프로그램의 기본 골격이 만들어져 있는 프로젝트의 압축 화일을 다운로드 받습니다. Get Started에 공개된 링크를 통해서도 다운로드 가능하지만, GitHub에서 다음 주소의 화일로 바로 다운로드 가능합니다. zip으로 압축된 화일이므로, 압축을 해제한 후, 희망하는 디렉토리에 위치시킵니다.

https://github.com/angular-examples/quickstart/archive/master.zip

두번째로 압축이 해제된 디렉토리 안으로 이동해서 CLI 커맨드로 pub get 명령을 수행하여, AngularDart를 포함한 패키지 화일을 다운 받아 설치할 수 있도록 합니다.

세번째로 webdev serve 명령으로 바로 실행을 해보도록 합니다. 그리고 http://localhost:8080으로 접속을 합니다. 이에 따른, 웹 브라우저 상에 단순히 아래와 같은 “Hello Angular” 문자열을 출력하는 기능입니다.

소스 코드를 살펴보면, 먼저 index.html의 body 부분이 다음처럼 되어 있는 것을 볼 수 있습니다. 특이하게 <my-app> 태그가 있는 것을 볼 수 있습니다.

<body>
  <my-app>Loading...</my-app>
</body>

다음으로 main.dart는 다음과 같이 짧게 되어 있습니다. import를 통해서 AngulartDart 패키지와 이에 기반하는 어플리케이션의 template를 불러 오는 것을 볼 수 있습니다.

import 'package:angular/angular.dart';
import 'package:angular_app/app_component.template.dart' as ng;

void main() {
  runApp(ng.AppComponentNgFactory);
}

사실 위의 두개 화일로는 왜 화면에 “Hello Angular”가 출력되었는지 이해를 하기 어렵습니다. 이 부분을 이해하기 위해서는 압축을 해제한 프로젝트 폴더안의 lib 서브 디렉토리에 위치한 app_component.dart 화일을 열어봐야 합니다. 이 화일의 내용은 다음과 같습니다.

import 'package:angular/angular.dart';
@Component(
   selector: 'my-app',
   template: 'Hello {{name}}',
)
class AppComponent {
   var name = 'Angular';
}

AngularDart 어플리케이션은 components 들로 구성 됩니다. component는 HTML template와 component class의 조합으로, 웹 브라우저의 스크린에 나타날 내용을 제어하는 역할을 합니다. 주어진 예제를 토대로 설명하면, component는 모두 @Component 키워드로 시작 합니다. component 안의 selector 문법을 볼 수 있는데, 이 부분이 index.html에서 <my-app> 태그로 되어 있는 부분에 대해서, 어떻게 출력이 되는지를 결정합니다. 주어진 예제에서는 “my-app” 태그 부분을 제어하는 내용으로 ‘<h1>Hello {{name}}</h1>’를 사용한다는 의미에서 template 문법이 사용되는 것을 볼 수 있습니다. 정리해서 이야기 하면, index.html의 <my-app>을 웹 브라우저가 처리해야 하면, app_component.dart에서 “my-app”을 selector로 지정한 component를 찾는 것 입니다. 이렇게 해서, 매칭되는 component를 찾으면 정해진 내용으로 index.html의 해당 부분을 업데이트 하는데, 현재는 ‘<h1>Hello {{name}}</h1>’ 내용으로 출력을 하는 것 입니다. 이는 HTML 구문인데, 이슈는 {{ … }}으로 되어진 부분입니다.

이런 문법을 AngularDart에서는 interpolation binding 표현이라고 부릅니다. 어플리케이션 실행시, AngularDart는 {{name}}으로 되어진 부분을 component의 name Property의 값으로 대체하는데, 위의 코드를 보면, 이 값이 ‘Angular’ 문자열 인 것을 볼 수 있습니다. 따라서, 화면에 “Hello Angular” 문자열이 출력되게 됩니다. 따라서 var name = ‘Angular’; 구문을 var name = ‘World’;으로 변경해 봅니다. 그리고 나서 다시 화면을 업데이트 하면 다음과 같이 출력이 바뀐 것을 알 수 있습니다.

우리가 실행한 위의 프로그램을 공식 사이트에서는 “Starter App”이라고 하며, 공식 사이트인 https://angulardart.dev/tutorial/toh-pt0 에서 더 세부적인 설명을 볼 수 있습니다.

마무리

이 Tutorial에서는 Flutter를 주로 다루는 것을 방향으로 하고 있기에, AngularDart에 대한 소개는 이 정도로 마칩니다. AngularDart는 Angular 프레임워크를 선호하는 개발자라면 관심을 기울여 볼만한 기술입니다. Angular에 대한 지식을 갖춘 개발자라면 공식 사이트의 Tutorial( https://angulardart.dev/tutorial )을 따라해 보면서, 기존 Angular와 같은 부분과 다른 부분에 대해서 이해해 보도록 하기 바랍니다.

주의할 사항은 아직 공식 릴리즈가 아닌 만큼 업데이트 되는 상황을 수시로 모니터링 하면서 기술을 활용할 필요가 있습니다.

Creative Commons License (CC BY-NC-ND)

Dart Programmer 되기 [39]

< Web 개발 – “Hello, World!” Flutter multi-platform web apps >

앞의 글에서 Dart 언어를 사용한 Web 어플리케이션을 개발해 보았다면, 이번에는 Flutter  기반의 Web 어플리케이션을 만들어 봅니다. 앞의 글에서 사용한 기술이 Dart 언어의 코어 라이브러리인 dart:html 을 사용한 방법이라면, 이번에는 말그대로 Flutter 프레임워크를 사용합니다. 사실 이 tutorial의 주제는 Flutter를 통한 개발에 주안점을 두기에, Web 어플리케이션도 지금 설명하는 Flutter 기반으로 이루어 지기를 권장합니다.

Flutter for Web은 아직 공식 릴리즈가 아니기에, 추가로 설치해야 할 사항이 있습니다. Web 어플리케이션을 Flutter로 개발하기 위한 개발환경 설정은 Flutter의 공식 사이트( https://flutter.dev/docs/get-started/web )에서 확인할 수 있습니다. 설치는 어렵지 않으나, 현재 Flutter for Web은 베타 버전이므로, 문제가 발생하면, 공식 사이트에서 언급한대로 버그 리포트를 하기 바랍니다.

Flutter for Web 개발 환경을 구축하기 위해서 다음의 명령을 수행합니다. 이해를 위해서 실행한 결과 화면도 명령어의 밑에 추가 하였습니다.

[Step.1] flutter channel beta

[Step.2] flutter upgrade

[Step.3] flutter config –enable-web

[Step.4] flutter devices

Step.4를 실행하여, flutter device에 Chrome 브라우저가 등록된 것을 볼 수 있습니다. 그리고 Web Server는 Web 어플리케이션을 실행하기 위하여 활용 됩니다.

이제 Flutter for Web을 위한 개발 환경은 구축이 되었습니다. 다음은 앞서 Flutter를 통해서 프로젝트를 만들고 실행하는 환경과 유사합니다. 다음은 Flutter for Web 공식 사이트의 내용대로, myapp이라는 이름의 project를 만들고, 이를 Chrome 브라우저에서 실행한 예제 입니다.

flutter create myapp
cd myapp
flutter run -d chrome

여기서 flutter run 명령은 Web 어플리케이션을 development 모드(dartdevc 기반)로 동작하게 합니다.이제 아주 익숙한 화면이 넓은 Chrome 브라우저의 화면에 [그림 1]과 같이 펼쳐지는 것을 확인 할 수 있습니다.

[그림 1] Flutter for Web 기본 실행 화면

한가지 장난을 해보도록 하겠습니다. 앞서 darttutorial-35-01.dart 프로그램으로, 이번에 새롭게 만든 myapp 프로젝트의 main.dart를 대치해 보겠습니다. 이를 위해서 flutter가 자동 생성한 lib 디렉토리의 main.dart를 지우고, darttutorial-35-01.dart를 main.dart로 저장합니다. 그리고 flutter run -d chrome

 명령을 실행하면, 익숙한 프로그램이 [그림 2]와 같이 Chrome 브라우저 화면 가득하게 채워지는 것을 볼 수 있습니다. 즉, 기존에 모바일 용도로 만든 프로그램도 수정 없이 Web 어플리케이션으로 변환하는 것이 가능합니다. 단, Web의 UI/UX는 이에 맞춰야 하니, 제대로 된 프로그램은 아니라고 할 수 있습니다. 하지만, 이제 모바일 환경을 위하여 만든 프로그램을 Web 어플리케이션에서도 seamless 하게 제공할 수 있는 열쇠를 확보한 셈입니다.

[그림 2] 기존 모바일 프로그램을 Web 어플리케이션으로 실행한 화면

Flutter 공식 사이트에서는 이렇게 기존의 프로젝트를 Web 어플리케이션으로 적용하기 위한 첫번째 작업으로, 기존 프로젝트의 디렉토리 안에서 다음의 명령을 수행하도록 하고 있습니다.

flutter create .

그리고 Web 어플리케이션으로 실행하기 위해서, 앞서 사용한 flutter run -d chrome을 실행하도록 권장하고 있습니다.

앞서 flutter run이 dartdevc를 기반으로 동작한다고 하였으니, 이제 작업을 마친 Web 어플리케이션을 정식 릴리즈 하려면 dart2js를 기반으로 동작하는 방법이 필요합니다. 이는 flutter build web 명령을 실행하여 가능합니다. 이 명령은 릴리즈를 위한 코드를 생성해 줍니다. 이 명령을 수행 하기 전에, 프로젝트 안의 /build 서브 디렉토리를 확인하면, asset 화일이 있는 것을 볼 수 있습니다. 이제 flutter build web을 실행하고, 다시 /build 서브 디렉토리로 들어가면 /web 서브 디렉토리가 생성되어 있으며, 안으로 들어가면 Dart 언어에서 변환된 코드들이 자동 생성되어 있는 것을 볼 수 있습니다. flutter build web 외에 flutter run –release 옵션을 줌으로써 릴리즈 모드에서 수행하는 것도 가능합니다. 따라서 향후 Web 어플레이션을 릴리즈 하는 경우는 /build 서브 디렉토리의 /web과 asset 화일들을 사용하는 것 입니다.

마무리

익숙한 Flutter의 자동생성 예제 프로그램을 커다란 Web 브라우저 화면에 채워서 나타나도록 해 보았습니다. 그리고 이미 모바일 용도로 만든 프로그램을 Web 브라우저에서 구동하는 방법도 배웠습니다. 아직 Flutter for Web은 공식 릴리즈가 나오지 않아 안정적이라고 보기는 어렵지만, 많은 개발자들의 지지속에 낙관적인 미래를 조심스레 예상해 봅니다.

Creative Commons License (CC BY-NC-ND)

Dart Programmer 되기 [38]

< Web 개발 – “Hello, World!” Dart web-only app >

사실 앞서의 글을 통해서, Dart 언어로 웹 브라우저를 통해서 컨텐츠를 제공하는 Web 어플리케이션을 만든 셈 입니다. 앞서 글의 main.dart 소스 코드의 내용을 아래와 같이 “Hello, World!”를 출력하도록 변경합니다.

querySelector(‘#output’).text = ‘Hello, World!’;

그리고, 다시 WebDev를 통해서 해당 Web 어플리케이션을 실행해서 화면 결과가 [그림 1]과 같이 나오는지 확인 바랍니다.

[그림 1] “Hello, World!” 실행 화면

그렇다면, 굳이 “Hello, World!”를 별도의 챕터로 다룬 이유는 무엇 일까요? 바로 Dart로 만든 Web 어플리케이션이 어떻게 Web 브라우저에서 동작하는 지를 이해하기 위함입니다. 이 글은 HTML/CSS/JS에 대한 경험이 일부 있다면, 이해가 용이합니다. 그렇지 않다면, “아 이런 개념이구나” 정도의 큰 그림만 이해하면 되지 않을까 싶습니다.

Dart에서는 개발중인 프로그램은 development 버전, 그리고 개발을 마치고 실제 서비스에 들어가는 프로그램을 deploy(혹은 release)로 구분 합니다. 이렇게 구분하는 이유는 같은 프로그램이지만, Dart에서 적용하는 도구가 다르기 때문 입니다. Dart 언어로 만든 Web 어플리케이션을 기존의 Web 브라우저에서 실행 할 수 있도록 하기 위해서는 Dart 언어로 만든 프로그램을 기존 Web 브라우저가 이해할 수 있는 언어로 표현하는 중간 과정이 필요한데, 이를 위해서 development 단계시에는 dartdevc( https://dart.dev/tools/dartdevc )가 사용되고 deploy를 하여 실제 서비스를 릴리즈(release)하는 단계에서는 dart2js( https://dart.dev/tools/dart2js )가 사용됩니다. 이번 글은 이 두가지 tool을 이해함으로서, 어떻게 Dart 언어로 만든 프로그램을 Web 브라우저들이 이해하고 수행하는지에 대해서 중점적으로 다룹니다.

dart2js는 Dart 코드를 실제 서비스를 운영하는 수준의 JavaScript 코드로 바꿔주는 tool 입니다.dartdevc도 목적은 같지만, 개발 용도로만 사용하는 것으로 권장합니다. 앞서 WebDev는 webdev serve로 실행하는 경우는 dartdevc를 실행하고 (–release 플래그 옵션을 줘서 dart2js를 실행하도록 변경 가능), webdev build로 실행하면 dart2js를 실행합니다.

갑자기 JavaScript에 대한 이야기가 나오니 당황할 수 있습니다. Dart 언어는 새롭게 나온 언어로서, 기존의 Web 브라우저들에 대한 호환성을 고려할 필요가 있었습니다. 이에 Source-to-Source 컴파일 방식을 생각하게 되었습니다. 즉 Dart 언어로 만든 프로그램을 JaVascript 언어 기반 프로그램으로 변환하는 것 입니다. 따라서 대부분의 Web 브라우저들이 JavaScript를 지원하기에, Dart 언어로 만든 프로그램도 궁극적으로 JavaScript를 지원하는 모든 Web 브라우저에서 수행이 가능한 것 입니다. 성능 적인 부분에 대해서 고민을 할 지 모르겠지만, dart2js와 dartdevc로 만들어진 JavaScript 코드들은 최적화된 형태로 만들어진다고 하니, 성능에 관심이 있는 경우는 직접 분석해 보기 바랍니다.

참고로 Desktop에서 다시 설명하겠지만, Dart 언어로 만든 프로그램이 운영체제 위에서 직접 구동하는 Native 방식(프로그램이 Intel/ARM의 시스템 코드(기계어)로 동작한다는 의미)인 경우, 즉 노트북이나 데스크탑 위에서 직접 Dart로 만든 프로그램을 실행하는 경우는 Stand-alone 모드 혹은 Ahead-of-time (AOT) 컴파일 방식을 사용합니다. Stand-alone 방식의 경우는 Dart VM(Virtual Machine)을 사용하는데, Dart SDK에 포함된 Dart VM이 CLI 환경에서 Dart 언어로 만든 프로그램을 실행하는 방식 입니다. AOT 방식은 Dart 코드를 기계언어로 변경하는 방식이며, 사실 앞서 설명한 모바일 앱 들도, 앱 스토어 등재시 AOT 컴파일된 Dart 코드 형태로 배포되게 됩니다.

먼저, dart2js에 대해서 설명 하겠습니다. dart2js를 사용하여, Dart 언어로 만들어진 프로그램을 JavaScript 언어의 프로그램으로 변경하는 명령은 다음과 같습니다.

dart2js -O1 -o target.js source.dart

이 명령은 source.dart 프로그램을 target.js 프로그램으로 변환하라는 의미이며, 이 경우 최적화 옵션은 -O1으로 할당하여 default 모드를 사용한다는 것 입니다. -o 옵션이 output 화일을 지정하는 것으로 “-o target.js”가 하나의 의미로 묶이는 형태 입니다. dart2js를 통해서 만들어지는 화일은 하나가 아니고 복수 개 입니다. 앞서의 “Hello, World!” 프로그램의 main.dart 화일에 대해서 다음과 같이 실행해 봅니다.

dart2js -o target.js main.dart

위의 명령을 실행해 보면, 가장 기본적으로 target.js가 생성되고 부수적인 화일들이 생성된 것을 확인할 수 있습니다. 각각의 화일들의 내용에 관심이 있다면 Googling을 통해서 좀 더 심도 있게 이해하기를 권장 합니다.

최적화 옵션은 -O0의 경우 최적하 하지 않음 이고, -O1이 default 수준의 최적화이고, 그리고 -O2/-O3/-O4는 숫자가 클수록 강화된 최적화를 지원합니다. 최적화에 대해서는 각각의 단계가 어떤 작업을 수행하는지를 명확하게 이해하고, 특별히 문제가 될 수 있는 부분에 대한 설명을 사전에 숙지하여 본인의 프로그램에 적용 시 문제가 없는지를 확인하기 바랍니다.

dart2js를 사용하는 경우를 위해서 Dart 공식 사이트에서는 다음의 문법에 주의하면 더 작고 빠른 JavaScript 코드가 생성된다고 가이드라인을 제시하고 있습니다. 우리가 지나온 내용 안에서 설명이 된 문법도 있고, 다루지 않은 내용도 있으니, 지금은 유념하고, 나중에 본인이 직접 Web 어플리케이션을 전문적으로 개발해야 할 때 참조하기 바랍니다.

  • Don’t use Function.apply().
  • Don’t override noSuchMethod().
  • Avoid setting variables to null.
  • Be consistent with the types of arguments you pass into each function or method.

dart2js와 같이, 자동으로 소스 코드를 생성하는 경우는 항상 자동 생성된 코드의 품질에 걱정을 하게됩니다. 이에 대해서 Dart 공식 사이트에서는 자동 생성으로 포함 된 라이브러리의 크기에 대해 걱정하지 않아도 된다고 설명하고 있습니다. 이에 대한 이유를, dart2js 도구는 tree shaking 기법을 수행하여 사용하지 않는 클래스, 함수, 메소드 등을 생략한다고 합니다. 따라서, 개발자가 부담없이, 필요한 라이브러리를 가져 와서 프로그램을 만들면, dart2js가 필요없는 것을 제거하도록 한다고 안심 시키고 있습니다.

dart2js로 생성된 JavaScript 코드가 Web 브라우저에서 실행할 때, 이를 디버그 하고자 한다면, Web 브라우저의 개발자 모드에서 실행이 가능합니다. 이를 위한 Web 브라우저별 설정에 대해서는 Dart 공식 사이트의 dart2js 설명 부분을 참조( https://dart.dev/tools/dart2js#debugging )하기 바랍니다.

dartdevc은 Dart로 만든 Web 어플리케이션을 개발 및 디버그 목적으로 Chomer 브라우저를 통해서 수행하는 경우에 사용합니다.

dart2js와 달리 dartdevc는 incremental 컴파일(프로그램 전체를 컴파일 하는 방식이 아니고, 수정된 프로그램의 일부만 다시 컴파일하는 방식)을 지원하고 modular JavaScript를 생성합니다. dartdevc를 사용하는 webdev serve와 같은 도구를 사용하면, Dart 파일을 수정하고, Chrome을 새로 고침하면, 수정 사항을 거의 즉시 확인할 수 있습니다. 이 속도는 dartdevc가 Web 어플리케이션이 의존하는 모든 패키지가 아니라 업데이트 된 모듈 만 컴파일하기 때문에 가능합니다.

dartdevc를 사용한 첫 번째 컴파일은 전체 Web 어플리케이션을 컴파일 해야 하므로 가장 오래 걸립니다. 그 후, serve 명령이 계속 실행되는 한 dartdevc를 사용한 새로 고침 시간은 dart2js보다 훨씬 빠릅니다. 이러한 이유로 webdev serve의 개발/디버그 모드 동작 시에는 dartdevc를 사용하여 Dart 언어로 만들어진 프로그램을 실행 합니다.

마무리

이제 우리는 앞서 WebDev와 Stagehand를 설치하고 실행한다는 것이 어떤 의미인지를 보다 구체적으로 이해하게 되었습니다. Stagehand를 통해서는 새롭게 시작하는 프로젝트에 필요한 화일들을 자동으로 생성해주는 작업을 합니다. 이를 토대로 만든 Web 어플리케이션은 Web 브라우저와의 호환성으로 인하여 JavaScript로 변환하는 단계를 거치게 되는데, WebDev는 이를 위한 dart2js와 dartdevc 도구를 사용하여 Dart 언어 프로그램을 JavaScript 언어로 변경하고, Web 서버가 기본적으로 수행해야 하는 HTTP 서버의 기능을 수행하여 줍니다.

Creative Commons License (CC BY-NC-ND)

Dart Programmer 되기 [37]

< Web 개발 – Development Environment >

WebDev는 Dart로 Web 어플리케이션을 만들고 실행하는 CLI 도구입니다. 오픈소스 코드는 GitHub( https://github.com/dart-lang/webdev )에 공개 되어 있습니다. Dart와 Flutter로 개발된 패키지 소프트웨어들을 관리하는 pub.dev( https://pub.dev/packages/webdev )를 통해서도 확인이 가능합니다.

WebDev는 pub 명령을 통해서 설치가 이루어 집니다. Pub는 Dart/Flutter의 패키지 소프트웨어를 다루는 명령으로, (이 tutorial을 진행해 온 경우는) Dart SDK와 Flutter SDK를 설치하였기에, 이미 컴퓨터에 설치되어 있을 겁니다. 실행여부를 Teminal에서 pub를 실행하여 확인 바랍니다. Pub 명령에 대한 자세한 설명은 Dart 공식 홈페이지의 The pub tool 사이트( https://dart.dev/tools/pub/cmd )를 통해서 확인할 수 있습니다.

참고로 Dart SDK에 추가로 Flutter SDK가 설치된 경우는 “pub …” 명령의 수행이 아닌 “flutter pub …” 명령을 수행하도록 Dart 공식 홈페이지( https://dart.dev/tools/pub/cmd )에서 강조하고 있습니다.

설치는 간단합니다. 사실 설치가 아니고 활성화가 맞는 표현인데, 위에서 언급한 WebDev의 GitHub 혹은 pub.dev 사이트의 가이드라인에 맞춰서 install을 합니다. 아마도 다음과 같은 명령어 한 줄이 있을 겁니다. 실행 후 화면의 메시지에 따라서, 운영체제의 path 설정 등을 요구할 수 있으니, 이 부분까지 마치면 됩니다. 실행하는 부분에 대해서는 추후 설명하도록 합니다.

flutter pub global activate webdev

다음으로 설치할 도구는 Stagehand 입니다. 일명 “Dart project generator” 입니다. 앞서 MS Visual Code에서 처음 만드는 Flutter 프로젝트를 생성하면, 개발자가 집중해야 하는 부분 외의 화일들을 자동으로 만들어 줬습니다. Stagehand는 Web을 개발하기 위하여 필요한 화일들(template)을 자동으로 만들어 주는 역할을 수행하는 CLI 프로그램 입니다. Stagehand의 소스 코드는 GitHub( https://github.com/dart-lang/stagehand )에서 확인 가능합니다. 그리고 WebDev와 마찬가지로 pub.dev 사이트( https://pub.dev/packages/stagehand )에서 관리를 하는 패키지 입니다.  

설치는 pub 명령으로 WebDev 처럼 간단하게 이루어 지며, 아래의 명령을 실행하면 됩니다. WebDev와 마찬가지로 실행하는 부분에 대해서는 추후 설명하도록 합니다.

flutter pub global activate stagehand

Stagehand는 다양한 유형의 Web 어플리케이션에 대한 template을 지원하는데, 다음과 같습니다.

  • console-simple – 간단한 CLI 어플리케이션
  • console-full – CLI 어플리케이션 예제
  • package-simple – Dart 라이브러리 혹은 어플리케이션
  • server-shelf – Shelf 패키지 기반 Web Server
  • web-angular – Material 디자인 컴퍼넌트 기반 Web 어플리케이션
  • web-simple – Dart의 코어 라이브러리 기반 Web 어플리케이션
  • web-stagexl – 2D 애니메이션과 게임을 위한 Web 어플리케이션

이제 WebDev와 Stagehand가 제대로 설치되었는지, 확인해 보도록 하겠습니다. 먼저, 작업을 수행할 디렉토리로 이동한 후, 아래의 명령을 순차적으로 실행합니다.

stagehand web-simple
pub get
webdev serve

첫번째와 두번째 명령의 실행시 화면에 에러 메시지가 나오면, 이를 해결하기 위한 작업을 수행하기 바랍니다.

세번째 명령을 수행하면, [그림 1]과 같이 화면 출력이 이루어지고, 프로그램이 멈춰있는 것처럼 보입니다. 프로그램은 정상적으로 수행하고 있는 것이며, 앞서 Stagehand로 만든 예제 Web 사이트를 WebDev 명령이 실행하고 있는 상태입니다. 이때, 별도의 웹 브라우저를 구동하여, WebDev가 구동중인 웹 서버에 접속해 봅니다. 웹 브라우저에서 접속할 웹 주소는 [그림 1]의 세번째 줄에 있는 http://127.0.0.1:8080 입니다.

[그림 1] WebDev 명령 실행 화면

웹 브라우저에서 [그림 2]와 같이 화면이 나타나면, 두개의 프로그램은 성공적으로 설치된 것 입니다.

[그림 2] WebDev가 실행 중인 웹 서버에 웹 브라우저로 접속한 화면

실행 중인 웹 서버를 멈추고 싶으면, WebDev를 실행한 터미날에서 Ctrl-c를 타이핑 하면 됩니다.

무슨 일이 이루어 진 건가요? 사실 매우 간단합니다. 먼저, 작업 중인 디렉토리의 화일들 중 /web 서브 디렉토리로 이동합니다. 그리고 화일들을 보면, 4개의 화일이 있습니다. 우리가 선택한 옵션은 web-simple 입니다. 즉, Dart 언어로 만든 웹사이트 입니다. 따라서 main.dart 화일이 있으며, 열어보면 HTML 화일의 #output 식별자 부분의 문장(.text)을 “Your Dart app is running.”으로 바꾸라는 실행 구문이 있습니다. 그리고 Dart의 코어 문법만 사용하므로, import 되는 패키지는 dart:html 입니다. 혹시 JavaScript를 다룰줄 아는 개발자라면, 이 화일이 기존 JavaScript 화일을 대체하는 용도입니다. 그리고 웹 사이트의 가장 기초가 되는 index.html 메일 페이지가 있습니다. 열어보면, body 부분에 #output 식별자 하나만 정의하고, 내용은 빈칸임을 볼 수 있습니다. 그리고 head의 script에 main.dart.js를 포함하도록 되어 있습니다. 여기서 .js는 JavaScript를 의미하며, 추후 이에 대한 설명이 있을 겁니다. 마지막으로 기본 모양을 정의하는 css 화일이 있습니다. 그리고 웹 브라우저의 tab에서 이 사이트를 표현할 아이콘인 favicon이 있습니다.

Stagehand는 이렇게 웹 사이트의 가장 기초가 되는 화일들을 요구한 template에 맞춰서 만들어 줍니다. WebDev는 이렇게 만들어진 컨텐츠를 토대로 웹 서버를 구동하는 프로그램 입니다. 외부로 부터의 HTTP 요청을 받아 컨텐츠를 제공하는 기능을 제공합니다. WebDev가 있어서, 만든 콘텐츠를 별도의 웹서버 프로그램을 사용하지 않아도 편하게 실행해 볼 수 있습니다.

/web 디렉토리를 나와서 새로 만든 프로젝트의 root 디렉토리로 오면, 확장자가 .md인 화일들이 있으면, 이들은 Stagehand로 만든 화일이라는 태그와 라이센스, 그리고 변경 로그 등을 저장합니다. 그외의 .yaml과 .lock 화일은, 우리가 새롭게 만든 화일을 실행하기 위하여 필요한 프로그램들과 환경설정 등에 대한 내용을 포함합니다.

마무리

이번 글에서 Dart와 Flutter로 Web을 구현하기 위한 개발 환경은 마쳤습니다. 이제 Web 어플리케이션을 어떻게 Dart와 Flutter를 통해서 개발하는지 하나 하나 알아보도록 하겠습니다.

Creative Commons License (CC BY-NC-ND)

Dart Programmer 되기 [36]

< Web & Desktop 개발 – Introduction >

우리는 지금까지 Dart의 기본 문법과 기능들, 그리고 Flutter를 통한 모바일 프로그래밍에 대해서 알아 왔습니다. 그리고 다음으로 Dart/Flutter를 통한 Web과 Desktop 어플리케이션을 다루고 합니다. 같은 언어와 프레임워크로 다양한 분야를 지원할 수 있는 것은, Dart 언어의 가장 큰 장점 중 하나 이며, 앞서의 연재 글에서 주요 프로그래밍 언어 별로 지원하는 플랫폼에 대한 부분을 정리한 내용에서 세부적으로 다루었습니다.

앞으로 Web으로 지칭하는 것은, 일반적인 Web browser 류의 client가 Web server에 접속하여 다양한 컨텐츠와 정보를 제공받고, client의 요청에 따라 server에서 작업을 수행하는 형태의 서비스를 의미합니다. 앞서 Dart 언의 HTTP 기능에 대해서 설명하면서, client와 server를 HTTP 프로토콜을 사용하여 주고 받는 부분을 설명했는데, 지금부터 설명할 내용은 이렇게 주고 받는 (HTTP가 실어 나르는) 내용에 대한 부분에 집중한다고 보면 됩니다. 이 부분은 통상 HTML/CSS/JavaScript가 보편적으로 사용되는 영역으로, 해당 부분을 어떻게 Dart와 Flutter로 개발하는지가 앞으로의 핵심 주제 입니다.

다음으로 Desktop은 일반적인 데스크탑과 노트북 컴퓨터 상에서 MS Windows, macOS, Linux 운영체제를 기반으로 동작하는 프로그램을 의미합니다.

Dart/Flutter로 Web을 프로그래밍 하는 것은 지금까지의 내용에 대한 연장선상에 있습니다. 하지만 추가적으로 설치할 프로그램과 개념들에 대한 이해가 필요하므로, 남은 내용은 다음의 단계로 설명이 이루어질 예정입니다.

첫째로 Dart/Flutter로 Web을 개발하기 위한 개발 환경을 구축합니다. 우리가 앞으로 다룰 내용은 Web browser와 Web server 사이에서 주고 받는 컨텐츠 적인 측면을 주로 다룰 예정이므로, (비록 우리가 Dart를 통하여 이러한 컨텐츠를 네트워크에서 주고 받도록 하는 HTTP를 배웠지만) Dart/Flutter 언어에서 이런 부분을 용이하게 지원하는 개발용 웹 서버인 WebDev를 이해하고 설치합니다. 또한, 앞서 Flutter에서 개발에 필요한 화일들을 자동으로 생성하여 줌으로써, 개발자가 프로젝트를 용이하게 시작하도록 도와준 것처럼, Web을 개발하는데 있어 필요한 화일들을 자동으로 먼저 생성해 주는 도구로서 Stagehand를 이해하고 설치합니다.

둘째로 모든 프로그래밍 언어의 시작과 동일하게 Dart/Flutter를 토대로 Web 개발을 시작해 보는 “Hello, World!”를 만들어 봅니다. 이를 통해서, 가장 간단한 프로그램이지만, 완전한 프로그램을 만들기에 필요한 부분을 점검하게 됩니다.

셋째로 Dart/Flutter로 Web을 개발할 수 있는 세가지 접근 방법을 설명합니다. 이는 Dart for Web, AngularDart, 그리고 Flutter for Web 입니다. 각각의 방법은 장단점이 있으며, 이들 기술이 갖는 특징과 개발자의 역량에 맞춰서, 개발하고자 하는 프로그램에 맞는 방법을 선택하면 됩니다. 간단히 설명하면, Dart for Web은 Dart 언어의 built-in-features인 dart:html을 사용하는 방법입니다. HTML/CSS를 이해하는 개발자가, 이들을 직접 다루는 방법이라고 보면 됩니다. AngularDart는 기존의 Angular 혹은 AngularJS에 대한 지식이 있는 개발자가 선택할 수 있는 방법입니다. 마지막으로 Flutter for Web은 HTML/CSS에 대한 지식이 없어도, 지금까지 우리가 수행한 Flutter 기술을 그대로 사용하여 Web을 개방하는 방법 입니다. 세가지 방법에 대해서 전체적으로 다루겠고, 세가지 방법에 각각의 장단점이 있지만, 우리는 Dart/Flutter의 장점에 집중하자는 의미에서 세번째 방법인 Flutter for Web에 조금 더 방범을 두고 진행할 예정입니다.

넷째로 이미 우리가 살펴보았던 Dart 언어의 HTTP 기능을 사용한 Web server 개발에 대해서 다시 한번 살펴본다. 단 처음부터 모든 것을 개발자가 스스로 만들어야 하는 이 접근 방법이 아니고, HTTP server 들이 제공해야 하는 주요 기능을 이미 대부분 구현한 HTTP Framework를 사용할 것 입니다. Dart 언어의 인기가 올라가기 시작한 것이 최근의 일이지만, 이미 몇가지 훌륭한 오픈소스 HTTP Framework들이 존재하며, 이들을 통하여 개발자가 보다 수월하게 하지만 보다 완성도 있는 HTTP server를 만들도록 합니다.

다섯째로 Server 프로그래밍에서 일반화되고 있는 컨테이너 기술을 적용해 보는 방법을 설명하고자 합니다. 이를 통해서 Dart와 Flutter를 지원하는 공식(official) Docker 이미지를 살펴보고, 우리가 직접 만들 HTTP 서버를 Docker 상에서 돌려보는 경험을 갖게 됩니다. 이 부분을 위해서는 읽는 이가 Docker에 대한 기본 지식과 경험이 있어야 합니다.

마지막으로 Dart/Flutter for Desktop에서는 현재 공식 릴리즈가 나오지 않은 Flutter Desktop을 중심으로 설명합니다. 이 글을 쓰는 시점에서 Flutter for Desktop은 macOS에서 나름 완만한 동작을 하고 있으며, MS Windows 등에서는 미흡한 것으로 되어 있습니다. 하지만, 수 많은 사람들이 큰 관심을 기울이고 있고, 2020년에는 나름 의미 있는 소프트웨어가 출시될 예정이기에, 이 부분을 다루고자 합니다.

마무리

Dart와 Flutter의 능력을 확인하는 마지막 주제라고 생각합니다. Web의 경우는 개발자의 성향과 역량에 맞춰서 선택할 수 있는 세가지 길이 있으니, 장단점과 앞으로의 발전 방향을 보고 본인에게 맞는 길을 선택하기 바랍니다. Desktop은 개발자들의 흥미가 점차 낮아지는 영역이지만, 분명 필요로 하는 분야가 존재합니다. 아직은 Dart/Flutter가 경쟁력을 확보하고 있지는 않지만, Flutter의 발전 가능성을 높게 평가한다면, 꼭 경험해 보기 바랍니다.

Creative Commons License (CC BY-NC-ND)