이번 학기에 나는 3-2를 맞아, 졸업 프로젝트 첫학기인 스타트를 시작하게 되었다.
팀에서 내가 기술적으로 담당하는 스택은 Front-end이고, 이 글에서는 우리 팀의 프로젝트에서 스타트 학기말까지 거쳤던 Front-end측의 기술 회고를 다뤄보려고 한다.
1. 주제 소개
먼저 본격적인 기술 회고에 들어가기 전에, 우리 팀의 프로젝트 주제에 관해 소개하고자 한다
우리팀의 주제는 '예산 설정과 지출관리에 어려움을 겪는 사회 초년생을 위한 개인 일정 및 소비 특성 기반 밀착관리 가계부 서비스" 이다.
한줄소개에서 예측할 수 있듯, 서비스 주제의 타겟층은 예산 수립 및 지출 관리에 익숙하지 않아 어려움을 겪는 사회 초년생(20대 초중반) 이다.
기존의 가계부 서비스(애플리케이션)에서는
- 적절한 예산을 스스로 설정하기가 어렵고 (예산의 모든 부분을 사용자가 일일이 스스로 설정해야하므로)
- 예산을 세웠더라도 이를 실질적으로 지키기가 어렵고 (그날의 일정이나 약속 등을 고려하지 않고 하루 예산을 무조건 1/n 하여 알려주므로)
- 기존 예산이 적절하지 못하다고 생각되어도, 새로운 달의 예산을 기존에서 변경하기가 어렵다 (사용자가 어떤 카테고리의 예산을 얼만큼 증감할지 모든걸 스스로 판단하여 일일이 조정해야하므로)
는 불편함이 있다는 것을 리서치와 설문조사를 통해 확인했고,
이에 우리는
"예산 설정과 지출관리에 어려움을 겪는 사회 초년생을 위한 개인 일정 및 소비 특성 기반 밀착관리 가계부 서비스" 주제로 결정하게 되었다
위의 페인 포인트 해결을 위해 우리는,
1) 첫 가입 후 첫 달 예산 수립 시, 단계별 안내를 통해 사용자가 질문에 답하다 보면 예산을 어렵지 않게 완성할 수 있도록
2) (캘린더 연동을 통해) 매일 내일의 일정을 고려한 하루 예산을 배정하며,
3) 매일 사용자의 결제내역 문자/앱 푸시알림을 파싱하여 소비내역을 추적하여 예산 사용현황을 업데이트하고
4) 월말에는, 이번달 소비에 대한 피드백 및 개선안을 제공한 후 다음달의 주요 일정(여행 등)과 사용자의 요구사항을 반영하여 다음달 예산을 수립해주는
기능을 제공한다.
이 중 ai가 사용되는 부분은 2 와 4이기에, 우리는 이 부분들을 기말 기술시연에서 구현하기로 했다
더불어 기말에 구현할 기능에 직접적으로 사용되진 않지만 우리의 서비스를 구현하기 위해선, 프론트엔드 측에서 사용자의 결제내역 알림에 해당하는 문자를 읽어오는 것이 필수적이었으므로, 이 부분 또한 구현하게 되었다
2. Front-end 기술회고 : flutter 앱에서 사용자의 sms중, 결제내역 문자 긁어오기
위에서 정한 기말에서의 구현사항에 따라, Front-end 담당인 나는 구현하기로 한 기능들의 뷰와 api연동, 사용자의 결제내역 문자 읽어오기를 담당하게 되었다. 이중 결제내역 문자 긁어오기 기능을 구현하는 법에 대해 기술 튜토리얼을 작성해보려고 한다.
무니 서비스가 구현되기 위해선, 사용자의 결제 내역을 인식하여 이를 사용자에게 보여주고, 세운 예산안 대비 실제 사용 내역(현황)을 계속해서 갱신해주어야 한다.
이를 위해 기획 초기에는, 마이데이터 api를 사용하는 것을 고려했었다
그러나 팀 내부적으로도 조사를 해보고, 금융 관련 어플 창업 경험이 있으신 멘토님께도 자문을 구해본 결과 마이데이터 api는 접근이 사실상 불가능하다는 결론을 얻게 되었다. (마이데이터 api를 사용하기 위해선, 보안 등급을 기업 수준으로 갖추어 심사를 통과해야 하는데, 이것이 학부생 팀플 수준에서는 사실상 불가능)
그러나 가계부 서비스를 구현하기 위해서는 사용자의 결제내역을 받아오는 것은 필수적이었고, 이에 우리 팀이 생각해낸 대안책이 사용자의 결제내역 문자, 은행앱 결제 푸시알림을 읽어오는 방식이었다
이번 기말 발표에서는 그 중 결제내역 "문자" (sms)를 읽어오는 기능을 먼저 구현하게 되었다
이를 위해선 사용자의 문자에 접근하여 읽어올 수 있는 라이브러리를 선정하고, 이를 적용하여 사용자의 문자함의 문자들을 읽어오되, 여러 문자 중 "결제내역" 문자만을 읽어오도록 구현해야 했다
그리고 본격적인 구현을 위해 레퍼런스를 서치해보려 했는데, 생각보다 레퍼런스가 많지 않음을 알게 되었다
요즘 웬만한 기능은 구글링을 하면, 구현해야할 기능과 fit한(같은 내용의 기능) 내용의 포스팅이 주루룩 나오는 것이 너무 당연했는데, 이 기능은 사진에서 보다시피 한글로 된 포스팅은 상단에 하나정도 뜨고(사실 맨 위 포스팅도 내가 구현하려는 기능과는 구조적으로 다른 기능이었다), 그 뒤론 바로 외국문서+원하는 기능과 다른 기능을 내용으로 하는 포스팅이 나오는 것을 볼 수 있다 (아주 많은 검색어를 시도한 끝에 그나마 내가 원하는 포스팅이 있는 결과화면을 캡처한 것이다) 심지어 플러터 문자 읽어오기 를 검색하면 sms 가 아닌, 텍스트파일을 읽거나 다루는 법이 한가득 나온다...^^
특히 내가 구현하려는, "사용자의 문자함의 sms 중, '오늘' 온 , 특정한 내용(내 경우는 결제내역)의 문자에 해당하는" sms를 읽어오는 것에 대해선 한글로 된 포스팅 자체를 찾지 못했기에, 나의 작은 경험기가 추후 나와 같은 기능을 구현하려는 누군가에게 조금이나마 도움이 되길 바란다
(1) 라이브러리 선택 : readsms vs flutter_sms_inbox
먼저 결제내역 문자 읽어오기를 위한 적절한 라이브러리를 결정해주어야 한다
플러터 앱에서 sms를 읽어오는 것에 관해, 주로 이 두가지 라이브러리가 많이 사용되는 듯 하다
그렇다면 둘의 차이는 무엇일까? 둘중 아무거나 써도 우리가 원하는 기능을 구현 가능할까?
결론은 아니다 !
이에 대해서도 다루고있는 포스팅이 없었기에, 본격적인 기술 튜토리얼에 앞서 두 라이브러리의 차이를 간단히 언급하고자 한다.
왼쪽은 readsms 의 공식문서 첫줄 소개, 오른쪽은 flutter_sms_inbox 공식문서의 첫줄소개이다.
둘의 차이를 알겠는가?
주목해야할 것은 "incoming" (readsms) 이라는 단어, 그리고 "inbox"(flutter_sms_inbox)라는 단어이다
즉, readsms 라이브러리는 incoming으로 사용자에게 실시간으로 "들어오는" 문자를,
flutter_sms_inbox 라이브러리는 사용자의 sms inbox에 있는 (즉 사용자의 문자함에 있는) 문자들을 읽어오는 것이다.
그렇다면 우리는?
앱을 켜고있는 상황에서 실시간으로 들어오는 문자를 그때그때 받으려는 것이 아니라,
사용자의 inbox의 문자 중 '오늘'의 '결제내역'문자를 전체 다 받아오는 것이므로
후자에 해당하는 flutter_sms_inbox 라이브러리를 사용해야 한다.
(2) 의존성 설치
사용할 라이브러리와 더불어, 사용자에게 문자 권한 접근을 허용받기 위해 permission_handler 의존성을 같이 설치해주어야 한다
설치 과정에서 위와 같은 에러가 난다면, 컴퓨터의 설정->시스템->개발자용 에서 개발자모드를 켜주면 된다
(3) 구현 튜토리얼
1) 권한 요청 (문자에 접근하기 위해, 사용자에게 권한을 부여받아야 한다)
이를 위해 AndroidManifest.xml에 위와 같은 코드를 추가한다
문자 읽기 권한 부여 코드만 추가하면 Permission exists without corresponding hardware <uses-feature android:name="android.hardware.telephony" required="false"> tag 라는 에러가 난다
Permission exists without corresponding hardware 오류는 AndroidManifest에 특정 권한(SMS 관련 권한)이 있는 경우, 해당 권한이 기본적으로 하드웨어(전화 기능)를 필요로 한다고 가정하기 때문에 발생하는 에러이다
이를 해결하기 위해선, 전화 기능이 필수가 아님을 명시하는 <uses-feature> 관련 코드를 추가하면 된다
2) 단계별로 구현하기
https://pub.dev/packages/flutter_sms_inbox
flutter_sms_inbox | Flutter package
Flutter SMS Inbox Plugin (Android only). This library allows users to easily query inbox messages.
pub.dev
step 1. 라이브러리 기본 코드 적용 (권한 관련 코드 세팅, 공식문서의 기본 코드를 통해 사용자의 문자내역이 잘 불러와지는지 확인)
우선 기능을 커스텀하기에 앞서, 라이브러리 공식문서에 있는 기본코드가 문제없이 우리 서비스 위에서 작동하는지를 보고자 했다
**여기서, 에뮬레이터는 실제 휴대폰이 아니므로 문자가 오지 않는데, 어떻게 테스트할까?
안드로이드 스튜디오의 에뮬레이터에서 제공하는, extended controls-phone 기능을 통해 가능하다!
이처럼 번호와 내용을 마음대로 설정해서, 에뮬레이터의 문자함에 sms를 보낼 수 있다
그렇게 한 후 에뮬레이터의 배경화면에서 sms함 아이콘을 누르면, 오른쪽과 같이 실제 휴대폰처럼 문자내역을 볼 수 있다
이제 공식문서의 코드를 적용하고 테스트해보자. 공식문서에서 제공하는 예시코드는 아래와 같다
import 'package:flutter/material.dart';
import 'package:flutter_sms_inbox/flutter_sms_inbox.dart';
import 'package:permission_handler/permission_handler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final SmsQuery _query = SmsQuery();
List<SmsMessage> _messages = [];
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter SMS Inbox App',
theme: ThemeData(
primarySwatch: Colors.teal,
),
home: Scaffold(
appBar: AppBar(
title: const Text('SMS Inbox Example'),
),
body: Container(
padding: const EdgeInsets.all(10.0),
child: _messages.isNotEmpty
? _MessagesListView(
messages: _messages,
)
: Center(
child: Text(
'No messages to show.\n Tap refresh button...',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
var permission = await Permission.sms.status;
if (permission.isGranted) {
final messages = await _query.querySms(
kinds: [
SmsQueryKind.inbox,
SmsQueryKind.sent,
],
// address: '+254712345789',
count: 10,
);
debugPrint('sms inbox messages: ${messages.length}');
setState(() => _messages = messages);
} else {
await Permission.sms.request();
}
},
child: const Icon(Icons.refresh),
),
),
);
}
}
class _MessagesListView extends StatelessWidget {
const _MessagesListView({
Key? key,
required this.messages,
}) : super(key: key);
final List<SmsMessage> messages;
@override
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
itemCount: messages.length,
itemBuilder: (BuildContext context, int i) {
var message = messages[i];
return ListTile(
title: Text('${message.sender} [${message.date}]'),
subtitle: Text('${message.body}'),
);
},
);
}
}
위 로직의 대략적인 내용은,
SMS 데이터를 쿼리하기 위해 flutter_sms_inbox 라이브러리에서 제공하는 객체인 SmsQuery _query를 통해 쿼리한 sms 메세지를 , List<SmsMessage> _messages 라는 리스트에 저장하여 화면에 렌더링해주는 로직이다.
이때 사용자의 sms에 접근할 권한을 관리하기 위해 permission_handler를 사용한다.
위 코드를 적용한 후 앱을 실행해보면, 위와 같이 사용자의 sms inbox에 있는 문자 목록들을 모두 잘 불러오는 것을 볼 수 있다.
이제 라이브러리 코드를 수정하여, 우리가 원하는 기능으로 커스텀해보자!
step 2. 사용자의 문자내역 중, "오늘" 날짜에 해당하는 것만 불러오기
사용자의 전체 문자 내역 중, 오늘 온 메세지만을 가져오기 위해, 코드를 위와 같이 수정한다.
message(사용자의 전체 문자내역) 객체에서 오늘 날짜의 문자만을 필터링하고, 이를 filtteredMessages 객체에 저장한 후 이 객체를 화면에 렌더링한다
왼쪽의 문자함에 보면 맨 위 문자는 오늘(당시 요일은 화요일이었다), 두번째 문자는 당시로부터 하루 전에 온 문자이다.
위에서 수정한 코드를 적용하면, 두 문자 중 '오늘' 문자에 해당하는 문자만 필터링하는 것을 볼 수 있다.
step 3. step2를 통해 필터링한 "오늘" 문자 중, 카드사에서 보낸 결제내역 문자에 해당하는 것만 필터링하기
이제 사용자에게 오늘 온 문자 중, 카드사에서 보낸 결제내역 문자만을 필터링하도록 커스텀해보자.
카드사 문자의 형태는 대부분,
[web발신] 으로 도착하여 '신용', '승인', '체크', '사용', '카드' 등의 키워드 중 하나 이상을 포함하는 형식이다
이에 맞춰, todayMessages 중 이러한 패턴을 가진 문자를 필터링하여 cardMessges 에 저장하고, 이를 화면에 렌더링 하도록 수정하였다.
이를 테스트해보면,
현재 문자함에는 왼쪽 사진에 보듯 오늘 온 결제내역 문자 / 오늘 온 친구의 문자 / 며칠 전 온 문자 (855-1212 번호로 온 문자내역에 과거 문자들이 있는 상태이다) 가 있다
위에서 변경한 코드를 적용해보면, 오른쪽과 같이 이중 '오늘 온' , '결제내역' 문자만을 필터링해주는 것을 볼 수 있다.
이렇게 사용자의 문자함의 문자 (inbox) 중, '오늘'의 '결제내역' 문자만을 필터링해서 불러오는 것에 성공하였다.
예상보다 레퍼런스가 너무 없어서 처음엔 당황스러웠지만, 공식문서를 충실히 보며 구현하는 좋은 경험이 되었다.
이제 방학동안은 최대한 예외없이 모든 카드사의 결제내역 문자를 읽어올 수 있도록 보완하고, 문자 뿐만 아니라 앱 푸시알림 (은행앱에서 보내는 결제내역 푸시알림)도 읽을 수 있도록 구현할 예정이다.
부록 ) Front-end 개발 초기세팅 및 github 연결 (부제 : 내 노트북의 모든 파일이 git에게 추적당하고 있었다고 ?!)
내가 다루고자 했던 주요 내용들은 위에서 끝이지만, 안드로이드 스튜디오에 플러터 프로젝트 초기세팅을 하고 이를 깃허브 레포와 연동하는 과정에서 겪은 작은 에러가 있어서 이를 부록 삼아 덧붙여보고자 한다.
(1) 플러터 프로젝트 초기 세팅
본격적인 개발에 앞서 플러터 프로젝트를 생성하고, 폴더구조를 세팅해준다
플러터 프로젝트를 만들면 기본적으로 생기는 폴더 구조 외에, 내가 별도로 추가한 부분은 다음과 같다
- lib/screens : 스크린(뷰) 전체에 해당되는 위젝들을 모아두는 폴더
- lib/models : 데이터 모델을 따로 모아둠 - 애플리케이션의 데이터 구조나 비즈니스 로직을 정의하는 곳(앱에서 다루는 데이터를 표현하는 클래스들이 들어가게 된다)
- lib/widgets : 여러 곳에서 중복 사용되는 위젯(컴포넌트)을 모아두는 폴더
- assets : svg, png 등의 에셋들을 모아둔다
이정도가 개발을 기본적으로 시작하려면 있어야 하는 구조인 것 같고, 추후 더 필요한 부분이 생기면 추가해나갈 예정이다
그 후 에뮬레이터를 연결하고, main.dart를 run하면 사진과 같이 기본적으로 main.dart에 작성되어있는 코드대로 앱이 실행되는 것이 보인다
**첫 앱 실행은 매우.. 오래걸린다 (나처럼 안된다고 자꾸 중지하지 말고 인내심을 갖고 기다리자)
(2) 깃허브 연동
안드로이드 스튜디오와 깃허브 레포를 연동하려던 중, 문제가 발생했다
안드로이드 스튜디오 상에서 깃허브 계정 연결 -> 로컬 깃 생성 -> 깃허브상의 원격 레포 연동 순으로 진행하면 되는데, 로컬 깃을 생성하는 버튼을 누르니 이미 플러터 프로젝트가 git 하에 있다 (git의 추적 하에 있다)는 알림이 나왔다
그래서 현재 깃 관리 상태를 보기 위해, cmd 에서 플러터 프로젝트 경로로 이동 -> git status를 했더니 수상한 결과가 나왔다
문제는, 플러터 프로젝트의 git 루트가 플러터 프로젝트 디렉토리가 아닌, 상위 디렉토리 (users/j3woody : 그냥 내 로컬폴더의 홈 경로..이니까 결국 내 사용자 디렉토리 전체가 git의 추적대상이 되어있는 상태였다)
즉 git 리포지토리가 사용자 디렉토리에 설정되어 있어 그 하위 모든 파일 및 폴더가 git으로 추적되고 있었다
정확히 언제 어떤 일 때문에 이렇게 되었는지는 모르겠지만, 현재 무늬 플러터 프로젝트의 git 루트를 올바르게 바꿔야 했다
플러터 프로젝트 경로(무늬)에서 git 루트 디렉터리를 확인해보니, 아니나 다를까 사용자 디렉터리가 git루트로 설정되어 있었다
*그런데 여기서 바로 삭제를 할 수 없었던게, 인텔리제이에서도 깃을 사용중이라 혹시 이걸 함부로 건드렸다가 문제가 생길까 우려가 되었다
-> 이에 인텔리제이에서 작업중인 스프링부트 프로젝트 경로에서도 git 루트 디렉터리를 확인해 보았는데, 다행히 이 프로젝트는 git루트가 올바르게(프로젝트의 루트경로로) 설정되어 있었다
이를 확인한 후 안심하고 내 사용자 루트 디렉토리에 잘못 생성된 .git을 지워주었다
이후 안드로이드 스튜디오에서 version control -> directory mapping 에서 확인하니 현재 연결된 git 루트가 잘 지워진 것을 볼 수 있었다 (인텔리제이에서도 같은 이름의 메뉴에서 확인이 가능하다)
이후 다시 안드로이드 스튜디오에서 version control -> create git repo (로컬 깃 생성)을 누르면
이렇게 로컬 깃 생성이 정상적으로 되는걸 확인 가능하다 (파일 색상들이 바뀌어있다)
이제 commit -> push 를 하면 정상적으로 원격 깃헙 레포에 코드를 올릴 수 있다