Titanium에서 JSS(javascript style sheet) 사용법

Titanium에서 1.5버전부터 JSS (javascript style sheet)을 지원하기 시작했다.
jss란 무엇인가? html에 스타일을 적용할 때 CSS(Cascading Style Sheets)를 쓰는 것 처럼 javascript를 기반으로 style을 다룰 때 사용하는 방식이다.

js파일에 해당하는 jss만들기
app.js에 적용할 jss파일은 동일한 이름으로 같은 디렉토리에 생성하면 된다. (app.js 와 app.jss)

jss에 스타일 정의하기
css에서는 id, class, dom element 3가지 방법으로 스타일을 정의하고 적용할 수 있다. Titanium에의 JSS도 이와 유사하다. 다음과 같은 3가지 방법을 제공한다.

ID-based Style : #id로 형태로 선언하고 ui component 생성시 id를 지정한다.

// file : app.js
var styleWindow = Titanium.UI.createWindow({
id:'styleWin',
title:'스타일 적용 할 윈도우'
});
/* file : app.jss */
#styleWin {
background-color:#2A2623;
}

Type-based Style : component type이름으로 선언만하면 해당 component에 적용된다.

// file : app.js
var styleWindow = Titanium.UI.createWindow({
title:'스타일 적용 할 윈도우'
});
/* file : app.jss */
window { //app.js의 모든 window에 기본적으로 적용된다.
background-color:#2A2623;
}

Class-based Style :  .class 형태로 ui component 생성시 className을 지정한다.
(css의 경우 하나의 dom 엘러먼트에 여러개의 class지정하여 사용하나 jss에서는 아직 지원 안하는 모양이다.^^)

// file : app.js
var styleWindow = Titanium.UI.createWindow({
className:'style_win',
title:'스타일 적용 할 윈도우'
});
/* file : app.jss */
.style_win {
background-color:#2A2623;
}

3가지를 전부 정의 했을 때는 다음과 같은 우선 순위를 갖는다.

ID-based styles > type-based styles > class-based styles

이 순서를 확인도 할겸 예제 파일을 만들어 보았다.

p.s. 모든 js파일에 자동적용되는 공통적인 jss는 global.jss에 정의하면 된다고 나와있으나 실제 해보면 적용이 되지 않았다. 하지만 기본적인 css에서 사용하는 import로 공통적으로 사용하는 jss를 불러와서 사용할 수 있다.
따라서 공통적으로 쓸 jss의 경우 base.jss와 같이 독립된 파일을 만들고 base.jss가 필요한 jss에서 import를 시켜 사용하면 편리하다.

/* file : base.jss */
window {
barColor:#2A2623;
backgroundColor :#000;
}
/* file : app.jss */
@import url('./base.jss');
app {
background-color:#000;
}

reference
http://wiki.appcelerator.org/display/guides/Designing+the+User+Interface#DesigningtheUserInterface-CrossplatformlayoutusingJSS

Advertisements

이제 Titanium이 Mobile Web까지..

출처: Appcelerator Developer Blog (CCL 적용대상 아님)

Appcelerator가  Titanium Studio 및 Mobile SDK 1.7의 RC1 버전을 공개했다. 그리고 다양한 내용을 담고 있는 블로그 포스트가 올라왔다.
http://developer.appcelerator.com/blog/2011/05/major-titanium-updates.html

위 포스트의 내용은 다음과 같다.

  1. Titanium Mobile 1.7 RC1
  2. Titanium Desktop 1.2
  3. Titamium Mobile Web
  4. Titanium Mobile for BlackBerry Beta 2
  5. Titanium Studio RC1
  6. 새로운 Titanium 유료 계정 (inde, professional, enterprise)

Titanium을 사용하고 있는 개발자라면 하나를 제외하고 이미 예고된 내용들일 것이다.  처음으로 등장한 것이 바로 Mobile Web 관련이다.
아래 시연 동영상처럼 Titanium Mobile SDK를 통해 App을 제작하면 그대로 Mobile Web 으로 구동 가능하게 해준다는 것이다. indie 계정 이상에게만 공개된것이다 보니 아직 써보진 못했다. 당연히 완벽하게 되진 않겠지?

[iframe http://player.vimeo.com/video/23832536 640 480]

동영상 출처: Appcelerator Developer Blog (CCL 적용대상 아님)

하지만 요즘 Titanium의 행보를 보면 Titanium 으로 모든 것을 다 할 수 있는 맥가이버 칼(?) 같은 걸 만들고 싶은게 아닌가 하는 생각이든다. (맥가이버 칼은 매력적이지만 정교한 작업은 힘들듯이 이것이 장점이 되기도 하지만 단점이 되기도..)

비록 새롭고 쓸만한 것들은 이제 유료 계정에서만 사용가능하게 끔 하는 것 같다. ㅠㅠ
어찌 되었든 Titanium의 새로운 소식들은 나에게는 재미있고 반가운 소식이다^^


Android Device로 Titanium App 넣을때 R.java Error

Titanium에서 안드로이드 앱을 설정 한다는 것은 쉬운 일이 아닌 것 같다. 설정해줘야 할 것도 생각 보다 많고 설정하다가 안되는 경우도 많았다.

어렵게 설정을 다해 성공을 했는데 나중에 다시 새로운 프로젝트를 만들고 안드로이드 폰에 넣으려고 하니 R.java 에러가 날때가 있다. Resource 안의 폴더를 지웠다가 다시 실행도 해보고 새로운 프로젝트도 생성해서 해봤지만 소용 없었다.

결국, 찾은 해답은 App ID의 문제 였다. App ID에 최소한 하나의 ‘.’ (dot) 이 중간에 들어가야 한다. 이를 지키지 않으면 다음과 같은 에러가 나더라.

[ERROR] Error generating R.java from manifest

이런 error 때문에 고생한다면 appid 를 확인해보자!

p.s. Apple의 앱 아이디의 경우 apple에서는 domain을 거꾸로 적은 형태의 id를 권한다.( com.yomybaby.c ) 하지만 ‘test’ 와 같은 일반적인 텍스트 형태를 넣어도 Titanium에서는 문제 없이 돌아간다.
App ID는 말 그대로 앱을 나타내는 고유의 ID 이며 app을 구분하는데 쓰인다. push notification을 사용하기 위해서는 *와 같은 id를 쓰지 않고 com.domain.www 형태 이어야 한다.


모바일웹 개발시 URL 입력하기 어려우시죠? Titanium과 Nodejs로 해결하기

모바일 웹을 개발하다보면 실제 폰에서 테스트를 하기위해 개발중인 URL을 모바일 기기에 일일이 입력해야하는 어려움이 있다. 조그마한 터치 키보드로 긴 url을 치려다보면 속이 터진다. (사실 나는 모바일 웹개발이 주 업무가 아니어서 속이 터지는걸 자주 경험해보진 않았다.^^ 주변에서 개발하시는 분들이 이 일을 상당히 귀찮아 한다.)

이 귀찮음을 해결할 만한 어플을 Titanium과 Nodejs를 이용하면 뚝딱(?!) 만들어 보기로 했다. 컨샙은 이렇다.
내가 데스크탑 브라우저에서 보고 있는 웹페이지의 URL을 자동으로 모바일 기기로 보내서 모바일 기기에서도 똑같은 웹페이지가 보이도록 하는 것이다. (스마트폰에 키 입력을 하지 않아도 되도록 말이다!)
만들어 봤고 실제 작동하는 모습을 영상으로 담아보았다.


위에서 처럼 동작하기 위해 코딩한 파일은 2개이며 모두 .js파일 즉 javascript이다. (+북마클렛용 js코드 한줄)
파일 두개로 뚝딱(!?) 가능한 이유는 바로 Titanium과 Nodejs 덕분이다.

Titanium Appcelerator : javscript로 다양한 플랫폼의 application 개발이 가능한 framework ( appcelerator.com )
Nodejs : Server side javascript로 js만으로 server를 구현 할 수 있다. ( nodejs.org )

#1 app.js for Titanium mobile
아이폰/안드로이드 앱은 nodejs 서버에서 url을 가져온다. 아이폰의 경우 tcp socket을 통해 연결되어 있다. 아쉽게도 titanium sdk 1.7 preview 현재 버전까지는 안드로이드에서 tcp socket을 지원하지 않는다. 일단 안드로이드에서는 그냥 xmlhttprequest로 계속 주소 변화를 확인하도록 했다. (업데이트 : 1.7 정식버전에는 안드로이드도 지원하는 Socket API가 추가 되었다.)

// this sets the background color of the master UIView (when there are no windows/tab groups on it)
Titanium.UI.setBackgroundColor('#000');

var webview = Titanium.UI.createWebView({
	url:'http://m.daum.net',
	bottom:40
});

var win = Titanium.UI.createWindow();
win.add(webview);

Ti.App.Properties.setBool('_watching', false);
var watchURLChange = function(host, port, win) {
	if(Ti.App.Properties.getBool('_watching')) {
		return false;
	}
	Ti.App.Properties.setBool('_watching', true);

	var socket = Titanium.Network.createTCPSocket({
		hostName: host,
		port: port,
		mode: Titanium.Network.READ_WRITE_MODE
	});

	socket.addEventListener('read', function(e) {
		try {
			var o = JSON.parse(e.data.text);
			switch(o.action) {
				case 'changeURL':
					Ti.API.info('url Changed : '+o.url);
					webview.url = o.url;
					break;
				case 'connect':
					Ti.API.info('Socket connected');
					webview.url = o.url;
					break;
			}
		} catch(event) {
			Ti.API.error('read error', event);
		}
	});
	// Cleanup
	win = win || Ti.UI.currentWindow;
	if(win) {
		win.addEventListener('close', function(e) {
			if (socket.isValid) {
				Ti.API.log('close socket');
				socket.close();
			}
		});
	}

	socket.connect();
	socket.write(JSON.stringify({
		action: 'echo',
		message: 'Socket connected'
	}));

};
//webview 아래 컨트롤 view 생성
(function() {
	var autoIntervalID;
	//reload 버
	var reloadBtn = Titanium.UI.createButton({
		title : "Reload",
		width : 60
	});
	reloadBtn.addEventListener('click', function() {
		webview.reload();
	});
	//뒤로가기 버튼
	var backBtn = Titanium.UI.createButton({
		title:'◀',
		enabled:false,
	});
	backBtn.addEventListener('click', function() {
		webview.goBack();
	});
	//앞으로가기 버튼
	var forwardBtn = Titanium.UI.createButton({
		title:'▶',
		enabled:false
	});
	forwardBtn.addEventListener('click', function() {
		webview.goForward();
	});
	//자동 리프레쉬 스위치
	var autoSwitch = Ti.UI.createSwitch({
		value:false,
		top:5,
		left:50,
		title:"",
		titleOn:"",
		titleOff:""
	});
	autoSwitch.addEventListener('change', function(e) {
		if(e.value) {
			autoIntervalID = setInterval( function() {
				webview.reload();
			},10000)
		} else {
			if(autoIntervalID)
				clearInterval(autoIntervalID);
		}
	});
	// 버튼 wrap
	var btnView = Titanium.UI.createView({
		width:'100%',
		height:40,
		backgroundColor:'#2A2623',
		layout:'horizontal',
		bottom:0
	});
	btnView.add(backBtn);
	btnView.add(forwardBtn);
	btnView.add(reloadBtn);
	btnView.add(autoSwitch);
	win.add(btnView);

	//webview의 loding activity indicator
	var toolActInd = Titanium.UI.createActivityIndicator({
		width:30,
		height:30
	});
	toolActInd.style = Titanium.UI.iPhone.ActivityIndicatorStyle.DARK;
	webview.add(toolActInd);
	webview.addEventListener('load', function(e) {
		toolActInd.hide();
		forwardBtn.enabled = webview.canGoForward();
		backBtn.enabled = webview.canGoBack();
	});
	webview.addEventListener('beforeload', function(e) {
		toolActInd.show();
		forwardBtn.enabled = webview.canGoForward();
		backBtn.enabled = webview.canGoBack();
	});
})();
win.open();

if (Titanium.Platform.name == 'iPhone OS') {
	//iphone의 경우 socket으로 연결
	watchURLChange("192.168.12.102", 8128, win);
} else {
	//android의 경우
	var oldModifyTime = "";
	setInterval( function() {
		var xhr = Ti.Network.createHTTPClient();
		xhr.open("GET","http://192.168.12.102:8080/getURL");
		xhr.onload = function () {
			var resultObj = JSON.parse(this.responseText);
			oldMtime = resultObj.mTime;
			if(oldModifyTime != resultObj.mTime) {
				oldModifyTime = resultObj.mTime;
				//안드로이드 웹뷰의 경우 주소가 같으면 reload하지 않아 강제로 reload
				if(webview.url == resultObj.url)
					webview.reload();
				webview.url = resultObj.url;
			}
		};
		xhr.send();
	},2000);
}

#2 server.js for Nodejs
nodejs에서는 두개의 서버를 띄운다. 1) socket연결을 위한 stream서버 2)android에서 주소를 가져가기 위한 http 서버

// extend array
Array.prototype.remove = function(e) {
	for(var i=0;i<this.length;i++){
	if(e==this[i]) {
		return this.splice(i,1);
	}
	}
};
var file = './url.txt', //browser에서 보낸 url을 저장해놓는 파
fileModifyTime=0; //url.txt파일의 수정된 시간 저장용

/**
 * stream server
 */
var sys = require('util'),
fs = require('fs'),
net = require('net'),
clients = [];

// 다중 접속이 가능하도록 clinet관리
function Client(stream) {
	this.name = null;
	this.stream = stream;
}

net.createServer( function(stream) {
	var client = new Client(stream);
	clients.push(client);

	stream.setTimeout(0);
	stream.setEncoding('utf8');

	stream.on('connect', function() {
		sys.puts('[S#1] App connected');
		fs.readFile(file, function(err, data) {
			stream.write(JSON.stringify({
				action: 'connect',
				url: data.toString()
			}));
		});
	});
	stream.on('end', function() {
		sys.puts('[S#1] disconnected');
		clients.remove(client);
		stream.end();
	});
}).listen(8128, function() {
	sys.puts('[S#1] Stream server running, please start application');
	fs.watchFile(file, {
		interval: 100,
		persistent: true
	}, function(curr, prev) {
		//파일 수정 시간을 비교하여 판
		if(curr.mtime.getTime() != prev.mtime.getTime()) {
			fileModifyTime = curr.mtime.getTime();
			sys.puts("modify Time :" + fileModifyTime);
			fs.readFile(file, function(err, data) {
				var cnt=0;
				//접속된 모든 client로 url을 수정시간과 함께 JSON으로 보냄
				clients.forEach( function(c) {
					c.stream.write(JSON.stringify({
						action: 'changeURL',
						url: data.toString()
					}));
					sys.puts('[S#1] URL file updated: ' + file + ":" + data.toString());
					sys.puts(++cnt);
				});
			});
		}
	});
});
/**
 * http Server
 */
var http = require('http'),
path = require('path'),
url = require('url');

http.createServer( function(req,res) {
	var uri = url.parse(req.url).pathname;
	var queryString = url.parse(req.url).query;
	console.log('[S#2] ' + uri);
	switch(uri) {
		case '/rd':
		case '/redirect':
			fs.readFile(file, function(err, data) {
				res.writeHead(302, {
					'Location': data.toString()
				});
				res.end();
				console.log("[S#2] redirect : " + data.toString());
			});
			break;
		case '/getURL':
			fs.readFile(file, function(err, data) {
				res.writeHead(200, {
					"Content-Type" : "text/html"
				});
				console.log("[S#2] getURL : " + data.toString());
				res.write(JSON.stringify({
					url : data.toString(),
					mTime : fileModifyTime
				}));
				res.end();
			});
			break;
		case '/newURL':
		default:
			res.writeHead(200, {
				"Content-Type" : "text/html"
			});
			res.write(queryString);
			console.log("[S#2] bookmarklet update :" + queryString);
			fs.writeFile(file,queryString, function(err) {
				if (err) {
					console.log("[S#2] write file error");
				} else {
					console.log("[S#2] write file success");
				}
			});
			res.end();
			break;
	}
}).listen(8080, function() {
	console.log('[S#2] http Server Running');
});

#3 browser bookmarklet script
브라우저에서 현재 보고 있는 페이지의 url을 node로 보내기 위한 북마크 javascript이다. 이 javascript를 북마크에 저장해두고 클릭하면 node로 url을 보낸다.

javascript:var xmlhttp = new XMLHttpRequest(); xmlhttp.open("GET","http://192.168.1.104:8080/?"+location.href,true); xmlhttp.send(null);

서버와 모바일 어플 모두 만드는데 직접 코딩한 파일은 겨우 두개의 .js, 두개 파일 합쳐도 300줄이 안된다. 이건 간단한 예시에 불과하다. 앞으로 발전하는 titanium, node와 함께 재미있는 일을 많이 할 수 있을 것 같다.^^


Titanium Studio에서 Android SDK 설정이 안될 때..

Titanium Studio가 공개되기 전에 Titanium Developer를 통해 Android, iOS 양쪽 모두 컴파일 하고 실행하는데 아무런 문제가 없었다.  Titanium Studio를 설치하고 나니 Android SDK를 찾지 못하는 이상한 문제가 발생했다. 처음엔 preview 버그겠지하고 지나가려고 했는데 자꾸 눈에 거슬렸다.

위 캡쳐에서 볼수 있듯이 “Could not locate the SDK at the given path” 라고 나온다. 분명 저 path에는 android sdk가 있다. 근데 왜 안될까? 어제 이걸로 고생했는데 혹시나 하고 이것 저것해보니 해결방법을 찾았다.
“SDK Platform Android API7″이 없어서 그런것이다. 꼭 1.7로 컴파일 해야되는 것도 아닌데 못찾는다. Titanium Developer를 쓸 때는 잘만 되었는데.. 왜 그런진 모른다. 어쨋든 문제 해결!! 나중에 원인을 알게되면 적어둬야지.


Titanium과 Aptana의 합작품 "Titanium Studio" 공개!

지난 1월 Titanium*을 만드는 회사인 Appcelerator가 통합 개발 환경 툴(IDE)을 만드는 Aptana를 인수했다.
웹 개발자라면 Aptana는 다들 들어 봤을 정도로 유명하다. (이클립스를 기반으로 하는 독립된 프로그램은 물론 이클립스 플러그인 으로도 제공 한다.)
[* titanium 은 javascript언어로만 여러 플랫폼의 navtive app개발이 가능한  framework이다. 공식 site product 소개 페이지 바로가기]

Appcelerator가 Aptana를 인수 했다는 사실은 매우 반가운 소식이었다. 보나마나 Titanium용 IDE가 나올 것이 뻔하기 때문이었다. 아니나 다를까 발표당시 2011년 1분기에 Aptana와Titanium의 합작품의 Beta버전을 발표한다고 했다. (인수 관련 블로그 글) 비록 1분기라는 약속은 지키지 못했지만 며칠 전 Titanium Studio preview버전을 공개했다. 근데 공개 한건 알겠는데 나는 왜이리 호들갑을 떨고 있는 걸까? Titanium의 장점인 빠른 개발인데 이제는 Titanium Studio를 통해 더욱 빠르고 편리하게 개발 가능해졌기 때문이다.

1. 개발시 사용하는 툴의 감소
과거 : Eclipse(혹은 기타 다른 편집기) + Titanium Developer(컴파일 및 실행을 위해)
현재 : Titanium Studio
이클립스 환경에서 개발할때 Code completion을 하는 방법에 대해 블로그에 정리한 적이 있는데(관련 글) 이젠 이 설정도 필요 없다. 기본적으로 제공한다. 이제 각종 sdk를 제외하면 Titanium Studio 하나로 코드짜고 실행하고 할 수 있다.^^

2. Debuging의 편리함
기존의 Titanium 개발을 할때는  javascript code를 Firebug에서 처럼 breakpoint를 건다거나 특정 값을 바꾼다거나 object를 inspect할 수 없었다. 이젠 요런 것도 다 가능하다. ^^ (breakpoint에 조건도 넣을 수 있다.)
Titanium은 Javascript로 개발 하지만 실제 앱은 native이기 때문에 여러 Thread가 생성/동작 하게 된다. Titanium Studio에서는 각 Thread에 대한 정보는 보여줌은 물론  call stack 까지 보여준다. 자세한 사항은 아래 동영상 참조.


Titanium Studio Debugger Demo from Appcelerator Video Channel on Vimeo.

Aptana를 인수 했기에 너무나 당연한 결과이자 너무나도 예상된 결과이지만 Titanium이 계속 성장한다는 사실이 날 흥분하게 만든다. (비록 개인적으로 Titanium을 끄적이고 있지만..ㅋㅋ)

Titanium Studio를 살짝 써봤다. 아직 preview 버전이라 그런지 android sdk 설정이 제대로 되지 않았지만 (이 문제가 있다면 이 글 참고) iOS관련 해서는 잘 작동했다.  Code completion이 기존 Eclipse에 설정했을 때보다도 친절하다.후훗.  Titanium 개발이 한결 편해지겠네.하하

p.s. Titanium Studio 다운로드는 http://preview.appcelerator.com/studio/


Titanium의 대표적 개발 사례 Wunderlist


Wunderlist.com
1백만 이상 다운로드와 1천만 이상 to-do task생성을 자랑하는 To-do 앱이 있다. 이 앱은 아이폰, 아이패드, 안드로이드, 맥os, windows 전용 앱을 제공하며 Web 버전도 제공한다. 게다가 cloud 연결을 통해 언제 어디서나 똑같은 Task가 보여지도록 동기화 되며 지원하는 플랫폼은 앞으로 계속 확대 예정이다.

이 모든게 무료이다. 다른 사람들과 특정 카테고리 공유까지 가능하니 있을 건 다 있고 없을 건 없는 심플한 서비스이다.

“머리 아프다. 페이스북에 새 기능을 추가하면 동시에 7개의 플랫폼을 작업해야한다. HTML5가 미래의 플랫폼이다.” – Facebook CTO Bret Taylor

“구글조차도 모든 플랫폼을 네이트브로 지원하기엔 예산이 모자르다.” – Google VP Vic Gundotra

다양한 플랫폼을 지원하려고 고민해 보신 분들은 이미 뼈저리게 느꼈을 사실중 하나, ‘다양한 플랫폼을 지원하기란 정말 쉬원일이 아니다. ‘ 웹서비스의 경우 javascript, html, css로 이루어진 한벌의 코드를 크로스 브라우징이 되도록 손보는 일도 쉬운 일이 아닌데 서로 다른 언어를 통해 개발해야기에 플랫폼별 네이티브 어플리케이션을 다양하게 지원한다는 것은 더더욱 쉬운 일이 아니다.

그것도 Wunderlist를 만든 직원이 9명에 불과한 6wunderkinder회사의 경우에는 어떻겠는가?
(이 회사 직원은 회사 소개 페이지를 보면 9명에 불과하다. http://www.6wunderkinder.com/about/ )
더 놀라울 것은 그 9명에서 개발자라고 보이는 사람은 3명이다.

현재 지원 플랫폼 5개 + 웹서비스 1개…. 단순하게 개발자 수로 나눠보면 한사람당 2개!!!

다양한 플렛폼을 지원가능한 이유는 바로 Titanium Framework을 통해 개발했기에 다양한 플렛폼을 지원하는데 드는 비용을 많이 줄일 수 있었다고 한다.  iPhone 앱을 만들고 이때 개발한  코드의 90% 를 재사용 할 수 있어서 android 에 맞게 완벽히 porting하는데 4주 밖에 안 걸렸다고 한다. ( 관련기사 )

내가 Titanium을 처음 접했을 때가 작년 10월경으로 기억한다. 그 때부터 혼자 아름 아름 Titanium을 가지고 놀았다. 몇달 전 Aptana를 인수하는 등 범상치 않은 움직임을 보이고 있는 Titanium Appcelerator의 미래는 상당히 재미있을 거라 기대된다. 더 늦기 Titanium과 함께한 경험을 정리하고 공유하려 하려 한다. 하루에 하나씩!!!^^

p.s. Titanium을 통한 개발이 정답이라는 얘기는 아니다. Titanium을 통한 방법이 가지는 가장 큰 매력은 쉽고 빠른 멀티 플랫폼 개발이라는 점이다! 나는 그 매력에 빠졌다.^^ (특히 요즘 재미들인 Javascript 기반이라는 점도 매력^^)