Promise의 기능과 필요한 이유
- -
Promise - JavaScript | MDN
Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다.
developer.mozilla.org
들어가기 전 (동기적 vs 비동기적, 콜백 지옥)
자바스크립트는 동기적(Synchrous)으로 코드를 실행한다. 동기적으로 처리가 된다는 것은 우리가 작성한 순서대로 처리가 되는 것이기 때문에 어떠한 작업이 끝나야 다음 작업을 진행할 수 있으므로 어떤 작업에 대해 특정 시간을 정해놓은 후 실행을 시키고 싶다면 비동기적으로 처리를 해야한다.
비동기(Asynchronous)처리를 할 때 대표적인 함수는 setTimeout()인데, 두 개의 인자를 받고 있다. 하나는 TimeHandler라는 콜백 함수와 시간을 지정해주는 timeout을 받는다. 그래서 setTimeout()은 지정한 시간이 지난 후 콜백함수를 실행시킨다.
콜백함수는 'Call Back' 준비되면 '나중에 불러줘'와 같은 의미다. 하지만 콜백함수는 비동기 처리할 때만 사용하는 것은 아니고 synchronous callback과 asynchronous callback으로 구분되어 진다.
Promise 객체는 왜 등장했을까?(feat. callback hell 🔥)
정답: 콜백지옥 때문! 그렇다면 콜백지옥이란 무엇일까?
콜백 안에서 다시 콜백을 부르고, 다시 콜백 안에서 다시 콜백을 네스팅하는 것을 콜백 지옥이라고 한다. 이러한 코드 구조는 가독성이 떨어지고 로직을 수정, 변경하기 어렵기 때문에 이러한 콜백 지옥을 해결하기 위해 Promise, async/await 으로 해결할 수 있다.
사실 Promise 객체가 등장하기 전에도 비동기적인 처리를 할 수 있는 방법은 있었다. (setTimeout 함수나, addEventListener 메소드 같은)
setTimeout(callback, milliseconds); addEventListener(eventname, callback);
이것들은 모두 직접 파라미터에 콜백을 전달하는 형식으로 정의되어 있는데. 만약 fetch 함수를 이런 식으로 만들었다면
fetch('https;//first.com', callback)
fetch 함수도 이런 식으로 사용했을것이다. 그런데 왜 이런 방법이 선택되지 않고, 굳이 Promise 객체라는 문법이 도입된 것일까?
그 이유는 바로 함수에 콜백을 직접 넣는 형식은 콜백 헬(callback hell)이라고 하는 문제를 일으킬 수도 있기 때문이다.
아래 코드를 살펴보자. 만약 fetch 함수가 지금과 같이 Promise 객체를 리턴하는 게 아니라 setTimeout 함수처럼 콜백을 직접 집어넣는 형식의 함수였다면 우리는 여러 비동기 작업을 순차적으로 수행해야할 때 아래처럼 코드를 작성해야 했을 것이다.
fetch('https://first.com', (response) => {
// Do Something
fetch('https://second.com', (response) => {
// Do Something
fetch('https;//third.com', (response) => {
// Do Something
fetch('https;//fourth.com', (response) => {
// Do Something
});
});
});
});
지금 fetch 함수 안의 콜백에 fetch 함수가 있고 그 함수의 콜백 안에 fetch 함수가 있고 또.. 계속 이런 식으로 들어가있다. 그런데 이 코드를 보면 어떤 느낌이 느껴지는가? 뭔가 읽기 어렵고 복잡함? 한마디로 가독성이 떨어진다. 그나마 지금은 실제 코드가 들어가야 할 자리에 "// Do Something" 주석이 들어가 있어서 괜찮지만, 실제로 필요한 코드들까지 들어가게 되면 이 코드의 가독성은 현저하게 떨어지게 된다. 이런 현상을 콜백 지옥 또는 콜백 헬(callback hell)이라고 하거나 지옥의 피라미드(Pyramid of Doom)라고도 한다.
하지만 fetch 함수는 Promise 객체를 리턴하기 때문에 아래처럼 Promise Chaining을 해서 좀 더 깔끔한 코드로 여러 비동기 작업을 순차적으로 처리할 수 있다.
fetch('https://first.com')
.then(response => {
// Do Something
return fetch('https://second.com');
})
.then(response => {
// Do Something
return fetch('https://third.com');
})
.then(response => {
// Do Something
return fetch('https://fourth.com');
});
이렇게 Promise 객체를 사용하면 callback hell 문제를 해결할 수 있다.
이 뿐만 아니라 기존에 콜백을 직접 넣는 방식에 비해 Promise 객체의 문법은 비동기 작업에 관한 좀 더 세밀한 개념들이 반영되어 있다.
이전의 방식에서는 콜백에 필요한 인자를 넣어주고 실행하면 되는 단순한 방식이었다면, Promise 객체 문법에는 pending, fulfilled, fulfilled 상태, 작업 성공 결과 및 작업 실패 정보(이유), then, catch, finally 메소드 등과 같은 비동기 작업에 관한 보다 정교한 설계가 문법 자체에 반영되어 있다는 것을 알 수 있다.
Promise 란?
프로미스는 자바스크립트 제공하는 비동기를 간편하게 해결할 수 있도록 도와주는 오브젝트(객체)이다.
프로미스는 정해진 기능을 수행하고 나서 정상적으로 기능이 수행되어 졌다면 성공 메시지와 함께 처리된(fulfilled) 결과값을 전달해주고 만약 기능을 수행하다가 예상치 못한 문제가 발생했다면 에러값(rejected)을 전달한다.
Promise를 공부하려면 2가지(states, producer/consumer)를 알고 있는것이 중요하다.
- states : 프로세스가 무거운 operation(연산)을 수행하고 있는 중(pending) 인지 아니면 수행이 다 완료가되어 성공(fulfilled)했는지 실패(rejected)했는지 상태. (Promise는 다음 중 하나의 상태(states)를 가집니다.)
- 대기(pending): 이행하지도, 거부하지도 않은 초기 상태.
- 이행(fulfilled): 연산이 성공적으로 완료됨.
- 거부(rejected): 연산이 실패함.
Pending 상태
아래와 같이 Promise를 호출하면 Pending 상태가 된다. 이때 콜백 함수의 인자로 resolve, reject에 접근할 수 있다.
new Promise(function(resolve, reject){
// ...
});
Fulfilled 상태
콜백 함수의 인자 resolve를 실행하면 Fulfilled 상태가 된다. (완료 상태)
new Promise(function (resolve, reject) {
resolve();
});
이후 이행 상태가 되면 then()을 이용해 처리 결과 값을 받을 수 있다.Rejected 상태
콜백 함수의 인자 reject를 실행하면 Rejected 상태가 된다. (실패 상태)
new Promise(function(resolve, reject) {
reject();
});
이후 실패 상태가 되면 catch()를 이용해 error를 다룰 수 있다.
- producer / consumer : 우리가 원하는 데이터를 제공하는 사람(producing)과 제공된 데이터를 쓰는 사람(필요로 하는 사람) 두가지의 차이점 및 다른 견해를 이해하기. (프로미스 선언부 / 호출부)
// 프로미스는 자바스크립트에서 비동기적인것을 수행할때 콜백함수 대신 유용하게 쓰이는 오브젝트입니다.
// State: pending -> fuliflled or rejected
// 프로미스의 상태는 우리가 만들어서 오퍼레이팅이 진행중일때는 펜딩상태이고 오퍼레이팅이 성공적으로 끝냈을때는 fulfilled 상태(완벽히 완료), 파일을 찾지 못하거나 네트워크 문제시 rejected 상태
// Producer vs Consumer
// 원하는 기능을 수행해서 해당하는 데이터를 만들어 내는 Producer 즉, producer object와 원하는 데이터를 소비하는 Consumer
Promise 사용시기
보통 네트워크에서 데이터를 받거나 파일에서 큰 데이터를 읽어오는 과정은 시간이 꽤 걸린다. 이러한 것들을 동기적으로 네트워크에서 데이터를 읽어오면 데이터를 받아오는 동안 다음 라인에 코드가 실행되지 않기 때문에 시간이 조금 걸리는 일들은 이렇게 promise를 만들어서 비동기적으로 만들어 처리하는 것이 좋다.
promise를 만드는 순간 우리가 만든 excuter라는 콜백 함수가 바로 실행이 되는것을 알 수 있다. 이 말은 즉, promise 안에 네트워크 통신을 하는 코드를 작성했다면 pormise가 만들어진 순간 네트워크 통신이 이뤄진다.
만약 네트워크 통신을 사용자가 요청했을 경우에만 해야하는 경우라면 promise를 사용하면 불필요한 네트워크 통신이 발생한다.
(ex. 사용자가 버튼을 클릭했을 경우에만 네트워크 통신이 이뤄져야 하는경우)
Promise 생성자 코드설명
프로미스 생성자를 살펴보면 excutor라는 콜백함수를 전달해줘야 하며 excutor는 또 다른 2가지 콜백함수 resolve(기능 정상적 수행하여 최종적으로 데이터 전달하는), reject(기능을 수행하다 문제가 생기면 호출하는) 를 갖는다.
// 프로미스 생성자 살펴보기
var Promise(): PromiseConstructor
new <any>(excutor: (resolve: (value?: any) => void, reject: (reason?: any) => void) => Promise<any>)
Promise 만들기
Producer 만들기(우리가 원하는 기능을 비동기적으로 실해하는 promise 만들어 보기)
프로미스는 클래스이기 때문에 new 라는 키워드를 이용해 오브젝트를 생성할 수 있다.
// 1.Producer 만들기(우리가 원하는 기능을 비동기적으로 실해하는 promise 만들어 보기)
// 프로미스는 클래스이기 때문에 new 라는 키워드를 이용해 오브젝트를 생성할 수 있습니다.
const promise = new Promise((resolve, reject) =>{
// doning some heavy work (network, readfiles)
console.log("무거운 작업을 실행할때...")
})
// 1.Producer 만들기(우리가 원하는 기능을 비동기적으로 실해하는 promise 만들어 보기)
// 프로미스는 클래스이기 때문에 new 라는 키워드를 이용해 오브젝트를 생성할 수 있습니다.
const promise = new Promise((resolve, reject) =>{
// doning some heavy work (network, readfiles)
console.log("무거운 작업을 실행할때...")
// 프로미스 오브젝트를 만들때 비동기적으로 작동시키고 싶은 코드를 작성하고
setTimeout() => {
// resolve('ellie') 성공적으로 작성되었을때 resolve
reject(new Error('no network')); // 실패했을때 reject. 왜 실패했는지 에러 전달.
}, 2000);
})
// 2. Consumers : then, catch, finally
// 위에 프로미스 변수로 만든것 불러와서 성공할때, 실패할때 원하는 방식으로 처리해주면 됩니다.
promise
.then(value => { // 성공시 잡아주는 .then()
console.log(value);
})
.catch(error => { // 실패시 잡아주기 .catch()
console.log(error);
});
.finally(()=>{ // 성공하든 실패하든 무조건 실행되는 .finally()
console.log('finally');
})
// 위 코드에서는 위에서 reject로 되어있서 catch에러가 와 finally가 작동한다.
출력 결과를 통해 정상적인 인자를 넘긴 경우엔 then()이 호출되고, 비정상적인 인자를 넘긴 경우엔 catch()가 호출됨을 알 수 있다.
하지만 주로 코딩을 할 때는 위와 같이 Promise를 직접 생성해서 리턴해주는 코드 보다는 어떤 API를 호출해서 리턴 받은 Promise 객체를 사용하는 경우가 더 많을 것이다.
대표적 예시로 fetch API가 있다. 이는 브라우저 내장함수로 네트워크 요청을 날리기 위한 API이다. 요청을 보내고 응답을 받는 과정에는 불가피하게 딜레이가 발생할 수밖에 없다.
fetch 함수 역시 Prmoise 객체를 반환한다.
fetch("요청할 URL")
.then((res) => console.log(res)) //정상 응답 시 resolve() 호출
.catch(err) => console.log(err)) //비정상 응답 시 reject() 호출
Promise 에서 error 처리
개개인의 코딩 스타일에 따라서 then()의 두 번째 인자로 처리할 수도 있고 catch()로 처리할 수도 있겠지만 가급적 catch()로 에러를 처리하는 게 더 효율적이다.
그 이유는 아래의 코드를 보시면 알 수 있다.
// then()의 두 번째 인자로는 감지하지 못하는 오류
function getData() {
return new Promise(function(resolve, reject) {
resolve('hi');
});
}
getData().then(function(result) {
console.log(result);
throw new Error("Error in then()"); // Uncaught (in promise) Error: Error in then()
}, function(err) {
console.log('then error : ', err);
});
getData() 함수의 프로미스에서 resolve() 메서드를 호출하여 정상적으로 로직을 처리했지만, then()의 첫 번째 콜백 함수 내부에서 오류가 나는 경우 오류를 제대로 잡아내지 못한다. 따라서 코드를 실행하면 아래와 같은 오류가 난다.
하지만 똑같은 오류를 catch()로 처리하면 다른 결과가 나온다.
// catch()로 오류를 감지하는 코드
function getData() {
return new Promise(function(resolve, reject) {
resolve('hi');
});
}
getData().then(function(result) {
console.log(result); // hi
throw new Error("Error in then()");
}).catch(function(err) {
console.log('then error : ', err); // then error : Error: Error in then()
});
※ 따라서, 더 많은 예외 처리 상황을 위해 프로미스의 끝에 가급적 catch()를 붙여야 한다.
프로미스 체이닝(promise chaining)을 이용한 비동기 처리
위에서 언급했던 후속 처리 메서드(then, catch)를 통해 반환되는 Promise를 체이닝하면, 여러 개의 Promise를 연결해서 사용 가능하다.
기본적인 Promise 체이닝의 예시를 보면 다음과 같다.
const promise = doSomethingAsync();
// promise는 Promise 인스턴스
promise
.then(doSomething1)
.then(doSomething2)
.catch(handleError)
.then(doSomething3)
...
.finally(finishLine)
이 구조를 잘 파악해보면, 2개의 API를 연결해서 하나의 데이터로 받아오는 것도 가능하다.
const movieAPI = "https://movieapi.com/data";
const tvAPI = "https://tvapi.com/data";
const getData = () => {
return fetch(movieAPI)
.then(res => res.json())
.then(movieJson => {
return fetch(tvAPI)
.then(res => res.json())
.then(tvJson => {
return {
movies: movieJson,
tvs: tvJson
}
})
})
}
체이닝이 가능한 이유는 promise.then을 호출하면 새로운 프라미스 객체가 반환되기 때문이다. 따라서 반환된 프라미스엔 당연히 .then을 호출할 수 있다.
이터러블을 전달받는 Promise.all
Promise.all 메서드는 프로미스가 담겨있는 Array와 같이 순회 가능한 이터러블을 인자로 받는다. 그리고 전달받은 모든 Promise를 병렬로 처리하고 그 결과를 resolve하는 새로운 Promise를 반환한다.
Promise.all()은 배열 내 요소 중 어느 하나라도 reject되면 다른 프로미스의 이행 여부와 상관없이 즉시 reject한다.
Promise.all 예제)
아래는 3개의 프로미스를 Promise.all을 통해 이터러블로 받아서, 이행 결과값들을 배열에 담아서 반환한다.
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'bar');
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'baz');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values); // ["foo", "bar", "baz"]
});
이번엔 위에서 다뤘던 Promise 체이닝 예제를 Promise.all로 표현
const movieAPI = "https://movieapi.com/data";
const tvAPI = "https://tvapi.com/data";
const getData = () => {
return Promise.all([
fetch(movieAPI),
fetch(tvAPI)
])
.then(([movieRes, tvRes]) => {
return Promise.all([movieRes.json(), tvRes.json()])
})
.then(([movieJson, tvJson]) => {
return {
movies: movieJson,
tvs: tvJson
}
})
}
콜백함수방식
callback함수는 다른 함수의 인수로 넘겨줌으로써(두번째 인자로 콜백을 넣음) 실행이 가능한 코드를 말합니다.
callback을 반복적으로 실행시키기 때문에 코드의 양이 길어지고 비교적 가독성이 떨어집니다.
promise, callback차이점
- callback함수는 함수안에서만 결과값처리와 결과값을 알 수 있지만 promise는 비동기 로직에서 처리된 결과값이 promise객체에 저장되기 때문에 로직 밖에서도 사용 가능합니다.
- callback함수는 함수 내부에서 계속해서 연달아 호출하므로 가독성이 떨어지지만 promise함수는 promiseAPI를 사용해 가독성을 높여줍니다.
다음으로 배워야 할 것
비동기를 더 쉽게 다루는 async / await 을 학습하도록 합시다.
참조:
1 https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise
2 https://triplexlab.tistory.com/79
3 https://sohyunsaurus.tistory.com/92
4 https://velog.io/@solimlee/
5 https://youtube.com/watch?v=JB_yU6Oe2eE&si=EnSIkaIECMiOmarE
6 https://joshua1988.github.io/web-development/javascript/promise-for-beginners/#promise%EA%B0%80-%EB%AD%94%EA%B0%80%EC%9A%94
'프론트엔드 공부 > 자바스크립트' 카테고리의 다른 글
Beesbeesbees 과제 (0) | 2023.01.16 |
---|---|
프로토타입(Prototype), 클래스, 인스턴스, 프로토타입의 관계 (1) | 2023.01.13 |
객체지향 프로그래밍 OOP (캡슐화, 상속, 추상화, 다형성) (0) | 2023.01.13 |
클래스(class)와 인스턴스(instance) 그리고 객체(object) (0) | 2023.01.13 |
고차함수, 일급객체, 내장고차함수(filter, map, reduce) (0) | 2023.01.12 |
소중한 공감 감사합니다