본문 바로가기

프로젝트/1등 오답노트 - 기능도 1등 편리함도 1등

[App] 1등 오답노트 출시

학생이나 고시생을 위한 오답노트 앱입니다.

최대한 단순하면서도 많은 정보와 기능을 담으려고 노력했습니다.

 

아래 랜딩페이지에서 기본 정보와 스토어로 이동할 수 있는 버튼이 포함되어 있습니다.

 

일등오답노트

편리함 1등 기능도 1등 오답노트 앱

xn--o80b61as7bhx6adrbf25a.kr

 

모든 기능을 어떻게 구현했는지 자세히 설명하지는 않고, 주요 기능에 어떤 라이브러리를 사용하고 고려했으며, 구현할 때 겪었던 문제나 고민을 위주로 말씀드리겠습니다.

 

사용자 인터페이스

사용자가 정리한 오답들을 구글 Keep처럼 2열로 벽돌 쌓듯이 보여줍니다.

하지만 ScrollView 안에서 그냥 쌓기만 하면 안됩니다. 50~100개가 넘어가면 특히 안드로이드에서 버벅거리기 때문입니다. (탈락)

FlatList는 다중 컬럼 기능을 제공하지만 행 높이가 같은 행의 가장 큰 열의 높이에 맞춰지기 때문에 빈 공간이 생깁니다. (탈락)

이를 위해 사용된 것이 react-native-virtualized-waterfall입니다.

하지만 제작자 분이 편집됐을 때 재배치는 고려하지 않으셨기에 라이브러리 내외부에서 약간의 조정이 필요했습니다.

 

구글 드라이브 동기화

원노트처럼다수의 기기에서 최신 상태를 유지하는 것을 목표로 했습니다.

하지만 개인 서버로 구현하기에는 비용이 부담스러웠습니다.

그래서 구글 드라이브를 사용하기로 했습니다.

구글 드라이브와 통신하기 위해 react-native-google-drive-api-wrapper라는 순수 자바스크립트 라이브러리를 사용했습니다.

그리고 구글 드라이브에 파일을 읽고 쓰기위한 권한으로 drive.appdata를 사용했습니다.

구글 드라이브에 파일을 읽고 쓸 수 있는 권한은 이외에도 여러가지가 있지만, 민감한 데이터로 분류되어 OAuth 동의화면 승인이 까다로워 질 수도 있기 때문에 drive.appdata를 사용했습니다.

drive.appdata는 구글 드라이브에 저장되지만 사용자가 볼 수 없고, 오로지 승인된 앱에서 drive.appdata가 승인된 Access 토큰으로만 접근할 수 있는 공간입니다.

그래서 민감하지 않은 데이터로 분류되서 OAuth 동의하면 승인이 단순해 지지만, 디버깅할 때 로그로만 해야해서 상당히 힘들었습니다.

또한 react-native-google-drive-api-wrapper 라이브러리가 drive.appdata를 지원하지 않아 https://gist.github.com/.../2cddb9033d65403d595e069b95dad005 gist를 참조해서 수정하여 사용했습니다.

그리고 최소한의 통신과 용량만 사용하기 위해 여러가지를 고려했으며, 테스트할 때도 기기 5대로 가혹하게 테스트했습니다.

일반, 삭제, 완전 삭제 3가지 상태를 다수의 기기에서 최신상태로 유지해야해서 많이 어려웠습니다. 이 기능 구현하는데 거의 일주일은 걸린 것 같네요.

 

인증

구글 로그인에 주로 사용하는 react-native-google-signin 라이브러리를 사용했습니다.

이전 앱을 만들 땐 사용자 정보(이름, 사진, 이메일)만 얻어오면 됐기 때문에 Access 토큰의 만료를 신경쓰지 않았습니다. (그래도 보안을 위해 따로 토큰을 만들어 사용하긴 했습니다.)

하지만 이번에는 구글 드라이브에 접근해야 하기 때문에 Access 토큰을 항상 최신 상태로 유지해야 했습니다.

그런데 구글에서는 Access 토큰을 재발급받기 위한 Refresh 토큰을 바로 발급해주지 않더군요.

serverAuthCode를 발급받은 후에 이것을 사용하여 서버에서 Refresh 토큰을 발급받아야 했습니다. (다들 이미 알고 계시겠지만요.)

어쨌은 이렇게 토큰을 최신 상태로 유지할 수 있었고, 발급받은 토큰들은 키-값을 저장할 때 주로 사용하는 react-native-async-storage가 아니라 react-native-secure-key-store 라이브러리를 사용하여 저장했습니다.

그리고 토큰뿐만 아니라 expiredAt 정보도 함께 저장하여 동기화 요청을 할 때마다 발급받은 후 55분이 지났는지 검사하여 지났다면 Refresh 토큰으로 Access 토큰을 재발급 받은다음에 진행하도록 했습니다.

추가적으로 OAuth 동의화면 승인이 생각보다 까다로워서 놀랐습니다. drive.appdata를 추가했더니 개인정보처리방침을 꼼꼼하게 확인해서 어디가 잘못됐는지도 알려주더라구요. 승인받는데 1주일 정도 걸렸습니다.

 

광고

애드몹과 이를 구현하기 위해 react-native-firebase/admob 라이브러리를 사용했습니다.

단순히 광고를 뿌려주는게 아니라 광고관련 함수와 데이터를 Context로 관리해서 사용자가 광고를 터치하면 1시간 동안 더 이상 광고가 나타나지 않도록 했습니다.

처음에는 "광고를 터치하면 모든 광고가 1시간 동안 차단됩니다."라는 문구를 앱에 넣었으나, 무효 트래픽으로 간주되어 제재를 당하기도 했으니 다른 개발자분들도 주의하셨으면 합니다.

광고 관련 문구를 모두 삭제했더니 eCPM이 확~ 떨어지더군요. 그래도 사용자가 언젠간 눈치채지 않을까 기대하고 있습니다.

 

이미지 처리

CamScanner 처럼 미화 기능과 스캔 기능을 구현하고 싶었습니다.

이런 기능은 OpenCV로 할 수 있더군요.

그리고 다행히 react-native-opencv3라는 라이브러리가 있었습니다.

예제나 문서는 상당히 빈약하지만, opencv 공식 홈페이지의 풍부한 예제를 참고하여 구현하면 됩니다.

현재는 이미지 미화 기능만 구현한 상태입니다. 이것은 OpenCV의 Threshold 기법을 사용하면 됩니다.

문서를 비스듬하게 찍어서 모서리를 맞추면 사각형으로 만들어 주는 스캔 기법은 Prespective Transform이라고 합니다.

react-native-perspective-image-cropper 라이브러리가 있지만 제대로 동작하지 않았습니다.

지금은 5~10초짜리 홍보 영상을 먼저 만든 후에 직접 만들 계획입니다. 어떻게 만들지는 구상해 놓은 상태입니다.

다만 react-native-opencv3 라이브러리가 약간 불안합니다.

OpenCV는 이미지를 Mat 객체로 만들어 처리하는데, 기본적으로 완성된 Mat 객체는 결과 이미지로 변환시킨 후에 삭제해 주어야합니다.

RNCv.deleteMat 메소드로 삭제할 수 있는데, 안드로이드에서 오류가 발생하더군요. 아직 사용하지 않은 메소드에도 비슷한 오류가 있을까 두렵습니다.

[+] 현재 구현 완료

 

PDF 생성

노트 단위 또는 노트 안에서 선택한 문제들을 2열 레이아웃의 PDF로 만들어서 다른 앱에 공유할 수 있습니다.

이를 구현하기 위해 react-native-html-to-pdf 라이브러리를 사용했습니다.

그런데 안드로이드와 iOS의 규격(?)이 달라서 일일히 값을 넣고 수정하느라 시간이 많이 들었습니다.

PDFKit의 리액트 네이티브 버전으로 하면 정교하게 조작가능하다고 하던데 유료라 사용하지 못했습니다.

그리고 문제들을 2열 레이아웃으로 만들기 위해 거의 하루의 시간을 투입했습니다.

 

용량 최적화

이미지를 다루는 앱이다 보니 사용되지 않는 이미지를 관리하기 위해 보이지 않는 많은 고민이 있었습니다.

예를 들어 사용자가 페이지를 만들 때 문제 사진까지 찍었는데, 취소를 한 경우, 앱 캐시에는 이미지가 남아 있을 겁니다.

아니면 이미지를 크롭했는데 취소한 경우도 있겠지요. 특히 라이브러리마다 앱을 저장하는 경로가 달라서 귀찮았습니다.

안드로이드는 주로 캐시 폴더에 저장하는데 iOS는 tmp 폴더에 저장하는 라이브러리도 있더군요.

 

당황스러웠을 때

iOS는 앱을 업데이트할 때마다 앱의 iOS 내 절대 경로가 바뀌더군요.

이걸 모르고 페이지(문제, 해답, 메모 데이터)에 이미지의 절대 경로를 저장하는 방식을 사용했다가 사용자한테 쌍욕 먹었습니다.