모바일웹 개발시 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 기반이라는 점도 매력^^)


Eclipse에서 Titanium 코드 자동완성 사용하기

Appcelerator가 aptana 를 인수 했을 당시만 해도 titanium용 aptana studio 베타를 3월에 공개한다고 했었는데 안타 최근 뉴스에 따르면 12월에나 선보일거란다.ㅠㅠ

어쨋든 eclipse에서 aptana plugin를 이용하여 Titanium 프로그래밍시 code assistance 기능을 이용할 수 있다. 방법은 아래 블로그에 잘 나와있다.

http://jameslow.com/2010/05/31/titanium-autocomplete-eclipse/

간단히 요약 하자면 다음과 같다.

설치 방법
1)  Aptana 를 설치한다. ( standalone 이나  eclipse plugin 둘 다 가능)
2)  javascript header file 를 다운로드 한다.
3) 이클립스의 Perspective를 Aptana로 변경한다.
4)  이클립스 환경설정(preference)에서 Aptana를  javascrit  기본 편집기(default Editor)로 지정한다.
5)  timobile.js 를 aptana references에 추가한다.

아래 동영상을 보면서 따라하면 큰 문제 없이 설정 된다.

httpv://www.youtube.com/watch?v=d10j2-tEgVY&feature=player_embedded

설치시 주의할점은  tmobile.js 파일을 reference에 추가하기 전에 js의 기본 편집기를 aptana 로 설정하고 해야한다.

그렇지 않으면 지원하지 않는 형식이라는 메시지가 나온다. ( Addubg this type of file is not supported.)