자바스크립트는 비동기 처리를 위해 콜백함수를 사용한다. 하지만 콜백을 너무 남용하게 되면 우리가 흔히 부르는 콜백 지옥에 빠질 수가 있다. 이러한 단점을 보완해서 사용하는 것이 Promise 이다.
Promise는
비동기 처리에 사용되는 자바스크립트 객체
비동기 작업이 맞이할 성공 혹은 실패를 나타냄
promise에 대해 이론적으로 접근하면 어려우니 추상적으로 접근해보자.
여기 비동기 작업이 시작될때 생성되는 promise라는 상자가 있다.
이 상자는 처음에 텅 비어있다가 비동기 작업이 완료될때 결과물로 가득 차있게 된다.
이처럼 promise는 비동기 작업의 상태를 나타낸다.
비동기 작업에는 시간이 걸리고 언젠가 끝나게 된다. 이 작업에는 성공할 때도 있고 실패할 때도 있는데
예를 들어 서버로부터 데이터를 받아오는 비동기 작업을 진행할 때 데이터를 정상적으로 받아오면 성공한 것이고
도중에 네트워크 에러로 인해 데이터를 받아오지 못하면 작업이 실패한 것이다.
promise상자에는 비동기 작업이 진행될 때 대기라는 태그가 붙는다. 비동기 작업이 완료되길 대기하고 있는 상태라는 뜻.
시간이 지나 진행중이던 비동기 작업이 완료 될 때 성공하거나 실패하게 되는데 이에 따라 태그가 성공 또는 실패로 바뀐다.
성공 태그가 붙은 promise 상자에는 결과값을 담게 되고
실패 태그가 붙은 promise 상자에는 에러가 들어기게 된다.
여기서 비동기 작업의 상태를 나타내는 태그 부분이 state, 결과값이나 에러를 나타내는 결과물이 result이다.
이 state에는
- Pending
- Fulfilled
- Rejected
의 3가지 상태로 구성되어 있다.
pending state일때는 result: undefined
fulfilled state 일때는 result: 결과값
rejected state 일때는 result: Error
가 객체에 담기게 된다.
Promise 사용해보기
const promise = new Promise((resolve, reject) => {
console.log("비동기 작업")
})
1. promise 상자를 생성하려면 new를 사용하여 만들 수 있다. ( new Promise )
2. promise 상자에는 함수가 들어가는데 이 함수는 executor라고 불린다.
3. executor 함수는 promise가 자체적으로 전달해주는 resolve와 reject를 인수로 가진다. (resolve와 reject도 함수)
이 resolve와 reject는 executor안에서 호출해줄 수 있는 함수.
비동기 작업이 성공하면 resolve, 실패하면 reject를 호출.
const promise = new Promise ((resolve, reject) => {
setTimeout(() => {
const data = {name: '철수'};
console.log('네트워크 요청 성공');
}, 1000);
}
// 네트워크 요청 성공
setTimeout을 사용하여 비동기 작업을 진행할때 1초 뒤 "네트워크 요청 성공"이 뜨게 된다.
executor 함수는 Promise 객체가 생성되는 즉시 실행이 된다.
promise가 어떻게 동작하는지 확인하기 위해 console.log(promise); 를 찍어보았다.
promise의 state는 pending이고 result는 undefined인 것을 확인할 수 있다.
아직 promise한테 작업이 완료되었다는 것을 알려주지 않았기 때문에 pending 상태로 출력이 되었다.
const promise = new Promise ((resolve, reject) => {
setTimeout(() => {
const data = {name: '철수'};
console.log('네트워크 요청 성공');
resolve(data)
}, 1000);
})
비동기 작업이 완료 됐다 라는 것을 알려주려면 resolve() 함수를 호출하는데 여기서 resolve 함수의 인자로는 비동기 작업의 결과물을 넣어준다.
이후 setTimeout을 통해 2초뒤 promise를 출력해보면
state가 fulfilled로 변경되어있고 result에도 인자로 넣어준 객체, data값이 들어가있다.
반대로 작업이 실패했을때는
const promise = new Promise ((resolve, reject) => {
setTimeout(() => {
// const data = {name: '철수'};
const data = null
if(data) {
console.log('네트워크 요청 성공');
resolve(data);
} else {
reject(new Error("네트워크 문제!"))
}
}, 1000);
});
조건문을 사용해 data값이 있을땐 resolve를, data가 없을땐 reject 함수를 호출하도록 한다.
reject함수에는 Error 객체를 생성하여 넣어주는게 보편적.
state에 reject, result에 에러 메세지가 출력되는 것을 볼 수 있다.
중간에 설명했지만 promise는 new Promise로 생성되는 즉시 내부 함수인 executor 함수가 실행된다.
이러한 비동기 작업을 특정 함수가 호출됐을때 시작하도록 만들고 싶을때는 어떻게해야할까?
간단하게 비동기 함수를 만들고 그 안에 promise를 생성하면 된다. (비동기 함수라고 어렵게 생각할 필요 없다. 그냥 함수를 만들고 그 함수가 비동기 작업을 하면 비동기함수니깐.)
function getData() {
const promise = new Promise ((resolve, reject) => {
setTimeout(() => {
// const data = {name: '철수'};
const data = null
if(data) {
console.log('네트워크 요청 성공');
resolve(data);
} else {
reject(new Error("네트워크 문제!"))
}
}, 1000);
});
return promise;
}
const promise = getData();
setTimeout(() => {
console.log(promise);
}, 2000);
1. getData라는 함수를 만들어주고 그안에 만들었던 promise를 그대로 넣어준다.
2. 이렇게 되면 getData라는 함수가 호출될때 promise객체가 생성되고 내부함수를 실행하게 된다.
3. 그리고 함수를 호출하는 입장에서 비동기 작업이 어떻게 진행되고 있는지 상태를 알아야 하기 때문에 promise를
반환하는 return promise를 작성해준다.
4. getData를 호출하는 curious 변수를 만들어주고 실행하면 비동기작업 결과가 1초뒤 출력되고 promise의 상태가 2초뒤 출력된다. ( 왜 2초뒤냐면 1초뒤에 작업이 끝나고 상태가 변경될 시간을 넉넉하게 잡아 출력하도록하기 위함 )
promise 객체에는 then(), catch(), finally() 라는 api를 제공하는데 이를 사용해 비동기 작업의 후처리를 간편하게 할 수 있음
.then()
function getData() {
const promise = new Promise ((resolve, reject) => {
setTimeout(() => {
const data = {name: '철수'};
// const data = null
if(data) {
console.log('네트워크 요청 성공');
resolve(data);
} else {
reject(new Error("네트워크 문제!"))
}
}, 1000);
});
return promise;
}
const promise = getData();
promise.then((data) => {
console.log("data")
}
처음에는 setTimeout을 사용하여 작업이 완료되길 기다렸다가 promise의 상태를 호출하였다.
하지만 .then()을 사용하면 비동기 작업이 완료될 때 까지 기다렸다가 promise의 상태가 성공으로 바뀌게 되면 then안의 콜백함수를 실행하게 된다.
또한 이 then의 콜백함수에는 매개변수를 전달 받는데 이 매개 변수는 promise의 result를 담고 있다. 즉 우리가 resolve에 전달해준 data를 담고 있게 된다. 그래서 console.log에 data의 내용을 출력해볼 수 있다.
마지막 부분을 더 간단하게 만들어보면
비동기 작업이 성공할 경우
getData().then((data) => {
const name = data.name;
console.log(`${name}님 안녕하세요`);
});
// 철수님 안녕하세요
promise를 선언할 필요 없이 getData를 바로 실행시켜주면서 then을 호출 하여 비동기 작업 후처리를 진행할 수 있다.
.catch()
비동기 작업이 실패할 경우
getData().then((data) => {
const name = data.name;
console.log(`${name}님 안녕하세요`);
}).catch(()=> {
console.log(error);
})
// Error: 네트워크 문제!
만약 promise의 상태가 rejected로 실패하게 된다면 then 뒤에 .catch()를 붙여주고 catch안에 넣어준 콜백함수가 실행된다.
catch안에 있는 콜백함수도 인자를 받는데 이 인자는 error가 들어간다.
처음 reject(new Error()); 로 에러를 만들어줬던게 여기 들어가게 되는 것이다.
실행시키면 reject에 에러를 생성하며 넣어줬던 console.log('네트워크 문제!') 가 출력 된다.
.finally()
promise의 성공, 실패 여부를 떠나 무조건 실행 되어야 하는 코드가 있을때 finally를 사용한다.
getData().then((data) => {
const name = data.name;
console.log(`${name}님 안녕하세요`);
}).catch(()=> {
console.log(error);
}).finally(() => {
console.log('마무리 작업');
})
// Error: 네트워크 문제!
// 마무리 작업
비동기 작업이 실패해도 마무리 작업이 콘솔에 출력되는 것을 확인할 수 있다.
Promise Chaining
1.
const promise = getData();
promise
.then((data) => {
console.log("data");
return getData();
})
.then((data) => {
console.log("data");
return getData();
})
.then((data) => {
console.log("data");
return getData();
})
2.
const promise = getData();
promise
.then((data) => getData())
.then((data) => getData())
.then((data) => getData())
또다시 data를 받아와야할 때는 getData를 리턴해주고 그 뒤에 또 then을 붙이면 된다.
이를 간단하게 작성하는 방법이 2번이고 여러개의 비동기함수를 순차적으로 실행해야할때 코드를 깔끔하게 작성 가능.
이전에 공부했던 Callback패턴에서는
getData((data) => {
getData((data) => {
getData((data) => {
// ...
})
})
})
콜백지옥이라 불리는 패턴이 발생하는데 같은 내용을 출력하더라도 promise는 promise chaining 덕분에 가독성 측면에서 훨씬 뛰어나다.
여기서 같은 값을 다시 리턴하는게 아닌 다른 값을 리턴하게 되면 어떻게 될까?
const promise = getData();
promise
.then((data) => {
console.log("data");
return 'hello'; <<
})
.then((data) => {
console.log("data");
})
첫번째 then에 getData가 아닌 hello를 반환하도록 하였다.
값을 return해줘도 then은 항상 promise를 return해준다. 이렇게 리턴된 값은 promise로 감싸져서 곧바로 resolve 상태가 된다. 그러고 나서 그 다음에 오는 then에 넘겨진다. 그래서 promise가 resolve될 때까지 기다릴 필요 없이 즉시 사용 가능하다.
web API 중 fetch() 는 promise를 리턴하도록 구현되어있다. 그래서 fetch().then().catch().finally()다 쓸 수 있다.
Promise Static 함수들
위에서 공부한 promise는 비동기 함수들을 순차적으로 처리한다. 하지만 작업 시간이 1분 넘게 걸리는 등 오래 걸리는 함수가 있으면 순차적으로 처리하는 방식은 비효율적이게 된다. 이럴 때 병렬적으로 처리를 할 수 있는 promise Static 함수들이 있다.
Promise.all()
promise.all을 사용하면 여러개의 promise들을 동시에 병렬적으로 처리할 수 있다.
인자로 배열을 받는다. 이 배열 안에는 여러개의 promise를 넣어준다.
전부 성공하여 resolve 상태가 되었을 때 promise에 반환해준다.
하지만 하나라도 실패하면 reject 상태가 되어 에러를 출력한다.
Promise.allSettled()
promise.all에서 작업에 실패하게 되면 실패한 것 중 제일 처음 순서의 에러를 나타낸다.
이럴 경우 그 뒤에는 어떤 작업이 실패했는지 몰라 더 자세하게 알고 싶을때 promise.allSettled()를 사용한다.
promise.all과 똑같이 배열을 인자로 받는다. 그리고 배열안에 들어간 promise들이 성공 실패 상관없이 일단 작업이 다 끝날때까지 기다리고 다 끝난 후에야 promise에 resolve로 반환된다.
출력을 해보면 배열형태로 각 promise들의 status와 value, 에러가 났으면 에러 메세지와 상태들이 출력된다.
promise.any()
배열 형태로 인자를 받는다. 특이하게 배열 안의 promise 중 가장 먼저 resolve가 된 것의 값을 가지게 된다.
배열 안의 promise가 모두 실패해야 반환된 promise가 실패한다.
promise.race()
배열을 인자로 받고 이름처럼 배열안의 모든 promise들을 달리기를 시킨다.
그 중 가장 빨리 완료된 promise 결과를 반환한다. promise가 성공하면 반환도 성공한 것으로, 실패하면 반환도 실패로.