반응형
블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

카테고리


반응형

jQuery Mobile and Dynamic Page Generation


jQuery Mobile은 default click hijacking behavior나 매뉴얼로 $.mobile.changePage()를 call 하는 것을 통해 다이나믹하게 DOM으로 page이 당겨져 올 수 있도록 해 줍니다. 이것은 서버사이드에서 HTML pages/fragments 를 generate 하는데 아주 좋습니다. 하지만 가끔 JSON이나 다른 포맷으로부터 클라이언트 사이드에서 page content를 다이나믹하게 generate 할 필요가 있습니다. 아마도 네트워크 광역폭이나 퍼포먼스적인 이유로 그런 일이 필요할 겁니다. 혹은 서버와 서로 소통하기 위해 서버가 선택할 데이터 포맷일 수도 있겠죠.


클라이언트 사이드에서 page markup을 generate 할 필요가 있는 어플리케이션을 위해 $.mobile.changePage() call 하는 동안 트리거되는 notification에 대해 이해하고 있는것이 중요합니다. 왜냐하면 해당 시간에 여러분의 content를 generate 할 수 있도록  navigation system 에 hook 하는것을 가능하게 해 주기 때문입니다. changePage()를 call 하게 되면 대개 아래와 같은 event notification이 trigger 됩니다.


  • pagebeforechange
    • Fired off before any page loading or transition.
    • NOTE: This event was formerly known as "beforechangepage".
  • pagechange
    • Fired off after all page loading and transitions.
    • NOTE: this event was formerly known as "changepage".
  • pagechangefailed
    • Fired off if an error has occurred while attempting to dynamically load a new page.


이런 notification들은 페이지의 parent container element(

$.mobile.pageContainer)에서 trigger 됩니다. 그리고 document element와 window에까지 계속 떠 다니게 될 겁니다. JSON이나 in-memory JS object 같은 non HTML data를 이용해서 어플리케이션에 page를 inject 하기를 원한다던가 아니면 빠르게 현재 존재하는 페이지의 내용을 modify 하기를 원한다면 pagebeforechange event 가 아주 유용할 겁니다. pagebeforechange event는 URL이나 page element들을 analyzing 하기위해 hook 할 수 있도록 해 줍니다.  어플리케이션은 load 하거나 switch 할거냐고 질문을 받게 되고 pagebeforechange event에서 preventDefault()를 call 함으로서 default changePage() behavior가 short-circuit 할지에 대해서도 질문을 받게 됩니다.




이 기술을 이용하기 위해 working sample을 한번 살펴 보시기 바랍니다. 이 샘플에서는 유저가 navigate 할 수 있는 카테고리 리스트로 메인페이지가 시작합니다. 각 카테고리의 실제 아이템들은 메모리의 javaScript object에 저장되어 있습니다. 이 데이터들은 어떤 곳에서든지 올 수가 있습니다.


var categoryData = {
	animals: {
	  name: "Animals",
	  description: "All your favorites from aardvarks to zebras.",
		items: [
			{
				name: "Pets"
			},
			{
				name: "Farm Animals"
			},
			{
				name: "Wild Animals"
			}
		]
	},
	colors: {
		name: "Colors",
		description: "Fresh colors from the magic rainbow.",
		items: [
			{
				name: "Blue"
			},
			{
				name: "Green"
			},
			{
				name: "Orange"
			},
			{
				name: "Purple"
			},
			{
				name: "Red"
			},
			{
				name: "Yellow"
			},
			{
				name: "Violet"
			}
		]
	},
	vehicles: {
		name: "Vehicles",
		description: "Everything from cars to planes.",
		items: [
			{
				name: "Cars"
			},
			{
				name: "Planes"
			},
			{
				name: "Construction"
			}
		]
	}
};


이 어플리케이션은 어플리케이션에게 어떤 카테고리 아이템이 display 되어야 하는지를 말해주는 hash를 포함한 url이 있는 링크를 사용합니다.



 <h2>Select a Category Below:</h2>
 <ul data-role="listview" data-inset="true">
    <li><a href="#category-items?category=animals">Animals</a></li>
    <li><a href="#category-items?category=colors">Colors</a></li>
    <li><a href="#category-items?category=vehicles">Vehicles</a></li>
 </ul>


내부적으로 이 링크 중 하나를 클릭하면 어플리케이션은 internal $.mobile.changePage() call을 intercept 합니다. 이 $.mobile.changePage() 는 프레임워크의 default link hijacking behavior에 의해 invoke 된 것이죠. 그러면 이제 로드되기 위해 페이지에 대한 URL을 analyze 하게 됩니다. 그리고 나서 이것이 로딩 자체만을 위한 것인지 아니면 일반적인 changePage() 코드로 처리해야 될 것인지에 대해 판단하게 되죠.

어플리케이션은 도큐먼트 레벨에서 pagebeforechange event로 binding 됨으로서 changePage() 로 그 자체를 insert 할 수 있게 됩니다.



// Listen for any attempts to call changePage().
$(document).bind( "pagebeforechange", function( e, data ) {

// We only want to handle changePage() calls where the caller is
// asking us to load a page by URL.
	if ( typeof data.toPage === "string" ) {

// We are being asked to load a page by URL, but we only
// want to handle URLs that request the data for a specific
// category.
		var u = $.mobile.path.parseUrl( data.toPage ),
			re = /^#category-item/;

		if ( u.hash.search(re) !== -1 ) {

// We're being asked to display the items for a specific category.
// Call our internal method that builds the content for the category
// on the fly based on our in-memory category data structure.
			showCategory( u, data.options );

// Make sure to tell changePage() we've handled this call so it doesn't
// have to do anything.
			e.preventDefault();
		}
	}
});



왜 도큐먼트 레벨에서 listen할까요? 간단히 말하면 deep-linking 때문입니다. 우리는 jQuery Mobile 프레임워크가 initialize 하기전에 active 되기위해 binding이 필요합니다. 그리고 어플리케이션을 invoke 한 initial URL을 어떻게 process 할지 결정하게 됩니다. pagebeforechange binding에 대한 callback이 invoke 됐을 때 callback에 대한 두번째 argument는 initial $.mobile.changePage() call에 pass 될 argument들을 포함한 data object가 될 겁니다. 이 object의 프로퍼티들은 아래와 같습니다.
  • toPage
    • transition 될 페이지를 포함한  jQuery collection object가 될 수도 있고 로드되거나 transition 될 페이지에 대한 URL reference가 될 수 있습니다.
  • options
    • $.mobile.changePage() function 함수의 caller에 의해 pass 된 옵션들을 포함한 Object
    • 옵션의 리스트는 여기에서 찾아 보실 수 있습니다.
우리의 샘플 어플리케이션에서는 URL들이 initial 하게 pass 된 changePage() calls 에 대해서만 관심이 있습니다. 그래서 우리의 callback이 하는 첫번째 일은 toPage의 type을 체크하는 겁니다 그 다음으로는 어떤 URL parsing 유틸리티의 도움을 받아서 우리가 스스로 handling 하기위해 관심을 가지고 있는 hash를 포함한 URL인가에 대해 체크를 합니다. 만약 그렇다면 showCategory()라는 어플리케이션 함수를 call 합니다. 이 함수는 URL hash에 의해 명시된 카테고리에 대한 content를 다이나믹하게 create 할 겁니다. 그리고 이벤트에서 preventDefault()를 call 할 겁니다. pagebeforechange event에서 preventDefault()를 call 하는 것은 다른 작업 없이 exit 하기 위해 $.mobile.changePage() call 을 유발하게 됩니다. 이 이벤트에서 preventDefault() method를 call 하는 것은 changePage() request를 여러분 스스로 핸들링하게되는 jQuery Mobile과 같다고 말할 수 있습니다.


preventDefault()가 call 되지 않는다면 changePage()는 평상시에 하던대로의 작업을 계속 이어나갈 겁니다. 우리의 callback에 pass 된 data object에 대해 짚고 넘어갈 부분은 여러분이 toPage 프로퍼티나 options 프로퍼티에 어떤 change를 했던지 간에 preventDefault()가 call 되지 않으면 changePage() processing에 영향을 줄 것이라는 겁니다. 예를 들어 다른 internal/external 페이지에 특정 URL을 redirect하거나 map 하고 싶다면 우리의 callback은 그 URL이나 redirect 될 페이지의 DOM 엘리먼트로 callback에 data.toPage 프로퍼티를 set 해야 합니다. 마찬가지로 우리는 우리 callback 안의 어떤 옵션에 대한 set이나 un-set을 할 수 있습니다. 그러면 changePage()는 새로운 세팅을 사용하게 될 겁니다.


이제 우리는 어떻게 changePage() call을 intercept 하는지 알게 됐습니다 이제 이 샘플 소스가 그 페이지에 대한 markup을 실제로 어떻게 generate 하는지 자세히 살펴 보겠습니다. 우리의 샘플 소스는 각 카테고리를 display 하기 위해 같은 페이지를 사용하거나 재 사용합니다. 우리의 special link 가 클릭 될 때마다 showCategory()가 invoke 됩니다.


// Load the data for a specific category, based on
// the URL passed in. Generate markup for the items in the
// category, inject it into an embedded page, and then make
// that page the current active page.
function showCategory( urlObj, options )
{
	var categoryName = urlObj.hash.replace( /.*category=/, "" ),

// Get the object that represents the category we
// are interested in. Note, that at this point we could
// instead fire off an ajax request to fetch the data, but
// for the purposes of this sample, it's already in memory.
		category = categoryData[ categoryName ],

// The pages we use to display our content are already in
// the DOM. The id of the page we are going to write our
// content into is specified in the hash before the '?'.
		pageSelector = urlObj.hash.replace( /\?.*$/, "" );

	if ( category ) {
// Get the page we are going to dump our content into.
		var $page = $( pageSelector ),

// Get the header for the page.
		$header = $page.children( ":jqmData(role=header)" ),

// Get the content area element for the page.
		$content = $page.children( ":jqmData(role=content)" ),

// The markup we are going to inject into the content
// area of the page.
		markup = "<p>" + category.description + 
"</p><ul data-role='listview' data-inset='true'>", // The array of items for this category. cItems = category.items, // The number of items in the category. numItems = cItems.length; // Generate a list item for each item in the category // and add it to our markup. for ( var i = 0; i < numItems; i++ ) { markup += "<li>" + cItems[i].name + "</li>"; } markup += "</ul>"; // Find the h1 element in our header and inject the name of // the category into it. $header.find( "h1" ).html( category.name ); // Inject the category items markup into the content element. $content.html( markup ); // Pages are lazily enhanced. We call page() on the page // element to make sure it is always enhanced before we // attempt to enhance the listview markup we just injected. // Subsequent calls to page() are ignored since a page/widget // can only be enhanced once. $page.page(); // Enhance the listview we just injected. $content.find( ":jqmData(role=listview)" ).listview(); // We don't want the data-url of the page we just modified // to be the url that shows up in the browser's location field, // so set the dataUrl option to the URL for the category // we just loaded. options.dataUrl = urlObj.href; // Now call changePage() and tell it to switch to // the page we just modified. $.mobile.changePage( $page, options ); } }


위 샘플에는 우리가 handle 하는 URL의 해쉬는 아래 두가지 부분을 포함하고 있습니다.


#category-items?category=vehicles

? 전에 있는 첫번째 부분은 content를 write할 page의 id입니다.  ? 다음 부분은 어떤 데이터가 사용되고 언제  페이지에 대한 markup을 generate 할지에 대해 알 수 있는 정보 입니다. showCategory()가 하는 첫번째 일은 이 content 를 write 할 페이지의 id를 추출하기 위해 hash를 deconstruct(해채) 하는 겁니다. 그리고 카테고리 이름은 우리의  in-memory JavaScript category object로부터 올바른 data 세트를 찾아올 때 사용됩니다. 어떤 카테고리 데이터를 사용할 지에 대한 작업이 끝난 이후 그 카테고리에 대한 markup을 generate 합니다. 그리고 그것을 그 페이지의 header와 content 부분에 inject 합니다. 그 element에 이전에 있었던 markup들은 모두 씻겨 나갑니다.


이 markup을 inject 한 이후에 막 inject 된 markup list를 잘 사용하기 위해 적당한 jQuery Mobile widget을 call 하게 됩니다. 이 과정은 styled listview에 list markup이 적용되는 일반적인 과정입니다.


이 작업이 일단 끝나면 $.mobile.changePage()를 call 합니다. 그리고 우리가 방금 modify 한 페이지의 DOM 엘리먼트를 pass 합니다. 이 페이지가 보여지기를 원한다고 프레임워크에게 말하기 위해서죠. 이제 이 부분에서 흥미로운 부분은 jQuery Mobile은 보여지는 페이지와 연관된 URL과 함께 브라우저의 location hash를 update 한다는 겁니다. 이러한 일이 일어 날 수 있는 이유는 우리가 각 카테고리에 대해 같은 페이지를 재사용했기 때문입니다. 이것은 이상적인 상황만은 아닙니다. 왜냐하면 그 페이지에 대한 URL은 그것과 관련된 특정 category 정보를 가지고 있지 않기 때문입니다. 이 문제를 해결하기 위해서는 showCategory()가 changePage()에 pass 한 options object에 대한 dataUrl 프로퍼티를 세팅해 주는 일입니다. 우리의 오리지날 URL 대신에 이것을 display 해 달라고 얘기하기 위해서죠.


여기까지가 샘플입니다. 이 샘플이 아주 좋은 예제는 아닙니다. 특히 grade가 낮아서 JavaScript가 turn off 됐을 때에는요. 이 의미는 C-Grade 브라우저에서는 제대로 작동하지 않을 거라는 겁니다. 이렇게 낮은 grade의 브라우저에서 어떻게 제대로 동작할 수 있도록 하는지에 대해서는 나중에 글을 올릴겁니다. 업데이트 된 내용을 보시려면 여기를 참조하세요.



반응형