(푸딩얼굴인식앱을통해서본)
하이브리드앱
아키텍쳐및개발사례
앱스프레소팀|장동수




                                         1
index

1. 하이브리드앱아키텍쳐개요

2. 하이브드리앱유형및특징

3. 푸딩얼굴인식앱개발사례공유

4. 앱스프레소플러그인활용

5. LessonsLearned

6. References


                                                           2
이런거...
아님-_-;




        하이브리드앱아키텍쳐개요
                                       3
하이브리드앱아키텍쳐구성요소

    네이티브                            하이브리드                                                  웹


     UI툴킷     웹UI툴킷          자바스크립트프레임웍/라이브러리

                                                       웹표준기술

    프레임웍
                              HTML5                          CSS                        자바스크립트



네이티브라이브러리          비표준DeviceAPIs                       표준DeviceAPIs


    개발도구             웹브라우져“엔진”                                  웹브라우져“앱”

                                    플랫폼SDK

    안드로이드
                    iOSSDK                 윈폰7SDK                          …⋯
     SDK


                                                                                                              4
하이브리드앱의꿈


ApplicationQuality

                             BEST           네이티브




                                    하이브리드




                             웹              WORST



                                               DevelopmentCost

                                                                         5
하이브리드라는이름의“짬뽕”...



                         난,
                    물~H2O~


    O


H             H




                                        6
앱개발자들을유혹하는“파란”짬뽕~



                                내가,
                               하이브리드~


    네이티브


웹                      웹




                                                        7
웹개발자들을유혹하는“빨간”짬뽕~



                                    나도,
                                  하이브리드~


           웹


네이티브                 네이티브




                                                           8
네이티브와웹의결합



                Flash/Flex?
                              Active-X?
JavaApplet?                    문제는....
                                        다리!!




                      Native-Web
네이티브                    Bridge             웹




                                                                 9
네이티브와웹의결합



                                WebView
WebViewClientWebChromeClient
                                    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 : UIViewControllerUIWebViewDelegate, 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
            Page


show/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
        h1span 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”/spansmall%/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”,
                                                  ...
다국어 지원 자리 잡기 태그
buttonspan data-nls=”common.back”이전/span/button
...
h1span data-nls=”setupTwitterPage.title”설정 - 트위터/span/h1
...
다국어 번역 텍스트 치환 결과
buttonspan data-nls=”common.back”Back/span/button
...
h1span 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:jslint

2.merge:antconcat

3.compress:YUICompressor

4.preprocess:antfilter

5.test:JSTestDriver

6.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               Appspresso
Appspresso                              Project                                      Plugin
Application
                                                                                            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”
!    descriptionContact Extension API Appspresso Plugin
!    /description
!    urlhttp://appspresso.com/url
!    authorAppspresso Dev. Team/author
!    licenseCopyright (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 contact
plusplus: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.java

package 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 extends
DefaultAxPlugin {                                     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.java

package 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 : NSObjectAxPlugin, ABPeoplePickerNavigationControllerDelegate {
@private
    NSObjectAxRuntimeContext *_runtimeContext;
}

@property (nonatomic,readonly,retain) NSObjectAxRuntimeContext* runtimeContext;

- (void)activate:(NSObjectAxRuntimeContext*)runtimeContext;
- (void)deactivate:(NSObjectAxRuntimeContext*)runtimeContext;
- (void)execute:(NSObjectAxPluginContext*)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];
  (NSObjectAxRuntimeContext*)runtimeContext {       // [picker release];
   _runtimeContext = [runtimeContext retain];       });
}                                                 }
                                                  ...
- (void)deactivate:                               중략
  (NSObjectAxRuntimeContext*)runtimeContext {   ...
   [_runtimeContext release];                     -
   _runtimeContext = nil;                         (void)peoplePickerNavigationControllerDidCance
}                                                 l:(ABPeoplePickerNavigationController
                                                  *)peoplePicker {
- (void)execute:(idAxPluginContext)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

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

  • 1.
  • 2.
    index 1. 하이브리드앱아키텍쳐개요 2. 하이브드리앱유형및특징 3.푸딩얼굴인식앱개발사례공유 4. 앱스프레소플러그인활용 5. LessonsLearned 6. References 2
  • 3.
    이런거... 아님-_-; 하이브리드앱아키텍쳐개요 3
  • 4.
    하이브리드앱아키텍쳐구성요소 네이티브 하이브리드 웹 UI툴킷 웹UI툴킷 자바스크립트프레임웍/라이브러리 웹표준기술 프레임웍 HTML5 CSS 자바스크립트 네이티브라이브러리 비표준DeviceAPIs 표준DeviceAPIs 개발도구 웹브라우져“엔진” 웹브라우져“앱” 플랫폼SDK 안드로이드 iOSSDK 윈폰7SDK …⋯ SDK 4
  • 5.
    하이브리드앱의꿈 ApplicationQuality BEST 네이티브 하이브리드 웹 WORST DevelopmentCost 5
  • 6.
  • 7.
    앱개발자들을유혹하는“파란”짬뽕~ 내가, 하이브리드~ 네이티브 웹 웹 7
  • 8.
    웹개발자들을유혹하는“빨간”짬뽕~ 나도, 하이브리드~ 웹 네이티브 네이티브 8
  • 9.
    네이티브와웹의결합 Flash/Flex? Active-X? JavaApplet? 문제는.... 다리!! Native-Web 네이티브 Bridge 웹 9
  • 10.
    네이티브와웹의결합 WebView WebViewClientWebChromeClient loadUrl addJavascriptInterface UIWebView UIWebViewDelegate loadRequest stringByEvaluatingJavascriptFromString 10
  • 11.
    네이티브와웹의결합 자바스크립트 캐시 그래봤자, 문자열~ URL 쿠키 어차피, 꼼수 그리고... HTTP! 그림 출처: http://petticoatsandpistols.com/2010/05/12/ 11
  • 12.
  • 13.
    네이티브지향하이브리드앱 사실상네이티브, 웹은거들뿐... · 제한적이고직관적인네이티브와웹의결합 · 웹브라우져as-aUI컴포넌트 · 도움말,앱/개발사소개,공지사항/새소식... · 웹기반사용자인증(OAuth)... 13
  • 14.
  • 15.
  • 16.
    예제코드(안드로이드) 웹서버 컨텐츠 불러오기 publicclass 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.
  • 20.
  • 21.
    예제코드(안드로이드) 링크 클릭 가로채기 ... WebViewwebView = (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) 링크 클릭 가로채기 @interfaceNoticeViewController : UIViewControllerUIWebViewDelegate, 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.
  • 27.
  • 28.
  • 29.
    “푸딩얼굴인식앱”소개 푸딩얼굴인식앱은... · 600만+다운로드! · @iolothebard와@seti222 · 5500+줄의자바스립트 · 2700+줄의CSS · 200+줄의네이티브코드 · 앱스프레소0.9+내부개발버전 29
  • 30.
    단일페이지인터페이스 index.html #pageId pageId.css pageId.js Active Page show/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 h1span 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”/spansmall%/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.
  • 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”, ... 다국어 지원 자리 잡기 태그 buttonspan data-nls=”common.back”이전/span/button ... h1span data-nls=”setupTwitterPage.title”설정 - 트위터/span/h1 ... 다국어 번역 텍스트 치환 결과 buttonspan data-nls=”common.back”Back/span/button ... h1span 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.
  • 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.
  • 48.
    앱스프레소플러그인구조 AppspressoPlugin Appspresso Appspresso Project Plugin Application 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”? axpluginid=”ax.ext.contact” version=”1.0” ! descriptionContact Extension API Appspresso Plugin ! /description ! urlhttp://appspresso.com/url ! authorAppspresso Dev. Team/author ! licenseCopyright (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 contact plusplus: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.java package 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 extends DefaultAxPlugin { 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.java package 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 : NSObjectAxPlugin, ABPeoplePickerNavigationControllerDelegate { @private NSObjectAxRuntimeContext *_runtimeContext; } @property (nonatomic,readonly,retain) NSObjectAxRuntimeContext* runtimeContext; - (void)activate:(NSObjectAxRuntimeContext*)runtimeContext; - (void)deactivate:(NSObjectAxRuntimeContext*)runtimeContext; - (void)execute:(NSObjectAxPluginContext*)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]; (NSObjectAxRuntimeContext*)runtimeContext { // [picker release]; _runtimeContext = [runtimeContext retain]; }); } } ... - (void)deactivate: 중략 (NSObjectAxRuntimeContext*)runtimeContext { ... [_runtimeContext release]; - _runtimeContext = nil; (void)peoplePickerNavigationControllerDidCance } l:(ABPeoplePickerNavigationController *)peoplePicker { - (void)execute:(idAxPluginContext)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.
  • 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.
  • 64.
    감사합니다 모바일개발실 / 앱스프레소팀/ 장동수 iolothebard at kthcorp dot com @iolothebard 64