본문 바로가기
회고

[TEAM 369] Flutter, SpringBoot, AWS로 효율적인 어플리케이션 구축하기 기본편

by 선의 2022. 11. 25.

효정언니가 정리해준 어플의 메인 화면

 

  졸업프로젝트를 기획하면서 여러가지 주제가 후보로 나왔지만, 그중에서도 우리 팀은 '냉장고 관리'를 테마로 삼기로 결정했다. 네이버 마이플레이스에서 영수증을 촬영하면 가게에 대한 정보를 인식해서 공인된 후기를 남길 수 있게 하는 기술에서 착안해서, 마트에서 식재료 구매 후 발급한 영수증과 쿠팡/마켓컬리 등 이커머스 거래내역 캡쳐 사진에서 식재료와 수량을 추출하는 OCR 기술을 탑재한 하이브리드 어플리케이션을 개발하기로 하였다.

 

  이 과정에서 약 1달의 아이디에이션 회의, 아이디어 디벨롭 회의, 멘토님과의 UX/UI 관련 피드백, 지도교수님과의 면담 등 우리의 아이템을 1년이라는 시간 안에 개발 및 사용자 테스트까지 할 수 있을지, 기술적으로 가능한 수준인지에 대한 면밀한 검토가 있었다. 나는 대개의 서류작성과 이메일 컨택, UX/UI에 대해 '기술적으로 가능한가'를 고려하는 포지션이었다.

 

  팀에서 효정언니가 전체적인 기획의 방향 정리(IA 작성 등)와 UX/UI의 사용자성 높이고, 지혜언니가 OCR 모델을 구축하는 역할을 맡았다. 나는 기존 교내 웹 개발 동아리의 프로젝트, 교외 해커톤, 다양한 프로젝트 수업 수강 경험을 살려 어플리케이션 기술 흐름의 전체적인 그림을 그려 개발 구조를 설계하고, 효율적인 프로젝트 개발을 위한 Git/GitHub 관리, 개발에 필요한 기술 스택의 문서 정리 등, 대개 일반적인 스타트업에서 CTO가 하는 일을 했다.

 

프로젝트 구조 설계하기

  생각보다 개발에 있어서 중요한 것은 '초기에 얼마나 설계를 잘 했는가'이다. 수정할 필요가 없는 ERD 구조, 프로젝트 흐름이 한눈에 들어오는 기술 구조도, 클라이언트와 서버(REST 서버와 ML 서버 포함)의 효율적인 연결을 위한 API 문서, DevOps와 MLOps 등, 생각보다 문서화하는 작업이 효율적이고 오차 없는 개발 프로세스를 만들어낸다.

 

  다른 팀원들이 OCR 모델을 구축하고 기획을 세부적으로 정리하는 동안, 나는 프로젝트의 큰 그림을 계속해서 만들어냈다. 우선 아래 구조도를 살펴보자.

 

  한 눈에 프로젝트의 구성에 대해서 알 수 있다. 그중에서도 서버의 흐름을 조금 더 살펴보면,

 

  한 눈에 서버의 CI/CD DevOps 구조를 볼 수 있다.

 

  마지막으로 ERD를 살펴보자.

  위 구조대로 스프링부트의 Entity를 매핑하고, 필요한 형태로 DTO를 만들어 깔끔한 CRUD API를 만들어 낼 수 있다.

 

  위처럼 구조적으로 프로젝트의 '큰 그림'을 어떻게 바라보고 접근할 것인가에 대한 정의가 확실하다면, 개발할 때도 추가적으로 협의하거나 회의할 사항 없이 기술적인 것들에만 집중할 수 있게 된다.

 

첫번째 기술적 어려움: Flutter

  Flutter는 구글에서 만든 하이브리드 어플리케이션으로, 안드로이드/iOS/MacOS/Window/Linux 등 거의 모든 범용적인 OS에서의 빌드가 가능한 프레임워크이다. Dart라는 구글에서 만든 언어를 기반으로 하기 때문에 새로운 언어, 새로운 프레임워크 모두 익혀야 할 필요가 있었다. 다행히 나는 3학년 1학기 인간컴퓨터상호작용 강의에서 플러터를 다루어 본 경험이 있었고, 미리 가이드라인 문서를 노션으로 작성해두었다. 아래 발췌한다.

 

Flutter Naver Blog Clone

Begin With…

export PATH="$PATH:`pwd`/flutter/bin"

References

macOS install

 

macOS install

How to install on macOS.

docs.flutter.dev

[Flutter] 위젯

 

[Flutter] 위젯

Flutter를 이용하여 앱을 개발해 봅시다. 이번 블로그 포스트에서는 Flutter에서 사용되는 위젯에 관해서 알아봅시다.

dev-yakuza.posstree.com

ListView class - widgets library - Dart API

 

ListView class - widgets library - Dart API

A scrollable list of widgets arranged linearly. ListView is the most commonly used scrolling widget. It displays its children one after another in the scroll direction. In the cross axis, the children are required to fill the ListView. If non-null, the ite

api.flutter.dev

Colors class - material library - Dart API

 

Colors class - material library - Dart API

Color and ColorSwatch constants which represent Material design's color palette. Instead of using an absolute color from these palettes, consider using Theme.of to obtain the local ThemeData.colorScheme, which defines the colors that most of the Material c

api.flutter.dev

Text class - widgets library - Dart API

 

Text class - widgets library - Dart API

A run of text with a single style. The Text widget displays a string of text with single style. The string might break across multiple lines or might all be displayed on the same line depending on the layout constraints. The style argument is optional. Whe

api.flutter.dev

widgets library - Dart API

 

widgets library - Dart API

The Flutter widgets framework. To use, import package:flutter/widgets.dart. See also: Classes AbsorbPointer A widget that absorbs pointers during hit testing. Accumulator Mutable wrapper of an integer that can be passed by reference to track a value across

api.flutter.dev

Icons class - material library - Dart API

 

Icons class - material library - Dart API

Identifiers for the supported Material Icons. Use with the Icon class to show specific icons. Icons are identified by their name as listed below, e.g. Icons.airplanemode_on. Search and find the perfect icon on the Google Fonts website. To use this class, m

api.flutter.dev

Subclass a class that extends StatelessWidget or StatefulWidget class

 

Subclass a class that extends StatelessWidget or StatefulWidget class

Is it possible to create a class that extends a class extending StatelessWidget or StatefulWidget. For example: class MyButton extends StatelessWidget { final String label; Button({this.label}); @

stackoverflow.com

Conventions

Use Gitmoji

[GIT] ⚡️ Gitmoji 사용법 정리 (+ 깃모지 툴 소개)

 

[GIT] ⚡️ Gitmoji 사용법 정리 (+ 깃모지 툴 소개)

Gitmoji 란? gitmoji란 git + emoji를 합쳐서 부르는 말로 emoji를 이용하여 commit message를 작성하는 tool이라고 보면 될 듯하다. 지금까지 그냥 글로만 커밋 메세지를 써왔겠지만, 메세지에 이모지(이모티

inpa.tistory.com

Main.dart

라우팅

import 'package:flutter/material.dart';
import 'package:naver_blog/pages/home.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: "/home",
      routes: {
        "/home": (context) => HomePage(),
      },
    );
  }
}

Home.dart : BottomNavigationBar를 커스텀하여 Routing

return Scaffold(
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        currentIndex: index,
        onTap: (x) {
          setState(() {
            index = x;
          });
        },
        elevation: 30.0,
        showUnselectedLabels: false,
        showSelectedLabels: false,
        unselectedItemColor: Colors.black,
        selectedItemColor: Colors.green[900],
        items: itemList
            .map((Items item) => BottomNavigationBarItem(
                  backgroundColor: Colors.white,
                  icon: Icon(item.iconData),
                  label: item.text,
                ))
            .toList(),
      ),
      body: _buildBody[index],
    );

Explore: 이웃새글 올라오는 페이지

AppBar

appBar: AppBar(
        centerTitle: false,
        backgroundColor: Colors.white,
        title: Text(
          "이웃 새글",
          style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black),
        ),
      ),

ListView

body: Container(
          color: Colors.grey[200],
          child: ListView.separated(
              padding: const EdgeInsets.all(8),
              itemCount: entries.length,
              itemBuilder: (BuildContext context, int index) {
                return Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Container(
                      // margin: const EdgeInsets.all(3.0),
                      alignment: Alignment.centerLeft,
                      height: 70,
                      color: Colors.white,
                      child: Container(
                          padding: const EdgeInsets.all(3),
                          child: Row(
                            // crossAxisAlignment: CrossAxisAlignment.start,
                            mainAxisSize: MainAxisSize.min,
                            mainAxisAlignment: MainAxisAlignment.start,
                            children: [
                              Container(
                                margin: const EdgeInsets.all(10.0),
                                child: const CircleAvatar(
                                  backgroundColor: Color(0xffE6E6E6),
                                  radius: 17,
                                  child: Icon(
                                    Icons.person,
                                    color: Color(0xffCCCCCC),
                                  ),
                                ),
                              ),
                              Container(
                                  margin: const EdgeInsets.all(10.0),
                                  child: const Text(
                                    "이웃1",
                                    textAlign: TextAlign.center,
                                    overflow: TextOverflow.ellipsis,
                                    style: TextStyle(
                                        fontSize: 15,
                                        fontWeight: FontWeight.bold),
                                  )),
                            ],
                          )),
                    ),
                    const Image(
                        image: NetworkImage(
                            '<https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg>'),
                        fit: BoxFit.fill),
                    /*
                    Container(
                        // margin: const EdgeInsets.all(3.0),
                        height: 250,
                        color: Colors.white,
                        alignment: Alignment.center,
                        child: const ),
                        */
                    Container(
                        // margin: const EdgeInsets.all(3.0),
                        height: 150,
                        alignment: Alignment.centerLeft,
                        padding: const EdgeInsets.all(10.0),
                        color: Colors.white,
                        child: const Text(
                          "Text Area",
                          overflow: TextOverflow.ellipsis,
                          style: TextStyle(
                              fontSize: 15, fontWeight: FontWeight.normal),
                        )),
                  ],
                );
              },
              separatorBuilder: (BuildContext context, int index) =>
                  const Divider()),
        )

Post: 글쓰기 페이지

import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter/src/widgets/text.dart' as title;

class Post extends StatefulWidget {
  const Post({super.key, required this.title});

  final String title;

  @override
  State<Post> createState() => _PostState();
}

class _PostState extends State<Post> {
  final TextEditingController _titleController = TextEditingController();
  final QuillController _quillController = QuillController.basic();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          centerTitle: false,
          backgroundColor: Colors.white,
          title: const title.Text(
            "글쓰기",
            style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black),
          ),
        ),
        body: Container(
            color: Colors.white,
            child: ListView(
              padding: const EdgeInsets.all(10),
              children: <Widget>[
                const SizedBox(height: 15),
                Padding(
                  padding: const EdgeInsets.all(20),
                  child: TextField(
                    controller: _titleController,
                    decoration: const InputDecoration(
                        border: InputBorder.none, hintText: "제목을 입력하세요"),
                  ),
                ),
                const SizedBox(height: 15),
                QuillToolbar.basic(controller: _quillController),
                const SizedBox(height: 30),
                Expanded(
                  child: QuillEditor.basic(
                      controller: _quillController, readOnly: false),
                )
              ],
            )));
  }
}

References

A blog app with Flutter Firebase Cloud Firestore

 

A blog app with Flutter Firebase Cloud Firestore

A blog app with Flutter Firebase Cloud Firestore

flutterawesome.com

https://github.com/doitduri/blog_app

 

GitHub - doitduri/blog_app: blog_app

blog_app. Contribute to doitduri/blog_app development by creating an account on GitHub.

github.com

TextEditingController class - widgets library - Dart API

 

TextEditingController class - widgets library - Dart API

A controller for an editable text field. Whenever the user modifies a text field with an associated TextEditingController, the text field updates value and the controller notifies its listeners. Listeners can then read the text and selection properties to

api.flutter.dev

flutter_quill | Flutter Package

 

flutter_quill | Flutter Package

A rich text editor supporting mobile and web (Demo App @ bulletjournal.us)

pub.dev

[Flutter] Textfield widget

 

[Flutter] Textfield widget

In this blog post, I will introduce how to use the TextField widget to get the user input in Flutter.

dev-yakuza.posstree.com

FlutterQuill - Rich Text Editor for Flutter

 

FlutterQuill — Rich Text Editor for Flutter

FlutterQuill is a rich text editor and a Quill component for Flutter.

singerdmx.medium.com

더 효율적으로 개발하기: 위젯 재사용을 할 수 있을까?

Flutter 커스텀 위젯 (Stateless, Stateful)

 

Flutter 커스텀 위젯 (Stateless, Stateful)

기본적으로 플러터 UI 는 ListView, Column, Text 등 기본 위젯을 이용해서 구성한다. 하지만, 어플리케이션의 구현이 복잡해 질수록 재사용 가능한 커스텀 위젯의 개발이 불가피하다. 이번 포스트에서

drogrammer.tistory.com

 

두번째 기술적 호기심: Spring 이외의 프레임워크

Naver Blog Clone Coding — Backend

🔥 Nest.js 찍먹하기

점프 투 파이썬

 

1.1 NestJS 소개

NestJS는 Node.js에 기반을 둔 웹 API 프레임워크로써 Express 또는 Fastify 프레임워크를 래핑하여 동작합니다. 기본으로 설치하면 Express를 사용합니다…

wikidocs.net

Git main/master branch 에러 해결

Git 에러: refs/heads/master 해결하기

 

Git 에러: refs/heads/master 해결하기

프로젝트 초기 셋팅을 위해서 깃 레포를 만들고 local에서 만든 폴더와 연결하는 과정에서 제목과 같은 에러가 떴다. git init -> git remote add origin 까지는 문제가 없는데 main 브랜치를 설정하는 과정

hazel-developer.tistory.com