졸업프로젝트를 기획하면서 여러가지 주제가 후보로 나왔지만, 그중에서도 우리 팀은 '냉장고 관리'를 테마로 삼기로 결정했다. 네이버 마이플레이스에서 영수증을 촬영하면 가게에 대한 정보를 인식해서 공인된 후기를 남길 수 있게 하는 기술에서 착안해서, 마트에서 식재료 구매 후 발급한 영수증과 쿠팡/마켓컬리 등 이커머스 거래내역 캡쳐 사진에서 식재료와 수량을 추출하는 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
ListView class - widgets library - Dart API
Colors class - material library - Dart API
Text class - widgets library - Dart API
Icons class - material library - Dart API
Subclass a class that extends StatelessWidget or StatefulWidget class
Conventions
Use Gitmoji
[GIT] ⚡️ Gitmoji 사용법 정리 (+ 깃모지 툴 소개)
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
https://github.com/doitduri/blog_app
TextEditingController class - widgets library - Dart API
flutter_quill | Flutter Package
FlutterQuill - Rich Text Editor for Flutter
더 효율적으로 개발하기: 위젯 재사용을 할 수 있을까?
Flutter 커스텀 위젯 (Stateless, Stateful)
두번째 기술적 호기심: Spring 이외의 프레임워크
Naver Blog Clone Coding — Backend
🔥 Nest.js 찍먹하기
Git main/master branch 에러 해결
Git 에러: refs/heads/master 해결하기
'회고' 카테고리의 다른 글
2023 정보처리기사 3회 필기 합격 후기 / 4일 준비 (0) | 2023.07.19 |
---|---|
[CAPSTONE_TEAM369] ZEF 개발 트러블슈팅 로그 (0) | 2023.05.16 |
[이상청] 기상청 단기예보 API를 받아와서 처리하기 (0) | 2022.08.08 |
[이상청] OAuth2를 활용한 로그인, 세션 유지 방식의 실패 원인과 해결방안 (0) | 2022.08.07 |