모바일웹 개발시 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와 함께 재미있는 일을 많이 할 수 있을 것 같다.^^

Advertisements


답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중