DAY 09 | Flutter 실습을 시작해 보자

git 복습을 하는 동안 적당히 들으면서 자기소개서를 준비했다. 어떤 이야기를 담아야 할지 Gemini에게 물어보면서 작업했다. 이런 식으로 작성하면 되는지는 잘 모르겠지만 일단 초안을 작성해서 강사님이랑 잡코디님하고 얘기해 봐야지. 아무것도 없는 상태에서 얘기하는 것보다는 어설픈 초안이라도 들고 얘기하는 게 나을 테니 말이다.

Flutter 실습을 위한 준비

설치할 건 처음에 다 설치했고, 프로젝트를 생성해 보자. 그 전에 Flutter에 대해 간단히 다시 살펴보고 넘어가면...

[Flutter 개요]

  • 구글에서 제작하여 배포한 오픈소스 프레임워크
  • 크로스 플랫폼 개발 지원
    • 구글의 차세대 OS인 Fuchsia도 지원한다!
    • 모바일 중심으로 배우겠지만 단일 코드베이스로 다양하게 개발 가능
    • 전반적인 크로스플랫폼: 기초적인 기능은 호환률이 높지만 실무적인 수준에서는 70% 정도로 떨어진다.
    • 데스크톱 앱: 상용 애플리케이션을 만들 정도는 아니고 개인용 앱 정도는 만들 수 있다.
    • 웹: 성숙도가 낮은 편이라 일반적인 웹 프레임워크보다는 부족하지만 약간 불편하게 쓸 수는 있다.

[여담: 안드로이드 스튜디오의 가상머신]

구분에뮬레이터 (Android)시뮬레이터 (iOS 등)
동작 방식하드웨어 + OS 모두 가상화소프트웨어(OS)만 흉내
목적실제 기기를 완벽히 대체하여 테스트기기의 논리적 동작과 UI 프로토타입 확인
성능/속도하드웨어를 전부 흉내 내므로 상대적으로 리소스 소모가 큼컴퓨터의 리소스를 직접 사용하여 상대적으로 빠름
정확도실기기와 매우 유사하여 하드웨어 센서 등 검증에 유리실제 기기와 내부 구조가 달라 하드웨어적 요소 테스트는 제한됨

프로젝트 생성

Andorid Studio를 열고 [New Flutter Project] > [Flutter] > [Flutter SDK path] 설정을 하여 프로젝트를 생성한다. 이 때, path가 잘못 설정되면 다음과 같은 경고가 뜬다.

The folder specified as the Flutter SDK home does not exist.

프로젝트 이름 및 설명은 임의로 작성하면 되지만, 너무 대충 짓진 말고 의미 있는 내용을 담는 습관을 들이도록 하자.

프로젝트를 시작할 때는 새로 생성한 거든 온라인에 가져온 거든 Flutter 버전을 확인하고 config가 꼬이지 않았는지 한 번 빌드해 보는 게 좋다.

구조 살펴보기

❯ tree -aL 1
.
├── .dart_tool
├── .gitignore
├── .idea
├── .metadata
├── README.md # 프로젝트에 대한 설명
├── analysis_options.yaml # 정적 분석 옵션
├── android # android 네이티브 코드
├── build # 빌드 결과물
├── ios # iOS 네이티브 코드
├── lib # 우리가 Flutter 코드 작성할 곳
├── pubspec.lock # 하위 의존성까지 정확한 버전 고정 (직접 수정 X)
├── pubspec.yaml # 의존성 정보 (사용할 패키지 버전 범위 명시)
├── test # 테스트 코드
├── web # 웹 네이티브 코드
└── workout_tracker_2026.iml # 모듈 설정 파일

9 directories, 7 files

Dart는 depth가 깊어지기 쉬운데, lint rule에 따라 가독성 차이가 있다. 예전에는 콤마 기준으로 개행되게 설정되어 있었는데 지금은 라인 길이 기준으로 기본값이 바뀌었다더라. analysis_options.yaml 파일에서 lint rule을 수정할 수 있다.

analysis_options.yaml

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    prefer_single_quotes: true # Enable the prefer_single_quotes rule
    require_trailing_commas: true # Require trailing commas for functions, methods, etc.

나는 추가하는 김에 큰 따옴표 사용을 지양하고 작은 따옴표를 강제하는 옵션도 추가했다. 이게 섞여 있는 것도 은근 거슬리니 말이다.

데모 앱 뜯어보기

Flutter 앱은 main.dart 파일의 main() function으로 시작한다. main.dart 가 시작지점인 건 구성 파일에 그렇게 작성되어 있는 거지만 main() 가 시작인 건 정해진 규칙이다.

Dart 레벨에서 보면 StatelessWidget 을 extends한 MyApp class를 생성하여 runApp(const MyApp()); 한다. 이걸 Flutter 관점에서는 StatelessWidgetMyApp 위젯을 생성하여 실행한다고 본다.

Dart의 모든 것은 object였다. 그리고 Flutter의 대부분으 것(모든까지는 아니고)은 widget이다. 모든 widget은 근본적으로 불변이며, 변할 수 있는 state object를 별도로 결합할 수 있다.

UI=f(State)UI = f(State)

위젯(ff)에 데이터(StateState)를 집어넣으면 화면(UIUI)이 튀어나오는 구조
기존의 앱 개발은 데이터의 변화에 따라 개발자가 UI를 적절히 수정해 주는 코드를 작성해야 했지만, Flutter는 데이터가 변경되면 그 부분의 UI를 새로 그려 버린다. 이를 선언형 UI 라고 한다.
widget은 불변이고 state의 변화에 따라 새로 그릴 뿐이다.

Flutter 프로젝트의 UI 흐름은 기본적으로 다음과 같다.

MyAppMaterialAppMyHomepageScaffold

MyAppMyHompage 는 개발자가 작성한 widget으로, 원하는 이름을 설정할 수 있다. 이 녀석들은 반드시 StatefulWidget 혹은 StatelessWidget 을 extends 해야 하며, 이에 따라 Widget 을 반환하는 build() method를 작성해야 한다.

MyApp 은 앱의 기본 테마나 라우팅 설정 등을 초기화하므로 주로 StatelessWidget 을 사용한다. MyHomepage 가 무엇을 extends 할지는 이것이 가변 데이터를 포함하는지 여부에 따라 달라진다.

MaterialApp 은 Flutter가 제공하는 기본 Material Design 컴포넌트들을 사용할 수 있게 해주는 built-in widget이다. 구글 진영의 Material Design을 사용할 땐 이걸 쓰고, iOS의 Cupertino Design을 사용할 땐 CupertinoApp 을 사용한다. 이 녀석은 앱 전체를 감싸는 틀, 최상단 root widget이라고 할 수 있다.

Scaffold 도 Flutter의 built-in widget인데, 화면의 기본 뼈대를 잡아주는 역할을 한다. Scaffold 는 앱 page의 시작으로, 앱의 화면 개수만큼 고유한 Scaffold widget이 존재한다.

MaterialApp 이나 Scaffold 같은 Flutter SDK에서 제공하는 built-in widget은 extends 없이 그대로 생성하여 사용하면 되고, MyApp 이나 MyHomepage 같은 custom widget은 적절한 widget을 extends 하여 구현해야 한다.

[여담: runApp()MaterialApp widget을 직접 넣지 않고 MyApp widget을 거치는 것에 관하여]

runApp(const MaterialApp(...)) 와 같이 작성해도 작동은 한다. 하지만 MaterialApp widget을 build하는 MyApp widget을 사용하는 게 좋다.

우선, 코드 분리와 가독성 측면에서 이점이 있다.

main() function에는 직접적인 기능을 구현하기보다는 entry point 역할만 수행하도록 두는 편이 가독성이 좋다. 우리 코드에서는 MaterialApp widget에 home: HelloWorld() 만 전달했지만 프로젝트가 커지면 테마 설정, 라우팅, 전역 상태 관리 등 수많은 설정 코드가 들어갈 수 있다. 이 때, main() function에서 runApp(const MyApp()); 만 하지 않고 MaterialApp widget의 세부 설정 코드를 작성할 경우, main() function이 지나치게 복잡해진다.

그리고, hot reload 기능이 활용성 측면에서도 이점이 있다.

Flutter의 강력한 기능 중 하나인 hot reload는 build() function 내부의 변경 사항을 감지하여 화면에 바로 반영한다. 그런데 build() function을 거치지 않고 widget을 바로 전달했을 경우에는 hot reload 기능을 사용할 수 없게 된다. 기본 상태값을 유지하지 못하고 모든 상태를 초기화하는 hot restart만 가능만 사용할 수 있다.

구조적 확장성 측면에서도 이점이 있다.

프로젝트가 커져 앱 전체에서 사용하는 데이터나 상태를 관리하는 widget을 사용하게 될 경우, 코드가 분리되어 있는 편이 확장하기 용이하다. Provider, Bloc, Riverpod 등을 사용하게 되면 이들은 MaterialApp 을 감싸서 작성해야 하는데 runApp(const MaterialApp(...)) 로 구현되어 있다면 main() function이 점점 더 복잡해져 유지보수 하기 어려워진다.

[여담: hot reload와 hot restart]

기능핫 리로드 (Hot Reload)핫 리스타트 (Hot Restart)
상태 유지기존 상태 유지 (variable 값, navigation 유지)모든 상태 초기화 (처음부터 다시 시작)
속도매우 빠름 (약 1초 내외)빠름 (약 3~5초 내외)
실행 함수build() method만 재실행main(), initState() 부터 전체 재실행
단축키 (터미널)소문자 r 입력대문자 R 입력
용도위젯의 디자인 수정, 색상 변경, 간단한 UI 구조 변경에 적합앱의 상태를 완전히 초기화하고 처음부터 다시 실행할 때 full restart보다 빠르게 수행

native 설정이나 native 코드를 수정하거나 pubspec.yaml 파일을 수정했을 경우에는 앱을 완전히 끄고 재시작하는 full restart를 해야 한다.

코드를 직접 작성해 보자

텍스트 출력

데모 앱 코드를 다 지우고 Hello World 를 출력하는 앱 화면을 만들어 보자.

Android Studio에는 code snippet 기능이 있어 stless 라고 입력하고 엔터를 치면 StatelessWidget 을 extends 하는 코드를 자동완성 해준다. 정식 명칭은 Live Templates인 모양이다.

StatefulWidget 을 extends 하는 HelloWorld widget을 생성하고 Scaffold widget을 반환하도록 작성한다. 이 Scaffold widget의 bodyText widget을 담은 Center widget을 넣으면 원하는 텍스트를 중앙 정렬이 되게 출력할 수 있다.

lib/main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HelloWorld(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Text('Hello World'),
      ),
    );
  }
}

네트워크 너머의 이미지 출력

먼저 로컬에 있는 asset 이미지가 아닌 URL로 접근하는 network 이미지를 출력해 보도록 하겠다. 둘 다 장단점은 있다. asset 이미지는 빠르고 안정적이지만 많아지면 앱 크기가 커지고 network 이미지는 통신이 늘어나지만 앱 크기는 작게 유지할 수 있다. 로고 같은 건 asset 이미지로 담고 저장 공간을 많이 차지하는 건 network 이미지로 하는 등 유동적으로 사용할 수 있다.

network 이미지를 출력할 땐 NetworkImage widget을 Image widget에 전달하여 사용한다.

lib/main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HelloWorld(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        // child: Text('Hello World'),
        child: Image(image: NetworkImage('https://images.unsplash.com/photo-1609377375724-8fadc82cd50e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NTB8fHJ1bm5pbmd8ZW58MHx8MHx8fDA%3D')),
      ),
    );
  }
}

앱 내부의 이미지 출력

네트워크 너머의 사진을 사용하지 않고 앱 내부에 이미지를 저장해 놓으려면 pubspec.yaml 파일을 수정해야 한다. 여기에 assets 디렉토리를 추가해 놓아도 되고 구체적인 이미지 파일명을 지정해 놓아도 된다. 이 때, 디렉토리를 추가할 경우 하위 디렉토리에 있는 리소스는 포함되지 않고 해당 계층에 있는 것만 포함된다. 디렉토리 이름을 적어놓을 경우 뒤에 / 를 붙여 디렉토리임을 명시하도록 하자.

assets 디렉토리는 말 그대로 assets 라는 이름을 사용하는 게 암묵적인 규칙이다. assets을 추가하고 나면 hot reload가 아닌 rebuild를 해야 제대로 불러올 수 있다.

pubspec.yaml

# 앞 부분 생략

flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg
  assets:
    - assets/

나는 Gemini가 내 그림을 리터칭해주었던 것을 넣어 보았다.

lib/main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HelloWorld(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        // child: Text('Hello World'),
        // child: Image(image: NetworkImage('https://images.unsplash.com/photo-1609377375724-8fadc82cd50e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NTB8fHJ1bm5pbmd8ZW58MHx8MHx8fDA%3D')),
        child: Image(image: AssetImage('assets/IMG_2119.JPG')),
      ),
    );
  }
}

실습 시간

오늘은 Hello World 수준의 가벼운 내용을 다루어 실습은 따로 하지 않았다. 대신 이력서, 자기소개서, 포트폴리오 초안을 검토했다. 전부 v0.1의 아주 날 것 그대로의 draft다. 주변에 일경험, 일자리 사업을 제외한 본격적인 취업 준비를 해 본 사람이 별로 없고 내가 그나마 선발 주자 같은 느낌이라 도움을 받을 곳은 SeSAC의 강사님과 잡코디님뿐이더라. 사회인들은 당장은 무직 상태인 경우에도 대체로 경력이 몇 년 정도는 되는 반면 우리 같은 고립은둔 출신 청년들은 서른 언저리에서야 아둥바둥 하고 있으니.

그래도 고립 상태에서 은둔 상태로 파고 내려가기 전에 오픈소스 컨트리뷰션 아카데미 같은 활동을 했던 게 있어서 뭐라도 적을 건 있더라. 대학생 때도 친목 집단은 없어도 멘토링이나 스터디 같은 건 있었고. 은둔 상태의 몇 년도 요즘 워낙 취업 시장이 어려워서 공백기는 다들 있으니까 크게 문제되지 않을 것 같다. 서울시 지원사업 참여하면서는 3년 차쯤 되었을 때부터 이것저것 연계받아 서울시 기술교육원도 수료하고 SeSAC도 고립은둔청년 전형으로 가산점 받고 들어올 수 있었고 그렇게 꾸준히 뭔가 해 나가기 시작했으니까.

일단은 초안이니까 디테일을 잡기보다는 내용을 채워 넣는 걸 중점으로 했다. 처음부터 문장을 다듬고 완성도 높게 가려고 하면 초안부터가 안 만들어지니까. 주변에 레퍼런스 삼을 만한 것도 없고 검색해 봐도 잘 모르겠고 해서 어떤 톤으로 어떻게 표현해야 할지는 뒤로 미뤄 두었다. 그랬더니 문장이 너무 딱딱하고 전문 용어가 많이 들어가 있는 글이 되었다.

문학 소설을 어려워하고 인문 교양서나 기술 문서 같은 걸 주로 읽는 게 나의 문체에 영향을 많이 줬을 것 같기도 하고. 초안이라 AI의 도움을 많이 받았기에 내 표현보다는 AI의 표현이 더 많은 것도 한몫 했을 거고. 좀 더 가볍고 부드럽고 인간적인 톤을 사용하는 건 역시 잘 되지 않는다. 공적인 글이라고 생각해서 더 그런가.

일단 넣을 수 있는 건 다 넣어보자는 식으로 끌어모아 적다 보니 특별히 "이 경험에서는 이걸 강조해야지!" 하고 목적이 있는 주장을 하기보다는 그냥 내용을 채워 넣은 것뿐이라 읽는 사람 입장에서 "그래서 무슨 말을 하고 싶은 거지?" 하게 되는 것도 있는 것 같다. 그걸 통해 어떤 걸 보여주고 싶은지를 고민하며 보완할 필요가 있다. 지금은 그냥 사실을 나열했을 뿐, 정보가 되지 못한 자료 같은 느낌?

조만간 좀 더 다등고 잡코디님 피드백도 받아봐야지...ㅎ

profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다 (지금은 학생 때 하던 거 아무거나 공부하고 있고요, 취업시켜 주시면 그 분야로 공부할게요)

0개의 댓글