스캐니(Scanny) – 아이폰용 스캐너앱

저는 개인적으로 개발언어를 스터디 하는 방법으로 마켓 릴리즈를 목표로 제품을 하나 만들어가며 스터디하는 것을 좋아합니다. 목적이 분명하면 생각보다 빠르게 프레임워크의 구조와 내용을 파악할 수 있습니다. 플러터(Flutter)를 스터디하기 시작하면서 벌써 세번째 앱을 앱스토어 릴리즈 했는데요. 앞의 두개는 이것을 만들면서 앱스토어에서 플러터가 어떻게 업로드 되고 돌아가는지를 확인하기 위해 짬짬이 만든것 이었습니다.

스캐니(Scanny)는 폰카메라를 이용해 문서를 촬영해서 자동으로 문서를 인식하고 사진내에서 문서만 추출해서 노이즈를 제거하는 등의 전처리를 거치고 최종적으로 OCR을 위한 깨끗한 문서를 만들어 문자인식(OCR)을 하거나 PDF로 변환하는 기능을 가진 앱입니다.

책을 단위별로 스캔하기위하여 폴더 시스템의 UI를 만들어 책단위 혹은 챕터 단위로 스캔을 할수도 있습니다. PDF 출력을 위하여 리스트에서 전체를 선택하거나 일부 그리고 드랙 & 드롭으로 순서를 바꾸어 PDF를 만들 수도 있습니다.

촬영한 문서는 OCR 문자인식 기능을 이용하여 한글/영문을 자동인식하여 텍스트로 변환하여 저장하고 공유할 수 있습니다. 향후 더 많은 언어를 지원할 계획입니다.

https://apps.apple.com/kr/app/id1529856835

앱은 무료 아닙니다. 4,900원입니다. 광고 없습니다. 현재 목표는 앞으로 10년간은 업그레이드를 하려고 생각하고 있습니다. 아래 링크에서 확인하세요.


둘째가 만들어준 아이콘

이 앱은 플러터를 이용해 개발되었기 때문에 당연히 안드로이드를 지원합니다. 플레이스토어에 런칭도 할 수 있습니다. 앞의 테스트 앱에서 테스트 해봤습니다. 하지만 앱의 기능상 카메라에서 문서 인식 기능이 매우 중요한데 안드로이드에서 약간 불안정한 상황이 발생하고 있어서 좀더 보완을 거친후에 업로드 하려고 생각중입니다.

위의 글은 스토어 런칭 광고성 글입니다. 여기서부터는 개발자 입장입니다. 플러터(Flutter)는 모바일 개발을 위한 다할나위없이 훌륭한 도구입니다. 기본적으로 아이폰과 안드로이드를 동시에 지원하는 멀티 플랫폼 개발툴이구요. 참! 최근에는 베타상태 이긴 하지만 웹과 윈도우도 지원하기 시작했습니다. 그리고 프레임워크가 수년째 접어들면서 패키지 라이브러리와 개발자 생태계도 아주 훌륭해 졌습니다.

개인적으로는 지난 수년간 React & Vue 들을 통하여 상태(State)와 그 상태의 변경이 익숙해져 있었기 때문에 여럽지 않게 적응 했을 수도 있지 않았을까 싶지만 네이티브만 개발하던 개발자라면 Provider와 State 에대한 개념이 약간(?)은 걸림돌이 되지 않을까 싶기도 합니다만 매우 좋은 도구이고 충분히 투자 할만한 가치가 있다고 생각합니다.

Change Log

2020-11-13 : V1.0 Initial Release
2020-11-19 : V1.1.0
컬러문서 스캔 기능 추가

iOS/Swift – iCloud Drive에 파일 업로드 하기

2011년 4월에 애플 앱스토어에 업로드한 Fake Location이 어뷰징 요소가 있다는 이유로 2020년 4월에 스토어에서 강제로 퇴출당했다. 이걸 마지막으로 앱스토어에 판매중인 앱이 한개도 남지 않게되어 새로운 앱을 하나 만들기로 했다.

몇일간 남는 시간을 쪼개어 그럭저럭 보이스를 녹음하고 플레이하는 작업을 하고 있었는데 알수 없는 문제에 봉착했다. 녹음된 보이스 파일은 아이폰에 저장되어야 하고 옵션으로 iCloud Drive에 저장되어야 해서 iCloud에 저장하는 기능을 추가 했는데 파일은 분명히 저장이 되고 원격 플레이까지 정상적으로 되는데 iCloud Drive에 폴더와 파일이 보이지 않는 문제가 발생했다.

알고보면 간단한 문제였지만 문제라는게 항상 그렇듯 모르면 괴로울 뿐이다. 게다가 코드 문제라기보단 설정(?) 문제라면 해결후 허무함까지 동반한다.

아이폰 로컬 폴더 얻기

아이폰 로컬 도큐먼트 폴더를 얻는 방법은 아래의 코드로 저장할 파일을 생성 할 수 있다. 생성된 audioFilename을 AVAudioRecorder에 넘겨주면 녹음이 완료된후 파일이 생성되는 것을 확인 할 수 있다. 아래 코드의 결과로 대략 이런 로케이션이 얻어진다.

file:///var/mobile/Containers/Data/Application/9F57DEC2-3A40-40AA-9EC5-5D104E94053F/Documents/voice.m4a

let saveFilename = "voice.m4a"
let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
let audioFilename = path!.appendingPathComponent(saveFilename) 

iCloud Drive 원격 폴더 얻기

원격 폴더를 얻는 방법은 약간의 설정작업을 해야한다. Xcode의 Signning & Capability탭에서 +Capability 탭을 선택하여 iCloud 사용설정을 해야한다. 물론 이걸 하려면 개발자 계정이 필요하다.

설정방법

위와 같은 설정을 해주면 .entitlement 파일이 생성된다. 그리고 아래 코드를 이용하면 iCloud Drive 폴더에 파일을 생성 할 수 있다. 파일명이 아래처럼 생성된다. 레코딩 결과가 생성된 파일명으로 잘 저장되었다.

file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~*****/Documents/voice.m4a

let saveFilename = "voice.m4a"
let path = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
let audioFilename = path!.appendingPathComponent(saveFilename) 

그런데 폴더가 보이지 않는다

iCloud Drive에 파일을 업로드 했으니 당연히 나의 맥북 Finder 에 iCloud Drive에 오디오 파일이 보여야 하는데 보이지 않았다. 여기에 보이지 않으면 파일을 복사하거나 이동 시킬 수 없다. 난감하네… 그래서 3일을 까먹었다. 결론적인 이야기지만 클라우드에 파일을 업로드 하고 사용자가 손대지 못하게 하려면 여기까지만 하면 된다. 물론 아이폰의 설정 – iCloud – 저장공간관리 – (앱) – 에서 삭제 할 수는 있다.

문제의 해결은 매우 간단했다. info.plist에 아래와 같은 Key값을 추가 해주면 된다. 물론 key는 각자의 것을 사용해야 한다. 개발자가 코드로 문제를 해결 못하고 설정때문에 몇일씩 까먹고 나면 매우 허무해진다. ^^; 이건 아래의 stackoverflow 링크의 중간쯤에 ‘난 이렇게 해결했어’ 가 있었는데… 글타래가 길어서 대충 읽고 넘어가느라 못본거 였던것 뿐이었다. 결국 3일째 모든 대답들을 다시 꼼꼼히 읽고 다 시도해 보고서야 문제를 찾을 수 있었다. 교훈은 좀 더 꼼꼼해 져야 한다.. 정도?

<key>NSUbiquitousContainers</key>
<dict>
    <key>iCloud.net.redacted.docTest</key>
    <dict>
        <key>NSUbiquitousContainerIsDocumentScopePublic</key>
        <true/>
        <key>NSUbiquitousContainerSupportedFolderLevels</key>
        <string>Any</string>
    </dict>
</dict>

https://stackoverflow.com/questions/25203697/exposing-an-apps-ubiquitous-container-to-icloud-drive-in-ios-8

이 설정을 추가하면 아이폰의 파일앱 그리고 계정이 연결된 맥북 또는 다른 컴퓨터에서 생성된 폴더와 파일을 확인 할 수 있다.

이 프로젝트는 현재 대략 20% 정도 진행되었다. 시작만 해놓고 마무리는 하지 못한 일들이 너무 많아서 이제는 외부에 공표를 해가며 스스로를 채찍질을 해보려고 하고는 있지만 과연 마무리를 할 수 있을지는 잘 모르겠다. 블로그 한개 썻으니 그나마 보람이 있는것일까?

React Native 다시 공부하기

React Native는 이미 2017년에 버즈아트에서 나름 빠르게 도입해서 프로젝트에 성공적으로 적용했던 적이 있었다. 링크참조(http://practical.kr/?cat=6) 그때 버전이 v0.45 수준이었는데 3년이 지난 지금도 React Native는 v0.61에 머물러 있다. 대체 언제쯤 v1.0을 내놓으려나…

2020년 현재 여전히 크로스(멀티)플랫폼(iOS, Android)을 지원하는것은 매력적이고 그동안 꽤 많은 기술적 진보가 있었을 것이라는 생각을 염두에 두고 다시 한번 공부를 시작해 볼까? 하는 생각에 연습용 프로젝트를 하나 만들기 시작했다. 이글을 쓰는 이유는 프로젝트를 만들고 보니 기억 나는게 하나도 없어서 뭘 했는지 정리하기 위함이다.

모바일 멀티플랫폼

잠시 멀티 플랫폼 이야기를 하고 넘어가보자. 최근 2~3년 사이에 모바일 플랫폼 개발과 관련한 프레임워크는 전통적인 Swift기반의 Xcode, Java & Kotlin 기반의 Android Studio와 더불어 Dart 기반의 Flutter, C# 기반의 Xamarin 등이 쏟아지며 개발툴 전성시대가 열린것 같다. 성능도 많은 향상을 보여서 거의 네이티브 성능을 따라오는 수준까지 도달한 것으로 보인다. 어떤것을 선택해도 크게 문제가 없을것으로 생각된다. 하지만 역시 가장 좋은것은 손에 익은것이 아닐까 싶다.

나 개인적으로는 오래도록 사용해온 Obj-C 기반의 Xcode가 가장 좋지만 이건 멀티 플랫폼이 아닌지라(iOS만지원) 제외하고 최근 2~3년간 JavaScript 특별히 Vue.js / Node.js를 사용해 오고 있었기 때문에 JS 기술을 그래도 활용할 수 있는 React Native에 끌리는건 당연한 선택일 수 밖에 없다. 하지만 처음 접하는 모바일 멀티 플랫폼 개발이라면 Dart/Flutter이 좀더 낫지 않을까 하는 생각인데 이유는 구글의 개발자 문서 지원이 매우 좋고 React Native에 비하여 좀 더 좋은 실행 성능을 내고 있다고 알려져 있기 때문이다.

Getting-Started

프로젝트 시작은 여기서 한다. Expo를 이용하는 방법과 Cli 툴을 이용하는 방법이 있는데 나는 Cli 툴을 이용하여 프로젝트를 생성 했다. Expo를 쓰면 폰에 직접 앱을 올려서 바로 결과를 볼 수 있고 Cli툴을 쓰면 Xcode 및 각종 도구를 깔아서 시뮬레이터에서 테스트를 할 수 있다. 나중에 스토어용으로 빌드를 하기 위해서 후자의 방법으로 설치하는 것이 좋다. 하지만 간단한 테스트는 Expo가 훨씬 편하다.

https://reactnative.dev/docs/getting-started

Navigation

앱을 만들려면 기본적으로 메뉴 시스템이 있어야 한다. 모바일 시스템에서 메뉴 시스템은 보통 Tabbar – Navigation, Drawer – Navigation 두가지 중에서 선택하는 것이 일반적인 선택이다. 한동안 Drawer 메뉴가 유행을 하다가 최근 다시 Tabbar 시스템이 유행인것 같아서 나는 Tabbar 시스템 기반으로 했다.

네비게이션 라이브러리는 ReactNavigation을 사용했다. 아마 개발자들이 가장 많이 사용하는 메뉴 시스템이고 Tabbar와 Drawer를 모두 지원한다.

https://reactnavigation.org/docs/getting-started

import React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

import Home from './src/Home';
import Profile from './src/Profile';
import Settings from './src/Settings';

const Tab = createBottomTabNavigator();

export default function App() {

  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen name="Home" component={Home}/>
        <Tab.Screen name="Profile" component={Profile}/>
        <Tab.Screen name="Settings" component={Settings}/>
      </Tab.Navigator>
    </NavigationContainer>
  );
}

코드의 결과로 아래와 같은 메뉴 시스템이 나왔다. 메뉴에 아이콘을 넣거나 좀더 예쁘게 꾸미려면 https://reactnavigation.org/docs/tab-based-navigation 를 참조하자.

Rest API Communication

Rest API 서버에서 데이터를 가지고 와서 목록 형태로 보여주는 것은 axios를 이용한다. https://github.com/axios/axios 를 참조한다. 컴포넌트가 마운트 될때 API 서버에서 데이터를 가져와서 state에 저장했다.

import axios from "axios";

...

componentDidMount() {
 axios.get("https://jsonplaceholder.typicode.com/users")
  .then(res =>{
   this.setState({ persons:res.data });
  });
}

FlatList – 목록보여주기

모바일 화면의 대부분은 목록(List)이다. 상하스크롤을 통하여 데이터 리스트를 조회 하고 화면을 선택하면 상세화면으로 진입하는 구성이 대부분이다. 이 리스트는 스크롤이 계속되는 특성상 화면에 보이는 만큼만 메모리 관리를 하지 않으면 너무 많은 메모리를 사용해서 앱을 다은 시키는 원인이 된다. React Native의 FlatList는 자동으로 메모리 관리를 해주기 때문에 필수적으로 쓰는 컴포넌트이다.

대략 아래와 같이 연결한다.

function Item({item, navigation}) {
 return (
   <View>
    <Text>{item.name}</Text>
   </View>
 )
}
....

render() {
 return (
  <View>
   <FlatList
    data={this.state.persons}
    renderItem={({item})=><Item item={item}/>}
    keyExtractor={item=>item.id}
   />
  </View>
 )
}
FlatList 결과화면

상태관리(Store)

React Native의 상태 관리는 Redux로 하는것이 기본이다. 하지만 Redux의 구조와 사용법이 직관적이지 않아서 좀더 직관적으로 쓸 수 있는 MobX를 사용했다. Redux와 MobX의 비교와 관련해서 우아한 형제의 기술블로그에 좋은 자료가 있어서 링크에서 자세한 정보를 얻을 수 있다.

https://woowabros.github.io/experience/2019/01/02/kimcj-react-mobx.html

MobX는 매우 직관적으로 Store를 사용할 수 있는데 이것은 마치 Vue.js의 Store 처럼 단순하게 사용할 수 있다. 사실 그에 비하면 Redux는 너무 복잡한 구조를 가진것이 사실이다. 대략 아래와 같은 코드를 이용하여 두개의 페이지에서 동일한 값의 증가를 확인 할 수 있다.

import {observable} from 'mobx';

class CounterStore {
 @observable counter = 0;
 
 increment() {
  this.counter++;
 }

 decrement() {
  this.counter--;
 }
}
export default new CounterStore();
import React, {Component} from 'react';
import { observer } from 'mobx-react';
import { StyleSheet, Text, View, Button } from 'react-native';

import CounterStore from '../../mobx/store'

@observer
export default class HomeScreen extends Component {
 render() {
  return (
   <View >
    <Text style={{fontSize: 60}}>
     {CounterStore.counter}
    </Text>
    <Button
     title="Increase"
     onPress={() => CounterStore.increment()}
    />
   </View>
  )
 }
}

Source Code – GitHub

아래 링크에서 위의 셈플 프로젝트의 소스코드는 아래 링크에서 다운로드 할 수 있다.

https://github.com/bipark/react_native_study

관련 링크

React 스터디 – https://ko.reactjs.org/docs/hello-world.html

Components – https://reactnative.dev/docs/activityindicator

Navigation – https://reactnavigation.org/docs/getting-started

Axios – https://github.com/axios/axios

Awesome React Native – https://github.com/jondot/awesome-react-native

리액트 프로젝트에서 MobX 사용하기 – https://velog.io/@velopert/MobX-2-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-MobX-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-oejltas52z

React에서 Mobx 경험기 (Redux와 비교기) – https://woowabros.github.io/experience/2019/01/02/kimcj-react-mobx.html

카카오 로그인 모듈 – https://github.com/react-native-seoul/react-native-kakao-login

OpenCV를 이용한 Face Detection & Blur

Face Detection & Blur

OpenCV 공부중에 주말 프로젝트로 만들어본 Go 언어 기반 얼굴인식 기능 및 블러 처리 프로그램입니다.

얼굴인식 – 이미 학습된 Caffe 기반의 딥러닝 얼굴인식 모델을 활용하여 인식하고 인식된 얼굴 부분을 블러 처리하여 웹요청에 응답합니다.

API 서버 – Go Gin 기반으로 웹 프론트에 용청에 응답을 처리

프론트 – Vue.js & Axios 등으로 Go 서버에 이미지를 전송하고 결과를 받아서 화면에 뿌려줍니다.

Open Source

이 프로젝트의 소스코드는 아래 링크를 참조하세요.

https://github.com/bipark/go-opencv-caffe-facedetect-blur

의존성

실행방법

GraphQL + MySQL 데이터 가져오기 스터디

네이버에 개발자 모집 공고를 보다가 React Native 개발자를 구하는데 GraphQL 기반의 백엔드 프로그래밍 역할도 필요하다는 것을 보고 놀랐다. 세상이 진짜 변하긴 했다.

물론 프론트 개발자가 백엔드를 동시에 작업할 수 있으면 매우 생산성이 높고 문서작업 & 협의 과정 없이 빠르게 서비스를 만들수 있다. 나는 Node.js 와 Vue.js를 이용해서 그런방식으로 작업을 하고 있는데 매번 프론트에서 엔드포인트가 필요할때마다 Node에서 api를 만들고 그걸 받아서 쓰고 있지만 프론트에서 이걸 끝내는 방법이 없나 하는 생각은 늘 하고 있던 차였다.

GraphQL이 이걸 해줄 수 있다고 해서 공부를 해봐야지 한게 벌써 두해를 넘기고 있기도 하고 여러가지 문서를 읽어 봤지만 역시 만들어 보는게 최고라… 그게 뭔지는 대충 아래 링크로 퉁치고 Hello World 와 좀 더 실제적인 Mysql DB에서 데이터를 읽어와서 뿌려주는데까지 한번 해보기로 했다.

Node.js & Express.js 로 서버를 구성하면 빠른방법으로 Hello World를 찍어 볼 수 있다.

> express --view=pug
> npm install express-graphql graphql

Express Generator를 이용해 프로젝트를 생성하고 npm으로 express-graphql, graphql을 설치했다.

var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');

var app = express();

var schema = buildSchema(`
  type Query {
    hello: String
  }  
`);

var root = {
  hello: ()=>{
    return 'Hello World'
  },
};

app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}));

코드를 실행하고 http://localhost:3000/graphql 에 접속하면 다음과 같은 화면을 볼 수 있다. 이것은 app.use() 에서 graphiql 옵션을 true로 셋팅하면 나타나는 화면으로 GraphQL의 테스트 인터페이스이다. 여기서 api를 미리 테스트 해볼 수 있다. 자동완성 기능도 지원한다.

Hello World는 찍었다. 대충 이제 다 알았다…는 아니고 현재 가장 많이 쓰고 있는 DB인 MySQL에 연결해서 데이터를 가져오는데 까지 해야한다. Node에서 MySQL 연결과 기타 과정은 생략하고 아래와 같은 코드를 작성 했다.

var app = express();
var schema = buildSchema(`
  type Query {
    user: [User]
  }
  
  type User {
    name: String
    age: Int
    sex: String
  }  
`);

var root = {
  user: ()=>{
    return new Promise(function (resolve) {
      pool.query('select * from users', function (err, res) {
        resolve(res);
      });
    });
  },
};

app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}));

의의 코드를 실행한 결과로 아래와 같은 화면을 얻었다.

분명 데이터는 나왔는데 뭔가 잘못한게 아닌가 싶은 생각이 들었다. 타입도 서버에서 선언해 줘야 하고 쿼리도 서버에서 실행시킨다면 이게 기존의 REST API와 무었이 다른가? 그저 한개의 엔드 포인트를 쓴거 말고 다른게 있나 싶은 생각이 들었다.

그래서 좀 더 검색을 해봤다. 아래 링크에서 하나의 엔드포인트가 중요한 차이점이며 Query의 요청과정에서 파라메타의 차이로 결과를 다르게 가져올 수 있다는 점이 중요하다고 알려줬다.

https://www.holaxprogramming.com/2018/01/20/graphql-vs-restful-api/

결론

나는 이걸 어떻게 써야 하나 하는 문제가 고민이었는데 어차피 현재 Node.js 를 서버로 쓰고 있기 때문에 기존의 REST API에 GraphQL을 더 붙여 써도 괜찮지 않을까 하는 생각을 했다.

개발자들이 뭔가 바꾸고 싶으면 아주 갈아엎는(?) 나쁜 습성이 있지만 그러지 않아도 된다면 엔드 포인트 한두개씩 GraphQL로 바꿔도 무방할지 않을까 싶다.

소스코드는 아래 링크에 있습니다.

https://github.com/bipark/graphql_study