원활하고 반응이 빠른 HTML5 애플리케이션을 빌드하는 데 있어 가장 중요한 측면 중 하나는 데이터 가져오기, 처리, 애니메이션, 사용자 인터페이스 요소와 같은 애플리케이션의 모든 다양한 부분 간의 동기화입니다.
데스크톱 또는 네이티브 환경과의 주요 차이점은 브라우저가 스레딩 모델에 대한 액세스 권한을 부여하지 않고 사용자 인터페이스(예: DOM)에 액세스하는 모든 항목에 단일 스레드를 제공한다는 점입니다. 즉, 사용자 인터페이스 요소에 액세스하고 수정하는 모든 애플리케이션 로직이 항상 동일한 스레드에 있으므로 모든 애플리케이션 작업 단위를 최대한 작고 효율적으로 유지하고 브라우저에서 제공하는 비동기 기능을 최대한 활용하는 것이 중요합니다.
브라우저 비동기 API
다행히 브라우저에서는 일반적으로 사용되는 XHR (XMLHttpRequest 또는 'AJAX') API와 같은 여러 비동기 API뿐만 아니라 IndexedDB, SQLite, HTML5 Web worker, HTML5 GeoLocation API도 제공합니다. 일부 DOM 관련 작업(예: transitionEnd 이벤트를 통한 CSS3 애니메이션)도 비동기식으로 노출됩니다.
브라우저가 비동기 프로그래밍을 애플리케이션 로직에 노출하는 방법은 이벤트 또는 콜백을 통해서입니다.
이벤트 기반 비동기 API에서 개발자는 지정된 객체(예: HTML 요소 또는 기타 DOM 객체)의 이벤트 핸들러를 등록한 다음 작업을 호출합니다. 브라우저는 일반적으로 다른 스레드에서 작업을 실행하고 필요한 경우 기본 스레드에서 이벤트를 트리거합니다.
예를 들어 이벤트 기반 비동기 API인 XHR API를 사용하는 코드는 다음과 같습니다.
// Create the XHR object to do GET to /data resource
var xhr = new XMLHttpRequest();
xhr.open("GET","data",true);
// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
alert("We got data: " + xhr.response);
}
},false)
// perform the work
xhr.send();
CSS3 transitionEnd 이벤트는 이벤트 기반 비동기식 API의 또 다른 예입니다.
// get the html element with id 'flyingCar'
var flyingCarElem = document.getElementById("flyingCar");
// register an event handler
// ('transitionEnd' for FireFox, 'webkitTransitionEnd' for webkit)
flyingCarElem.addEventListener("transitionEnd",function(){
// will be called when the transition has finished.
alert("The car arrived");
});
// add the CSS3 class that will trigger the animation
// Note: some browers delegate some transitions to the GPU , but
// developer does not and should not have to care about it.
flyingCarElemen.classList.add('makeItFly')
SQLite 및 HTML5 Geolocation과 같은 다른 브라우저 API는 콜백 기반입니다. 즉, 개발자가 함수를 인수로 전달하면 해당 해상도로 기본 구현에서 콜백합니다.
예를 들어 HTML5 위치정보의 코드는 다음과 같습니다.
// call and pass the function to callback when done.
navigator.geolocation.getCurrentPosition(function(position){
alert('Lat: ' + position.coords.latitude + ' ' +
'Lon: ' + position.coords.longitude);
});
이 경우 메서드를 호출하고 요청된 결과와 함께 다시 호출될 함수를 전달합니다. 이렇게 하면 브라우저가 이 기능을 동기식 또는 비동기식으로 구현하고 구현 세부정보와 관계없이 개발자에게 단일 API를 제공할 수 있습니다.
애플리케이션을 비동기식으로 준비하기
잘 설계된 애플리케이션은 브라우저의 기본 제공 비동기 API 외에도 특히 일종의 I/O 또는 컴퓨팅 집약적인 처리를 실행할 때 하위 수준 API를 비동기 방식으로 노출해야 합니다. 예를 들어 데이터를 가져오는 API는 비동기 방식이어야 하며 다음과 같이 표시되어서는 안 됩니다.
// WRONG: this will make the UI freeze when getting the data
var data = getData();
alert("We got data: " + data);
이 API 설계에서는 getData()가 차단되어야 하며, 그러면 데이터가 가져올 때까지 사용자 인터페이스가 정지됩니다. 데이터가 JavaScript 컨텍스트에서 로컬이면 문제가 되지 않을 수 있지만, 데이터를 네트워크에서 가져오거나 SQLite 또는 색인 저장소에서 로컬로 가져와야 하는 경우 사용자 환경에 큰 영향을 미칠 수 있습니다.
동기식 애플리케이션 코드를 비동기식으로 개조하는 것이 쉽지 않을 수 있으므로 처리하는 데 시간이 걸릴 수 있는 모든 애플리케이션 API를 처음부터 비동기식으로 사전 설계하는 것이 좋습니다.
예를 들어 간단한 getData() API는 다음과 같이 됩니다.
getData(function(data){
alert("We got data: " + data);
});
이 접근 방식의 좋은 점은 애플리케이션 UI 코드가 처음부터 비동기 중심이 되도록 강제하고 기본 API가 나중에 비동기화 여부를 결정할 수 있다는 것입니다.
일부 애플리케이션 API는 비동기식일 필요가 없거나 비동기식일 필요가 없습니다. 일반적으로 모든 유형의 I/O 또는 과도한 처리(15ms 이상 걸릴 수 있는 모든 작업)를 실행하는 API는 첫 번째 구현이 동기식인 경우에도 처음부터 비동기식으로 노출되어야 합니다.
실패 처리
비동기 프로그래밍의 한 가지 문제는 오류가 일반적으로 다른 스레드에서 발생하므로 실패를 처리하는 기존의 try/catch 방식이 더 이상 작동하지 않는다는 점입니다. 따라서 호출 대상은 처리 중에 문제가 발생하면 호출자에게 알리는 구조화된 방법을 갖추어야 합니다.
이벤트 기반 비동기 API에서는 이벤트를 수신할 때 이벤트 또는 객체를 쿼리하는 애플리케이션 코드로 이를 실행하는 경우가 많습니다. 콜백 기반 비동기 API의 경우 적절한 오류 정보를 인수로 사용하여 오류가 발생할 경우 호출되는 함수를 사용하는 두 번째 인수를 사용하는 것이 좋습니다.
getData 호출은 다음과 같습니다.
// getData(successFunc,failFunc);
getData(function(data){
alert("We got data: " + data);
}, function(ex){
alert("oops, some problem occured: " + ex);
});
$.Deferred를 사용하여 결합
위 콜백 접근 방식의 한 가지 제한사항은 적당히 고급 동기화 로직을 작성하는 것이 정말 번거로워질 수 있다는 것입니다.
예를 들어 세 번째 API를 실행하기 전에 두 개의 비동기 API가 완료될 때까지 기다려야 한다면 코드 복잡성이 빠르게 증가할 수 있습니다.
// first do the get data.
getData(function(data){
// then get the location
getLocation(function(location){
alert("we got data: " + data + " and location: " + location);
},function(ex){
alert("getLocation failed: " + ex);
});
},function(ex){
alert("getData failed: " + ex);
});
애플리케이션이 애플리케이션의 여러 부분에서 동일한 호출을 해야 하는 경우 더 복잡해질 수 있습니다. 모든 호출이 이러한 다중 단계 호출을 실행하거나 애플리케이션이 자체 캐싱 메커니즘을 구현해야 하기 때문입니다.
다행히 Promises(Java의 Future와 유사)라는 비교적 오래된 패턴과 jQuery 핵심의 강력하고 현대적인 구현인 $.Deferred가 있습니다. 이 패턴은 비동기 프로그래밍에 간단하고 강력한 솔루션을 제공합니다.
간단히 말해 Promise 패턴은 비동기 API가 '결과가 상응하는 데이터로 확인될 것이라는 Promise'의 일종인 Promise 객체를 반환한다고 정의합니다. 호출자는 해결 방법을 가져오기 위해 Promise 객체를 가져와 done(successFunc(data))를 호출합니다. 그러면 Promise 객체에 'data'가 확인되면 이 successFunc를 호출하라는 메시지가 전달됩니다.
따라서 위의 getData 호출 예시는 다음과 같이 됩니다.
// get the promise object for this API
var dataPromise = getData();
// register a function to get called when the data is resolved
dataPromise.done(function(data){
alert("We got data: " + data);
});
// register the failure function
dataPromise.fail(function(ex){
alert("oops, some problem occured: " + ex);
});
// Note: we can have as many dataPromise.done(...) as we want.
dataPromise.done(function(data){
alert("We asked it twice, we get it twice: " + data);
});
여기서는 먼저 dataPromise 객체를 가져온 다음 .done 메서드를 호출하여 데이터가 확인될 때 다시 호출할 함수를 등록합니다. .fail 메서드를 호출하여 최종 실패를 처리할 수도 있습니다. 기본 Promise 구현(jQuery 코드)이 등록 및 콜백을 처리하므로 필요한 만큼 .done 또는 .fail 호출을 가질 수 있습니다.
이 패턴을 사용하면 비교적 쉽게 고급 동기화 코드를 구현할 수 있으며 jQuery는 이미 $.when과 같은 가장 일반적인 동기화 코드를 제공합니다.
예를 들어 위의 중첩된 getData/getLocation 콜백은 다음과 같이 됩니다.
// assuming both getData and getLocation return their respective Promise
var combinedPromise = $.when(getData(), getLocation())
// function will be called when both getData and getLocation resolve
combinePromise.done(function(data,location){
alert("We got data: " + dataResult + " and location: " + location);
});
가장 좋은 점은 jQuery.Deferred를 사용하면 개발자가 비동기 함수를 매우 쉽게 구현할 수 있다는 것입니다. 예를 들어 getData는 다음과 같이 표시될 수 있습니다.
function getData(){
// 1) create the jQuery Deferred object that will be used
var deferred = $.Deferred();
// ---- AJAX Call ---- //
XMLHttpRequest xhr = new XMLHttpRequest();
xhr.open("GET","data",true);
// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
// 3.1) RESOLVE the DEFERRED (this will trigger all the done()...)
deferred.resolve(xhr.response);
}else{
// 3.2) REJECT the DEFERRED (this will trigger all the fail()...)
deferred.reject("HTTP error: " + xhr.status);
}
},false)
// perform the work
xhr.send();
// Note: could and should have used jQuery.ajax.
// Note: jQuery.ajax return Promise, but it is always a good idea to wrap it
// with application semantic in another Deferred/Promise
// ---- /AJAX Call ---- //
// 2) return the promise of this deferred
return deferred.promise();
}
따라서 getData()가 호출되면 먼저 새 jQuery.Deferred 객체(1)를 만든 다음 호출자가 done 및 fail 함수를 등록할 수 있도록 Promise(2)를 반환합니다. 그런 다음 XHR 호출이 반환되면 지연된 항목을 확인(3.1)하거나 거부(3.2)합니다. deferred.resolve를 실행하면 모든 done(…) 함수와 다른 promise 함수(예: then 및 pipe)가 트리거되고 deferred.reject를 호출하면 모든 fail() 함수가 호출됩니다.
사용 사례
다음은 지연이 매우 유용할 수 있는 몇 가지 사용 사례입니다.
데이터 액세스: 데이터 액세스 API를 $.Deferred로 노출하는 것이 올바른 설계인 경우가 많습니다. 동기식 원격 호출은 사용자 환경을 완전히 망치므로 원격 데이터의 경우 이는 분명하지만 로컬 데이터의 경우에도 마찬가지입니다. SQLite 및 IndexedDB)는 그 자체로 비동기입니다. Deferred API의 $.when 및 .pipe는 비동기 서브 쿼리를 동기화하고 연결하는 데 매우 강력합니다.
UI 애니메이션: transitionEnd 이벤트로 하나 이상의 애니메이션을 조정하는 것은 매우 번거로울 수 있습니다. 특히 애니메이션이 CSS3 애니메이션과 JavaScript가 혼합된 경우 (흔히 그렇습니다) 더욱 그렇습니다. 애니메이션 함수를 지연 함수로 래핑하면 코드 복잡성을 크게 줄이고 유연성을 개선할 수 있습니다. transitionEnd에서 확인되는 Promise 객체를 반환하는 cssAnimation(className)과 같은 간단한 일반 래퍼 함수라도 큰 도움이 될 수 있습니다.
UI 구성요소 표시: 조금 더 고급이지만 고급 HTML 구성요소 프레임워크도 지연된 함수를 사용해야 합니다. 애플리케이션이 사용자 인터페이스의 여러 부분을 표시해야 하는 경우 이러한 구성요소의 수명 주기를 지연으로 캡슐화하면 타이밍을 보다 세밀하게 제어할 수 있습니다 (이 내용은 다른 게시물에서 다루도록 하겠습니다).
모든 브라우저 비동기 API: 정규화를 위해 브라우저 API 호출을 Deferred로 래핑하는 것이 좋습니다. 이 작업은 각각 말 그대로 4~5줄의 코드가 필요하지만 모든 애플리케이션 코드를 크게 단순화합니다. 위의 getData/getLocation 의사코드에서 볼 수 있듯이, 이렇게 하면 애플리케이션 코드가 모든 유형의 API (브라우저, 애플리케이션별, 복합)에 걸쳐 하나의 비동기 모델을 가질 수 있습니다.
캐싱: 이는 부수적인 이점이지만 경우에 따라 매우 유용할 수 있습니다. Promise API (예: .done(…) 및 .fail(…))를 비동기 호출이 실행되기 전후에 호출할 수 있으므로 지연 객체를 비동기 호출의 캐싱 핸들로 사용할 수 있습니다. 예를 들어 CacheManager는 지정된 요청에 대해 지연된 작업을 추적하고 무효화되지 않은 경우 일치하는 지연된 작업의 Promise를 반환할 수 있습니다. 호출자가 호출이 이미 해결되었는지 아니면 해결 중인지 알 필요가 없다는 것이 이 방법의 장점입니다. 콜백 함수는 정확히 동일한 방식으로 호출됩니다.
결론
$.Deferred 개념은 간단하지만 이를 제대로 이해하는 데는 시간이 걸릴 수 있습니다. 하지만 브라우저 환경의 특성상 자바스크립트 비동기 프로그래밍을 마스터하는 것은 HTML5 애플리케이션 개발자에게 필수이며 프라미스 패턴 (및 jQuery 구현)은 비동기 프로그래밍을 안정적이고 강력하게 만들기 위한 엄청난 도구입니다.