하이브리드앱 아키텍쳐 및 개발 사례
Upcoming SlideShare
Loading in...5
×

Like this? Share it with your network

Share

하이브리드앱 아키텍쳐 및 개발 사례

  • 5,494 views
Uploaded on

2011.11.30 H3에서 발표했던 "(푸딩얼구인식앱을 통해서 보는)하이브드리앱 아키텍쳐 및 개발 사례"의 발표 자료입니다. ...

2011.11.30 H3에서 발표했던 "(푸딩얼구인식앱을 통해서 보는)하이브드리앱 아키텍쳐 및 개발 사례"의 발표 자료입니다.

More in: Technology , Design
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
5,494
On Slideshare
5,445
From Embeds
49
Number of Embeds
2

Actions

Shares
Downloads
129
Comments
0
Likes
16

Embeds 49

http://geekple.com 40
https://twitter.com 9

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. (푸딩얼굴인식앱을통해서본)하이브리드앱아키텍쳐및개발사례앱스프레소팀|장동수 1
  • 2. index1. 하이브리드앱아키텍쳐개요2. 하이브드리앱유형및특징3. 푸딩얼굴인식앱개발사례공유4. 앱스프레소플러그인활용5. LessonsLearned6. References 2
  • 3. 이런거...아님-_-; 하이브리드앱아키텍쳐개요 3
  • 4. 하이브리드앱아키텍쳐구성요소 네이티브 하이브리드 웹 UI툴킷 웹UI툴킷 자바스크립트프레임웍/라이브러리 웹표준기술 프레임웍 HTML5 CSS 자바스크립트네이티브라이브러리 비표준DeviceAPIs 표준DeviceAPIs 개발도구 웹브라우져“엔진” 웹브라우져“앱” 플랫폼SDK 안드로이드 iOSSDK 윈폰7SDK …⋯ SDK 4
  • 5. 하이브리드앱의꿈ApplicationQuality BEST 네이티브 하이브리드 웹 WORST DevelopmentCost 5
  • 6. 하이브리드라는이름의“짬뽕”... 난, 물~H2O~ OH H 6
  • 7. 앱개발자들을유혹하는“파란”짬뽕~ 내가, 하이브리드~ 네이티브웹 웹 7
  • 8. 웹개발자들을유혹하는“빨간”짬뽕~ 나도, 하이브리드~ 웹네이티브 네이티브 8
  • 9. 네이티브와웹의결합 Flash/Flex? Active-X?JavaApplet? 문제는.... 다리!! Native-Web네이티브 Bridge 웹 9
  • 10. 네이티브와웹의결합 WebViewWebViewClient&WebChromeClient loadUrl addJavascriptInterface UIWebView UIWebViewDelegate loadRequest stringByEvaluatingJavascriptFromString 10
  • 11. 네이티브와웹의결합 자바스크립트 캐시 그래봤자, 문자열~ URL 쿠키어차피,꼼수 그리고... HTTP! 그림 출처: http://petticoatsandpistols.com/2010/05/12/ 11
  • 12. 하이브리드앱유형및특징 12
  • 13. 네이티브지향하이브리드앱 사실상네이티브, 웹은거들뿐...· 제한적이고직관적인네이티브와웹의결합· 웹브라우져as-aUI컴포넌트· 도움말,앱/개발사소개,공지사항/새소식...· 웹기반사용자인증(OAuth)... 13
  • 14. 웹브라우져as-aUI컴포넌트 14
  • 15. 웹기반사용자인증 15
  • 16. 예제코드(안드로이드)<<웹서버 컨텐츠 불러오기>>public class NoticeActivity extends Activity { ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); WebView webView = (WebView)findViewById(R.id.webView); webView.getSettings().setJavaScriptEnabled(true); webView.setWebChromeClient(new WebChromeClient()); ... webView.loadUrl(“http://m.pudding.kr/pud/mNotice.kth”); ... } ...} <<앱에 포함된 정적 컨텐츠 불러오기>> public class HelpActivity extends Activity { ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); WebView webView = (WebView)findViewById(R.id.webView); webView.getSettings().setJavaScriptEnabled(true); webView.setWebChromeClient(new WebChromeClient()); ... webView.loadUrl(“file:///android_asset/www/ help.html”); ... } ... } 16
  • 17. 예제코드(iOS)<<웹 서버 컨텐츠 불러오기>>@interface NoticeViewController : UIViewController { IBOutlet UIWebView *webView;...@end@implementation HelpViewController...- (void)viewDidLoad { ... NSURL *url = [NSURL URLWithString:@”http://m.pudding.kr/pud/mNotice.kth”]; NSURLRequest *requestObj = [NSURLRequest requestWithURL:url]; [webView loadRequest:requestObj]; ...}...@end <<앱에 포함된 정적 컨텐츠 불러오기>> @interface HelpViewController : UIViewController { IBOutlet UIWebView *webView; ... @end @implementation HelpViewController ... - (void)viewDidLoad { ... NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; NSString *path = [bundlePath stringByAppendingPathComponent:@”/www/help.html”]; NSURL *url = [NSURL fileURLWithPath:path]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [webView loadRequest:request]; ... } ... @end 17
  • 18. “한지붕두가족”하이브리드앱웹은아니지만네이티브도아닌, 그러나웹스러운...· 광범위하고일관성없는네이티브와웹의결합· 웹브라우져를내장한네이티브클라이언트· 기존웹서버“조금손봐서...”재활용· 기존웹컨텐츠“조금손봐서...”재활용 18
  • 19. 여기도하이브리드~ 19
  • 20. 저기도하이브리드~ 20
  • 21. 예제코드(안드로이드)<<링크 클릭 가로채기>>...WebView webView = (WebView)findViewById(R.id.webView);webView.getSettings().setJavaScriptEnabled(true);webView.setWebChromeClient(new WebChromeClient());webView.setWebViewClient(new WebViewClient() { public boolean shouldOverrideUrlLoading(WebView webView, String url) { if(!url.startsWith(“http://m.pudding.kr/pud/”) { new AndroidDialog.Builder(NoticeActivity.this) .setMessage(“딴데로 갈라구?? -_-+”) .setPositiveButton(“아니... 여기 있을께 ㅠㅠ”, new DialogInterface.OnClickListener() { dialog.dismiss(); }) .setNegativeButton(“갈꼬얌!”, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int witch) { dialog.dismiss(); Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); NoticeActivity.this.startActivity(intent); } }).show(); return false; } else if ... ...이러쿵 저러쿵... } else if ... ...어쩌구 저쩌구... } else if ... ...구시렁 구시렁... } view.loadUrl(url); return true; }});webView.loadUrl(“http://m.pudding.kr/pud/mNotice.kth”);... 21
  • 22. 예제코드(iOS)<<링크 클릭 가로채기>>@interface NoticeViewController : UIViewController<UIWebViewDelegate, UIAlertViewDelegate> { IBOutlet UIWebView *webView; NSString *externalUrl;...@implementation HelpViewController...- (void)viewDidLoad { NSURL *requestUrl = [NSURL URLWithString:@”http://m.pudding.kr/pud/mNotice.kth”]; [webView loadRequest:[NSURLRequest requestWithURL:requestUrl]]; [webView setDelegate:self]}- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { NSString *url = [[request URL] absoluteString]; if(![url hasPrefix:@”http://m.pudding.kr/pud/mNotice.kth”]) { self.externalUrl = url; UIAlertView *alertView = [UIAlertView alloc] initWithTitle:nil message:@”딴데로 갈라구?? -_-+” delegate:self cancelButtonTitle:@”아니... 여기 있을께 ㅠㅠ“ otherButtonTitles:@”갈꼬얌!”, nil]; [alertView show]; [alertView release]; return NO; } else if ... ...이러쿵 저러쿵... } else if ... ...어쩌구 저쩌구... } else if ... ...구시렁 구시렁... } return YES;}- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if(buttonIndex == YES && self.externalUrl) { [[UIApplication sharedApplication] openURL:[NSURL URLWithString:self.externalUrl]; }}... 22
  • 23. 예제코드(안드로이드)<<URL을 이용한 네이티브와 웹의 통신::자바스크립트>>function getFieldValue(fieldId) { var fieldValue = document.getElementById(fieldId).value; location.href = ‘custom://getFieldValue?fieldId=’ + fieldId + ‘&fieldValue=’ + fieldValue;}function setFieldValue(fieldId, fieldValue) { document.getElementById(fieldId).value = fieldValue;}<<URL을 이용한 네이티브와 웹의 통신>>webView.loadUrl(“javascrpt:getFieldValue(‘userName’)”); // 결과는 나중에... 비동기!! -_-;...webView.loadUrl(“javascrpt:setFieldValue(‘userName’, ‘“ + userName + “‘“);...webView.setWebViewClient(new WebViewClient() { public boolean shouldOverrideUrlLoading(WebView webView, String url) { if(!url.startsWith(“custom://getFieldValue”) { Uri uri = Uri.parse(url); String fieldId = uri.getQueryParameter(“fieldId”); String fieldValue = uri.getQueryParameter(“fieldValue”); if(fieldId.equals(“userName”)) { userName = fieldValue; // 결과가 도착했다! 이제 어떡하지? 비동기!! OTL } else if ... } else if ... } else if ... // 나는 엘시프가 씨러요! ㅠㅠ } return false; } else if ... } else if ... } else if ... // 나는 엘시프가 씨러요! ㅠㅠ } view.loadUrl(url); return true; }});... 23
  • 24. 예제코드(iOS)<<URL을 이용한 네이티브와 웹의 통신>>NSString *script = [NSString stringWithFormat:@“getFieldValue(‘%@’)”, fieldId];[webView stringByEvaluatingJavaScriptString:script];...NSString *script = [NSString stringWithFormat:@“setFieldValue(‘%@’, ‘%@‘)“, fieldId, fieldValue];[webView stringByEvaluatingJavaScriptString:script];...- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { NSString *url = [[request URL] absoluteString]; if(![url hasPrefix:@”custom://getFieldName”]) { NSDictionary *params = [HttUtils decodeQueryString:[[request URL] query]]; NSString *fieldId = [params objectForKey:@”fieldId”]; NSString *fieldValue = [params objectForKey:@”fieldValue”]; if(fieldId isEqualToString:@”userName”) { self.userName = fieldValue; } else if ... } else if ... } else if ... // 나는 엘시프가 씨러요! ㅠㅠ } [paramArray release]; return NO; } else if ... } else if ... } else if ... // 나는 엘시프가 씨러요! ㅠㅠ } return YES;}... 24
  • 25. 웹지향하이브리드앱 사실상웹, 네이티브는거들뿐...· 광범위하지만일관성있는네이티브와웹의결합· 클라이언트사이드“웹앱”· 기존웹서버+RESTfulAPI서버· 기본적인웹컨텐츠는앱에포함 25
  • 26. 하이브리드모바일앱프레임웍 26
  • 27. 이것도하이브리드! 27
  • 28. 푸딩얼굴인식앱개발사례공유 28
  • 29. “푸딩얼굴인식앱”소개 푸딩얼굴인식앱은...· 600만+다운로드!· @iolothebard와@seti222· 5500+줄의자바스립트· 2700+줄의CSS· 200+줄의네이티브코드· 앱스프레소0.9+내부개발버전 29
  • 30. 단일페이지인터페이스 index.html #pageId pageId.css pageId.js Active Pageshow/hide 30
  • 31. 단일페이지인터페이스 웹앱도MVC가필요해! 자바스크립트가컨트롤러!· 웹서버도없는데...페이지이동은왜?!· (GUI)애플리케이션스타일의“상태”관리· 빠른화면전환&화면전환효과· 체감성능UP!· 메모리사용량UP!· 그런데...공동작업은어떻게?-_-; 31
  • 32. 단일페이지인터페이스<<HTML마크업: section#faceFoundPage>><section id=”faceFoundPage” class=”jj-page”> <header> <button class=”jj-left jj-back”><span data-nls=”common.back”>이전</span></button> <button class=”jj-right jj-home”><span data-nls=”common.home”>홈</span></button> <h1><span data-nls=”faceFoundPage.title”>얼굴인식 결과</span></h1> </header> <article> <<CSS스타일:faceFoundPage.css>> ... #faceFoundPage { </article> background-color:rgb(255,255,255); <footer> } ... #faceFoundPage > article, #faceFoundPage > footer { </footer> background-color:rgb(138,135,136);</section> } ...<<자바스크립트:faceFoundPage.js>>var jj.ui = jj.require(‘jj.ui’);export.FaceFoundPage = new jj.defclass(jj.ui.Page, { onInit: function() { $(this.pageNode).bind(‘onpagebeforeshow’, function() { … }); $(this.pageNode).bind(‘onpageaftershow’, function() { … }); $(this.pageNode).bind(‘onpagebeforehide’, function() { … }); $(this.pageNode).bind(‘onpageaftershow’, function() { … }); ... }, <<자바스크립트를 이용한 화면 전환>> ... // 화면을 수동으로 초기화}); var faceFoundPage = new FaceFoundPage( $(‘#faceFoundPage’), // 화면을 구성하는 DOM 노드 facePickerPage, // 화면내의 .jj-back 노드를 클릭할 때 전환될 페이지 mainPage); // 화면내의 .jj-home 노드를 클릭할 때 전환될 페이지 // 화면 표시에 필요한 상태 정보를 전달 faceFoundPag.setFaceInfo(faceInfo); // 현재 화면이 왼쪽으로 사라지면서, 새 화면이 오른쪽에서(slideleft) 나타남 faceFoundPage.show(‘slideleft’); // 현재 화면을 유지한 채, 새 화면을 아래에서 위로(slideup) 나타남(modal) //faceFoundPage.show(‘slideup’, true); 32
  • 33. “Placeholder”HTMLMarkup 자바스크립트 API서버 웹서버 HTML 로컬 캐시 스토리지 캐시 앱StaticHTMLFragment Placeholder Data Template + 이름 클래스 레벨 ★ ♥ 이름 클래스 레벨 iolo bard 만렙 iolo bard 만렙 <<foreach>> 장동수 개발자 쪼렙 장동수 개발자 쪼렙 ★ ♥ ... ... ... ... ... ... <<end>> 33
  • 34. “Placeholder”HTMLMarkup 웹앱도MVC가필요해! 뷰와모델의분리· HTML마크업을위한“변수”· 웹서버개발에서클라이언트에적용 smarty,...)을웹 널리쓰이는템플릿(velocity,· 서버부하는DOWN!· 데이터전송량도DOWN!· 캐시히트율은UP! 34
  • 35. “Placeholder”HTMLMarkup<div id=”faceFound_starTemplate” class=”jj-template”> <div class=”face” data-fastclick=”true”></div> <div class=”rank”></div> <p class=”rate”><span class=”rateText”></span><small>%</small></p> <p class=”nameText” data-fastclick=”true”></p> <p class=”info”></p> <p class=”descriptionText”></p></div><div id=”faceFound_star_wrapper”><div id=”faceFound_star_scroller”> <ul> <li class=”nomatch”> <div id=”faceFound_star_nomatch”> <h5 data-nls=”0ah002”>닮은 연예인을 찾지 못했습니다.</h5> <p data-nls=”0ah003”>얼굴이 가까이 나온<br />정면 사진으로 다시 시도해 보세요.</p> <div> <button id=”faceFound_retryBtn” class=”y” data-fastclick=”true”> <span data-nls=”0ah005”>다시 찾기</span> </button> </div> </div> </li> <li> <div id=”faceFound_star1” class=”jj-placeholder” data-template=”#faceFound_starTemplate”></div> </li> <li> <div id=”faceFound_star2” class=”jj-placeholder” data-template=”#faceFound_starTemplate”></div> <div id=”faceFound_star3” class=”jj-placeholder”></div> </li> <li> <div id=”faceFound_star4” class=”jj-placeholder” data-template=”#faceFound_starTemplate”></div> <div id=”faceFound_star5” class=”jj-placeholder” data-template=”#faceFound_starTemplate”></div> </li> </ul></div><!-- star_scroller --></div><!-- star_wrapper --> 35
  • 36. “Marker”CSSClass 웹앱도MVC가필요해! 뷰와컨트롤러의분리· CSS스타일시트를위한“조건문”· 프론트엔드UI개발자와자바스크립트개발자의약속· 화면의동적인변화를자바스크립트없이확인&제어· HTML/CSS는“쬐끔”복잡해지고...-_-;· 자바스크립트는“쬐끔”단순해지고...-_-;· 함께일하기는더좋아지고~^O^ 36
  • 37. “Marker”CSSClass<<스타일시트>>/* 일치하는 연예인이 하나라도 있으면 안내문을 표시하지 않는다 */#faceFoundPage .nomatch { display:none;}/* 일치하는 연예인이 없으면 그에 따른 안내문을 표시한다 */#faceFoundPage.nomatch .nomatch { display:block;}/* 일치하는 연예인이 없으면 carousel 페이지 인디케이터를 숨긴다 */#faceFoundPage.nomatch ul > li { display:none;}/* 일치하는 연예인이 없으면 하단의 공유 버튼들을 비활성화 */#faceFoundPage.nomatch footer button { opacity:0.5;}/* 일치하는 연예인 숫자에 따라 carousel 영역(iscroll)의 너비를 조절한다#faceFoundPage.nomatch #faceFound_star_scroller,#faceFoundPage.match_1 #faceFound_star_scroller { width:320px;/*320*1pages*/}#faceFoundPage.match_2 #faceFound_star_scroller,#faceFoundPage.match_3 #faceFound_star_scroller { width:640px;/*320*2pages*/}#faceFoundPage.match_4 #faceFound_star_scroller,#faceFoundPage.match_5 #faceFound_star_scroller { width:960px;/*320x3pages*/}<<자바스크립트>>if(matchingCelebs < 1) { $(‘#faceFoundPage’).addClass(‘nomatch’);} else { $(‘#faceFoundPage’).removeClass(‘nomatch’).addClass(‘match_’ + matchingCelebs);} 37
  • 38. 단말해상도별최적화 ResponsiveWebDesign?지금은곤란하니,기다려달라~· <HTML>태그에마커CSS클래스추가/활용· 기본적으로해상도에자유로운프론트엔드UI개발· 널리쓰이는해상도는꼼꼼하게“미세”조정· 특이한해상도는최소한의“미세”조정· 이한몸희생해서...모두가행복할수있다면...ㅠㅠ 38
  • 39. “Marker”CSSClass<<단말 플랫폼/해상도 마커 클래스 초기화 스크립트>>var platformCls = (/iOS/.test(navigator.userAgent)) ? ‘ios’ : (/Android/.test(navigator.userAgent) ? android : ‘generic’);var screenCls =‘screen’, screen.width, ‘x’, screen.height).join(‘’);var orientationCls = (screen.width < screen.height) ? ‘portrait’ : ‘landscape’;$(window).addClass(platformCls, screenCls, orientationCls); <<단말 플랫폼 마커 클래스를 활용한 스타일 최적화>> /* 플랫폼 고유의 체크박스(on/off 스위치) 이미지를 사용 */ input[type=”checkbox”] { background-repeat:no-repeat; background-size:100%; } .ios input[type=”checkbox”] { background-image:@url(‘img/ios/check.png’); }<<단말 해상도 마커 클래스를 활용한 스타일 최적화>> .android input[type=”checkbox”] {/* 해상도 독립적인 레이아웃 */ background-image:@url(‘img/android/check.png’);section { width:100%; height:100%; } }header, footer { height:10%; } ...article { height:80%; }article.noheader, article.nofooter { height:90%; }article.noheader.nofooter { height:100%; }.../* 아이폰 해상도에 맞춰 미세 조정 */.screen320x480 header { height:44px; }.screen320x480 footer { height:49px; }.screen320x480 article { height:387px;/*480-44-49*/ }.screen320x480 article.noheader { height:436px;/*480-44*/ }.screen320x480 article.nofooter { height:431px;/*480-49*/ }.../* 갤러시탭에 해상도에 맞춰 미세 조정 */.screen600x1024 header { height:80px; }... 39
  • 40. 다국어처리 I18N?L10N?A11Y? 이제는선택이아닌필수!· <HTML>태그에마커CSS클래스추가/활용· “그림글자”사용최소화:국제화&접근성· 일관성있는번역어식별자· 언어별어순/길이/너비차이고려· 언어별UI“미세”조정 40
  • 41. 다국어처리 41
  • 42. 다국어처리<<다국어 지원 초기화 스크립트>>var lang = navigator.language.substring(0, 2);if(lang !== ‘en’ && lang !== ‘ja’ && lang !== ‘zh’) { lang = ‘en’; }$.get(‘locales/’ + lang + ‘/messages.json’, function(messages) { $(document.documentElement).addClass(‘jj-nls-’ + lang);//언어 식별 마커 클래스 추가 $.each(document.body).find(‘*[data-nls]’).each(function(index, node) { var key = node.attr(‘data-nls’); var message = messages[key]; if(message) { node.html(message); } else { console.error(’missing nls message:’ + key); } <<다국어 번역 텍스트 파일(locales/언어/messages.json)>>}); “common.back”: “Back”, ... “setupTwitterPage.title”: “Setup - Twitter”, ...<<다국어 지원 자리 잡기 태그>><button><span data-nls=”common.back”>이전</span></button>...<h1><span data-nls=”setupTwitterPage.title”>설정 - 트위터</span></h1>...<<다국어 번역 텍스트 치환 결과>><button><span data-nls=”common.back”>Back</span></button>...<h1><span data-nls=”setupTwitterPage.title”>Setup - Twitter</span></h1>... <<마커 클래스를 활용한 언어별 스타일 최적화>> .jj-nls-zh #intro_title { /*locales/zh/img/intro_title.png*/ background-image:@url(‘../img/intro_title.png’); } .jj-nls-zh #find_cameraBtn > .text, .jj-nls-zh #find_albumBtn > .text { ! width:70px; display:inline-block; } ... 42
  • 43. Ant를이용한빌드자동화1.verify:jslint2.merge:antconcat3.compress:YUICompressor4.preprocess:antfilter5.test:JSTestDriver6.docs:JSDocToolkit(v2) 43
  • 44. Ant를이용한빌드자동화<target name=”verify_js” depends=”init”> <jslint jslint=”${jslint.js}” encoding=”${js.encoding}” options=”${jslint.options}” haltOnFailure=”${jslint.haltOnFailure}”> <predef>${jslint.predef}</predef> <formatter type=”plain”/> <filelist refid=”js.src.files”/> </jslint></target><target name=”merge_js” depends=”verify_js” if=”build.release”> <!-- pudface --> <concat destfile=”${js.out.merged}” encoding=”${js.encoding}” outputencoding=”${js.encoding}” fixlastline=”no” eol=”unix”> <filelist refid=”js.src.files”/> <filterchain> <deletecharacters chars=”&#xFEFF;”/> </filterchain> </concat> <echo message=”merged into ${js.out.merged}”/></target><target name=”compress_js_yuicompressor” depends=”verify_merged_js” if=”build.release”> <!-- pudface --> <java classname=”${yuicompressor.mainclass}” classpathref=”yuicompressor.classpath” fork=”true” failonerror=”true”> <arg value=”--verbose”/> <arg value=”--charset”/> <arg value=”${js.encoding}”/> <arg value=”--type”/> <arg value=”js”/> <arg value=”-o”/> <arg file=”${js.out.compressed}”/> <arg file=”${js.out.merged}”/> </java> <delete file=”${js.out.merged}”/> <echo message=”compressed into ${js.out.compressed}”/></target> 44
  • 45. Ant를이용한빌드자동화<target name=”build_debug” depends=”clean,copy_apis” if=”build.debug”> <copy todir=”${out.dir}” verbose=”true”> <fileset dir=”${src.dir}”> <exclude name=”index.html”/> </fileset> </copy> <copy todir=”${out.dir}” encoding=”${html.encoding}” outputencoding=”${html.encoding}” verbose=”true” overwrite=”true”> <fileset dir=”${src.dir}”> <include name=”index.html”/> </fileset> <filterchain> <linecontains negate=”true”><contains value=”@@RELEASE”/></linecontains> </filterchain> </copy></target><target name=”build_release” depends=”clean,compress_js,compress_css” if=”build.release”> <copy todir=”${out.dir}” verbose=”true”> <fileset dir=”${src.dir}”> <exclude name=”index.html”/> <exclude name=”js/pudface/**”/> <exclude name=”css/**”/> </fileset> </copy> <copy todir=”${out.dir}” encoding=”${html.encoding}” outputencoding=”${html.encoding}” verbose=”true” overwrite=”true”> <fileset dir=”${src.dir}”> <include name=”index.html”/> </fileset> <filterchain> <linecontains negate=”true”><contains value=”@@DEBUG”/></linecontains> </filterchain> </copy></target> 45
  • 46. 네이티브와의결합 사용한앱스프레소플러그인· deviceapis.filesystem:파일입출력· deviceapis.deviceinteraction:화면꺼짐방지/진동· ax.ext.media:카메라/포토앨범/효과음· ax.ext.net:업로드/다운로드· ax.ext.ui:네이티브UI/차일드브라우져· ax.ext.ga:구글통계· ax.ext.admob:애드몹광고· kth.puddingface:푸딩얼굴인식앱전용*^^* 46
  • 47. 앱스프레소플러그인활용 47
  • 48. 앱스프레소플러그인구조 AppspressoPlugin AppspressoAppspresso Project PluginApplication Archive Project axplugin.xml axplugin.js (*.axp) res overlay link export &run lib*.a *.jar &share iOSNativeModule AndroidNativeModule (XcodeStaticLibraryProject) (AndroidLibraryProject) 48
  • 49. 앱스프레소플러그인API AxPlugin AxPluginContext intgetId()/*YOURCODEHERE*/ 2 StringgetMethod() activate(AxRuntimeContext) Object[]getParams()deactivate(AxRuntimeContext) ... execute(AxPluginContext) sendResult([result]) ... sendError(code[,message]) 2 4 AxRuntimeContext 1 자바스크립트API getWebView() getWidget() ... requirePlugin(pluginId) 플랫폼확장API executeJavaScript(script) ... Android iOS 49
  • 50. 앱스프레소플러그인플랫폼확장API·안드로이드전용 · Activity · ActivityListener · WebViewListener · WebViewClientListener/WebChromeClientListener· iOS전용 · UIViewController · UIApplicationDelegate · UIWebViewDelegate · AxViewControllerDelegate 50
  • 51. 앱스프레소플러그인자바스크립트API·AxPlugin의자바스크립트“stub” · functionexecSync(method,params) · functionexecAsync(method,successCallback, errorCallback,params) · params:arrayofarguments · successCallback:function(result){...} · errorCallback:function(error){...}· ax,ax.error,ax.util,ax.console, ax.request,ax.bridge,ax.plugin,... 51
  • 52. 앱스프레소플러그인개발실습 네이티브주소록UI플러그인· deviceapis.pim.contact는...· 너무어려워+너무느려+뽀대도안나...x3· 그래서,@bluenmad 가만들었습니다~ ax.ext.contact.pickContact(function(contact) { if(contact && contact.phoneNumbers) { var firstPhoneNumber = phoneNumbers.split(‘,’)[0]; if(confirm(contact.firstName + ‘에게 전화걸까요?’)) { location.href = ‘tel:’ + firstPhoneNumber; } } }, function(error) { ... })· 어떻게? 52
  • 53. axplugin.xml<?xml version=”1.0” encoding=”UTF-8”?><axplugin id=”ax.ext.contact” version=”1.0”>! <description>Contact Extension API Appspresso Plugin! </description>! <url>http://appspresso.com</url>! <author>Appspresso Dev. Team</author>! <license>Copyright (c) 2011, KT Hitel Co., LTD. All Rights Reserved.! </license>! <feature id=”http://appspresso.com/api/ax.ext.contact“! ! category=”Extension” />! <module platform=”android” platform-version=”8”! ! min-platform-version=”7” max-platform-version=””! ! class=”com.appspresso.screw.contact.ContactPlugin”>! ! <property name=”permission” value=”android.permission.READ_CONTACTS” />! </module>! <module platform=”ios” platform-version=”4.1”! ! min-platform-version=”4.0” max-platform-version=””! ! class=”ax_ext_contact_MyPlugin”>! ! <property name=”framework” value=”AddressBook.framework, AddressBookUI.framework” />! </module></axplugin> 53
  • 54. axplugin.js/*jslint browser:true, confusion:true, /**debug:true, devel:true, nomen:true, * pick a contactplusplus:true, vars:true */ */** * @param {function} callback * @fileOverview Contact Extension API * @param {function} errback * @author blueNmad * @param {ax.ext.contact.ContactOpts} opts * @version 1.0 * @return AxRequest */ * @methodOf ax.ext.contact(function () { */ “use strict”; function pickContact(callback, errback, var NS_CONTACT = “ax.ext.contact”; opts) { var PREFIX_CONTACT = “ax.ext.contact”; onPickContactCallback = callback; /** return this.execAsync(‘pickContact’, * Contact Extension API ax.nop, errback, [opts || {}]); * } * @namespace * @name ax.ext.contact function onPickContact(contact) { */ if ( !! onPickContactCallback) { onPickContactCallback(eval(contact)); /** } * @class * @name ContactOpts onPickContactCallback = undefined; * @memberOf ax.ext.contact } */ ax.plugin(PREFIX_CONTACT, { /** ‘pickContact’: pickContact, * native callback for pickContact. ‘onPickContact’: onPickContact * }, NS_CONTACT); * @param result })(); * @memberOf ax.ext.contact * @private */ var onPickContactCallback = undefined; 54
  • 55. com...contact.ContactPlugin.javapackage com.appspresso.screw.contact; return super.onActivityResult(activity, requestCode, resultCode, data);import android.app.Activity; }import android.content.Intent; };import android.provider.ContactsContract; public void activate(import com.appspresso.api.AxPluginContext; AxRuntimeContext runtimeContext) {import com.appspresso.api.AxRuntimeContext; super.activate(runtimeContext);import com.appspresso.api.DefaultAxPlugin;... runtimeContext.addActivityListener(<<중간생략>> activityListener);... }/** public void deactivate( * Appspresso Plugin Android Module AxRuntimeContext runtimeContext) { * runtimeContext.removeActivityListener( * id: ax.ext.contact activityListener); * version: 1.0.0 * super.deactivate(runtimeContext); */ }public class ContactPlugin extendsDefaultAxPlugin { public void pickContact( private static final int AxPluginContext context) { REQ_PICK_CONTACT = 62000; Intent intent = new Intent( Intent.ACTION_PICK, private ActivityListener activityListener = ContactsContract.Contacts.CONTENT_URI); new ActivityAdapter() { runtimeContext.getActivity() public boolean onActivityResult( .startActivityForResult(intent, Activity activity, int requestCode, int REQ_PICK_CONTACT); resultCode, Intent data) { context.sendResult(); if (ContactPlugin.REQ_PICK_CONTACT == } requestCode && data != null) { } return ContactUtils.onPickContact( runtimeContext, data); } 55
  • 56. com...contact.ContactUtils.javapackage com.appspresso.screw.contact; String contactId = data.getData().getLastPathSegment();import java.util.ArrayList;import java.util.List; JSONObject contact = ContactUtils.getContactWithContactId(import org.apache.commons.logging.Log; activity, contactId);import org.json.JSONArray;import org.json.JSONObject; if (contact != null) { runtimeContex.invokeJavaScriptFunction(import android.app.Activity; JS_CALLBACK_ONPICKCONTACT, contact);import android.content.Intent; }import android.database.Cursor; return true;import android.provider.ContactsContract; }...<<중략>> static JSONObject getContactWithContactId(... Activity activity, String contactId) {import android.webkit.WebView; JSONObject contact = new JSONObject(); ...import com.appspresso.api.AxLog; Cursor cursor = null; Cursor rawContactIdsCursor = null;class ContactUtils { try { private static Log L = String[] rawContactIds = null; AxLog.getLog(“ContactPlugin”); rawContactIdsCursor = activity .getContentResolver().query( private static final String RawContacts.CONTENT_URI, JS_CALLBACK_ONPICKCONTACT = new String[] { RawContacts._ID }, “ax.ext.contact.onPickContact”; RawContacts.CONTACT_ID + “ = ?”, new String[] { contactId }, public static boolean onPickContact( null); RuntimeContext runtimeContext, ... Intent data) { <<중략>> if (data == null) { ... return false; } } } 56
  • 57. ax_ext_contact_MyPlugin.h#import <Foundation/Foundation.h>#import <UIKit/UIKit.h>#import <AddressBook/AddressBook.h>#import <AddressBookUI/AddressBookUI.h>#import “AxPlugin.h”@protocol AxContext;@protocol AxPluginContext;@interface ax_ext_contact_MyPlugin : NSObject<AxPlugin, ABPeoplePickerNavigationControllerDelegate> {@private NSObject<AxRuntimeContext> *_runtimeContext;}@property (nonatomic,readonly,retain) NSObject<AxRuntimeContext>* runtimeContext;- (void)activate:(NSObject<AxRuntimeContext>*)runtimeContext;- (void)deactivate:(NSObject<AxRuntimeContext>*)runtimeContext;- (void)execute:(NSObject<AxPluginContext>*)context;- (IBAction)presentABPeoplePickerNavigationController;// ABPeoplePickerNavigationControllerDelegate method- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker shouldContinueAfterSelectingPerson:(ABRecordRef)person;- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker shouldContinueAfterSelectingPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier;- (void)peoplePickerNavigationControllerDidCancel:(ABPeoplePickerNavigationController *)peoplePicker;@end 57
  • 58. ax_ext_contact_MyPlugin.m// [self// ax_ext_contact_MyPlugin.m presentABPeoplePickerNavigationController];// [context sendResult];// Copyright 2011 none. All rights reserved. }// else { [context sendError:AX_NOT_AVAILABLE_ERR];#import “AxRuntimeContext.h” }#import “AxPluginContext.h” }#import “AxError.h”#import “AxLog.h” -#import “ax_ext_contact_MyPlugin.h” (IBAction)presentABPeoplePickerNavigationContr oller {#define JS_CALLBACK_ONPICKCONTACT dispatch_async(dispatch_get_main_queue(), ^{@”ax.ext.contact.onPickContact” ABPeoplePickerNavigationController *picker = [[[ABPeoplePickerNavigationController alloc]@implementation ax_ext_contact_MyPlugin init] autorelease]; picker.peoplePickerDelegate = self;@synthesize runtimeContext = _runtimeContext; [[self.runtimeContext getViewController] presentModalViewController:picker- (void)activate: animated:YES]; (NSObject<AxRuntimeContext>*)runtimeContext { // [picker release]; _runtimeContext = [runtimeContext retain]; });} } ...- (void)deactivate: <<중략>> (NSObject<AxRuntimeContext>*)runtimeContext { ... [_runtimeContext release]; - _runtimeContext = nil; (void)peoplePickerNavigationControllerDidCance} l:(ABPeoplePickerNavigationController *)peoplePicker {- (void)execute:(id<AxPluginContext>)context { [[self.runtimeContext getViewController] NSString* method = [context getMethod]; dismissModalViewControllerAnimated:YES]; } AX_LOG_TRACE(@”ContactView_ios_method : %s”,method); @end if([method isEqualToString:@”pickContact”]){ 58
  • 59. LessonsLearned 59
  • 60. to.개발자[경고]이웹은당신이알았던그웹이아닙니다. TheWebisDead. 크로스플랫폼?새로운플랫폼!! 이자바스크립트는당신이알았던 그자바스크립트가아닙니다. 웹요소기술+(GUI)애플리케이션아키텍쳐 ... 60
  • 61. cc.기획자,디자이너,...[경고]이웹은당신이알았던그웹이아닙니다. TheWebisDead. 저비용고품질?!웹의한계,장점,단점을고려한기획&디자인과 적절한품질목표설정 무작정네이티브앱의UI/UX따라하기금지! ... 61
  • 62. References· 하이브리드모바일앱프레임웍http://slideshare.net/iolo/hybrid-mobile-application-framework· 단일페이지인터페이스웹/앱개발http://slideshare.net/iolo/ss-7719322· AndroidSDKWebView레퍼런스http://goo.gl/iqr9H· iOSSDKUIWebView레퍼런스http://goo.gl/U8XGy· Android용하이브리드앱템플릿https://github.com/iolo/hellowebapp-android· HowtobuildAndroidAppwithHTML/CSS/JavaScripthttp://youtube.com/watch?v=uVqp1zcMfbE· iOS용하이브리드앱템플릿https://github.com/iolo/hellowebapp-ios· HowtobuildiOSAppwithHTML/CSS/JavaScripthttp://youtube.com/watch?v=L28lGkoSQ2c· 푸딩얼굴인식앱(Android)https://market.android.com/details?id=com.kth.puddingface· 푸딩얼굴인식다국어앱(iOS)http://itunes.apple.com/us/app/id378461555?mt=8· 앱스프레소홈페이지http://appspresso.com/· 폰갭홈페이지http://phonegap.com/· 티타늄홈페이지http://appcelerator.com/· jQuery홈페이지http://jquery.com/· iScroll홈페이지http://cubiq.org/iscroll/· JSLint홈페이지http://jslint.com/· YUICompressor홈페이지http://developer.yahoo.com/yui/compressor/ 62
  • 63. That’sallfolks... 63
  • 64. 감사합니다모바일개발실 / 앱스프레소팀 / 장동수 iolothebard at kthcorp dot com @iolothebard 64