데브콘 활동 후기

Go To Learn, 첫번째 (Go To Learn 1기 - 한대호)

K-DEVCON 2024. 5. 9. 20:54
해당 글은 Go to Learn 1기 한대호님께서 작성하신 후기이며, 허락하에 게시하였습니다.
원글 :  https://medium.com/@eoho15/goto-run-%EC%B2%AB%EB%B2%88%EC%A7%B8-fb530795ecb9

 

K-DEVCON에서 주최한 Flutter 멘토링 프로그램에 참여하게 되었습니다.

신청계기

저는 Flutter를 처음 접한것도 아니고 1–2년간 Flutter 개발을 했습니다.

앱 개발을 하면서 단순히 기능을 구현하고 만족을 하는 모습이 잦아지고, 좋은 코드나 안정적인 코드에 대한 관심도 줄어드는것 같다는 생각이 들었습니다. 순전히 본인이 해결해야할 일이지만, 이번 멘토링을 통해 이러한 부족한 점들이나 기본기를 쌓아가고 싶다는 생각에 신청을 하게 되었습니다.

무엇을 하는지

킥오프 시간에 쿠로곰님이 앞으로 한달간 어떻게 진행할지에 대한 전체적인 얘기를 해주셨습니다.

우선, 과제는 Google CodeLabs를 Github이나 블로그를 통해 제출하고 이에 대한 얘기를 매주 목요일 22:00 에 진행한다는 것

그 외 다른 개발적인 궁금증이나 평소 관심있어하는 개발에 대한 궁금증 및 얘기를 통해 진행된다고 했습니다.

첫번째 과제 시작~~~~~합니다

아래 링크에 있는 코드랩 첫 번째 Flutter 앱에 대한 과제입니다.

https://codelabs.developers.google.com/codelabs/flutter-codelab-first?hl=ko#0

Flutter 설치

첫번째 앱에 대한 소개 답게, Flutter를 설치하는 방법부터 차근차근 나와있습니다.

현재 VS Code를 이용해 개발 중인데 다른 편집기를 사용해도 되지만, VS Code를 이용한 개발을 권하고 있습니다.

웹 개발에 대한 이야기도 나옵니다.

“웹에서는 hot reload를 사용할 수 없다.”

VSCode 및 Flutter를 설치하는 방법에 대해서는 정리 내용이 많아 설치 부분은 생략하겠습니다.

프로젝트 생성

VSCode 및 Flutter SDK를 설치했다면 터미널에서

flutter create (프로젝트 이름)

을 입력 후 Enter를 클릭합니다.

프로젝트 설정

아래와 같이 기본 플러터 프로젝트 코드가 나타납니다.

코드랩을 실행 전 개발을 위해 필요한 설정을 해보겠습니다.

우선 analysis_options.yaml 파일을 수정합니다.

생각해보니 이때까지 개발하면서 pubspec.yaml 파일에 코드 수정은 많이 해보았는데 해당 파일은 거의 신경을 쓰지 않았던것 같습니다.

해당 파일을 구글링하고 공식문서를 찾아보았습니다.

간단히 요약하면, Dart코드의 정적 분석 설정을 구성하는데 사용되고, 코드 실행 전에 문제를 찾고 잠재적인 버그 예방에 도움을 주는 파일이라고 합니다. CodeLab에서 정의된 내용대로 수정을 하겠습니다.

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    prefer_const_constructors: false
    prefer_final_fields: false
    use_key_in_widget_constructors: false
    prefer_const_literals_to_create_immutables: false
    prefer_const_constructors_in_immutables: false
    avoid_print: false

pubspec.yaml

Flutter 프로젝트의 의존성과 관련된 파일입니다.

앞으로 라이브러리 및 프로젝트의 버전 등과 같이 중요한 내용이 들어가 있습니다. 아래에서 코드랩에서 사용되는 상태관리인 Provider, english_words를 추가합니다. 코드를 추가한 후 flutter pub get을 합니다.

name: flutter_codelab_1
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: '>=3.3.0 <4.0.0'

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.6

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^3.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
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

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

main.dart 파일 코드 변경

코드랩에서 기본적으로 주어진 아래 코드를 main.dart에 넣습니다.

여기서 필요한 개념은 Provider를 사용하는 방법입니다.

MyAppState를 ChangeNotifier로 감싸 전역상태로 관리합니다.

또한 이를 사용하기 위해 MaterialApp을 ChangeNotifierProvider 상위에 위치시켜 어디서든 사용할 수 있도록 합니다.

현재 UI는 Column > Text 밖에 없으므로 거의 빈 화면이 나타납니다.

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    return Scaffold(
      body: Column(
        children: [
          Text('A random idea:'),
          Text(appState.current.asLowerCase),
        ],
      ),
    );
  }
}
실행화면

버튼추가 및 클릭시 랜덤 Text생성

아래 원하는 요구사항이 있다고 가정해보겠습니다.

  • 우선 UI를 신경쓰지 말 것
  • Next 버튼을 클릭 시 랜덤한 단어로 바뀔 것

이때 위의 요구사항을 위해 해야할 것은 아래와 같습니다.

  1. Next 버튼 만들기
  2. 클릭 시 이벤트 코드 작성

우선 1번 부터 하면, Flutter에는 버튼을 만드는 여러가지의 방법이 있지만 가장 간단한 방법은 ElevatedButton Widget을 사용하는 것입니다.

아래와 같이 버튼 UI를 만듭니다. 단 여기서 클릭 시 이벤트인 onPressed 의 코드는 비어있습니다.

 return Scaffold(
      body: Column(
        children: [
          Text('A random idea:'),
          Text(appState.current.asLowerCase),
          ElevatedButton(
            onPressed: () {},
            child: Text("Next"),
          ),
        ],
      ),
    );

어떻게 랜덤한 영어 단어를 만들까?

앞서 main.dart 파일에 작성한 코드 중 MyAppState 클래스에서 가능합니다.


class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }
}

위의 함수를 추가하여 랜덤한 current를 생성하고, notifyListeners(); 를 통해 상태의 변화를 알립니다.

아래 코드에서 context.watch<MyAppState>(); 를 통해 상태변화에 대한 감지를 하고 있습니다. 이를 appState라는 변수에 담고, 아래 Text에 MyAppState에 정의된 current를 Text로 나타내고 있습니다.

여기서 저희는 버튼클릭시 다음 랜덤한 단어를 나타내고 싶은 것이므로

ElevatedButton의 onPressed에 MyAppState에서 정의한 함수를 넣습니다.


class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    return Scaffold(
      body: Column(
        children: [
          Text('A random idea:'),
          Text(appState.current.asLowerCase),
          ElevatedButton(
            onPressed: () {
              // 추가된 코드
                appState.getNext();
              },
            child: Text("Next"),
          ),
        ],
      ),
    );
  }
}
클릭 시 랜덤한 영어단어가 나타납니다

더 멋진 앱 만들기

위에서 요구사항인 버튼 클릭 시 랜덤한 영어단어가 생성되었습니다.

하지만 이대로 끝내기엔 UI가 아쉬운것 같습니다.

CodeLab에 나와있는대로 더 멋진 UI를 만들어보겠습니다.

무려 코드랩에서는 아래와 같은 멋진 UI를 만들 수 있도록 해줍니다!

기존 코드를 보면 아래와 같은 코드가 있습니다.

아래 코드에서 수정할 부분은 appState.current.asLowerCase 입니다.

위의 부분을 왜 수정해야할 필요가 있나? 싶었지만 다음과 같은 설명을 합니다. 코드를 변경함으로써 Text위젯이 더 이상 appState를 참고하지 않게 된다고 합니다. 이후 해당 코드를 BigCard라는 위젯으로 분리합니다.

UI의 개별 논리적 부분을 위한 별도의 위젯을 갖는 것은 Flutter에서 복잡성을 관리하는 중요한 방법입니다.


class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    // 추가된 코드
    var pair = appState.current;
    return Scaffold(
      body: Column(
        children: [
          Text('A random idea:'),
         // 추가된 코드 
         // Text(pair.asLowerCase),
          Text(appState.current.asLowerCase),
          ElevatedButton(
            onPressed: () {},
            child: Text("Next"),
          ),
        ],
      ),
    );
  }
}



class BigCard extends StatelessWidget {
  const BigCard({
    super.key,
    required this.appState,
  });

  final MyAppState appState;

  @override
  Widget build(BuildContext context) {
    return Text(appState.current.asLowerCase);
  }
}

카드 UI만들기

위젯을 분리한 뒤에, Card Widget으로 감싸고 색과 Padding을 적용합니다.

여기서 색을 지정하는 방식은 Theme을 이용하는 방법을 사용합니다.

Card위젯에 아래 코드를 추가한 후, Card에 적용합니다.

또한 전체 theme에 대한 설정도 해줘야하는데 이는 MaterailApp의 속성중 theme 에서 설정해야 합니다. 물론 해당 코드에 대한 설명도 자세하게 나타내어 줍니다.

displayMedium은 왜 null이 될 수 없는지도 알아봐야겠습니다,

// Card Widget에 추가될 코드
final theme = Theme.of(context);
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );
// MAterialApp에 추가될 코드
  theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        ),

위의 과정을 거치면 비교적 깔끔해진 UI가 나타나게 됩니다.

기능추가

하지만 위의 기능만으로는 부족합니다. 지나가던 코드랩🐽이 새로운 요구사항을 들고 왔습니다.

요새 좋아요가 유행이라는데, 좋아요 기능도 넣추가해주세요~ 🙋‍♀️

좋아용~ 기능을 추가해봅시다.

우선 버튼이 있어야겠죠.

Row위젯에 Like Icon과 Next Button을 배치합니다.

여기서 MyHomePage는 StatelessWidget으로 되어있습니다.

사용자가 원하는 것은 좋아요를 누르면 빈 하트가 채워지는 것인데 아래 코드대로 실행하게 되면 채워져있는 하트(또는 비어져있는)밖에 나타내지 못합니다. 보통 저는 토글을 구현할 때 Stateful을 이용하여 구현하였는데 코드랩에서는 Provider를 통해 Stateful로 변환하지 않고 구현하였습니다.

 return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text('A random idea:'),
            SizedBox(height: 10),
            BigCard(
              pair: appState,
            ),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                ElevatedButton.icon(
                  onPressed: () {},
                  icon: Icon(Icons.favorite),
                  label: Text('Like'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
변하지 않는 하트..

하트 클릭시 색이 변하게 만들기🧚‍♀️

아무리 눌러도 변하지 않는 하트🥹

어떻게 변하게 할지 알아봅시다.

코드랩에서는 이러한 코드를 비즈니스 로직이라고 부릅니다.

우선 우리는 좋아요를 눌렀을 때 아래와 같은 기능을 원합니다.

  • 누른 좋아요를 List에 담기
  • 버튼 클릭 시 UI 변경

위의 요구사항을 구현하기 위해 다음과 같은 코드를 MyAppState 클래스에 작성합니다.

// 우선, <WordPair> 형태의 빈 리스트를 만듭니다.
var favorites = <WordPair>[];
// 눌렀을 때에 이벤트를 설정합니다.
void toggleFavorite(){
 // 담겨져 있다면 제거
  if(favorites.contains(current)) {
    favorites.remove(current);
  } else {
   // 리스트에 없다면 추가
    favorites.add(current);
  }
  // 변경사항 알림, Watch를 통해 변경된 사항을 알 수 있습니다.
    notifyListeners();
}

MyHomePage 클래스 수정

위에서 비즈니스 로직을 작성하였으니 UI를 바꿀 차례입니다.

우선 아래 코드에서 저희는 context.watch<MyAppState>(); 를 통해 MyAppState클래스의 상태를 계속 주시하고 있습니다.


class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    IconData icon;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text('A random idea:'),
            SizedBox(height: 10),
            BigCard(
              pair: appState,
            ),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                ElevatedButton.icon(
                  onPressed: () {
                    appState.toggleFavorite();
                  },
                  icon: Icon(Icons.favorite_border),
                  label: Text('Like'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

여기서 랜덤으로 생성되는 current의 변화에 대해 더 집중하는 코드를 작성합니다. 그리고 작성한 toggle함수를 사용합니다. 함수를 작성하고 ElevatedButton 이벤트에 toggle함수를 적용시킵니다! 이렇게 말이죠


class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;
    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text('A random idea:'),
            SizedBox(height: 10),
            BigCard(
              pair: appState,
            ),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                ElevatedButton.icon(
                  onPressed: () {
                    appState.toggleFavorite();
                  },
                  icon: Icon(icon),
                  label: Text('Like'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

위의 결과로 하트의 색이 변하게 됩니다 🥳

탐색레일추가

위의 화면은 단일 페이지였는데요, 좀 더 역동적인 화면을 만들어봅시다.