캠프앱 개발 사례를 통해 본 하이브리드앱 어디까지 | devon 2012
DESCRIPTION
TRANSCRIPT
캠프앱 개발 사례를 통해 본
하이브리드앱 어디까지
다음 커뮤니케이션유승근
�������������
오늘 얘기할 순서 ...
• 개발 전- Cross Platform
- 캠프 하이브리드
• 개발 중- 화면 영역별 구현 방법- WebView & Web-App Bridge
- Application Cache
- 버전 관리
�������������
오늘 얘기할 순서
• 오픈 후(최적화)
- WebView Request Intercept
- JS 크기 줄이기- Touch 이벤트
• 마무리하며
�������������
�������������
Write Once, Run Anywhere
�������������
• JavaScript Libraries :
- jQuery Mobile, Sencha Touch
• Wrappers around web applications :
- Adobe PhoneGap, KTH Appspresso
• Transformers to Native code :
- Appcelerator Titanium
• Adobe Air
Cross-Platform Tools: Build Once and Run Everywhere
Cross Platform Tools
�������������
Web App
캠프 하이브리드앱
�������������
Web App Native
캠프 하이브리드앱
�������������
Web App Native
캠프 하이브리드앱
Hybrid�������������
캠프 하이브리드앱
• 기대- One Source Multi Use
- 상대적으로 업데이트 용이- Web UI 구현 문제 해결
• 우려- 시스템 복잡도 증가- 웹과 앱의 배포 시점 동기화- WebView 성능 문제
�������������
개발 시작!
�������������
Native / Web무엇으로 구현해야 하나?
�������������
Native 영역
• 레이아웃- 네비게이션바, 메뉴- position: fixed
• 사용자 입력 Form
- 이미지 업로드- 날짜 입력 등 복잡한 입력 요소들
• 다음 공통 컴포넌트- 로그인, 지도, 이미지뷰어
�������������
Native 영역
• 레이아웃- 네비게이션바, 메뉴- position: fixed
• 사용자 입력 Form
- 이미지 업로드- 날짜 입력 등 복잡한 입력 요소들
• 다음 공통 컴포넌트- 로그인, 지도, 이미지뷰어
�������������
Native 영역
• 레이아웃- 네비게이션바, 메뉴- position: fixed
• 사용자 입력 Form
- 이미지 업로드- 날짜 입력 등 복잡한 입력 요소들
• 다음 공통 컴포넌트- 로그인, 지도, 이미지뷰어
�������������
Native 영역
• 레이아웃- 네비게이션바, 메뉴- position: fixed
• 사용자 입력 Form
- 이미지 업로드- 날짜 입력 등 복잡한 입력 요소들
• 다음 공통 컴포넌트- 로그인, 지도, 이미지뷰어
�������������
Native 영역
• 레이아웃- 네비게이션바, 메뉴- position: fixed
• 사용자 입력 Form
- 이미지 업로드- 날짜 입력 등 복잡한 입력 요소들
• 다음 공통 컴포넌트- 로그인, 지도, 이미지뷰어
�������������
input Focus
�������������
input Focus
iOS 6 webview.keyboardDisplayRequiresUserAction = NO;
�������������
WebView
�������������
WebView
• Android
- WebView
- WebSettings
- WebChromeClient
- WebViewClient
• iOS
- UIWebView
- UIWebViewDelegate
�������������
Android 4.0.4
Browser WebView
�������������
iOS 6
Safari UIWebView
�������������
Native to Web Call
• Android
- webview.loadUrl("http://camp.daum.net")
- webview.loadUrl("javascript:hello('world')")
• iOS
- [webview loadRequest:]
- [webview stringByEvaluatingJavaScriptFromString:]
�������������
Web to Native Call
• location.href
- WebViewClient.shouldOverrideUrlLoading()
- webView:shouldStartLoadWithRequest:navigationType:
• window.prompt
- Android에서만 사용- WebChromeClient.onJsPrompt()
• Android - webview.addJavaScriptInterface()
- Java 객체를 JavaScript 객체로 삽입하여 public method 호출 가능- iOS, Android 동일 인터페이스로 제공 위해 미사용
�������������
Internal URL Scheme
• Web to Native 명령 규칙
scheme://namespace/action/param1/...
ex) daumcamp://daum.camp/camp/10
�������������
location.href
• URL 변경을 연속해서 발생시키면 마지막 하나만 호출
• 해결책- setTimeout : 호출 시점 보장 못함
- iframe
function callByIframe(scheme) { var iframe = document.createElement('iframe'); iframe.src = scheme; document.body.appendChild(iframe); setTimeout(function() { document.body.removeChild(iframe); }, 100);};
�������������
Web to Native Callback
• Native 호출 후 응답이 필요한 경우
location.href = 'daumcamp://daum.camp/post/postComplete';
callback JS 함수
�������������
Cookie
• Android
- CookieManager
- CookieSyncManager
• iOS
- NSHTTPCookie
- NSHTTPCookieStorage
• Cookie 관리자는 Singleton 객체
�������������
Application Cache
�������������
Application Cache
<html manifest="appcache.manifest"> ...
<script>
applicationCache.addEventListener('updateready', function(e) { applicationCache.swapCache(); window.reload(); });
</script> ...
</html>
�������������
Application Cache Manifest
CACHE MANIFEST# 2012-06-18:v3
CACHE:index.htmlcss/style.css
FALLBACK:/ /offline.html
NETWORK:*
�������������
cached progress... downloadingchecking 최초실행
noupdatechecking 변경없음
updateready progress... downloadingchecking 앱캐쉬 업데이트
obsoletechecking 앱캐쉬 제거
error • • •checking 에러
Application Cache - Events
�������������
checking
�������������
downloadingchecking
�������������
progress... downloadingchecking
�������������
updateready progress... downloadingchecking
appCache.swapCache(); window.reload();
�������������
updateready progress... downloadingchecking
�������������
MDPI HDPI
이미지 해상도 대응
�������������
이미지 해상도 대응
• Cookie를 이용한 해결책- Native에서 해상도 정보 취득- 로딩 전 WebView에 해상도 정보를 cookie로 굽기- manifest 파일은 cookie 값을 바탕으로 동적 생성
�������������
float density = Context.getResources() .getDisplayMetrics().density;
String pixelRatio = "1";
if (density >= 2.0) { pixelRatio = "2";} else if (density >= 1.5) { pixelRatio = "1.5";}
// setCookie pixelRatio
이미지 해상도 대응
�������������
BOOL ios4 = [[UIScreen mainScreen] respondsToSelector: @selector(displayLinkWithTarget:selector:)];
String* pixelRatio = @"1";
if (ios4 && [UIScreen mainScreen].scale == 2.0) { // Retina display pixelRatio = @"2";}
// setCookie pixelRatio
이미지 해상도 대응
iOS 4 이상인지 확인
�������������
#set ($IMG_TYPE = "480")
#if ($PIXEL_RATIO == "1") #set ($IMG_TYPE = "320")#endif
#set ($PATH = "http://m1.daumcdn.net/m/${IMG_TYPE}/")
...
CACHE:$!{PATH}/ico_camp01.png$!{PATH}/ico_camp02.png
이미지 해상도 대응
Velocity Template
�������������
주의 사항
• html 파일은 항상 캐쉬
• 업데이트는 리소스 파일이 아닌 Manifest 파일의 변경
• Application cache 제거시 http 응답은 404
- obsolete 이벤트 처리 필요
• Android 2.1은 Network 섹션에서 * 선택자 미지원- 명시되지 않은 네트워크 요청은 모두 취소
�������������
버전 관리
�������������
App1.0.0 Web 1.0.0
�������������
App1.0.0 Web 1.0.0
Web 1.0.1
�������������
App1.0.0 Web 1.0.0
Web 1.0.1
Web 1.0.2
�������������
App1.0.0 Web 1.0.0
Web 1.0.1
Web 1.0.2
Web 1.0.3App1.0.1
�������������
App1.0.0 Web 1.0.0
Web 1.0.1
Web 1.0.2
Web 1.0.3
Web 1.0.4
App1.0.1
�������������
App1.0.0 Web 1.0.0
Web 1.0.1
Web 1.0.2
Web 1.0.3
Web 1.0.4
App1.0.1
MappingTable
�������������
Version Mapping Table
<bean ... >
" <property name="version" value="1.0.10-p6"/>
" <property name="androidVersionMap">" " <props>" " " <prop key="[1.0.0,1.0.4)">1.0.6</prop>" " " <prop key="[1.0.4,)">1.0.10-p6</prop>" " </props>" </property>
" <property name="iosVersionMap">" " <props>" " " <prop key="[1.0.0,1.0.2)">1.0.9-p3</prop>" " " <prop key="[1.0.2,)">1.0.10-p6</prop>" " </props>" </property>
</bean>
�������������
Version Mapping Table
<bean ... >
" <property name="version" value="1.0.10-p6"/>
" <property name="androidVersionMap">" " <props>" " " <prop key="[1.0.0,1.0.4)">1.0.6</prop>" " " <prop key="[1.0.4,)">1.0.10-p6</prop>" " </props>" </property>
" <property name="iosVersionMap">" " <props>" " " <prop key="[1.0.0,1.0.2)">1.0.9-p3</prop>" " " <prop key="[1.0.2,)">1.0.10-p6</prop>" " </props>" </property>
</bean>
최신 웹 버전
�������������
Version Mapping Table
<bean ... >
" <property name="version" value="1.0.10-p6"/>
" <property name="androidVersionMap">" " <props>" " " <prop key="[1.0.0,1.0.4)">1.0.6</prop>" " " <prop key="[1.0.4,)">1.0.10-p6</prop>" " </props>" </property>
" <property name="iosVersionMap">" " <props>" " " <prop key="[1.0.0,1.0.2)">1.0.9-p3</prop>" " " <prop key="[1.0.2,)">1.0.10-p6</prop>" " </props>" </property>
</bean>
Android VersionMapping
�������������
Version Mapping Table
<bean ... >
" <property name="version" value="1.0.10-p6"/>
" <property name="androidVersionMap">" " <props>" " " <prop key="[1.0.0,1.0.4)">1.0.6</prop>" " " <prop key="[1.0.4,)">1.0.10-p6</prop>" " </props>" </property>
" <property name="iosVersionMap">" " <props>" " " <prop key="[1.0.0,1.0.2)">1.0.9-p3</prop>" " " <prop key="[1.0.2,)">1.0.10-p6</prop>" " </props>" </property>
</bean>
iPhone VersionMapping
�������������
Version Mapping Table
<bean ... >
" <property name="version" value="1.0.10-p6"/>
" <property name="androidVersionMap">" " <props>" " " <prop key="[1.0.0,1.0.4)">1.0.6</prop>" " " <prop key="[1.0.4,)">1.0.10-p6</prop>" " </props>" </property>
" <property name="iosVersionMap">" " <props>" " " <prop key="[1.0.0,1.0.2)">1.0.9-p3</prop>" " " <prop key="[1.0.2,)">1.0.10-p6</prop>" " </props>" </property>
</bean>
App Version Range Web Version
�������������
App to Web URL
http://camp.daum.net/app/{platform}/{version}/...
ex) http://camp.daum.net/app/ios/1.0.2/...
�������������
고민거리
• 현재 JS/CSS 만 버전 관리 대상
• HTML, Manifest 파일도 버전관리 해야 하지 않을까?
�������������
오픈 후...
�������������
�������������
SunSpider JavaScript Benchmark
�������������
0
1,750
3,500
5,250
7,000
Android 2.3 Android 4.0
Browser WebView
Androidru
nnin
g tim
e(m
s)
�������������
0
3,000
6,000
9,000
12,000
iOS 4.3 iOS 5 iOS 6
Safari WebView
iOSru
nnin
g tim
e(m
s)
�������������
장인은 도구를 탓하지 않는다.
�������������
Web Application 최적화
�������������
로딩 과정
css, js load/parse/eval
init()
Load startonDOMContentLoaded
onLoad
Boot
router / data load / render
onHashchange
View
�������������
로딩 과정
css, js load/parse/eval
init()
Load startonDOMContentLoaded
onLoad
Boot
router / data load / render
onHashchange
View
App에서는 지속적 발생
�������������
로딩 중 구멍 찾기
�������������
Load Start
CSS
<script src="/env">
baselib.jscamp.js
init()
View Render
no-cache
application cache
application cache
웹앱 실행시 필수 정보그러나 앱에도 존재하는 정보
�������������
Script Blocking
Script Loading
�������������
Script Blocking
Script Loading API call
�������������
Script Blocking
Script Loading API call Complete
�������������
Script Blocking
Script Loading
�������������
Script BlockingLoad Start
CSS
<script src="/env">
baselib.jscamp.js
init()
View Render
no-cache
app cache
app cache
Web
�������������
Script BlockingLoad Start
CSS
<script src="/env">
baselib.jscamp.js
init()
View Render
no-cache
app cache
app cache
Web
App
Intercept
Http Response
App에 있는 정보 바탕으로응답 생성
�������������
WebView Request Intercept
• Android
- webViewClient.shouldInterceptRequest()
- API Level 11 - Android 3.0 이상 지원
• iOS
- NSURLCache class의cachedResponseForRequest: method override
- [NSURLCache setSharedURLCache:]
�������������
JS 코드 줄이기
�������������
JS 코드 줄이기
• Google Closure Compiler
- Minify & Code Merging
• SIMPLE_OPTIMIZATIONS
- 공백, 주석 삭제, Local 변수를 짧은 변수명으로 변경- 도달하지 않는 코드 제거 / 미사용 변수 제거
�������������
JS 코드 줄이기
var isDev = false;
function hello(name) { if (isDev) { console.log('hello called'); } return 'Hello, ' + name;}
hello('New user');
�������������
JS 코드 줄이기
var isDev = false;
function hello(name) { if (isDev) { console.log('hello called'); } return 'Hello, ' + name;}
hello('New user');
var isDev=!1;function hello(a){isDev&&console.log("hello called");return"Hello, "+a}hello("New user");
SIMPLE_OPTIMIZATION
�������������
JS 코드 줄이기
var isDev = false;
function hello(name) { if (false) { console.log('hello called'); } return 'Hello, ' + name;}
hello('New user');
�������������
JS 코드 줄이기
var isDev = false;
function hello(name) { if (false) { console.log('hello called'); } return 'Hello, ' + name;}
hello('New user');
function hello(a){return"Hello, "+a}hello("New user");
SIMPLE_OPTIMIZATION
�������������
JS Pre-Processor
• Idea
- 플랫폼 구분자를 상수로 치환하여 컴파일러가 제거 가능하게 하자- 플랫폼 별로 별도의 JavaScript 파일 생성
• JavaScript Parser
- Esprima : Source Code > Abstract Syntax Tree
- Escodegen : Abstract Syntax Tree > Source Code
�������������
Source Code
if (camp.isApp) {" camp.Util.installExtAnchorHandler();" if (camp.os === camp.OS.IOS) {" " camp.UI.navi.init();" " camp.Util.hideAddressBar();" }} else {" // 메뉴, 네비 적용" camp.UI.navi.init();" camp.UI.menu.init();" // body의 높이를 window.height에 맞춤" camp.Util.hideAddressBar();" // daum id 저장" camp.Env.uid = camp.Util.getUserId();" // 로그인 인증 쿠키 갱신 타이머 시작" camp.Function.startLoginCheckTimer();}
�������������
Pre-processed Code
if (true) { camp.Util.installExtAnchorHandler(); if ('ios' === 'ios') { camp.UI.navi.init(); camp.Util.hideAddressBar(); }} else { camp.UI.navi.init(); camp.UI.menu.init(); camp.Util.hideAddressBar(); camp.Env.uid = camp.Util.getUserId(); camp.Function.startLoginCheckTimer();}
아이폰 앱인 경우
�������������
Compiled Code
camp.Util.installExtAnchorHandler();camp.UI.navi.init();camp.Util.hideAddressBar();
camp.isApp?(camp.Util.installExtAnchorHandler(),camp.os===camp.OS.IOS&&(camp.UI.navi.init(),camp.Util.hideAddressBar())):(camp.UI.navi.init(),camp.UI.menu.init(),camp.Util.hideAddressBar(),camp.Env.uid=camp.Util.getUserId(),camp.Function.startLoginCheckTimer());
• 전처리기 처리후 컴파일된 코드 (83 byte)
• 원 소스 코드로 컴파일된 코드 (262 byte)
�������������
JavaScript Build Process
org-JS pre-processor
MW-JS
AND-JS
IOS-JS
google-closure
MW-bin-JS
AND-bin-JS
IOS-bin-JS
�������������
JS Pre-Processor
• Esprima에 없는 것들- Comment Parser
- Semantic Analysis
�������������
Touch
�������������
Single Touch ≒ Click
touchstart
touchend
mouseover
mousemove
mousedown
mouseup
click
Touch Events
EmulatedMouse Events
�������������
Single Touch ≒ Click
touchstart
touchend
mouseover
mousemove
mousedown
mouseup
click
Touch Events
EmulatedMouse Events
300ms delay
�������������
�������������
Touch 이벤트로 바꾸면 되겠네?
�������������
Ghost Click
• Touch 이후에 click 발생• Clickable Element
- Link
- Form Button
- onclick 이벤트 등록 element
�������������
Ghost Click
• Touch 이후에 click 발생• Clickable Element
- Link
- Form Button
- onclick 이벤트 등록 element
�������������
Ghost Click
• event.preventDefault()로 마우스 이벤트 제거- iOS 만 가능- Android 는 불가
• 해결책- Busting Ghost Clicks - touch와 click 동시 발생 제거- 모든 Clickable element의 click 이벤트 제거하고 touch 사용- jQuery Mobile 등 라이브러리 사용
�������������
touch click
�������������
기타
• Galaxy S3 Browser, Android Chrome 에서는- viewport 값을 user-scalable=no 로 설정하면 300ms 지연 없음
• iOS 5 이하에서는- DOM 위치를 변경하면 touch event handler가 사라짐- 자식 element의 event handler는 유지
�������������
마무리하며
�������������
지금까지 언급한 내용
• 화면 영역별 구현 방법• WebView & Web-App Bridge
• Application Cache
• 버전 관리
• WebView Request Intercept
• JS 크기 줄이기
• Touch 이벤트
�������������
캠프 하이브리드 앱
• 개발 속도- 상대적으로 빠르다- Mobile Web Application의 Front-End 개발 역량에 따름
• 실행 성능- WebView 성능 한계 존재- Native 성능 최적화 간과하면 안됨
• 운영- One Source Multi Use로 인해 테스트 부담 증가- 앱/웹 버전 관리의 Best Practice 찾기 쉽지 않다
�������������
마지막으로
• 하이브리드 앱의 성능은 Web Application의 성능에 좌우
• 하지만, 아무리 튜닝해도 2% 부족한 느낌
• Facebook이 Native로 바꾸는 이 시점에...
�������������
WebView 이해를 위한 추천 강의
• Android
- Google I/O 2012 : Android WebView
• iOS
- WWDC 2012 : Optimizing Web Content in UIWebViews and Websites on iOS
- WWDC 2012 : Debugging UIWebViews and Websites on iOS
�������������
참고
• Cross-Platform Tools: Build Once and Run Everywherehttp://www.infoq.com/presentations/Cross-Platform-Mobile-Tools
• Sunspider JavaScript Benchmarkhttp://www.webkit.org/perf/sunspider/sunspider.html
• Creating Fast Buttons for Mobile Web Applicationshttps://developers.google.com/mobile/articles/fast_buttons
• Substituting local data for remote UIWebView requestshttp://www.cocoawithlove.com/2010/09/substituting-local-data-for-remote.html
• Esprima & Escodegen
�������������
감사합니다.
�������������