8월 타이타늄 커뮤니티 티타임 후기

오랜만에 티타임을 가졌다. 하루 밖에 안지났는데,.. 벌서 기억나지 않는 것들이 너무 많다.. -_-;;.. 일단 생각나는 대로 정리!

Alloy 이슈들

Alloy 1.2 버전부터 ListView를 XML로도 정의할수 있게됐다. 물론 ListView는 아직 한창 개발중이기 때문에 drag 이벤트라든가 headerView나 Pulldown Refresh 기능들은 좀더 기다려야한다. 자세한 내용은 Alloy Jira 페이지를 확인해보자.

ListView에 데이터 바인딩시 삽질하기 쉬운 착각들

1. 먼저 dataCollection은 ListView가 아닌 ListSection에 정의해야한다.

이 문제 때문에 삽질한 시간은 정말 눈물 겹다. TableView의 경우 dataCollection을 TableView에 정의 하지만 ListView 조금 다르다. 다음 예제를 보자. 아래와 같이 ListView에 dataCollection을 정의하면 Alloy 컴파일 오류를 발생시킨다.

<ListView dataCollection='잘못된정의'>
    <Templates>
        <!-- 여기에 여러 템플릿을 정의 할수있다. -->
        <ItemTemplate name="tpl1" />
        <ItemTemplate name="tpl2" />
    </Templates>
    <ListSection></ListSection>
</ListView>

따라서 dataCollection은 반드시 아래와 같이 ListSection에 정의하도록 하자!! dataCollection으로 지정된 값은 models 폴더에 정의된 파일명과 동일한 콜랙션을 바인딩하게 된다. 아래 예제는 posts 콜랙션을 리스트뷰 템플릿에 바인딩하게 된다.

<ListView> 
    <Templates> 
        <ItemTemplate name="tpl1"></itemtemplate> 
        <ItemTemplate name="tpl2"></itemtemplate> 
    </Templates> 

    <!-- 데이터 바인딩시 dataCollection 속성은 필수 인자다. -->
    <ListSection dataCollection='posts'></ListSection>   
</ListView>

2. dataTransform과 dataFilter 속성의 차이점.

데이터 바인딩시 dataCollection 속성과 더불어 설정할 수 있는 2가지 속성이 더 있는데, 바로 dataTransformdataFilter 속성이다. 이 두 속성은 모두 아래와 같이 함수를 지정해야한다.

<ListView> 
    <Templates> 
        <ItemTemplate name="tpl1"></itemtemplate> 
        <ItemTemplate name="tpl2"></itemtemplate> 
    </Templates> 

    <!-- 데이터 바인딩시 추가로 dataTransform과 dataFilter를 지정할 수 있다. -->
    <ListSection dataCollection='posts' dataTransform='doTransform' dataFilter='doFilter'>
    </ListSection>   
</ListView>

두 함수의 차이는 간단하다. dataTransform 속성은 ItemTemplate에 데이터를 입힐 때 실행되는 함수로써 바인딩되는 콜렉션의 모델을 인자로 받아 가공할수있다. 반면에, dataFilter는 콜랙션 자체를 필터링 할때 사용한다.

3. 데이터 바인딩시 반드시 ListSection에 ListItem을 지정해라!

흔히 착각하는 것중에 하나가 ListSection을 비워두면 “초기에 아무런 값이 없고 나중에 동적으로 채워 넣을수 있겠지?” 하는 생각인데, ListView 자체를 컨트롤러에서 동적으로 직접 create해서 생성하는 경우에는 맞는말이다. 하지만 데이터 바인딩을 한 경우에는 반드시 아래와 같이 ListItem을 지정해야하고, 이렇게 지정된 녀석이 콜렉션의 갯수만큼 반복 된다는 사실을 잊지말자!

<ListView> 
    <Templates> 
        <ItemTemplate name="tpl1"></itemtemplate> 
        <ItemTemplate name="tpl2"></itemtemplate> 
    </Templates> 
    <ListSection dataCollection='posts' dataTransform='doTransform' dataFilter='doFilter'>
        <!-- 콜랙션을 반복하면서 아래에 정의된 아이템을 한벌로 생성한다. -->
        <ListItem template="tpl1"/>  
        <ListItem template="tpl2"/>
    </ListSection>   
</ListView>

성능 이슈

Alloy 이외에 일반적인 성능과 관련된 이야기들이 있었다. 정리하면 다음과 같다.

1. class 아니죠! className 입니다.

TableViewRow의 속성으로 같은 템플릿으로 구성되는 행들을 같은 className으로 지정해야 내부적으로 재활용하게 된다. class와 className을 헷갈리는 경우가 있으니 철자에 주의하자!

2. 디바이스 정보를 얻어오는 API는 한번만 호출하자!

Ti.Platform.xxxx 형태의 API는 비용이 비싸므로 반드시 로컬변수에 사용할 값을 캐시해서 사용한다.

var DEVICE_WIDTH = Ti.Platform.displayCaps.platformWidth;

3. 윈도우를 생성할 때 주의할 점.

윈도우는 비싼 비용을 지불하고 만드는 녀석이다. 특히 컨테이너로서 윈도우 안에 많은 뷰나 컨트롤들이 올라가게 되고, 이벤트를 바인딩 했다가 해제를 제대로 못하는 경우엔 메모리누수가 생길수가 있다. 따라서 습관적으로 윈도우가 닫힐때 $.destroy()를 실행시켜주자.

$.window.addEventListener("close", function(){
    // 특히, dataCollection 메소드를 이용해 데이터 바인딩을 한 경우 반드시 실행해야한다. 
    $.destroy();
});

4. 다중 윈도우를 이용해 뷰를 구성할때 주의할 점.

FaceBook과 같은 레이아웃을 구성할때는 가장 밑단에 깔리는 리스트 뷰는 매번 생성할 필요가 없다. 다만 쓰기나 상세 보기처럼 잠깐 나왔다가 사라지는 윈도우라면 매번 생성해되 윈도우가 닫힐때 걸린 핸들러를 잘 해제해준다. 위 3번 참고.

5. 이미지를 다룰때는 반드시 ImageView에 올리자.

캐시하는 방법은 Image_Best_Practices 문서를 참고한다.

6. 전역 이벤트 리스너를 윈도우와 혼합할때 생기는 메모리 누수

다음 코드처럼 동적으로 생성한 컨테이너를 전역 이벤트 리스너에서 사용한 경우 절대 지워지지 않는다! 주의하자!

var someFunction = function() { 
    var table = Ti.UI.createTableView(), 
        label = Ti.UI.createLabel(), 
        view = Ti.UI.createView();

    Ti.App.addEventListener('bad:move', function(e) {
        // table은 지역변수지만 앱이 종료되기 전까지 해제되지 않는다. 
        table.setData(e.data);
    });

    view.add(table);
    view.add(label);
    return view;
};

그밖에 ACS 이슈들..

1. ACS에서 있는 데이터를 fetch 할때 ORDER를 따로 지정하지 않으면, 랜덤하게 넘어오므로 ORDER를 지정하자.

2. ACS에 RESTful API는 구현되어 있으나 Ti.Cloud 객체에 구현되지 않는 객체도 있다.

3. 서버 이슈를 클라이언트로 끌어들이지 말자!

Alloy 모델과 DB 연동

아직 완성된 글이 아닙니다.
—-
Alloy의 모델은 Backbone의 모델을 사용하므로, Backbone의 API와 Event를 그대로 사용할수있다. 일단 스튜디오에서 Alloy모델을 생성하게 되면 /model 폴더에 해당 파일이 생성되고 기본 구조는 다음과 같다.

1. 모델의 기본 구조

exports.definition = {
 config : {
 // table schema and adapter information
 },
extendModel: function(Model) { 
 _.extend(Model.prototype, {
 // Extend, override or implement Backbone.Model 
 });

 return Model;
 },
extendCollection: function(Collection) { 
 _.extend(Collection.prototype, {
 // Extend, override or implement Backbone.Collection 
 });

 return Collection;
 }
}

일반적인 MVC 모델의 경우 다음 그림과 같이 컨트롤러에서 모델과 뷰를 생성하고 모델을 뷰의 인자로 넘기게 되는데, Alloy도 같은 MVC 구조를 가지기 때문에 Alloy 모델은 컨트롤러에서 생성되어야한다.
< 그림1, 삽입>
– MVC 기본 아키텍쳐 UML

모델 생성은 Alloy.createModel() 메소드를 이용한다. 샘플 코드를 보자.

 var book = Alloy.createModel('book', {title:'Green Eggs and Ham', author:'Dr. Seuss'}); 
 var title = book.get('title');
 var author = book.get('author');
 // Label object in the view with id = 'label'
 $.label.text = title + ' by ' + author;

위 코드에서 book 객체는 백본 모델이기 때문에 백본의 API를 그대로 사용할 수 있다. 그리고 이렇게 생성된 모델을 다른 컨트롤러에서도 사용하고 싶은 경우가 매우 많을 것이다. 이럴때는 Alloy.Models.instance() 메소드를 이용해 접근할수있다.

var book = Alloy.Models.instance('book');

물론 XML 뷰에도 모델을 넘겨서 사용하고 싶을수도 있다. 다음과 같이 XML 뷰와 함께 모델을 정의하면, 컨트롤러는 모델과 뷰를 자동으로 생성한다.

 <Alloy>
 <Model id="myBook" src="book" instance="true"/>
 <Window>
 <TableView id="table" />
 </Window>
</Alloy>

XML 뷰를 통해 자동으로 생성된 모델은 다음과 같은 방법은 접근한다.
– id 지정시, 컨트롤러에서 $.myBook 으로 접근
– src를 이용해 Alloy.Models.book 으로도 접근 가능 (전역변수)
– src는 book.js 와 맵핑된다.
– instance가 true면 싱글톤 모델을 만든다. false면 다른 인스턴스를 만든다.

2. 모델 설정

모델의 config 객체는 columns, defaults, adapters 라는 3개의 객체를 가진다.

  • columns – 테이블 스키마, SQLite가 인식하지 못하는 값은 모두 TEXT 타입로 지정된다.
  • defaults – undefined일 경우 설정되는 기본값
  • adapters – 실제 저장소와 맵핑하기 위한 type과 collection_name을 갖는다.
exports.definition = {
 config: {
 "columns": {
 "title": "String",
 "author": "String"
 },
 "defaults": {
 "title": "-",
 "author": "-"
 },
 "adapter": {
 "type": "sql",
 "collection_name": "books"
 }
 }
}

3. Backbone.Model 클래스 확장하기

기존 모델을 확장해서 필요한 필드나 함수를 만들수있다. 예를 들면 다음과 같다.

exports.definition = {
  config : {
   // table schema and adapter information
  },
  extendModel: function(Model) { 
    _.extend(Model.prototype, {
      // Implement the validate method 
      validate: function (attrs) {
        for (var key in attrs) {
           var value = attrs[key];
           if (key === "title") {
             if (value.length <= 0) {
               return "Error: No title!";
             }
           }
           if (key === "author") {
             if (value.length <= 0) {
               return "Error: No author!";
             } 
           } 
        }
      },
      // Extend Backbone.Model
      customProperty: 'book',
      customFunction: function() {
        Ti.API.info('I am a book model.');
      }, 
   });

   return Model;
 }
}

위 코드를 컨트롤러에서는 다음과 같이 사용한다.

var book = Alloy.createModel('book', {title:'Green Eggs and Ham', author:'Dr. Seuss'}); 
// Since set or save(attribute) is not being called, we can call isValid to validate the model object
if (book.isValid() && book.customProperty == "book") {
 // Save data to persistent storage
 book.save();
}
else {
 book.destroy();
}

4. 콜렉션

콜렉션은 순서가 있는 모델들의 집합을 의미한다. 마찬가지로 Backbone의 콜렉션을 상속받았으므로 똑같이 쓸수있다. 콜렉션 생성은 Alloy.createCollection() 메소드를 사용한다.

var library = Alloy.createCollection('book'); 
library.fetch(); // Grab data from persistent storage

모델과 마찬가지로 전역적으로 사용하고 싶다면 Alloy.Collections 객체를 이용한다.

var library = Alloy.Collections.instance('book');

5. 콜렉션 확장하기

모델과 마찬가지로 확장가능하고 백본에서 구현하지 않고 인터페이스만 뚫어둔 comparator 메소드를 구현해서 콜렉션을 정렬할 수 있다.

exports.definition = {
config : {
 // table schema and adapter information
 },
extendModel: function(Model) { 
 _.extend(Model.prototype, {
 // Extend, override or implement the Backbone.Model methods 
 });
 return Model;
 },
extendCollection: function(Collection) { 
 _.extend(Collection.prototype, {

 // Implement the comparator method.
 comparator : function(book) {
 return book.get('title');
 }
}); // end extend
  return Collection;
 }
}

6. 이벤트 핸들링

on, off, trigger 3개의 주요 메소드를 활용한다. 주의해야할 것은 3.0부터 Alloy모델과 콜렉션은 타이타늄 메소드를 지원하지 않는다. (addEventListener, removeEventListener, fireEvent)
또하나 주의할점,Backbone의 add, change, destroy, fetch, remove, reset 이벤트는 오버라이딩하지 말것!

var library = Alloy.createCollection('book');
function event_callback (context) {
 var output = context || 'change is bad.';
 Ti.API.info(output);
};
// Bind the callback to the change event of the collection.
library.on('change', event_callback);
// Trigger the change event and pass context to the handler.
library.trigger('change', 'change is good.');
// Passing no parameters to the off method unbinds all event callbacks to the object.
library.off();
// This trigger does not have a response.
library.trigger('change');

타이타늄 SDK3.0과 Alloy(beta)를 쓰면서 겪었던 문제들

타이타늄 3.0이 정식 릴리즈를 했고, 그동안 베타버전을 쓰면서 나타났던 문제들을 정리해본다.

1. 이유없이 에뮬레이터가 죽는 경우

Alloy를 사용하다보면 빌드하자마자 “Application has exited from Simulator” 라는 메시지를 던지면서 죽는 경우를 꼭 한번씩은 겪게 된다. 대표적인 경우가 console.log를 이용해 Alloy 모델이나 콜랙션을 그대로 출력할때 발생한다. 또 다른 예로 Fugitives 소스코드에서 29번째 라인 부근을 보면 다음과 같은 코드가 있다.

var model = atLargeCollection[i];
var row = Alloy.createController('FugitiveRow', model.toJSON()).getView();
row.model = model;
rows.push(row);

위 코드에서 문제가 되는 코드는 바로 3번 라인에 있는  row.model = model 코드다. 즉, Alloy 모델을 테이블 뷰에 할당할때 발생한다.

이 문제의 정확한 원인은 사실 나도 모른다. 현상만 따지자면 Alloy 모델이나 콜렉션을 자바스크립트 객체에 할당할때 문제가 발생한다는 사실이다. 이런 현상을 미루어 보아  강력하게 추정되는 몇가지가 있다. 타이타늄모듈 컨셉을 보면, 자바스크립트 객체는 네이티브 프록시 객체와 연결되어 있다. 예를 들어 아래 코드를 보자.

var win = Ti.UI.createWindow();
win.title = "이것은 윈도우 타이틀";
win.userData = "이것은 그냥 자바스크립트 변수";

위 코드에서 타이타늄 모듈로 생성한 win 객체는 자바스크립트 객체이지만 네이티브 프록시 객체이기도 하다. 즉, 위에서 title 속성은 네이티브 프록시 객체와 연결되어 윈도우의 타이틀을 변경하지만, userData 속성은 네이티브 프록시 객체와 연결되어 있지않기 때문에 그냥 자바스크립트 변수에 값을 할당한 것에 지나지 않는다.

이렇게 타이타늄에서 사용되는 자바스크립트 객체는 순수 자바스크립트 객체와 네이티브 프록시 객체가 혼합되어 있는데, 프록시 객체에 Alloy 객체의 인스턴스를 직접 할당하면 뭔가 충돌이 발생하는 것 같다.  따라서 Alloy 객체에서 데이터만 뽑아서 할당하도록 한다.

— updated  2012-12-18 종은님 제보로 수정 —

프록시 객체에 값을 할당할때 프리미티브(Primitive) 값이 아닌 참조 값을 넣으면 문제가 발생한다. 문제라기 보다 정확히 그 값을 보장받지 못함. 따라서 프록시 객체에 값을 할당할때는 가급적 문자열이나 숫자등의 값만 할당하고, 객체나 배열 할당은 주의해야함.

—-

여튼 해결책은 Alloy 모델을 toJSON() 메소드를 이용해 순수 자바스크립트 객체로 추출해서 할당하면 문제가 발생하지 않는다. 콘솔에 출력하고 싶은 경우에는 JSON.stringify() 메소드를 이용해 문자열로 변환해서 출력하면 문제가 발생하지 않는다.

2. SDK 3.0 개발자 인증서 인식못하는 버그

이것은 확실히 버그인 것 같다. 타이타늄 정식 버전이 나오자마자 디바이스 디버깅을 해보려고 폰에 포팅을 시도했는데.. 잘 인식되던 인증서가 인식이 안되는 상황이 발생했다.

왜 안되지? 이러면서 기존에 미리 깔았던 beta 버전도 싹다 지우고 타이타늄 정식버전을 다시 깔아봤는데도 마찬가지다. 그래서 SDK 2.x버전으로 바꿔서 디바이스로 포팅을 해봤는데. 2.x버전은 문제없이 인식도 잘되고 포팅도 잘된다. 이미 타이타늄 커뮤니티엔 같은 문제를 겪는 사람도 많다. 따라서 이 문제는 SDK 버그로 결론 지었다. 조만간 패치가 나오길 기대해본다.

3. 발견된 자잘한 버그들

UI와 관련한 자잘한 버그들도 많았다. 내가 발견한 버그들이 이미 신고되어 버그로 등록되어 있는지는 찾아보진 않았지만, 일단 발견한 것만 써보면.

다중 커스텀 픽커 초기화 문제

Alloy를 이용해 뷰.xml에 다중 커스텀 픽커(Picker)를 정의하고 각 픽커의 컬럼을 아래와 같이 자바스크립트로 설정하려고 할 때, 컬럼이 하나만 설정되는 버그가 있다.

$.picker.setSelectedRow(2, 0);
$.picker.setSelectedRow(1, 2, true);
$.picker.setSelectedRow(0, 3);

아마도 알로이에서 뷰를 만들 때, 뷰가 생성되는 시점이 다른것 같다. 그래서 간단히 setTimeout을 이용해 타이밍을 조절했더니 3개의 컬럼이 모두 잘 설정된다.

Alloy 뷰(.xml)파일에 UI를 미리 정의해 숨겨놓고 싶은 경우

Alloy를 쓰다보면 아무래도 Ti.UI.createXXX 형태로 UI를 만드는 것보다 xml과 tss를 이용해 마크업 방식으로 작성하는게 훨씬더 직관적이고, 쉽다는 것을 금방 알게 된다. 그래서 왠만한 UI들은 xml로 정의하고 싶은데… 동적으로 생성되어야하거나 조건에 따라 보일지 말지를 정의해야하는 경우가 종종 있다. 이런 경우 원래 Ti.UI 컴포넌트들이 가지고 있는 visible 속성을 바꿔서 처리하면 간단할텐데.. 그래서 이것을 xml로 정의하니까 visible속성이 제대로 먹질 않는다. 버그로 판단되고, 현재는 이 문제를 해결하려면 아래와 같은 꼼수를 써야한다.

<Window id="window">
   <TableView id="table"></TableView>
   <TableViewRow id="repeatRow">
      <Switch id="repeatSwitch" />
   </TableViewRow>
   <Button id="saveBtn" visible="false" title="완료"/>
   <Button id="cancelBtn" visible="false" title="취소"/>
</Window>

 

위 코드의 경우 윈도우에 테이블이 전체를 덥기 때문에 TibleViewRow는 윈도우 안에 정의해도 보이지 않는다. 그리고 Button도 마찬가진데, 이 버튼은 Window의 좌우 네비게이션 버튼으로 삽입하기 위해 미리 만들어 둔것이다.

윈도우 좌우 네비게이션의 경우 현재는 지원하지 않고 있는데, 공식적으로 Alloy뷰가 지원할 계획이라고 한다.

— updated 2012-12-21 —

윈도우 타이틀 바에 좌우 네비게이션 버튼을 추가하고 싶은 경우

앞에서와 같이 View.xml 에 미리 정의해둔 녀석을 Controller.js 에 다음과 같은 코드로 추가할 수있다.

if ( Ti.Platform.onsname === 'iphone' ){
   $.window.leftNavButton = $.cancelBtn;
}

 

그런데, 여기서 주의해야 할 점이 하나 있다. 위와 같이 코드를 작성하고 윈도우 타이틀 바에 버튼을 붙이게 되면, 실제로는 윈도우에 2개의 버튼이 붙게된다. 하나는 View.xml에 정의한대로 visible=false인 숨겨진 버튼이 윈도우 중앙에 배치되고, 또다른 하나는 윈도우 타이틀 바 왼쪽 네비게이션  버튼으로 추가된다. 즉, View.xml에 정의하게 되면 사실상 윈도우에 자동으로 추가되는 코드가 되므로 불필요하게 붙은 버튼은 제거해주는게 좋다.

따라서 위 코드는 아래와 같이 작성한다.

if ( Ti.Platform.onsname === 'iphone' ){
   $.window.remove($.cancelBtn);
   $.window.leftNavButton = $.cancelBtn;
}

 

—-