자바스크립트가 최근에 인기를 끌게된 이유 중 하나는, ajax때문일 것이다. 페이지를 한 번 불러오고 이후로는 자바스크립트를 이용해서 서버와 주고받은 모든 통신을 처리할 수 있으면서, 적은 데이터로 페이지를 다시 랜더링할 수 있게되었기 때문이다. 일련의 분리된 페이지들이 아니라 사실상 웹과 같은 사용자 경험을 얻을 수 있게 되었다. 페이지를 매번 불러오지 않아도 된다면 사용자의 시간과 자원을 정약할 수 있다. 특히 외부 데이터에 접근하는 것은 이른바 단일 페이지 웹 어플리케이션에 매우 중요하다.
자바스크립트는 비동기 언어로, 요청한 데이터를 기다리는 동안 코드 실행을 중단하지 않는다. 자바스크립트는 빠른 웹사이트를 제공하지만 비동기 요청은 다루기 어려울 수 있다.
비동기 언어는 그저 이전의 코드가 완전히 해결되지 않아도 이어지는 코드를 실행할 수 있는 언어를 의미한다. 코드 실행이 중단되는 이유를 생각해보자. API에서 데이터를 가져오는 경우가 있을 것이다. DOM이나 다른 곳에서 데이터를 가져올 수도 있다. 또는 상숑자 응답을 기다려야 할 수도 있다.
비동기 언어의 가치는 지연된 정보를 기다리는 동안 이 정보가 필요하지 않은 다른 코드를 실행할 수 있다는 장점이 있다. 지연된 정보를 기다리는 동안 코드가 멈추지 않는다.
그럼 언제 도착할지 모르는 이런 비동기 정보를 깔끔하게 해결할 수 있을까?
프로미스가 등장하기 전에는 콜백 함수를 사용해 비도익 작업을 처리했다.
function getUserPreferences(cb) {
return setTimeout(() => {
cb({
theme: 'dusk',
});
}, 1000);
}
function log(value) {
return console.log(value);
}
log('starting');
// starting
getUserPreferences(preferences => {
return log(preferences.theme.toUpperCase());
});
log('ending?');
// ending?
// DUSK
콜백함수는 비동기 데이터를 다루기 좋은 방법이다. 문제는 비동기 함수에서 또 비동기 함수를 호출하고, 거기서 또 비동기 함수를 호출해 마침내 너무나 많은 콜백 함수가 중첩되는 경우가 생긴다는 것이다. 이런 경우를 '콜백 지옥'에 빠졌다한다.
만약에 사용자 취향에 맞는 음악 목록을 가져오려면 어떻게 해야할까?
// START:music
function getMusic(theme, cb) {
return setTimeout(() => {
if (theme === 'dusk') {
return cb({
album: 'music for airports',
});
}
return cb({
album: 'kind of blue',
});
}, 1000);
}
// END:music
// START:hell
getUserPreferences(preferences => {
return getMusic(preferences.theme, music => {
console.log(music.album);
});
});
// END:hell
위의 예제는 두 단계만 중첩되었지만 벌써 가독성이 나빠졌다. 프로미스를 사용하면 콜백 함수문제를 해결할 수 있다.
프로미스는 콜백함수를 인수로 받는 대신에 성공과 실패에 대응하는 메서드를 사용한다. 이렇게 하면 시각적으로 평평해보인다. 게다가 콜백 함수를 중첩하는 대신에 여러 개의 비동기 프라미스를 연결할 수도 있다.
// START:define
function getUserPreferences() {
const preferences = new Promise((resolve, reject) => {
resolve({
theme: 'dusk',
});
});
return preferences;
}
// END:define
// START:pref
getUserPreferences()
.then(preferences => {
console.log(preferences.theme);
});
// 'dusk'
// END:pref
또는 실패도 처리할 수 있다.
// START:fail
function failUserPreference() {
const finder = new Promise((resolve, reject) => {
reject({
type: 'Access Denied',
});
});
return finder;
}
// END:fail
// START:catch
failUserPreference()
.then(preferences => {
// This won't execute
console.log(preferences.theme);
})
.catch(error => {
console.error(`Fail: ${error.type}`);
});
// Fail: Access Denied
// END:catch
콜백으로 중첩되지 않고 평평한 코드를 작성할 수 있게된다.
// START:music
function getMusic(theme) {
if (theme === 'dusk') {
return Promise.resolve({
album: 'music for airports',
});
}
return Promise.resolve({
album: 'kind of blue',
});
}
// END:music
// START:chain
getUserPreferences()
.then(preference => {
return getMusic(preference.theme);
})
.then(music => {
console.log(music.album);
});
// music for airports
// END:chain
프로미스는 비동기처리를 이전보다 편하게 해주었지만, 도구는 항상 발전한다. ES2017에서는 async/await
라는 새로운 방법을 승인했다.