[Flutter] 스나이퍼팩토리 36일차

KWANWOO·2023년 3월 15일
1
post-thumbnail

스나이퍼팩토리 플러터 36일차

36일차에서는 이미지와 비디오 등의파일을 저장할 수 있는 firebase_storage를 학습했고, 유저 프로필 사진을 등록하는 법을 배웠다. 추가로 예전에 만들었던 앱을 리팩토링 했다.

학습한 내용

  • firebase_storage
  • 유저 프로필 사진 등록
  • 리팩토링

추가 내용 정리

Firebase Storage

Firebase는 데이터 클라우드 저장 공간을 제공한다.

Firebase Storage는 이미지, 파일, 동영상 등 멀티미디어나 바이너리를 저장하여 사용할 수 있다.

  • 파일 업로드

    아래와 같이 경로를 설정하여 파일을 업로드할 수 있다. (ImagePicker 등을 사용하여 이미지를 불러온 뒤 파일을 저장하는 등의 경우에 사용 가능하다.)

final storageRef = FirebaseStorage.instance.ref("image/example.jpg");
await storageRef.putData(data);
  • 파일 다운로드

    파일 다운로드는 URL을 사용하여 처리할 수 있다.

final storageRef = FirebaseStorage.instance.ref("images/example.jpg");
var fileUrl = await storageRef.getDownloadUrl();

유저 프로필 사진 등록

Firebase의 유저 정보에는 프로필 사진을 포함하고 있다. 이를 등록하기 위해서는 아래와 같은 과정을 거친다.

  1. Firebase Storage에 프로필로 등록할 이미지를 저장한다.
    • 이 때 경로에 유저의 uid가 포함되도록 한다. 경로 예시) images/유저uId/profile.jpg
  2. getDownloadUrl()을 사용해 이미지의 URL을 가져온다.
  3. 해당 URL을 유저의 updatePhotoURL()을 사용해 등록한다.

이러한 과정으로 유저의 프로필을 등록할 수 있고, 유저의 정보에서 이를 가져와 출력할 수 있다.

아래는 유저의 프로필 사진을 등록하는 코드의 예시이다.

uploadUserImage() async {
	var picker = ImagePicker();
    var res = await picker.pickImage(source: ImageSource.gallery);
    if(res != null) {
    	//Firebase Storage 올리기
        var ref = FirebaseStorage.instance.ref('profile/$user.uid}');
        await ref.putFile(File(res.path));
        var downloadUrl = awiat ref.getDownloadURL();
        
        //유저 프로필 사진 등록
        user.updatePhotoURL(downloadUrl);
    }
}

리팩토링

소프트웨어 개발을 위해 코드를 작성할 때 여러명의 사람과 함께 작업을 한다. 따라서 협업을 하는 사람이 이해할 수 있는 코드를 작성하는 것이 중요하다.

리팩토링이란 이미 작성된 소스코드에서 구현된 일련의 행위들을 변경없이, 코드의 가독성과 유지보수성을 높이기 위해 내부 구조를 변경하는 것이다.

리팩토링의 목적

  • 소프트웨어 설계에서 질적 향상을 위해 리팩토링을 한다. 코드 중복을 제거하고, 수정 용이성을 향상 시킨다.
  • 가독성을 좋게 만들어 소프트웨어 이해도를 높인다.
  • 버그를 찾는데 도움이 될 수 있다.
  • 프로그램 개발 속도를 높일 수 있다.

리팩토링은 똑같은 내용이 반복되어 작성되었거나 새로운 기능을 추가하기 어려운 경우에 수행될 수 있다. 또한 코드 리뷰를 하면서 비효율적인 코드를 리팩토링할 수도 있다.

결론적으로 기능을 추가할 때는 기존 코드를 수정하지 않고, 기능과 테스트 코드만 추가하고, 리팩토링을 할 때는 기능을 추가하지 않는 것이 좋다.


36일차 과제

  1. 키오스크 앱 리팩토링

1. 키오스크 앱 리팩토링

2주차 주간과제로 만들었던 키오스크 앱의 코드 구성과 위젯 구성을 재배치하여 코드를 개선하는 리팩토링을 진행하고자 한다.

기존의 키오스크 앱의 코드는 아래 링크에서 확인할 수 있다.

[Flutter] 스나이퍼팩토리 2주차 주간평가 : 키오스크 앱 기반 작성

리팩토링

리팩토링이란, 소프트웨어를 보다 쉽게 이해할 수 있도록 만드는 작업이다.

  • 자신의 코드에서 부족했던 점을 개선한다.
  • 가독성과 유지 보수성을 높이는 목표를 가질 수 있도록 한다.

키오스크 앱 요구사항

기존의 키오스크 앱의 요구사항은 아래와 같다.

  • 음식을 누르면 주문 리스트에 담기는 키오스크앱을 만들어봅니다.
  • 음식이미지는 자유입니다.
  • 하단에 떠있는 버튼을 누르면 지금까지 주문된 주문 리스트를 초기화합니다.
  • 하단에 떠있는 버튼은 정중앙의 하단, 넓게 펴진 형태로 [ 초기화하기 ] 텍스트를 포함합니다.
  • 음식이 보여지는 것은 [갤러리] 형태로 보여지게 합니다.
  • 그 외 UI 디자인은 자유입니다.

리팩토링 적용 부분

기존의 코드는 main.dart 하나의 파일로 모든 코드를 작성하여 가독성이 떨어진다.

리팩토링을 진행하며 가장 중점을 둔 부분들은 아래와 같다.

  • MVC 패턴 적용 (기능 분류)
  • 불필요한 코드 제거
  • 커스텀 위젯 활용

이를 적용하여 프로젝트의 파일구조를 아래와 같이 설정했다.

lib
	ㄴ controller
    	ㄴ main_controller.dart
    ㄴ model
    	ㄴ food.dart
    ㄴ view
    	ㄴ page
        	ㄴ main_page.dart
        ㄴ widget
        	ㄴ food_tile.dart
	ㄴ main.dart

코드 작성

  • pubspec.yaml
dependencies:
  cupertino_icons: ^1.0.2
  flutter:
    sdk: flutter
  get: ^4.6.5
  
  flutter:
    assets:
    - assets/images/option_beer.png
    - assets/images/option_bokki.png
    - assets/images/option_kimbap.png
    - assets/images/option_omurice.png
    - assets/images/option_pork_cutlets.png
    - assets/images/option_ramen.png
    - assets/images/option_udon.png

pubspec.yaml에서 이미지 설정은 기존의 코드와 같고, 패키지에 get을 추가로 설치했다.

  • main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kiosk_app/controller/main_controller.dart';
import 'package:kiosk_app/view/page/main_page.dart';

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

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

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return GetMaterialApp(
      //컨트롤러 바인딩
      initialBinding: BindingsBuilder(() {
        Get.put(MainController());
      }),
      //페이지 라우트 설정
      getPages: [
        GetPage(name: MainPage.route, page: () => const MainPage()),
      ],
      initialRoute: MainPage.route,
    );
  }
}

main.dart는 기존의 모든 코드가 작성되어있는 형태에서 MainPage를 호출하는 코드로 변경했다.

해당 파일에서는 컨트롤러를 바인딩하고, 페이지 라우트를 설정한다. 이를 별도의 파일로 나눠 관리할 수도 있지만 보여지는 페이지가 하나이기 때문에 따로 나누지 않고 작성했다. 앱의 규모가 커진다면 util폴더에 appRoutesappPages 등의 파일로 따로 분류하는 것도 좋다.

  • lib/model/food.dart
// ignore_for_file: public_member_api_docs, sort_constructors_first
class Food {
  String name; //음식 이름
  String image; //음식 이미지

  Food({
    required this.name,
    required this.image,
  });
}

새로 생성한 Food모델은 음식의 정보를 가진다. 네트워크에서 JSON을 받아오지 않기 때문에 Food.fromMap은 따로 작성하지 않고, 기본 생성자만 만들었다.

  • lib/controller/main_controller.dart
import 'package:get/get.dart';
import 'package:kiosk_app/model/food.dart';

class MainController extends GetxController {
  //음식 리스트
  final List foodList = [
    Food(name: '떡볶이', image: 'assets/images/option_bokki.png'),
    Food(name: '맥주', image: 'assets/images/option_beer.png'),
    Food(name: '김밥', image: 'assets/images/option_kimbap.png'),
    Food(name: '오므라이스', image: 'assets/images/option_omurice.png'),
    Food(name: '돈까스', image: 'assets/images/option_pork_cutlets.png'),
    Food(name: '라면', image: 'assets/images/option_ramen.png'),
    Food(name: '우동', image: 'assets/images/option_udon.png'),
  ];

  RxList order = [].obs; //주문 리스트

  //주문 추가
  addOrder(String foodName) => order.add(foodName);

  //주문 초기화
  clearOrder() => order.clear();
}

MainController는 음식 리스트와 주문 리스트를 가지고 있다. 기존의 음식 리스트는 Map형태의 데이터였지만 이번에는 앞에서 작성한 Food 모델을 사용해 데이터를 저장했다.

주문 리스트는 변화를 관찰하기 위해 RxList 타입으로 생성했다.

주문을 추가하는 addOrder()와 주문을 초기화하는 clearOrder()메서드도 작성했다.

  • lib/view/widget/food_tile.dart
import 'package:flutter/material.dart';
import 'package:kiosk_app/model/food.dart';

class FoodTile extends StatelessWidget {
  const FoodTile({super.key, required this.food, required this.onTap});

  final Food food; //음식
  final VoidCallback onTap; //onTap 이벤트

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Card(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            //음식 이미지
            Expanded(
              child: Image(
                image: AssetImage(
                  food.image,
                ),
                width: double.infinity,
                fit: BoxFit.cover,
              ),
            ),
            //음식 이름
            Text(
              style: const TextStyle(
                color: Colors.black,
                fontWeight: FontWeight.bold,
              ),
              food.name,
            ),
            //담기 텍스트
            const Text(
              style: TextStyle(
                color: Colors.black,
              ),
              '[담기]',
            ),
          ],
        ),
      ),
    );
  }
}

FoodTile은 음식의 정보를 보여주는 타일이다. 기존에는 main.dart에서 모두 작성했지만 이번에는 커스텀 위젯으로 분류하였다.

음식 정보인 food와 현재 타일을 눌렀을 때 이벤트를 처리할 onTap을 전달받아 음식 정보를 그려준다.

  • lib/view/page/main_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kiosk_app/controller/main_controller.dart';
import 'package:kiosk_app/view/widget/food_tile.dart';

class MainPage extends GetView<MainController> {
  const MainPage({super.key});
  static const route = '/';

  
  Widget build(BuildContext context) {
    return Scaffold(
      //앱바
      appBar: AppBar(
        title: const Text('분식왕 이테디 주문하기'),
        centerTitle: true, //타이틀 글씨 가운데 정렬
        backgroundColor: Colors.transparent,
        foregroundColor: Colors.black,
        elevation: 0,
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              style: TextStyle(
                fontSize: 22,
                fontWeight: FontWeight.bold,
              ),
              '주문 리스트',
            ),
            //주문 리스트
            Obx(() => Text(controller.order.toString())),
            const SizedBox(height: 8),
            const Text(
              style: TextStyle(
                fontSize: 22,
                fontWeight: FontWeight.bold,
              ),
              '음식',
            ),
            Expanded(
              // 음식 리스트 그리드 뷰
              child: GridView.builder(
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 3,
                ),
                itemCount: controller.foodList.length,
                itemBuilder: (context, index) {
                  //음식 타일(눌렀을 때 주문 추가)
                  return FoodTile(
                    onTap: () =>
                        controller.addOrder(controller.foodList[index].name),
                    food: controller.foodList[index],
                  );
                },
              ),
            ),
          ],
        ),
      ),
      // 하단 버튼(눌렀을 때 주문 초기화)
      floatingActionButton: FloatingActionButton.extended(
        onPressed: controller.clearOrder,
        label: const Text('초기화하기'),
      ),
      // 하단 버튼 위치
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
    );
  }
}

MainPageStatefulWidget이 아닌 StatelessWidget으로 만들고 GetViewMainController를 가져왔다.

본문에서 기존의 주문 리스트를 Obx로 감싸 변화를 관찰해 그려주었고, 출력되는 값은 컨트롤러의 order를 가져왔다.

음식들은 기존의 GridView.countList.generate를 사용하는 코드가 불필요하다고 느껴 간단하게 GridView.builder로 변경했다.

그리드 뷰의 내부 요소들은 컨트롤러의 foodList를 가져와 그려주었고, 앞에서 만든 FoodTile을 사용했다. 타일을 누르면 주문에 추가되도록 메서드를 연결했다.

FAB은 기존과 같지만 onPressed 이벤트에 컨트롤러에서 작성한 주문 초기화 메서드를 연결했다.

결과

결과는 아래와 같이 기존의 키오스크 앱과 동일하지만 코드가 훨씬 가독성이 높아지고, 유지 보수가 수월하도록 기능이 분류되었다.


플러터 학습 강의는 끝

플러터의 학습 강의는 아마도 오늘이 끝이다.ㅠㅠ 36일차까지 많은 내용을 배웠고, 개발도 해보았다. 내일 부터는 월말 평가로 개인 앱 개발을 시작하게 된다.(금요일에 일별 과제가 하나 더 있다고는 함) 오늘은 간단하게 이미 만들었던 앱을 리팩토링 했는데 이전에 만든 코드가 가독성이 너무 안 좋았다고 느꼈다.ㅋㅋㅋㅋ 원래 추가 내용 정리로 firebase의 인증 과정이 어떤 방식으로 이루어지는지 정리하려고 했지만 생각 보다 내용이 좀 어려워서 이해를 하고 나중에 정리를 하는 것이 좋을 것 같다. 아 그리고 강의는 끝났지만 추가로 학습하는 내용과 프로젝트 진행 사항 등을 계속 블로그로 작성할 예정이다.

📄Reference

profile
관우로그

0개의 댓글

관련 채용 정보