• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
H3 2011 하이브리드 앱 아키텍쳐 및 개발방법
 

H3 2011 하이브리드 앱 아키텍쳐 및 개발방법

on

  • 5,588 views

 

Statistics

Views

Total Views
5,588
Views on SlideShare
3,560
Embed Views
2,028

Actions

Likes
9
Downloads
279
Comments
0

13 Embeds 2,028

http://h3.paran.com 1804
http://h3.kthcorp.com 153
http://localhost 18
http://gofficeplus.lgcns.com 18
http://gguriya.tistory.com 13
http://devkeaton.wordpress.com 6
http://search.lgcns.com 4
http://blog.naver.com 4
http://192.168.10.147 3
http://jhpark0803.wordpress.com 2
http://dntds.dyndns.org 1
http://contents.postach.io 1
http://webcache.googleusercontent.com 1
More...

Accessibility

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

    H3 2011 하이브리드 앱 아키텍쳐 및 개발방법 H3 2011 하이브리드 앱 아키텍쳐 및 개발방법 Presentation Transcript

    • (푸딩얼굴인식앱을통해서본)하이브리드앱아키텍쳐및개발사례앱스프레소팀|장동수 1
    • index1. 하이브리드앱아키텍쳐개요2. 하이브드리앱유형및특징3. 푸딩얼굴인식앱개발사례공유4. 앱스프레소플러그인활용5. LessonsLearned6. References 2
    • 이런거...아님-_-; 하이브리드앱아키텍쳐개요 3
    • 하이브리드앱아키텍쳐구성요소 네이티브 하이브리드 웹 UI툴킷 웹UI툴킷 자바스크립트프레임웍/라이브러리 웹표준기술 프레임웍 HTML5 CSS 자바스크립트네이티브라이브러리 비표준DeviceAPIs 표준DeviceAPIs 개발도구 웹브라우져“엔진” 웹브라우져“앱” 플랫폼SDK 안드로이드 iOSSDK 윈폰7SDK …⋯ SDK 4
    • 하이브리드앱의꿈ApplicationQuality BEST 네이티브 하이브리드 웹 WORST DevelopmentCost 5
    • 하이브리드라는이름의“짬뽕”... 난, 물~H2O~ OH H 6
    • 앱개발자들을유혹하는“파란”짬뽕~ 내가, 하이브리드~ 네이티브웹 웹 7
    • 웹개발자들을유혹하는“빨간”짬뽕~ 나도, 하이브리드~ 웹네이티브 네이티브 8
    • 네이티브와웹의결합 Flash/Flex? Active-X?JavaApplet? 문제는.... 다리!! Native-Web네이티브 Bridge 웹 9
    • 네이티브와웹의결합 WebViewWebViewClient&WebChromeClient loadUrl addJavascriptInterface UIWebView UIWebViewDelegate loadRequest stringByEvaluatingJavascriptFromString 10
    • 네이티브와웹의결합 자바스크립트 캐시 그래봤자, 문자열~ URL 쿠키어차피,꼼수 그리고... HTTP! 그림 출처: http://petticoatsandpistols.com/2010/05/12/ 11
    • 하이브리드앱유형및특징 12
    • 네이티브지향하이브리드앱 사실상네이티브, 웹은거들뿐...· 제한적이고직관적인네이티브와웹의결합· 웹브라우져as-aUI컴포넌트· 도움말,앱/개발사소개,공지사항/새소식...· 웹기반사용자인증(OAuth)... 13
    • 웹브라우져as-aUI컴포넌트 14
    • 웹기반사용자인증 15
    • 예제코드(안드로이드)<<웹서버 컨텐츠 불러오기>>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
    • 예제코드(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
    • 여기도하이브리드~ 19
    • 저기도하이브리드~ 20
    • 예제코드(안드로이드)<<링크 클릭 가로채기>>...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
    • 예제코드(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
    • 예제코드(안드로이드)<<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
    • 예제코드(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
    • 웹지향하이브리드앱 사실상웹, 네이티브는거들뿐...· 광범위하지만일관성있는네이티브와웹의결합· 클라이언트사이드“웹앱”· 기존웹서버+RESTfulAPI서버· 기본적인웹컨텐츠는앱에포함 25
    • 하이브리드모바일앱프레임웍 26
    • 이것도하이브리드! 27
    • 푸딩얼굴인식앱개발사례공유 28
    • “푸딩얼굴인식앱”소개 푸딩얼굴인식앱은...· 600만+다운로드!· @iolothebard와@seti222· 5500+줄의자바스립트· 2700+줄의CSS· 200+줄의네이티브코드· 앱스프레소0.9+내부개발버전 29
    • 단일페이지인터페이스 index.html #pageId pageId.css pageId.js Active Pageshow/hide 30
    • 단일페이지인터페이스 웹앱도MVC가필요해! 자바스크립트가컨트롤러!· 웹서버도없는데...페이지이동은왜?!· (GUI)애플리케이션스타일의“상태”관리· 빠른화면전환&화면전환효과· 체감성능UP!· 메모리사용량UP!· 그런데...공동작업은어떻게?-_-; 31
    • 단일페이지인터페이스<<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
    • “Placeholder”HTMLMarkup 자바스크립트 API서버 웹서버 HTML 로컬 캐시 스토리지 캐시 앱StaticHTMLFragment Placeholder Data Template + 이름 클래스 레벨 ★ ♥ 이름 클래스 레벨 iolo bard 만렙 iolo bard 만렙 <<foreach>> 장동수 개발자 쪼렙 장동수 개발자 쪼렙 ★ ♥ ... ... ... ... ... ... <<end>> 33
    • “Placeholder”HTMLMarkup 웹앱도MVC가필요해! 뷰와모델의분리· HTML마크업을위한“변수”· 웹서버개발에서클라이언트에적용 smarty,...)을웹 널리쓰이는템플릿(velocity,· 서버부하는DOWN!· 데이터전송량도DOWN!· 캐시히트율은UP! 34
    • “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
    • “Marker”CSSClass 웹앱도MVC가필요해! 뷰와컨트롤러의분리· CSS스타일시트를위한“조건문”· 프론트엔드UI개발자와자바스크립트개발자의약속· 화면의동적인변화를자바스크립트없이확인&제어· HTML/CSS는“쬐끔”복잡해지고...-_-;· 자바스크립트는“쬐끔”단순해지고...-_-;· 함께일하기는더좋아지고~^O^ 36
    • “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
    • 단말해상도별최적화 ResponsiveWebDesign?지금은곤란하니,기다려달라~· <HTML>태그에마커CSS클래스추가/활용· 기본적으로해상도에자유로운프론트엔드UI개발· 널리쓰이는해상도는꼼꼼하게“미세”조정· 특이한해상도는최소한의“미세”조정· 이한몸희생해서...모두가행복할수있다면...ㅠㅠ 38
    • “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
    • 다국어처리 I18N?L10N?A11Y? 이제는선택이아닌필수!· <HTML>태그에마커CSS클래스추가/활용· “그림글자”사용최소화:국제화&접근성· 일관성있는번역어식별자· 언어별어순/길이/너비차이고려· 언어별UI“미세”조정 40
    • 다국어처리 41
    • 다국어처리<<다국어 지원 초기화 스크립트>>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
    • Ant를이용한빌드자동화1.verify:jslint2.merge:antconcat3.compress:YUICompressor4.preprocess:antfilter5.test:JSTestDriver6.docs:JSDocToolkit(v2) 43
    • 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
    • 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
    • 네이티브와의결합 사용한앱스프레소플러그인· deviceapis.filesystem:파일입출력· deviceapis.deviceinteraction:화면꺼짐방지/진동· ax.ext.media:카메라/포토앨범/효과음· ax.ext.net:업로드/다운로드· ax.ext.ui:네이티브UI/차일드브라우져· ax.ext.ga:구글통계· ax.ext.admob:애드몹광고· kth.puddingface:푸딩얼굴인식앱전용*^^* 46
    • 앱스프레소플러그인활용 47
    • 앱스프레소플러그인구조 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
    • 앱스프레소플러그인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
    • 앱스프레소플러그인플랫폼확장API·안드로이드전용 · Activity · ActivityListener · WebViewListener · WebViewClientListener/WebChromeClientListener· iOS전용 · UIViewController · UIApplicationDelegate · UIWebViewDelegate · AxViewControllerDelegate 50
    • 앱스프레소플러그인자바스크립트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
    • 앱스프레소플러그인개발실습 네이티브주소록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
    • 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
    • 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
    • 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
    • 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
    • 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
    • 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
    • LessonsLearned 59
    • to.개발자[경고]이웹은당신이알았던그웹이아닙니다. TheWebisDead. 크로스플랫폼?새로운플랫폼!! 이자바스크립트는당신이알았던 그자바스크립트가아닙니다. 웹요소기술+(GUI)애플리케이션아키텍쳐 ... 60
    • cc.기획자,디자이너,...[경고]이웹은당신이알았던그웹이아닙니다. TheWebisDead. 저비용고품질?!웹의한계,장점,단점을고려한기획&디자인과 적절한품질목표설정 무작정네이티브앱의UI/UX따라하기금지! ... 61
    • 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
    • That’sallfolks... 63
    • 감사합니다모바일개발실 / 앱스프레소팀 / 장동수 iolothebard at kthcorp dot com @iolothebard 64