프로그램이 간단하고, 이런 코드들이 한 줄 한 줄 실행 되면 크게 어렵지 않겠지만, 자바스크립트에서 함수를 호출했는데 함수가 끝나기 전에도 프로그램이 계속 진행되어야 하는 상황이 많습니다.
이런 상황에서 비동기처리를 위해 하나의 패턴으로 콜백함수를 사용합니다.
😞 하지만 전통적인 콜백 패턴은 콜백 헬로 인해 가독성을 떨어트리고 중간에 발생한 에러처리가 곤란하거나, 여러개의 비동기처리를 한번에 하기도 힘듭니다.
이럴때 프로미스를 이용해서 비동기적인 상황에서 코드를 좀 더 명확하게 표현하고 실행할 수 있습니다.
콜백 헬이 나타나게 된 이유로는 비동기 처리모델의 특성 때문입니다. 비동기 처리 모델은 실행이 끝나는 것을 기다리지 않고 바로 다음 task를 실행합니다. 따라서 비동기 함수 내에서 처리 결과를 반환하면 기대한 대로 나오지 않습니다.
그래서 순서가 보장되지 않기 때문에 그 반환 결과에 따른 후속처리가 불가능하고, 비동기 함수의 처리를 비동기함수의 콜백함수 내에서 처리해야합니다.
이처럼 콜백헬은 비동기의 반환 결과 처리를 위해 콜백함수 내에 또 콜백함수를 사용하는 식으로 사용하다 보니 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상을 말합니다.
이렇게 비동기 함수의 처리 결과로 또 다른 비동기 함수를 호출해야하는 경우 함수가 계속 중첩되어 복잡도가 높아지고 가독성을 해칩니다.
무엇보다 더 큰 문제는
try{
throw new Error('ERR');
}catch(e){
console.log('에러 캐치 O');
console.log(e);
}
이 구문과 다르게 아래의 예제는 에러가 캐치가 되지 않습니다.
try{
setTimeout(()=> { throw new Error('ERR'); }, 1000);
}catch(e){
console.log('에러 캐치 x');
console.log(e);
}
동작과정
동작과정 시각화 해주는 사이트
이는 비동기 처리 함수의 콜백 함수는 이벤트가 발생하면 Task Queue로 이동 후 Call stack이 비워졌을 때 Event loop에 의해 Call stack에 의해 이동 되어 실행이 되기 때문입니다. setTimeout 함수는 비동기 함수이므로 콜백함수 실행 여부와 상관없이 즉시 수행되고 Call Stack에서 제거됩니다.
이후 setTimeout함수의 콜백함수가 Call stack에 가서 실행 될 땐 이미 setTimeout은 Callstack에서 제거 된 상태이고 이는 setTimeout함수의 콜백함수를 호출한 것은 setTimeout 함수가 아니라는 것을 의미합니다.
exception은 caller 방향으로 전파가 되지만 앞서 설명처럼, setTimeout의 콜백함수를 호출한 것은 setTimeout이 아니기 때문에 setTimeout내에서 발생시킨 에러는 catch 블록에서 캐치되지 않습니다..
이런 문제를 극복하기 위해 Promise가 등장했습니다.
프로미스 객체는 비동기 작업의 상태(states)와 그 결과 값(values)을 나타냅니다.
프로미스는 Promise 생성자 함수를 통해 인스턴스화 할 수 있습니다.
promise 생성자 함수는 비동기 작업을 수행할 Callback 함수를 인자로 전달 받고 이 콜백함수는resolve
와reject
를 전달 받습니다.
Promise는 비동기처리의 수행이 어떻게 되었는지 등의 상태 정보를 갖습니다.
pending
: 비동기 처리가 아직 수행되지 않은 상태 ( resolve, reject 함수가 아직 호출되지 않은 상태)fulfilled
: 비동기 처리가 수행된 상태 (성공 : resolve() 호출된 상태)rejected
: 비동기 처리가 수행된 상태 (실패 : reject() 호출된 상태)settled
: 비동기 처리가 수행된 상태 (성공 or 실패 : resolve() or reject() 호출된 상태)생성자를 통해 프로미스 객체를 만드는 순간 pending
상태가 됩니다. pending
상태가 된 이후에 excutor 함수 인자 중 하나인 resolve
함수를 실행하면 fulfilled (이행)
상태가 됩니다.
pending
상태가 된 후에 excutor 함수 인자 중 하나인 resolve 함수를 실행하면 fulfilled
상태가 됩니다.
❓ 그럼 resolve()를 안하면 어떤 결과를 반환 할까요?
결과는 비동기 처리가 아직 수행되지 않은 상태인 pending 상태를 반환합니다.
reject 함수가 실행되면 rejected
상태가 됩니다.
⚠️ 프로미스는 성공 혹은 실패만 합니다.
excutor는
resolve
나reject
중 하나만 호출해야 합니다. 이때 변경된 상태는 더 이상 변하지 않습니다.const promise = new Promise((resolve, reject) => { resolve(); reject(); // 무시됨 setTimeout(()=> resolve()); // 무시됨 });
then
- then 메소드는 두 개의 콜백 함수를 인자로 전달 받습니다. 첫 번째 콜백함수는 성공(fulfilled 즉 resolve 함수가 호출된 상태)시에 호출되고 두번째 함수는 실패 시(rejected, reject함수가 호출된 상태)에 호출이 됩니다.
⚠️ then 메소드는 promise를 반환
function get(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState !== XMLHttpRequest.DONE) return;
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.status));
}
};
xhr.open("GET", url);
xhr.send();
});
}
get("https://jsonplaceholder.typicode.com/posts")
.then((data) => {
const post = JSON.parse(data)[0];
console.log(`post id : ${post.id} \ntitle : ${post.title}`);
return get(`https://jsonplaceholder.typicode.com/comments/${post.id}`);
})
.then(
(data) => {
const res = JSON.parse(data);
console.log(`comment : ${res.body}`);
}
)
Promise로 구현된 비동기 함수는 Promise 객체를 반환해야 합니다. Promise로 구현된 비동기 함수를 호출하는 측에서는 Promise 객체의 then
이나 catch
를 통해 결과 또는 에러 메세지를 전달받아 처리할 수 있습니다.
catch
예외가 발생하면 호출
⚠️ catch 메소드는 Promise를 반환
const promise = new Promise((resolve, reject) => {
throw new Error('error');
reject('결과');
});
promise.then(res => console.log(res), err=> console.error('잡았다 에러'));
// or
promise
.then(res => console.log(res)) // 실행 안 됨.
.catch(err => console.error('잡았다 에러'));
const promise = new Promise((resolve, reject) => {
throw new Error('error');
reject('결과');
});
promise executor와 promise handler주위에는 암시적 try..catch가 있습니다. 예외가 발생하면 암시적으로 예외를 잡고 이를 reject처럼 다룹니다.
위의 코드는
const promise = new Promise((resolve, reject) => {
reject(new Error('error'));
reject('결과');
});
와 똑같이 동작하게 됩니다. executor 주변의 암시적try..catch는 스스로 에러를 잡고promise를 rejected 상태로 변경시킵니다
❓ setTimeout에서의 에러는 어떻게 될까요?
에러를 catch하지 못했습니다. 암시적으로 try..catch가 감싸고 있어도 에러가 executor 가 실행되는 동안 발생하는 것이 아니라, 나중에 발생하기 때문에 에러를 catch하지 못했습니다.
javascript 개발자들이 promise에도 만족하지 못하고 더 훌륭한 방법으로 고안해낸것이 async & await 입니다. async는 function 앞에 위치하고 function 앞에 async를 붙이면 해당 함수는 항상 Promise를 반환합니다. Promise가 아닌 값을 반환하더라도 이행상태의 Promise로 감싸서 반환합니다.
async function test(){
return 1
}
자바스크립트는 await 키워드를 만다면 promise가 settled 될 때까지 기다리고 결과는 그 이후에 반환됩니다.
1초 후에 이행되는 promise 를 만들어보았습니다.
async function get(url) {
const promise = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState !== XMLHttpRequest.DONE) return;
if (xhr.status === 200) {
setTimeout( () => {resolve(xhr.responseText);} , 1000);
} else {
reject(new Error(xhr.status));
}
};
xhr.open("GET", url);
xhr.send();
});
let res = await promise;
console.log(res)
}
get('https://jsonplaceholder.typicode.com/posts');
await은 promise가 settled될 때까지 함수실행을 기다리게 만듭니다. 그리고 promise가 처리되면 재개합니다. 그 동안에는 다른 이벤트 처리나 스크립트를 실행하기 때문에 cpu가 낭비되지는 않습니다.
이처럼 async await은 promise를 좀 더 쓰기 쉽고 가독성이 좋게 만들어줍니다.
그럼 promise로 만들어진 예제를 async & await을 사용해서 만들어 보겠습니다.
function get(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState !== XMLHttpRequest.DONE) return;
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.status));
}
};
xhr.open("GET", url);
xhr.send();
});
}
async function Board() {
const getId = await get("https://jsonplaceholder.typicode.com/posts");
const post = JSON.parse(getId)[0];
console.log(`post id : ${post.id} \ntitle : ${post.title}`);
const getComment = await get(
`https://jsonplaceholder.typicode.com/comments/${post.id}`
);
const comment = JSON.parse(getComment);
console.log(`comment : ${comment.body}`);
}
Board();
프로미스 체이닝도 싫다면 이렇게 async & await을 쓰면 됩니다.
await을 정상적으로 사용했을 때 입니다.
await은 promise 앞에 위치해 있을 때 정상작동합니다.
일반적으로 async await을 사용하지 않았다면 setTimeout에서 발생한 callback은 비동기적으로 실행되기 때문에 case1_2가 먼저 실행될 것입니다. 하지만 await은 프로미스 객체에 한해 해당 작업을 기다렸다 다음 작업을 할 수 있도록 해줍니다.
따라서 이와 같이 예측한 순서대로 console.log가 찍힙니다.
일반 함수 앞에 위치한 await은 어떻게 동작할까요?
await은 프라미스가 처리(settled)될 때까지 기다립니다. 하지만 일반함수는 promise가 아니죠.
따라서 원하는 결과를 얻을 수 없을 것입니다.