|
var worker = new Worker("worker.js"); worker.onmessage = function(message){ // do stuff }; worker.postMessage(someDataToDoStuffWith); |
Listing 1에서 Web Workers를 사용하는 세 가지의 기본 단계를 볼 수 있다. 먼저 Worker 오브젝트를 작성하고 새 스레드에서 실행될 스크립트의 URL로 전달한다. Worker가 실행할 모든 코드는 URL이 Worker의 생성자로 전달시킨 Worker 스크립트에 들어 있어야 한다. 이 Worker 스크립트의 URL은 브라우저의 동일한 기원 정책으로 한정된다. —Web Worker를 작성하는 중인 페이지 스크립트를 로드한 페이지를 로드한 페이지와 동일한 도메인에서부터 나와야 한다.
다음으로 할 일은 onmessage
함수를 사용하여 콜백 핸들러 함수를 지정하는 것이다. 이 콜백 함수는 Worker 스크립트가 실행된 후에 호출될 것이다. message
는 Worker 스크립트로부터 리턴된 데이터이며 이 메시지로 원하는 것을 모두 할 수 있다. 콜백 함수는 기본 스레드에서 실행되며 DOM으로의 액세스 권한이 있다. Worker 스크립트는 다른 스레드에서 실행되고 DOM으로의 액세스가 없기 때문에 데이터를 Worker 스크립트로부터 애플리케이션의 UI를 업데이트하기 위해 DOM을 안전하게 수정할 수 있는 기본 스레드로 다시 전송해야 한다. 이는 Web Workers의 비공유(nothing-shared) 설계의 핵심 기능이다.
Listing 1의 마지막 행에 Worker가 초기화되는 방법이 표시된다—postMessage
함수를 호출하여 수행한다. 여기에서 메시지(다시 말하지만 이것은 단지 데이터이다)를 Worker로 전달한다. 물론 postMessage
는 비동기 함수이므로 호출하면 즉시 리턴한다.
이제 Worker 스크립트에 대해 살펴보자. Listing 2에서 코드는 Listing 1로부터 worker.js
파일의 내용이다.
importScripts("utils.js"); var workerState = {}; onmessage = function(message){ workerState = message.data; // do stuff with the message postMessage({responseXml: this.responseText}); } |
Worker 스크립트가 고유의 onmessage
함수가 있는 것을 볼 수 있다. 이는 기본 스레드로부터 postMessage
를 호출할 때에 호출된다. 페이지 스크립트로부터 전달한 데이터는 message
오브젝트에서 postMessage
함수로 전달된다. message
오브젝트의 data
특성을 검색하여 데이터에 액세스한다. Worker 스크립트에서 데이터의 처리를 완료하면 데이터를 기본 스레드로 다시 전송하는 postMessage
함수를 호출한다. 또한 이 데이터는 수신한 메시지의 데이터 특성에 액세스하여 기본 스레드로 사용 가능하다.
지금까지 Web Workers의 단순하지만 강력한 시맨틱을 살펴보았다. 다음에는 모바일 웹 애플리케이션의 속도를 높이기 위해 이를 적용하는 방법에 대해 알아볼 것이다. 이에 들어가기에 앞서 장치 지원에 대해 논의해야 한다. 어찌 되었건 이 사항들은 모바일 웹 애플리케이션이며, 브라우저 사이의 기능 면에서 차이점을 다루는 것은 모바일 웹 애플리케이션 개발에 필수적이다.
Android 2.0으로 시작하여 Android 브라우저는 HTML 5 Web Worker 스펙에 대해 전체적인 지원을 해왔다. 이 기사를 쓰는 시점에 대부분의 새 Android 장치는 가장 인기있는 Motorola Droid를 포함하여 Android 2.1과 함께 제공된다. 또한 이 기능은 Maemo 운영 체제를 실행하는 Nokia 장치와 Windows Mobile 장치에서 사용 가능한 Mozilla Fennec 브라우저에서 전체적으로 지원된다. 여기에서 iPhone이 빠진 것은 주목할 만하다. iPhone OS 버전 3.1.3 및 3.2(iPad에서 실행되는 OS의 버전)는 아직 Web Workers를 지원하지 않는다. 하지만 이 기능은 이미 Safari에서 지원되어 iPhone에서 실행하는 Mobile Safari 브라우저에서 이 기능이 표시되는 것은 시간 문제에 불과할 것이다. iPhone의 독점(특히 미국)을 고려해 봤을 때 Web Workers가 있는지가 필요한 것이 아니라 있는 것을 발견했을 때 모바일 웹 애플리케이션을 향상시키기 위해 이를 사용하는 것만이 최선이다. 이러한 생각을 가지고 모바일 웹 애플리케이션을 더 빠르게 하기 위해 어떻게 Web Workers를 사용할 수 있는지 알아보자.
스마트폰 브라우저에서 Web Worker의 지원은 바람직하며 개선되고 있다. 이는 모바일 웹 애플리케이션에서 언제 Workers를 사용하려고 하는가라는 질문을 하게 된다. 정답은 간단하다. 시간이 오래 걸리는 작업을 해야 할 때면 아무때나이다. 일부 Worker 예제에서는 파이의 만 단위 숫자를 계산하는 것과 같이 복잡한 수학적 계산을 수행하기 위해 Worker를 사용한 것이 나와있다. 이러한 계산을 웹 애플리케이션에서 수행할 필요가 아마 거의 없을 것이고, 모바일 웹 애플리케이션은 더욱 없을 것이다. 하지만 원격 자원으로부터 데이터를 검색하는 것은 매우 일반적이기 때문에 이 기사의 예제에서는 이에 주목한다.
이 예제에서는 eBay로부터의 Daily Deals(매일 변경되는 거래)의 목록을 검색할 것이다. 거래 목록은 각 거래에 대한 간략한 정보가 들어 있다. 더 자세한 정보는 eBay의 Shopping API를 사용하여 얻을 수 있다. 사용자가 관심 있는 거래를 고르기 위해 거래 목록을 찾아 보는 동안 이러한 추가 정보를 프리페치하기 위해 Workers를 사용할 것이다. 웹 애플리케이션으로부터 이러한 eBay 데이터에 모두 액세스하려면 일반 프록시를 사용하여 브라우저의 동일한 기원 정책을 다루어야 할 것이다. 이 프록시에 단순한 Java servlet이 사용되었다. 이는 이 기사를 위해 코드로 포함되지만 여기에서는 표시되지 않는다. 대신에 Web Workers로 작동하는 코드에 집중하자. Listing 3에 거래 애플리케이션을 위한 기본 HTML 페이지가 표시되어 있다.
<!DOCTYPE HTML> <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta name = "viewport" content = "width = device-width"> <title>Worker Deals</title> <script type="text/xxjavascript" src="common.js"></script> </head> <body [안내]태그제한으로등록되지않습니다-xxonload="loadDeals()"> <h1>Deals</h1> <ol id="deals"> </ol> <h2>More Deals</h2> <ul id="moreDeals"> </ul> </body> </html> |
보는 것과 같이 이는 HTML의 매우 단순한 부분이며, 쉘에 불과하다. 데이터를 검색하고 xxJavaScript를 사용하여 UI를 생성한다. 이는 모바일 웹 애플리케이션을 위한 최적의 설계이다. 왜냐하면 이를 통해 모든 코드와 정적 마크업이 장치에 캐시될 수 있으며 사용자는 서버로부터의 데이터를 기다리기만 하면 되기 때문이다. Listing 3에서 본문이 로드되면 Listing 4에서 애플리케이션에 대한 초기 데이터를 로드하는 loadDeals
함수를 호출하는 것에 주목한다.
var deals = []; var sections = []; var dealDetails = {}; var dealsUrl = "http://deals.ebay.com/feeds/xml"; function loadDeals(){ var xhr = new XMLHttpRequest(); xhr.[안내]태그제한으로등록되지않습니다-onreadystatechange = function(){ if (this.readyState == 4 && this.status == 200){ var i = 0; var j = 0; var dealsXml = this.responseXML.firstChild; var childNode = {}; for (i=0; i< dealsXml.childNodes.length;i++){ childNode = dealsXml.childNodes.item(i); switch(childNode.localName){ case 'Item': deals.push(parseDeal(childNode)); break; case "MoreDeals": for (j=0;j<childNode.childNodes.length;j++){ var sectionXml= childNode.childNodes.item(j); if (sectionXml && sectionXml.hasChildNodes()){ sections.push(parseSection(sectionXml)); } } break; default: break; } } deals.forEach(function(deal){ var entry = createDealUi(deal); $("deals").appendChild(entry); }); loadDetails(deals); sections.forEach(function(section){ var ui = createSectionUi(section); $("moreDeals").appendChild(ui); loadDetails(section.deals); }); } }; xhr.open("GET", "proxy?url=" + escape(dealsUrl)); xhr.send(null); } |
Listing 4에는 loadDeals
함수와 애플리케이션에서 사용된 글로벌 변수가 표시된다. 거래 배열과 함께 섹션 배열을 사용한다. 이는 관련 거래의 추가 그룹이다(예: Deals under $10
). 또한 dealDetails
라는 이름의 맵은 그 키가 Item ID(거래 데이터로부터 받음)가 될 것이고, 그 값은 eBay Shopping API로부터 받은 더 자세한 정보이다.
첫 번째 할 일은 프록시로 Ajax를 호출하여 결과적으로 eBay Daily Deals REST API를 호출하는 것이다. 이렇게 하면 거래 목록이 XML 문서로 주어진다. Ajax 호출에 사용하는 XMLHttpRequest 오브젝트의 [안내]태그제한으로등록되지않습니다-onreadystatechange
함수에서 문서를 구문 분석한다. 두 가지의 다른 함수인 parseDeal
및 parseSection
을 사용하여 XML 노드를 더 사용하기 쉬운 xxJavaScript 오브젝트로 구문 분석한다. 이러한 함수는 다운로드 가능한 코드 샘플에서 찾을 수 있지만(다운로드 참조) 단지 지루한 XML 구문 분석 함수에 불과하기 때문에 여기에 포함시키지 않았다. 마지막으로 XML을 구문 분석한 후에 DOM을 수정하기 위해 createDealUi
과 createSectionUi
의 두 개의 함수를 더 사용한다. 이를 완료하면 UI가 그림 1과 같이 된다.
Listing 4로 다시 돌아가면 기본 거래가 로드된 후에 거래의 각 섹션에 대해 loadDetails
함수를 호출한다. 이 함수에서 eBay Shopping API를 사용하여 각 거래의 추가 세부 사항을 로드한다.—하지만 브라우저가 Web Workers를 지원하는 경우에만 해당된다. Listing 5에 loadDetails
함수가 표시되어 있다.
function loadDetails(items){ if (!!window.Worker){ items.forEach(function(item){ var xmlStr = null; if (window.localStorage){ xmlStr = localStorage.getItem(item.itemId); } if (xmlStr){ var itemDetails = parseFromXml(xmlStr); dealDetails[itemDetails.id] = itemDetails; } else { var worker = new Worker("details.js"); worker.onmessage = function(message){ var responseXmlStr =message.data.responseXml; var itemDetails=parseFromXml(responseXmlStr); if (window.localStorage){ localStorage.setItem( itemDetails.id, responseXmlStr); } dealDetails[itemDetails.id] = itemDetails; }; worker.postMessage(item.itemId); } }); } } |
loadDetails
에서는 먼저 글로벌 범위(window
오브젝트)에서 Worker
함수를 확인한다. 없는 경우에는 아무 일도 안 해도 된다. 있는 경우에는 먼저 XML에 대한 localStorage
를 거래의 세부 사항에 대해 확인한다. 이는 모바일 웹 애플리케이션에 일반적인 로컬 캐싱 전략이고, 이 시리즈의 Part 2에 세부 사항이 설명되어있다(링크를 보려면 참고 자료를 참조한다).
XML을 로컬에서 발견하면 이를 parseFromXml
함수에서 구문 분석하고 세부 사항을 dealDetails
오브젝트로 추가한다. 로컬에서 발견하지 못하면 Web Worker를 파생시키고 postMessage
를 사용하여 거래의 Item ID를 전송한다. Worker가 데이터를 검색하고 기본 스레드에 다시 게시하면 XML을 구문 분석하고 결과를 dealDetails
에 추가하고 XML을 localStorage
에 저장한다. Listing 6에 Worker 스크립트 details.js가 표시된다.
Listing 6. 거래 세부 사항 Worker 스크립트
importScripts("common.js"); onmessage = function(message){ var itemId = message.data; var xhr = new XMLHttpRequest(); xhr.[안내]태그제한으로등록되지않습니다-onreadystatechange = function(){ if (this.readyState == 4 && this.status == 200){ postMessage({responseXml: this.responseText}); } }; var urlStr = generateUrl(itemId); xhr.open("GET", "proxy?url=" + escape(urlStr)); xhr.send(null); } |
Worker 스크립트는 매우 단순하다. Ajax를 사용하여 프록시를 호출하고 결국 eBay Shopping API가 호출된다. XML을 프록시로부터 수신하면 xxJavaScript 오브젝트 리터럴을 사용하여 기본 스레드로 다시 전송한다. Worker에서부터 XMLHttpRequest
를 사용할 수 있을지라도 모두 그 responseText
특성에 돌아오고 responseXml
특성에는 절대 돌아오지 않는다. 왜냐하면 Worker 스크립트 범위 내에서 xxJavaScript DOM 구문 분석기가 없기 때문이다. generateUrl
함수가 common.js 파일(Listing 7에 있음)로부터 나오는 것을 참고한다. importScripts
함수를 사용하여 common.js를 가져왔다.
function generateUrl(itemId){ var appId = "YOUR APP ID GOES HERE"; return "http://open.api.ebay.com/shopping?callname=GetSingleItem&"+ "responseencoding=XML&appid=" + appId + "&siteid=0&version=665" +"&ItemID=" + itemId; } |
이제 거래 세부 사항을 입력하는 방법에 대해 알아보았다(Web Workers를 지원하는 브라우저용). 그림 1로 다시 돌아가서 애플리케이션에서 이를 사용하는 방법에 대해 살펴보자. 각 거래의 옆에 Show Details 단추가 있음을 주목한다. 그림 2와 같이 클릭하여 UI를 수정한다.
이 UI는 showDetails
함수를 호출할 때 표시된다. Listing 8에 이 함수가 표시되어 있다.
function showDetails(id){ var el = $(id); if (el.style.display == "block"){ el.style.display = "none"; } else { el.style.display = "block"; if (!el.innerHTML){ var details = dealDetails[id]; if (details){ var ui = createDetailUi(details); el.appendChild(ui); } else { var itemId = id; var xhr = new XMLHttpRequest(); xhr.[안내]태그제한으로등록되지않습니다-onreadystatechange = function(){ if (this.readyState == 4 && this.status == 200){ var itemDetails = parseFromXml(this.responseText); if (window.localStorage){ localStorage.setItem( itemDetails.id, this.responseText); } dealDetails[id] = itemDetails; var ui = createDetailUi(itemDetails); el.appendChild(ui); } }; var urlStr = generateUrl(id); xhr.open("GET", "proxy?url=" + escape(urlStr)); xhr.send(null); } } } } |
표시될 거래의 ID가 전달되고, 표시될 지와 안 될지 여부를 토글한다. 첫 번째로 호출되면 세부 사항이 이미 dealDetails 맵에 저장되어 있는지 보기 위해 확인한다. 브라우저가 Web Workers를 지원하면 그 다음에 이러한 세부 사항이 이미 저장되고 이에 대한 UI가 작성되어 DOM에 추가된다. 세부 사항이 이미 로드되지 않거나 브라우저가 Workers를 지원하지 않으면 이 데이터를 로드하기 위해 Ajax를 호출한다. 이렇게 하여 애플리케이션이 Workers가 있든지 없든지 여부에 관계 없이 동등하게 원활히 작동할 수 있다. 이는 Worker가 지원되면 데이터는 이미 로드되고 UI는 즉시 응답할 것이라는 의미이다. Workers가 없으면 UI는 여전히 로드하지만 몇 초가 걸린다.