지난 포스팅에서 소개했던 oauth2-restapi-server 의 샘플 앱은 브라우저 상에서 url 로 실행가능한 앱이다. 이번 포스팅에서는 동일한 기능을 하지만 브라우저 없이 독립적으로 실행가능한 oauth2-restapi-server 의 하이브리드 샘플 앱을 소개하려고 한다.
뭐, 하이브리드 앱 (Hybrid App) ?
모바일 단말 (스마트폰, 태블릿 등)의 네이티브 앱은 모바일 플랫폼 별로 다른 언어로 개발되는데 대표적으로 iOS 경우 Objective-c 로, Android 경우 Java 로 만들어진다. 그리고 모바일 앱으로 서비스를 런칭하기 위해 iOS, Android 용 둘 다 개발하는 것이 일반적이다. 이 경우 똑같은 기능을 하는 앱을 몇가지 언어로 만들어야 하니 최초 개발, 확장 및 유지보수를 생각했을 때 회사 그리고 개발자들에겐 참으로 번거로운 일이 아닐 수 없다. 하나의 언어로 앱을 개발하여 여러 모바일 플랫폼에서 사용할 수 있게 할 수는 없을까. ‘크로스 플랫폼’을 이용하면 가능하다. 컴파일을 해야하는 네이티브 언어 쪽의 대표적인 ‘크로스 플랫폼’ 은 Xamarin (C# 으로 앱개발) 이고, 웹 언어 쪽에선 Cordova (Phonegap 의 오픈소스 버전) 이다. 여기에선 웹에 초점을 맞추고 있기 때문에 Cordova 만 간략히 언급하도록 하겠다. Cordova 의 공식 사이트에선 다음과 같이 Cordova 를 정의하고 있다.
Apache Cordova is a platform for building native mobile applications using html, css, and javascript
즉, Cordova 로 만든 앱의 컨텐츠는 html, css, javascript 같은 웹 언어로 만들어진다. 모바일 플랫폼 별로 웹언어를 실행해주기 위한 네이티브 레벨의 WebView 컴포넌트들이 제공되고 있다. iOS는 UIWebView 클래스를, Android는 WebView 클래스를 제공하고 있다. 네이티브 레벨에서 이들 컴포넌트를 이용해서 html, css, javascript 로 구성된 컨텐츠를 화면에 표시할 수 있다. Cordova 의 경우 이러한 WebView 컴포넌트를 포함하는 네이티브 소스 코드를 자동으로 생성해주고, 패키징해준다. 그래서 Cordova를 native wrapper 라고 부르기도 한다. Cordova 앱 개발자는 네이티브 레벨은 생각할 필요가 없고, 단지 WebView 컴포넌트 위에서 로딩 및 렌더링될 html, css, javascript 컨텐츠를 만들기만 하면 된다. 또Cordova 는 native wrapper 역할 뿐만 아니라 카메라, 센서 등 디바이스의 자원에 접근하기 위한 javascript 형태의 web api 를 제공한다. 이 web api 은 iOS, Android, window phone 등 지원되는 모바일 플랫폼 별로 다르지 않기 때문에 Cordova app 을 하나 만들면 변경없이 여러 모바일 플랫폼에 사용할 수 있다. 이처럼 Cordova 로 만든 앱은 컨텐츠는 웹으로, 패키징/설치/실행은 네이티브로 하고 있기 때문에 하이브리드 (hybrid) 앱이라 부른다. 내가 생각하기에 하이브리드의 앱의 단점으로는 실행 성능이 순수 네이티브 앱에 비해 느리다는 점이다. 왜냐하면 iOS, Android 에서의 하이브리드 앱은 순수 네이티브 앱과 다르게 Webkit 웹엔진으로 렌더링 및 플랫폼 리소스 접근하고, Javascript 엔진으로 Javascript 코드 해석하고 실행하는 등 추가적인 작업을 하기 떄문이다. 또 다른 단점으로는 네이티브 레벨에서 지원되는 tab bar, navigation bar 같은 UX 관련 컴포넌트들을 사용할 수 없다는 점도 있겠다. 하이브리드 앱으로 서비스를 하던 링크드인과 페이스북이 순수 네이티브 앱으로 바꾼 이유도 방금 언급한 두가지 단점과 관련이 있다. 그렇다고 하이브리드 앱을 쓰지 않을 건가. 시간이 흐르면서 이러한 단점들은 사라지지 않을까 생각한다. 이유는 속도 측면에선 하드웨어 성능과 웹엔진과 javascript 엔진 자체의 성능이 꾸준히 높아지고 있고, UX 측면에선 네이티브 UX 컴포넌트를 html, css, javascript 로 구현하여 cordova 와 integration 시킨 오픈소스들이 생겨나고 있기 때문이다 (으흑, 정말 고마울 따름 흑흑;;). 대표적인 것으로는 ionic 과 onsen-ui 가 있다. 네이티브 앱을 쓸건가 하이브리드 앱을 쓸건가는 서비스의 성격과 회사/개발자가 처한 상황에 맞게 선택해야 할 부분이지만 나처럼 1인 개발을 하면서 아이디어를 구현하여 빨리 시장에 내놓으려고 한다면 하이브리드 앱이 적절하지 않을까 생각한다.
브라우저 기반 앱을 하이브리드 앱으로!
지난 포스팅에서 소개했던 oauth2-restapi-server 의 브라우저 기반 샘플 앱을 cordova 를 사용해 하이브리드 앱으로 변경한 과정을 설명하고자 한다. 기존의 브라우저 기반 샘플 앱의 소스 코드는 서버에 위치해 있지만, UI와 로직의 실제 실행은 서버가 아닌 서버에 접속한 디바이스에서 이루어진다. 이것은 중요한데 이유는 하이브리드 앱을 만들기 위해서 앱 소스코드를 서버가 아닌 로컬로 가져오더라도 oauth2-restapi-server 는 전혀 변경할 필요가 없기 때문이다. RESTful API로 서버와 데이터를 주고 받을 수 있도록 미리 샘플 앱을 만들어놓은 덕분이다. 브라우저 기반 샘플 앱을 하이브리드 앱으로 변환하기 위해서 다음과 같은 작업이 필요하다.
- cordova 설치
- cordova 프로젝트 생성
- cordova plugin인 InAppBrowser 설치
- server 상에 있던 app 소스를 “<project>/www/” 으로 복사
- bower.json 및 .bowerrc 를 “<project>/” 에 생성
- app 소스에 cordova.js import 및 초기화
- rest api 의 url 을 절대경로로 바꾸기
- ‘InAppBrowser’를 이용해 access token 받아오기
각 작업에 대해 하나씩 아래에서 설명하도록 하겠다.
cordova 설치, cordova project 생성, cordova plugin 설치
cordova 는 node.js 로 만들어져 있으며, 아래와 같은 순서로 진행하였다.
$ sudo npm install -g cordova $ cordova create hybrid com.example.hybrid MyHybrid $ cd hybrid $ cordova platform add ios $ cordova plugin add org.apache.cordova.inappbrowser
cordova 커맨드라인 명령어 대한 자세한 설명은 아래 링크를 참고 바란다.
http://cordova.apache.org/docs/en/3.5.0/guide_cli_index.md.html
bower, ‘cordova.js’ import, rest api 절대경로로 변경
브라우저 기반 샘플 앱은 angular.js, bootstrap 등 외부 javascript, css 라이브러리를 bower 툴을 이용해 하위 디렉토리에 다운로드 받아서 사용했었는데, 로딩 성능을 위해서 하이브리드 앱에서도 bower 를 이용해 미리 로컬에 그런 라이브러리들을 다운로드하여 앱에 패키징되게 하였다. bower 를 사용하기 위해서 두개의 파일 bower.json 과 .bowerrc 를 만든다. bower.json 에는 다운로드하려는 javascript, css 와 버전을 지정하고, .bowerrc 에는 다운로드될 위치를 지정한다. 그리고 아래의 명령어를 실행하면 bower.json 에 지정된 라이브러리들이 지정된 위치에 다운로드 된다.
$ bower install
그리고 변환된 하이브리드 앱이 cordova의 device api를 사용하지는 않지만, cordova의 plugin인 InAppBrowser 를 사용하기 때문에 cordova 관련 javascript 오브젝트들을 생성하여 초기화하는 cordova.js 파일을 하이브리드 앱의 시작페이지인 index.html 안에서 import 하였다. 다른 html 에는 cordova.js 를 import 하지 않았는데 이유는 샘플 앱이 Single page app (SPA)이기 때문이다. 아래처럼 하이브리드 앱의 시작 페이지에서 한번 cordova.js 를 로딩하는것으로 충분하다. [gist f597ddf76e59f169f62d] 그리고 angular.js 가 초기화되면서 cordova의 device api 를 호출하는 등 cordova 관련 작업을 수행할 수도 있기 때문에 하이브리드 샘플의 angular.js 의 초기화는 cordova 초기화가 완료되어 cordova 오브젝트를 사용할 수 있는 시점인 ‘deviceready’ 이벤트 발생 이후로 하였다. 만약 하이브리드 앱이 시작될 때 cordova 의 오브젝트를 전혀 사용하지 않는다면, <html ng-app=’myApp’> 또는 <body ng-app=’myApp> 처럼 시작페이지가 로딩될 때 자동으로 angular를 초기화하도록 해도 된다. 아래 angular.bootstrap() 은 명시적으로 angular 를 초기화하도록 요청하는 함수이다. [gist 55d93626673a022a9454] 그리고 서버 상에 앱 소스가 존재할 경우 oauth2-restapi-server 와 동일한 위치에 있었기 때문에, 앱 소스에서 angular.js의 $resource 에 상대 경로 url을 넣어줘도 AJAX 요청이 성공했다. 하지만 하이브리드 앱의 경우 $resource 를 수행하는 파일이 로컬에 있기 때문에 oauth2-restapi-server의 절대경로 넣어주지 않으면 안된다. 아래 prefix 변수에 할당된 url 은 내가 띄워놓은 oauth2-restapi-server 의 url 이다. [gist 864891177879dccb68c3]
InAppBrowser 로 타사 소셜 앱의 access token 받아오기
타사 소셜앱 계정으로 샘플 앱에 signup 또는 login 하기 위해서는 샘플 앱이 타사 소셜앱 서버가 발행해준 access token 을 받아와야 한다 (소셜 앱 서버가 발행한 access token 을 oauth2-restapi-server 와 샘플 앱이 어떻게 활용하는지에 대해서는 이전 포스팅의 내용을 참고바란다). 브라우저 기반의 앱으로 동작할 땐 아래처럼 same origin 관계를 활용하여 새롭게 열린 탭 또는 윈도우에서 window.opener.authState() 함수를 호출하면서 샘플 앱에게 타사 소셜앱 서버가 발행해준 access token 을 넘겨주었다. 아래는 oauth2-restapi-server 가 타사 소셜앱과 access token 교환을 마친후 샘플 앱의 열려있는 탭 또는 윈도우에 보내는 html 내용이다. ouath2-restapi-server 는 이 파일을 보내기 전에'<%= %>’ 로 된 부분에 적절한 값을 미리 넣는다. 아래 data.token 에는 실제로 타사 소셜앱 서버로부터 받은 access token 이 들어가 있다. [gist 06ea8386432e760b6961] 그러나 하이브리드 앱에선 서버의 페이지 내에서 window.opener 로 값을 전달하는 위의 솔루션이 동작 하지 않는다. 이유는 same origin 이 아니기 때문에 Webkit 엔진에서 서로의 window 오브젝트간 DOM 접근을 하지 못하게 막는다. 앱이 띄운 새 윈도우(또는 탭) 에 로딩된 서버 페이지의 origin은 나의 경우 https://ec2-54-199-141-31.ap-northeast-1.compute.amazonaws.com:3443 이다. 그리고 window.opener 에 해당하는 앱 자체의 origin은 없다 (원래 로컬 파일은 origin 이 없다). 그럼 어떻게 하이브리드 앱이 타사 소셜 앱의 access token 을 받게 할 수 있을까. cordova의 plugin인 InAppBrowser 를 이용하면 가능하다. 기본 아이디어는 다음과 같다.
- window.open()의 두번째 인자에 ‘_blank’ 를 지정해서 새로운 윈도우를 띄운다
- 이 새 윈도우에는 타사 소셜앱 서버의 공식 authorization page가 처음 로딩되고,
- oauth2-restapi-server 의callback url (사전에 해당 소셜앱 서버에 등록된 url) 에 code 및 access token 값이 붙어서 리다이렉션 된다.
- 그 다음, oauth2-restapi-server 는 타사 access token 값을 포함하는 html 을 하이브리드 앱이 띄워놓은 이 윈도우에게 전달한다.
- 하이브리드 앱은 oauth2-restapi-server 가 보내는 html (with access token) 의 url 을 모니터링한다.
- <child window>.addEventListener(‘loadstop’, callback) 를 이용해 페이지 로딩이 완료후 callback 함수에서 특정 url 체크를 한다
- url 이 매치된다면, callback 함수 내에서 <child window>.executeScript(code, callback) 를 이용해 새 윈도우 페이지 내 정의된 특정 함수를 호출한다.
- 새 윈도우 페이지 내 해당 함수가 정상적으로 호출되면 리턴값으로써 유효한 값이 들어있는 type, data 변수들을 보낸다.
- 정상적으로 <child window>.executeScript(code, callback)가 수행되었다면 지정된 callback함수가 호출되는데 callback함수 안에서 명시적으로 <child window>.close() 를 호출하여 열린 윈도우를 닫는다.
위의 아이디어를 구현하기 위해서 먼저 기존에 서버가 보내던 html 에 getResult() 라는 함수를 추가했다. 당연히 아래 소스는 oauth2-restapi-server 소스 디렉토리에 위치한다. 이 함수는 executeScript의 첫번째 인자 값으로 사용된다. [gist 190a648f0830a6348ff9] 아래 소스는 angular.js 에 등록한 service 들이 정의된 소스파일이다. 재사용을 위해서 InAppBrowser를 다루는 부분을 하나의 service 로 구현했다. ref 변수는 window.open 으로 생성된 child window object 이다. 여기서 하나 주의해야 할 것은, 이 child window object 가 일반적인 window object 가 아니라는 것이다. window.open 하는 순간 InAppBrowser 플러그인이 특수한 이벤트와 함수들을 window object 에 붙이고, 반대로 몇몇 표준 함수들 및 변수들은 제거한다. InAppBrowser 플러그인을 설치했다면, 새롭게 window.open으로 리턴된 child window object 를 다룰땐 InAppBrowser 플러그인이 제공하는 기능들만이 사용하는 것이 좋을 것 같다. getResult() 함수는 아래 ref.executeScript()의 첫번째 인자로 들어간다. ‘loadstop’ 이벤트 발생시 체크하는 url 은 /auth/login/<social service name>/callback/success과 /auth/login/<social service name>/callback/failure 두 가지 이다. 한가지 주의해야 할 점은, 실제로 위의 과정을 통해 타사 소셜 앱의 access token 갖게 되는 executeScript()에 지정된 콜백 함수가 angular.js 컨텍스트 밖에서 실행되고 있다는 것이다. 이 때문에 해당 콜백 함수에서 $rootScope 이나 $scope 의 자식 오브젝트 값을 변경하거나 함수를 호출하려는 경우에는 $apply() 를 이용해서 명시적으로 angular.js 컨텍스트 들어가서 $rootScope 이나 $scope 에 접근하게 해야 한다. 하이브리드 샘플 앱의 경우 executeScipt() 에 지정된 콜백 함수 내에서 $rootScope 의 커스텀 이벤트를 발생시켜야 해서 ‘$rootScope.$apply(function () { … }) ‘ 같이 $apply 사용하게 했다. [gist e66e97dcea9aa5b9e472] 사용자가 소셜앱 계정으로 로그인을 하려거나 로그인 이후 다른 소셜앱 계정을 연결(connect) 하려고 할 때 각각의 angular controller 에서 아래 $scope 함수들이 호출된다. 두 함수 모두 하는 일은 동일하며 window.open 의 첫 인자에 넣어주는 url 만 다르다. url 을 다르게 하는 이유는 oauth2-restapi-server 가 이 url 을 보고 로그인을 위한 것인지 연결을 위한 것인지 구분하도록 하기 위함이다. [gist 0a5fe47ba846101ed953] 아래는 iOS 플랫폼으로 동작하는 디바이스인 iPhone 에서 하이브리드 앱을 실행하여 타사 소셜앱 계정으로 로그인 수행 전/후를 캡처한 스크린 샷이다. 앱의 기능이나 UI 측면에선 지난 포스팅에서 소개한 브라우저 기반 샘플 앱과 다른 점이 없다. 이렇게 cordova 로 만든 이 하이브리드 앱은 Android 및 Window Phone 에서도 그대로 재사용될 수 있다.
하이브리드 샘플 앱 소스 코드
변경한 하이브리드 샘플 앱 소스코드는 아래 github 에서 확인할 수 있다.
마무리하며..
이번 포스팅에선 하이브리드 앱에 대해 설명하고, oauth2-restapi-server 와 함께 동작하는 브라우저 기반 앱을 cordova를 사용해 하이브리드 앱으로 변환하기 위해서 필요한 작업들을 살펴보았다. 이미 살펴본바대로 앱 소스를 서버에서 로컬로 가져올 때 발생하는 문제들만 해결해준다면, 하이브리드 앱에서는 기본적으로 브라우저 상에서 동작하던 html, js, css 를 그대로 사용할 수 있다. oauth 인증과 access token 기반으로 동작하는 하이브리드 앱을 준비하는 개발자들에게 작은 도움이 되었으면 하는 바램으로 글을 마무리 한다.