DAY 11 | Flutter 데모 앱 만들기: 목록 페이지 만들기

Pt J·4일 전
post-thumbnail

DAY 11 | Flutter 데모 앱 만들기: 목록 페이지 만들기

다음주에 있을 현직자 멘토링에 대한 안내를 받았다. 보니까 옆동네더라. 지난 번에 @보리스@다람이 강의를 들으러 간다고 하길래 @이안과 함께 갔던 바로 그곳. 수업 마친 이후 시간대에 진행되는데 어차피 집 가는 방향이랑 비슷해서 한 번 들려보자 하고 신청했는데, 같이 수업을 듣는 분들 중 대부분이 신청을 하신 모양이다. 다음주에 수업 마치고 같이 이동하면 좋을 것 같다.

난 아직 구체적인 방향성을 잡지 못했는데 그래도 아무튼 개발자의 범위 내에서 고민하게 될 테니 그룹 멘토링은 개발자 분과의 만남으로 신청했다. 토스 프론트엔드/웹 개발자와 카뱅 서비스/서버 개발자가 계시더라. 선착순 안에 늦지 않게 신청한 거길 바라며... 사실 디자인도 좀 배웠고 콘텐츠 디자인이나 제품 디자인은 어려웠지만 UI/UX 디자인은 그럭저럭 괜찮았어서 UIUX 분야도 살펴볼 만할 것 같긴 하지만 당분간은 개발 쪽에 좀 더 집중해 봐야지.

폰트를 적용해 보자

앱 개발 시 폰트를 지정하지 않으면 시스템 기본 폰트가 사용된다. 플랫폼마다 폰트가 달라지기도 하고, 폰트 크기 비율이 달라져 화면에 보여지는 배치가 의도치 않게 틀어질 수도 있다. 이를 방지하기 위해서 텍스트에 폰트를 지정해 주는 게 좋다.

폰트를 다운로드 받을 수 있는 곳은 다양하고, 대표적인 곳으로 Google Fonts가 있다. 폰트 라이선스를 포함한 다양한 정보를 살펴볼 수 있고, 원하는 조건으로 필터링하여 검색해 볼 수도 있다. 다운로드 받아서 사용할 수도 있고 webfont로 사용할 수도 있는데 각각 장단점이 있다.

다운로드한 폰트는 assets/fonts 등의 적절한 디렉토리에 모아놓고 pubspec.yaml 파일에 등록하여 사용한다. 그런데 사용하는 폰트가 많아질수록 앱 자체의 크기가 늘어난다. webfont로 사용할 경우에는 앱 크기를 작게 유지할 수 있지만 약간의 통신 딜레이가 있을 수 있다. 한 가지 방식을 고수하기 보다는 로딩 화면 및 주요 폰트 한두 개는 asset으로 추가하고 그 외에 더 필요하다면 webfont 사용하는 게 좋다.

pubspec.yaml

# 앞 부분 생략
flutter:

  uses-material-design: true

  assets:
    - assets/

  fonts:
    - family: Pretendard
      fonts:
        - asset: assets/fonts/Pretendard/Pretendard-Thin.otf
          weight: 100
        - asset: assets/fonts/Pretendard/Pretendard-ExtraLight.otf
          weight: 200
        - asset: assets/fonts/Pretendard/Pretendard-Light.otf
          weight: 300
        - asset: assets/fonts/Pretendard/Pretendard-Regular.otf
          weight: 400
        - asset: assets/fonts/Pretendard/Pretendard-Medium.otf
          weight: 500
        - asset: assets/fonts/Pretendard/Pretendard-SemiBold.otf
          weight: 600
        - asset: assets/fonts/Pretendard/Pretendard-Bold.otf
          weight: 700
        - asset: assets/fonts/Pretendard/Pretendard-ExtraBold.otf
          weight: 800
        - asset: assets/fonts/Pretendard/Pretendard-Black.otf
          weight: 900

특정 텍스트에 폰트를 적용하고자 한다면 pubspec.yaml 파일에 지정한 family 값을 fontFamily 에 대입한다.

Text(
  'My Perfect Workout Mate',
  style: TextStyle(
    fontSize: 16,
    color: Colors.white,
  	fontFamily: 'Pretendard', // THIS!
  ),
),

앱 전체의 기본 폰트를 적용하고자 한다면 themeThemeData 를 전달하여 적용할 수 있다.

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const LandingPage(),
      theme: ThemeData(
        fontFamily: 'Pretendard',
      ),
    );
  }
}

목록 페이지 작성하기

우리 프로젝트의 개발 규칙을 설정하고 가면 일관성을 유지하며 작업을 해 나갈 수 있다.

  • 페이지 widget의 이름은 Page 로 끝나도록 한다.
  • 모든 페이지 widget의 build() method는 Scaffold widget을 반환한다.
  • 모든 페이지 widget은 반드시 파일 분리를 한다.
  • 페이지 widget을 담는 파일의 이름은 해당 widget의 이름을 사용한다.
  • import 시 상대경로인지 절대경로인지는 개인 취향이지만 하나로 정하면 일관성 있게 작성한다.

이에 따라 프로젝트 구조를 잡아 보자. 이 구조가 최종 구조는 아니고, 프로젝트를 진행하며 구조를 개선해 나갈 것이다. 처음부터 구조를 명확하게 잡고 가지 않고 나중에 수정하는 건 내 성향에는 안 맞지만, 일단은 내 프로젝트 구성 방식을 고집하기보다는 이 방식도 경험해 봐야지.

lib 디렉토리 구조

lib
├── main.dart
├── landing_page.dart
└── workout_list_page.dart

lib/landing_page.dart 에는 지난 시간에 작성한 LandingPage widget을 옮겨 넣는다. 그리고 lib/workout_list_page.dart 을 새로 생성하여 구성해 보도록 하겠다.

이제 lib/main.dart 는 다음과 같이 단순해졌다.

lib/main.dart

import 'package:flutter/material.dart';
import 'landing_page.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const LandingPage(),
      theme: ThemeData(
        fontFamily: 'Pretendard',
      ),
    );
  }
}

앱 실행 흐름 차원에서는 LandingPage widget이 먼저 뜨고 버튼을 누르면 다음 화면으로 넘어가야 하지만, 우리는 아직 화면 넘기는 것을 배우지 않았으므로 임시로 처음부터 LandingPage widget이 아닌 WorkoutListPage widget을 실행시키도록 수정한 뒤 WorkoutListPage widget을 작성해 보도록 하겠다.

lib/main.dart

import 'package:flutter/material.dart';
import 'landing_page.dart';
import 'workout_list_page.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      //home: const LandingPage(),
      home: const WorkoutListPage(),
      theme: ThemeData(
        fontFamily: 'Pretendard',
      ),
    );
  }
}

lib/workout_list_page.dart

import 'package:flutter/material.dart';

class WorkoutListPage extends StatelessWidget {
  WorkoutListPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Center(child: Text('Workout List'))),
      body: Placeholder(),
    );
  }
}

목록의 항목 구성하기

목록은 ListView widget으로 작성하는 게 효과적이겠지만 일단 우리가 배운 Column widget으로 목록 페이지를 구성해 보도록 하겠다.

운동 목록의 역할을 하는 Column widget의 children으로는 각 운동 항목을 담는 widget을 넣는다. 운동 항목은 [사진][이름][시간]으로 구성되므로 Row widget을 사용하여 작성할 수 있다.

Row(
  children: [
    Image(image: AssetImage('assets/squat.png'),
    Text('1. 스쿼트'),
    Text('05:30'),
  ],
)

적절한 모양과 크기로 조정하면,

Row(
  spacing: 20,
  children: [
    Container(
      width: 100,
      height: 100,
      decoration: BoxDecoration(
        image: DecorationImage(
          image: AssetImage('assets/squat.png'),
        ),
        shape: BoxShape.circle,
      ),
    ),
    Text(
      '1. 스쿼트',
      style: TextStyle(fontSize: 20),
    ),
    Text(
      '05:30',
      style: TextStyle(fontSize: 20, color: Colors.blueAccent),
    ),
  ],
),

그리고 적절한 여백을 지정하면,

Row(
  spacing: 20,
  children: [
    Container(
      width: 100,
      height: 100,
      decoration: BoxDecoration(
        image: DecorationImage(
          image: AssetImage('assets/squat.png'),
        ),
        shape: BoxShape.circle,
      ),
    ),
    Expanded(
      child: Text(
        '1. 스쿼트',
        style: TextStyle(fontSize: 20),
      ),
    ),
    Text(
      '05:30',
      style: TextStyle(fontSize: 20, color: Colors.blueAccent),
    ),
  ],
),

다음과 같이 UI가 그려진 것을 확인할 수 있다.

하드코딩 제거하기

지금은 데이터가 하드코딩 되어 있지만 양이 늘어나면 일일이 작성하는 게 번거롭다. 따라서 적절한 collection object를 생성하여 반복 생성하는 편이 좋다.

List 를 사용할지 Map 을 사용할지 등등 적절한 방식으로 데이터를 작성한다. 수업에서는 이름, 사진, 시간을 담는 List 를 각각 작성하여 사용했는데 나는 List<Map<dynamic>> 하나에 다 집어넣었다.

Map 을 사용하는 편이 관련된 정보끼리 모여 있으니 응집도가 높고, 그래서 내용을 추가하거나 수정할 때 실수할 확률이 낮아져 무결성 차원에서도 이득이다. 그런데 운동 목록은 순서가 있는 목록이니 그것을 List로 묶었다. {'name': '스쿼트', 'image': 'squat.png', 'minutes': 30}, 와 같은 데이터를 담는 List 인 것이다.

기존에 작성했던 Row 를 우클릭하고 [Refactor > Extract Method] 를 선택하여 그것을 기반으로 helper function을 작성한다. collection object의 내용대로 Row widget을 반복적으로 생성하여 List 형태로 return 하는 거다.

List<Row> getWorkoutList() {
  List<Row> workoutRows = [];

  for (var i = 0; i < workoutList.length; i++) {
    String name = workoutList[i]['name'];
    String image = workoutList[i]['image'];
    int minutes = workoutList[i]['minutes'];

    workoutRows.add(
      Row(
        spacing: 20,
        children: [
          Container(
            width: 100,
            height: 100,
            decoration: BoxDecoration(
              image: DecorationImage(image: AssetImage('assets/$image')),
              shape: BoxShape.circle,
            ),
          ),
          Expanded(
            child: Text('${i + 1}. $name', style: TextStyle(fontSize: 20)),
          ),
          Text(
            '$minutes 분',
            style: TextStyle(fontSize: 20, color: Colors.blueAccent),
          )
        ],
      ),
    );
  }

  return workoutRows;

Row 를 refactor 한 것이므로 처음에 Row widget이 정의되어 있던 부분이 getWorkoutList() 로 대치되었다. 그런데 작성한 helper function는 Row 가 아닌 List<Row> 를 return하므로 children: [ getWorkoutList() ] 가 아니라 children: getWorkoutList() 로 수정한다.


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Center(child: Text('Workout List'))),
    body: Padding(
      padding: EdgeInsetsGeometry.all(20),
      child: Column(
        spacing: 20,
        children: getWorkoutList(),
      ),
    ),
  );
}

아직 화면을 넘어가는 크기를 가지고 있어 overflow 되는 widget에 대한 처리를 하지 않아 오류가 뜨지만 다음과 같이 목록은 잘 생성되는 것을 확인할 수 있다.

좀 더 공부해 보자

Flutter 공식문서의 architecture에 대한 설명을 읽어 보자. 서비스 개발은 배우지 않고 그냥 예제 따라해 보는 수준으로 해 본 게 전부라 이런 걸 익힐 필요가 있다. 제대로 공부하려면 양도 좀 되는 것 같고 강의실 분위기에서는 집중해서 읽긴 어려울 테니 주말에 읽어 봐야지. 얼마나 걸릴진 잘 모르겠다. 어떤 문서는 몇 페이지고 금방 읽고 어떤 문서는 한 페이지를 하루 종일 붙잡고 있기도 하는데 이건 공부해 본 적 없는 영역이라.

그리고 앱 기획에 대한 것도 좀 더 고민해 볼 필요가 있고. 바쁘다 바빠.

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

0개의 댓글