수정 사항
8/19 - 콜백의 문제점 추가, 가독성 수정
8/19 - Promise.all 추가
콜백으로 비동기를 처리 할 때 꼬리에 꼬리를 무는 처리가 많아지면, 중첩이 깊어집니다.
이를, Promise로 해결할 수 있습니다.
비동기 동작 처리를 위해 ES6에서 도입된 클래스입니다.
new
로 인스턴스화 한 Promise 객체로 비동기 동작을 처리합니다.
Promise 이해를 위해 다음 단어를 알아야 합니다.
제작 코드(Producing Code)는 시간이 걸리는 일을 하는 코드를 말합니다.
소비 코드(Consuming Code)는 제작 코드의 결과를 기다렸다가 이를 사용(소비)하는 코드를 말합니다. 여러 함수에서 소비할 수 있습니다.
프라미스(Promise)는 '제작 코드와 소비 코드를 연결하는 특별한 자바스크립트 객체'입니다.
시
시간이 얼마나 걸리든, 제작 코드가 준비 될 때, 모든 소비 코드가 결과를 사용할 수 있게 합니다.
Promise 객체는 아래와 같은 방법으로 만듭니다.
let promise = new Promise(function(resolve, reject){
//executor
})
Promise 생성자에 전달되는 함수를 executor
(실행자) 라고 부릅니다.
Promise 객체가 생성될 때, 자동으로 실행합니다.
excutor
는 콜백 중 하나를 반드시 호출해야 합니다.
Promise 객체는 state
와 result
라는 내부 프로퍼티를 가집니다.
특히, state
에 대한 이해가 필요합니다.
state
보류(Pending) : 제작 코드가 완료되지 않음
이행(Fulfilled) : 제작 코드가 완료되어, 프로미스가 값을 반환 함
실패(Rejected) : 제작 코드가 실패하거나 오류가 발생
result
제작 코드 결과에따라, 결과 값이나 오류 객체를 가집니다.
프라미스는 성공 또는 실패만 합니다.
또한, 변경된 상태는 더이상 변하지 않습니다.
state
와 result
는 내부 프로퍼티로 개발자가 직접 접근할 수 없습니다.
프라미스에서 가장 중요하고 기본이 되는 메서드입니다.
promise.then(
function(result){/*결과를 다룸*/},
function(error){/*에러를 다룸*/}
)
then
의 첫 번째 인자는 프라미스가 이행 되었을 때 실행되는 함수입니다.
두번째 인자는 프라미스가 거부되었을 때 실행되는 함수입니다.
에러가 발생한 경우만 다루고 싶다면, then
의 첫 번째 인수를 null
로 전달해도 되고,
.catch
를 써도 됩니다.
catch
는 then
의 첫 인수를 null
로 전달하는 것과 동일하게 작동합니다.
promise.catch(
function(error){/*에러를 다룸*/}
)
try...catch
절에 finally
가 있는 것 처럼 프라미스에도 있습니다.
프라미스가 이행이나 거부로 처리되면, 항상 실행됩니다.
즉, then(f,f)
와도 유사합니다.
결과가 어떻든 마무리가 필요할 때 유용합니다.
다만, then(f,f)
와 완전히 같진 않습니다.
1. finally
핸들러엔 인수가 없습니다.
보편적 작업을 수행하므로 이행인지 거부인지 여부를 알 수 없습니다.
2. finally
핸들러는 자동으로 다음 핸들러에 결과와 에러를 전달합니다.
promise
.finally(() => alert("프라미스가 준비되었습니다.")) // 다음 핸들러에 전달
.catch(err => alert(err)); // <-- .catch에서 에러 객체를 다룰 수 있음
때문에, finally
는 결과를 처리하기 보다, 통과되어 전달하는 역할을 합니다.
프라미스가 이미 처리 된 상태라면, 이 후 등록 된
.then/catch/finally
핸들러는 등록하면 즉각 실행됩니다.
예시
function loadScript(src){
//작업이 끝날 때 콜백을 호출하는 대신, Promise 객체를 반환
return new Promise(function(resolve,reject){
// executor : 시간이 걸리는 작업
let script = document.createElement('script');
script = src
//resolve, reject 중 반드시 호출
script.onload = ()=> resolve(script);
script.onerror = ()=> reject(new Error(`${src}를 불러오는 중 에러가 발생함`));
document.head.append(script)
})
}
let promise = loadScript('./src/script.js');
//프라미스가 처리되면, 등록된 핸들러를 결과와 함께 수행.
promise.then(
script=> console.log(`${script.src}를 불러왔습니다.`),
err=> console.log(`Error: ${err.massage}`)
)
promise.then(
// 또 다른 핸들러..
)
콜백 기반 비동기 프로그래밍 에서는, 인자로 callback
을 받고,
오류 우선 콜백이라면, 첫 인수에 에러 객체, 두 번째 인수에 결과를 넣어 호출했습니다.
이번에는, executor
에서 resolve
와 reject
를 호출하는 프라미스를 반환하고,
then
을 이용해 정상 상태와 에러 상태의 핸들러를 더합니다.
여러 프라미스를 다 하고 난 뒤,처리를 하고 싶을 때
Promise.all(promiseArray).then(handler);
다음 같은 상황에 사용합니다.
fetchUser = id => {
return fetch(`${url}/${id}`).then(response => response.json());
}
const requests = selectIds.map(id => fetchUser(id));
Promise
.all(requests)
.then(users => {
//...
})
순차적으로 처리해야하는 작업이 중첩되어 여러개 있다고 했을 때,
콜백 기반 비동기 프로그래밍은 중첩이 많아져 피라미드를 만들어 냈습니다.
프라미스를 사용하면 여러 해결책을 만들 수 있습니다.
resolve
가 호출되면 then
에서 결과를 처리할 수 있습니다.
그 결과가 then
체인을 통해 전달됩니다.
프라미스가 처리되고 then
핸들러가 호출됩니다.
호출된 then
핸들러에서 반환한 값은 다음 then
핸들러에 result
로 전달됩니다.
즉, 순차적으로 처리해야하는 작업이 있을 때,
앞선 작업을 수행하고, 프라미스 체이닝으로 그 결과를 처리할 수 있습니다.
new Promise( function(resolve,reject) {
setTimeout(()=> resolve(1),1000)
})
.then( result=>{
alert(result) // 1
return result*2
})
.then( result=>{
alert(result) // 2
return result*2
})
.then( result=>{
alert(result) // 4
return result*2
})
이렇게 체이닝이 가능한 이유는 then
을 호출하면 프라미스가 반환되기 때문입니다.
반환된 프라미스는 당연히 then
메서드를 가집니다.
then
에 사용된 핸들러가 프라미스를 생성하거나, 반환하는 경우도 있습니다.
이 때는, 다음 핸들러가 프라미스의 처리를 기다리고 실행됩니다.
new Promise( function(reslove) {
setTimeout(()=> resolve(1),1000);
}
.then( result=>{
return new Promise(resolve=>{
setTimeout(()=> resolve(result*2), 1000);
})
})
.then(result => {
//...
})
loadScript(src) // 프라미스 반환하는 함수
.then(script => loadScript(src2))
.then(script => loadScript(src3))
스크립트 로드가 끝나면 다음 스크립트를 로드해야하는 등, 순차적으로 실행해야 할 때
콜백을 사용할 때처럼 중첩이 깊어지지 않고, 코드를 작성할 수 있습니다.
프론트 단에서, 네트워크 요청시 프라미스를 자주 사용합니다.
let promise = fetch(url);
url
에 네트워크 요청을 보내고, promise
가 반환됩니다.
이제, 서버가 응답을 보내면 프라미스는 response
를 결과로 이행됩니다.
그런데, response
가 다운되지 않아도 응답이 오면 이행되기 때문에,
바로 respose
의 데이터를 쓸 수 없습니다.
response.text()
를 호출하면,
응답의 텍스트가 다운될 때, 텍스트를 결과로 하여 이행되는 프로미스가 반환됩니다.
메서드 response.json()
도 같은 역할을 하며, 데이터를 JSON으로 파싱합니다.
이처럼 사용합니다.
fetch(url)
.then(response => response.json())
.then(data => {
//...
})
예시
fetch(url)
.then(response => response.json())
.then(user => fetch(`xxxx.com/users/${user.id}`) )
.then(response => response.json())
.then(xxxUser => {
let img = document.createElement('img')
img.src = xxxUser.img_url;
img.className = "avartar";
doucument.body.append(img);
setTimeout(()=>img.remove(), 3000)
})
fetch API
를 통해 네트워크 요청을 하고, 프라미스 체인을 통해 응답이 다운되면,
결과를 가지고 이미지를 만들고 3초 후에, 지워지게 됩니다.
잘 동작하지만, 이 이후에 무언가 하고 싶다면 어떻게 해야 할까요?
체인을 확장할 수 있도록 이행 프로미스를 반환해야합니다.
fetch(url)
.then(response => response.json())
.then(user => fetch(`server/users/${user.id}`) )
.then(response => response.json())
.then(user => new Promise((resolve,reject)=> {
let img = document.createElement('img')
img.src = user.img_url;
img.className = "avartar";
doucument.body.append(img);
setTimeout(() => {
img.remove();
resolve(user);
}, 3000);
}))
//3초 후에 remove 되고 동작함
.then(user=> ....)
지금은 체인을 확장하지 않더라도, 비동기 동작은 항상 프라미스를 반환하는 것이 좋습니다.