오늘 타이타늄 티타임에서 나온 이야기 정리

오늘 타이타늄 티타임 모임에서 나온 이야기들을 정리해본다.

1. 말풍선 같은 가변 이미지 최적화 하기

기본 UI가 아닌 디자인된 UI를 앱에 적용하려면 많은 이미지를 사용하게 된다. 따라서 앱사이즈에도 영향을 받게 되는데, 20MB를 넘게 되면 3G 환경에서 앱을 다운로드 할수 없기 때문에 이미지 최적화 문제는 매우 크리티컬한 문제가 될수있다. 이런 이미지 문제를 해결하기 위한 첫번째 방법중 하나가 바로 이미지를 재활용해서 가변 디자인에 활용하는 것이다.
iOS의 경우, background*Cap을 활용하고,
Android의 경우, 9 patch tool을 활용한다.

2. Alloy 모델에서 생성한 DB 파일의 위치

예전에 “동구밭에서 삽질한 이야기“라는 발표 슬라이드에도 공유를 했었는데, 타이타늄에 인스톨한 DB는 특정한 위치에 저장이 되고, 강제로 설치된 DB파일을 지우지 않는한 두번 인스톨되지 않는다. 따라서 DB 스키마가 변경이 되면 이전 DB를 날리던지, 아니면 DB를 마이그레이션 해야한다. 참고로 DB뿐만 아니라 App Property로 저장한 값도 파일로 저장되기 때문에 앱을 지우지 않는한 남아있게 된다. 아무튼 시뮬레이터에서 이전 DB 파일의 위치는 다음과 같다.

/Users/[계정이름]/Library/Application%20Support/iPhone%20Simulator/[시뮬레이터버전]/Applications/[앱해쉬아이디]/Library/Private%20Documents/_alloy_.sql

시뮬레이터 경로자체가 다소 복잡하고 길기 때문에 보통은 console창에 찍힌 로그에서 복사한후에 붙여넣기해서 확인해보는게 가장 빠르다.
이전 DB 삭제 방법은 슬라이드를 참고하자!

3. join한 테이블을 이용해 백본 컬렉션 만들기

백본 모델은 기본적으로 key-value store라 불리는 NoSQL DB 형태로 생성되기 때문에 SQL의 join 테이블을 사용 할수가 없다. 두 모델에 있는 필드들을 join 하고 싶다면 보통은 forEach, filter, map 과 같은 순회 함수를 이용해 두 테이블의 컬럼값을 돌면서 하나로 합쳐야한다. 하지만 이 방법은 속도도 그렇고 비효율적인 면이 많다. 이럴때마다 “아~ join 테이블 한방이면 되는데..” 하는 아쉬움을 삼켜야했다. 하지만 역시나 쉬게 해결하는 방법이 있었다. 바로 Collection.fetch([option]) 메소드의 옵션을 사용하는 것이다. 옵션으로 query 속성을 넣으면 SQL문을 직접 사용할수있다.

내가 사용한 전략은 다음과 같다.

  1. 수정이 필요없는 static DB와 사용자의 액션에 따라 변하는 user DB를 따로 구분해둔다.
  2. static DB에 있는 두 테이블을 join해서 user DB에 새로운 테이블을 만든다.
  3. user DB에 join한 테이블을 백본 모델로 만든다.

그리고 샘플코드는 다음과 같다.

this.fetch({
    query : 'SELECT A.cropId, A.missionId, A.day, B.title FROM tb_mission_by_crop A JOIN tb_missions B ON A.missionId = B.missionId'
});

4. MapView에서 userLocation을 사용할때 주기적으로 그리고 자동적으로 현재 위치로 지도 중심이 이동되던 문제.

이 부분은 머리를 맞대고 얘기해본 결과 내가 코드를 잘못짠 것으로 확인됐다. 코드를 잘못 짜게 된 결정적 이유는 Geolocation의 ‘location’ 이벤트를 잘못 해석했기 때문이다.

// 디바이스의 현재 GPS 정보를 얻으면 해당 위치로 중심을 이동시킨다.
Ti.Geolocation.addEventListener('location', onLocation);

위 코드에서 나는 location 이벤트가 디바이스가 GPS 정보를 얻게되면 발생하는 줄 알았다. 물론 틀린 해석은 아니지만 원문은 이렇다.

Fired when a location update is received.

즉, 디바이스의 위치가 변경되면 발생한다!. 따라서 고정 위치라면 상관이 없겠지만 위치가 이동중이라면 location 이벤트가 계속 발생하기 때문에 기대한 결과가 나오지 않았다. 문서를 대충보면 꼭 이런 오류가 발생한다. API 문서는 꼼꼼히 읽을 필요가 있다.

5. 앱 배포를 위한 준비

그동안 만들어왔던 앱을 스토어에 올리기 위해 필요한 절차를 밟아봤다. 먼저 iTunes Connect에서 앱정보를 기술해야한다. 그리고 배포 인증서를 받아서 iTunes Store로 배포 빌드를 돌린다. 배포 빌드를 돌리면 빌드된 앱이 XCode의 아카이빙(Organizer – Archives탭에서 확인)된다. 이렇게 아카이빙 된 앱은 유효성 검사 후에 배포할 수 있다. 유효성 검사를 간단히 마치고 배포를 진행하면 자동으로 스토어에 앱이 제출 된다. 물론 심사과정을 거치고 리젝 당하면 위 과정을 반복해야한다.

6. 테스트 앱 배포 시스템

그동안 개발된 앱들은 빌드해서 개발 인증서와 .plist 파일을 만들어 함께 FTP에 올렸었다. 매번 개발버전이 나올때마다 앞의 과정을 반복했는데,.. 너무 귀찮다. 한방에 자동으로 해주는 툴이나 서비스가 있으면 참 좋겠다 싶어 종은님에게 여쭤봤다. 아니나 다를까 역시나! 귀찮은 작업을 간소화해주는 서비스가 존재한다고 한다. 자세한 내용은 다음 링크를 확인하자.

5년간 정든 회사를 떠나다.

한참을 고민했다. 퇴사도 선택지에 있었지만 이번엔 이직을 선택했다.
이직 결심과 이직을 확정하기까지 대략 4개월이 걸린것 같다.

이력서를 써야겠다고 다짐하는데만 2주를 고민했다.
그리고 이력서를 쓰는데도 5일이 걸렸다. 그만큼 5년이란 시간은 적지않은 시간이다.
한해한해를 정리하면서 참 많은 일들을 했구나 하는 생각도 들었다.

이직이라는 과정은 절대 쉽지 않다. 내가 그동안 쌓아온 이력을 증명하는 일! 그 자체가 스트레스다.
이미 설명하지 않아도 내 주변인들은 다 아는데, 나는 이걸 이직과정에서 증명해내야한다.
이력서 쓰는 것부터 프로그래밍 테스트, 인터뷰에 신체검사까지 많은 시간을 투자해야한다.
최종 면접에서 떨어지기라도 한다면 엄청난 멘붕을 이겨내야한다.
이미 떠만 마음을 되돌리긴 쉽지도 않다. 한마디로 이직은 어렵다.

이렇게 힘든 이직의 고민을 안겨준 회사가 참 밉다.
정말 좋았었는데.. 그만큼 더 야속하고 밉더라.  
한편으론 이직의 고민을 하지 않아도 되는 공무원이란 직업이..
이래서 좋은거구나라는 생각도 해본다.  

어찌됐든 나는 왜 이직을 선택했는지 곰곰히 생각해봤다.
많은 이유가 있다. 딱 꼬집어 말하긴 어렵다. 복합적이다.

그리고 새로운 곳…
좋은 대우와 새로운 환경이라는 이유만으로 선택했지만,..
지금은 함께 일하게 될 동료들이 궁금해지기 시작했다.
또 무슨일을 하게될까?… 재밌겠지?
기대된다!
 

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');