ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Google Codelabs Flutter 앱 만들기 실습 1
    Flutter 2023. 8. 2. 22:23
    728x90
    반응형
    SMALL

    현재 플러터를 활용한 크로스 플랫폼 앱을 만들기 위해 플러터 앱 만드는 방법에 대해 많이 알아보고 있는 와중, 구글에서 제공해주는 코드랩스에 대해 알게 되었습니다. 코드랩스는 여러가지 실습 자료들을 상세히 알려주는 플랫폼입니다. 여기서 플러터 앱 만들기 튜토리얼을 따라해보면서 플러터 앱 만들 때 필요한 것들을 한 번 알아보도록 하겠습니다.

     

    1. 프로젝트 소개

    구글 코드랩스에서 소개하는 이번 프로젝트는 두 개의 영어 단어를 합쳐 새로운 영어 단어를 만들어주어 표시해주기도 하고, 이 단어들 중 마음에 드는 단어들을 저장하고 저장된 단어들을 따로 보여주는 간단한 프로젝트를 만드는 프로젝트입니다.

     

    2. Flutter 환경 설정

    코드랩스에서는 VSCode를 활용하여 플러터앱을 만듭니다. 저와 같은 경우 이미 안드로이드 스튜디오를 통해 플러터 개발 환경을 만들었으므로 이 부분은 넘어갔습니다.

    아래 글은 플러터 설치 방법입니다. (Mac M1 Pro)

     

    Flutter 설치 (Mac M1 Pro)

    https://beomseok37.tistory.com/129

     

    Flutter 설치 (Mac M1 Pro)

    크로스 플랫폼 앱을 만들어보기 위해 플러터를 사용해보기로 했습니다. 리액트 네이티브와 플러터 중 어떤 것을 사용할 지 고민을 많이 했는데, 그래도 안 써본 프레임워크를 사용해보는 것이

    beomseok37.tistory.com

     

    3. 새로운 프로젝트 생성

    새로운 플러터 프로젝트를 생성해줍니다.

    이 후 코드랩스에서는 analysis_options.yaml 파일에 들어가 린트를 설정해줍니다.

    린트란 플러터가 우리 프로젝트의 코드를 분석하는 엄격도를 조정해준다고 생각하면 됩니다.

    코드랩스에서 제시한 린트 규칙들은 다음과 같습니다.

    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
    • prefer_const_constructors: constant constructors 앞에 const를 붙여줘야 한다.
    • prefer_final_fields: 멤버 변수가 이 후에 변경되지 않을 경우 final로 선언되도록 한다. (실수로 값을 재할당해주는 것을 방지하고 컴파일러가 최적화하는데에 도움이 됩니다.)
    • use_key_in_widget_constructors: widget의 생성자에 key를 사용해주도록 한다. (public widget이 key를 제공하는 기능을 가지고 있다는 것을 표출하기에 좋은 방법이기 때문)
    • prefer_const_literals_to_create_immutables : @immutable 클래스에서 list, map, set을 전달 할 경우 const를 붙여준다.
    • prefer_const_constructors_in_immutables: @immutable 클래스에서 const로 생성자를 선언해 준다.
    • avoid_print : print 호출을 피한다.

    위의 린트 규칙을 추가해줄 경우 코드 작성에서 엄격도가 낮아지기 때문에 난이도는 낮아질 수 있다고 합니다. 이 린트 규칙은 언제든 수정 가능하다고 합니다. 실제 프로덕션 앱의 경우 이보다 더 엄격한 분석도구를 이용한다고 합니다.

     

    lib/main.dart 수정

    앞으로 진행을 하면서 모두 둘러볼 것들이기 때문에 조금씩 훑어보겠습니다.

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

    위와 같은 패키지 세 개를 import 해줍니다.

    english_words는 무작위 단어를 생성해주는 패키지입니다.

    flutter/material은 구글의 여러 UI 위젯들을 사용 할 수 있도록 해줍니다.

    provider는 상태값을 정의하고 이 상태값이 변경 됨에 따라 UI가 변경되도록 해줍니다.

     

    위의 패키지에 대한 의존성을 pubspec.yaml에 작성해줍니다.

    dependencies:
      provider: ^6.0.5
      english_words: ^4.0.0-0

    위의 의존성을 추가해주고 코드 편집기 우측 상단에 Pub get 버튼을 눌러 패키지를 프로젝트에 설치해줍니다.

     

    아래는 미리 제시된 코드입니다.

    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),
            ],
          ),
        );
      }
    }

    MyApp에서 전체 화면에 대한 코드를 작성해줍니다. 앱의 이름시각적인 요소들을 지정해줍니다. 여기서는 앱 전체 상태를 생성해주기도 합니다.

    MyAppState 클래스에서 앱의 상태를 정의해줍니다. ChangeNotifier 클래스를 상속 받아 구현합니다. 이를 통해 상태값의 변경 사항을 다른 위젯에 알릴 수 있습니다. 현재는 생성된 단어 하나를 저장하는 형태입니다.

    만들어진 상태는 ChangeNotifierProvider 를 통해 전체 앱에 제공됩니다. 이를 통해 다른 위젯이 상태값을 알 수 있습니다.

    MyHomePage는 UI 구성 요소들을 담고 있고 상태값을 사용하는 위젯으로 항상 최신 상태로 유지되도록 위젯에 변경 사항이 있을 때마다 자동으로 build() 메서드가 호출됩니다.

    watch 메서드를 통해 앱의 상태값 변경 사항을 추적합니다. 반환된 값을 통해 상태값에 접근 할 수 있습니다.

    Scaffold는 앱을 세 개의 큰 부분으로 나누어주는 위젯입니다.

     

     

    4. 버튼 추가

    새로운 단어를 생성해주는 버튼을 만들어보도록 하겠습니다.

    우선 화면에 새로운 버튼을 만들어보도록 하겠습니다.

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

    버튼 생성
    버튼 생성

    버튼의 기능은 새로운 단어를 화면에 출력해주는 것입니다. 이는 상태값이 변경되어야 하는 것이므로 상태값을 정의한 MyAppState 클래스에 상태값을 변경해주는 메서드를 추가해줍니다.

    class MyAppState extends ChangeNotifier {
      var currentWord = WordPair.random();
    
      void getNext(){
        currentWord = WordPair.random();
        notifyListeners();
      }
    }

    상태값에 새로운 단어를 할당해주고 notifyListeners() 메서드를 통해 MyAppState의 변경 사항을 추적하고 있는 위젯에 알림을 보냅니다.

    이 후 버튼의 onPressedgetNext() 메서드를 추가해줍니다.

    ElevatedButton(onPressed: (){
        appState.getNext();
    }, child: Text('Next'))

     

     

    5. 스타일 수정

    현재 앱은 제대로 보이지도 않고 왼쪽 위로 쏠려 있기 때문에 굉장히 보기 힘듭니다.

    그렇기 때문에 스타일을 수정해주어 랜덤 단어가 더 잘 보이도록 바꿔주겠습니다.

     

    5.1 위젯 추출

    현재 MyHomePage에서는 UI를 구성하는 하나의 목적 말고 랜덤 단어라는 상태값을 보여주는 다른 기능도 하고 있습니다. 랜덤 단어를 보여주는 Text 위젯을 분리하고 해당 위젯을 선언해서 사용하는 것이 나중 프로젝트의 복잡도를 생각했을 때 좋을 것입니다.

    또한, Text 위젯 내부에 전체 상태값인 appState가 들어가는 것보다는 필요한 항목에만 접근하도록 해주는 것이 좋습니다.

    @override
      Widget build(BuildContext context) {
        var appState = context.watch<MyAppState>();
        var pairWords = appState.pairWords;
    
            ...
            Text(pairWords.asLowerCase),
            ...

     

    이 후 이 Text 위젯을 따로 분리해줍니다.

    해당 Text위젯을 전체 선택해준 다음, 마우스 우클릭 -> refactor -> extract flutter widget 을 선택해준 다음 새로운 위젯 명을 작성해주면 위젯을 분리해줌과 동시에 해당 위젯 명으로 변경해줍니다.

        return Scaffold(
          body: Column(
            children: [
              Text('A random Data'),
              BigCard(pairWords: pairWords),
              ElevatedButton(onPressed: (){
                appState.getNext();
              }, child: Text('Next'))
            ],
          )
        );
        ...
    
    class BigCard extends StatelessWidget {
      const BigCard({
        super.key,
        required this.pairWords,
      });
    
      final WordPair pairWords;
    
      @override
      Widget build(BuildContext context) {
        return Text(pairWords.asLowerCase);
      }
    }

     

    이 후 BigCard 위젯의 Text 칸에서 Wrap with Padding을 선택하면 자동으로 Padding 위젯으로 감싸지게 됩니다. 이 후 Card 위젯으로도 감싸줍니다.

        ...
        return Card(
         child:Padding(
           padding: const EdgeInsets.all(20),
           child: Text(pairWords.asLowerCase),
         )
        );

     

    5.2 테마, 스타일

    좀 더 앱을 꾸며보도록 하겠습니다.

    @override
    Widget build(BuildContext context) {
      final theme = Theme.of(context);
      final style = theme.textTheme.displayMedium!.copyWith(
        color: theme.colorScheme.onPrimary
      );
    
      return Card(
        color: theme.colorScheme.primary,
        child:Padding(
          padding: const EdgeInsets.all(20),
          child: Text(pairWords.asLowerCase,style:style),
        )
      );
    }
    • Theme.of(context) 현재 앱의 테마 속성을 반환합니다. MyApp에서 정의한 theme 데이터가 담겨 있습니다. 여기서는 MyApp에서 정의한 colorScheme 속성을 사용했습니다. primary앱을 정의하는 가장 두드러진 색상입니다. ColorScheme에는 secondary, surface 등 다양한 속성이 있습니다.. (MyApp의 theme의 seedColor를 바꾸면 이 색상을 바꿀 수 있습니다.)
    • theme.textTheme 를 사용하여 앱의 글꼴 테마에 접근합니다. 이 클래스에는 bodyMedium( 중간 크기의 표준 텍스트용) 또는 caption(이미지 설명용), headlineLarge(큰 헤드라인용) 등의 멤버가 있습니다. displayMedium은 null일 수 있으므로 null 이 아님을 단언해줍니다. copyWith() 을 호출하면 정의된 변경사항이 포함된 텍스트 스타일이 복사되어 반환됩니다. 여기서는 텍스트의 색상만 변경하는데, 앱의 테마에 접근하여 앱의 기본 색상을 배경으로 할 때 글자색으로 사용하기 적합한 색인 "on"Primary를 가져옵니다. (primary 속성과 onPrimary 속성의 차이 -> 링크)

     

    5.3 접근성 개선

    flutter는 기본적으로 접근성을 개선합니다. Flutter 앱은 TalkBack, VoiceOver와 같은 스크린 리더 앱의 모든 텍스트와 대화형 요소들을 올바르게 표시합니다.

    현재 우리의 앱은 두 단어가 합쳐져 있는 상태로 이를 기계가 읽게 되면 두 단어를 합친 상태로 발음하기 때문에 이해하기 힘들 수 있습니다.

    그렇기 때문에 이 두 단어를 따로 분리한 형태를 Text 위젯의 semanticsLabel 속성에 저장을 해줍니다.

    return Card(
      color: theme.colorScheme.primary,
      child:Padding(
       padding: const EdgeInsets.all(20),
       child: Text(
         pairWords.asLowerCase,
         style:style,
         semanticsLabel: "${pairWords.first} ${pairWords.second}",
       ),
     )
    );

     

    5.4 정렬

    현재 앱은 왼쪽 위에 치우친 형태입니다. 이를 중앙에 배치해보도록 하겠습니다.

          ...
          body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('A random Data'),
              BigCard(pairWords: pairWords),
              ElevatedButton(onPressed: (){
                appState.getNext();
              }, child: Text('Next'))
            ],
          )

    Column 위젯 내부에서 mainAxisAlignment 속성에 MainAxisAlignment.center를 넣어주면 주 축인 세로의 가운데 정렬이 되는 것을 확인 할 수 있습니다.

    Column MainAxis 가운데 정렬
    Column MainAxis 가운데 정렬

    여기서 가로 축의 중앙으로 이동시키기 위해서 Column 위젯의 반대 축에 대한 속성인 crossAxisAlignment을 사용하면 될 줄 알았으나 flutter inspector를 통해 안 된다는 것을 알게 되었습니다.

     

    Android Studio 검색 창에 flutter inspector를 치면 아래와 같은 창이 나오는데 Flutter Inspector 왼쪽 위의 select widget 버튼(아래 이미지 빨간 박스 안의 버튼)을 클릭하면 해당 위젯이 차지하고 있는 영역을 보여주게 됩니다. Column과 같은 경우 가로로는 화면 전체를 할당하지 않고 있음을 확인 할 수 있습니다.

    Flutter Inspector로 살펴본 Column
    Flutter Inspector로 살펴본 Column

    그러므로 Column 자체를 가로의 중앙으로 이동시키는 방법이 많겠지만 여기서는 Center 위젯으로 Column을 감싸주었습니다.

    Center 위젯의 경우 화면 전체를 차지하므로 위젯을 중앙으로 위치시켜 줍니다.

    가운데 정렬된 모습
    가운데 정렬된 모습

     

    5.5 추가

    현재 UI에서 위의 "A random Data" 는 지워주는 것이 깔끔해 보입니다.

    또한, 랜덤 단어와 버튼 사이에 공백을 넣어주면 좋을 것 같습니다. 이를 반영해보도록 하겠습니다.

    여기서 저는 속성에 마진을 추가해서 공백을 넣는 것과 SizedBox 위젯을 사용하는 두 가지 방법을 사용해보도록 하겠습니다.

    // marin 속성 추가
    
        ...
        return Card(
          margin: EdgeInsets.fromLTRB(0,0,0,20),
          color: theme.colorScheme.primary,
          child:Padding(
           padding: const EdgeInsets.all(20),
           child: Text(pairWords.asLowerCase,style:style,semanticsLabel: "${pairWords.first} ${pairWords.second}",),
         )
        );

    margin 속성에 EdgeInsets.fromLTRB()의 인자로 왼쪽, 위, 오른쪽, 아래애 넣을 공백을 입력해주면 됩니다.

    // SizedBox 위젯 사용
    
        ...
        children: [
            BigCard(pairWords: pairWords),
            SizedBox(height: 20),
            ElevatedButton(onPressed: (){
              appState.getNext();
            }, child: Text('Next'))
         ],

    SizedBox 안에 공백을 넣고 싶은 공간의 크기를 설정해주면 됩니다.

    공백을 넣은 모습
    공백을 넣은 모습

     

     

     

     

    마무리

    오늘은 랜덤 단어를 계속해서 추가해주는 플러터 앱을 만들어보았습니다. 위젯에서는 항상 StatelessWidget은 고정적인 UI를 담당하고 StatefulWidget이 상태값을 가짐으로써 유동적인 UI를 담당한다고 생각했는데, 위의 방법처럼 ChangeNotifier를 정의하는 방법도 있다는 것을 알게 되었습니다. 모든 상태값 변경 이후 또 호출해야하는 메서드가 있다는 것이 귀찮기는 하지만, 또 다른 상태값을 가진 위젯을 만들어보면서 사용법을 익히는 것이 중요할 것 같습니다.

    생각보다 오래 걸려 두 번에 걸쳐 포스팅을 진행해보도록 하겠습니다.

     

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

    반응형
    LIST

    댓글

Designed by Tistory.