모던 자바스크립트 Deep Dive 글 목록(스터디)
https://hello-kk.tistory.com/780
★프로미스 다시보기★
비동기 코드를 처리할 때 콜백 헬의 문제와 해결 방법
프로미스가 갖고있는 것이 무엇일까
[[PromiseState]]
[[PromiseResult]]
프로미스의 생명주기
프로미스 생성자에 파라미터로 들어가는 콜백의 파라미터는 2개
후속 메서드 then의 파라미터는 2개
Promise 인스턴스에서 resolve가 호출되면 then 메서드가 실행되고 catch 메서드는 실행되지 않는다
Promise 인스턴스에서 reject가 호출되면 then 메서드가 실행되지 않고 catch 메서드가 실행된다
Promise의 후속 메서드 then에서 return을 하게되면 반환값은? => Promise
정적 메서드: 인스턴스를 생성하지 않아도 호출 가능, (인스턴스가 아닌)클래스에 속한 메서드
(1) Promise.all
- 하나라도 rejected 된다면?
- 비동기적으로 수행되는 함수가 있고 각각 프로미스를 생성한다. 모든 프로미스가 fulfilled되고 될 때까지 걸리는 시간은?
- 병렬적 수행 ↔ 후속 메서드 then을 연달아 n번 사용하는 것: 순차적
(2) Promise.race
- 하나라도 pending이 있다면?
- reject, pending, fulfilled 상태를 각각 반환하는 프로미스 배열이 있을 때,① reject가 가장 먼저 수행이 끝나면? ② fulfilled가 가장 먼저 수행이 끝나면?
(3) Promise.allSettled
=> 전달받은 프로미스가 모두 settled 상태.
Promise.resolve(1)
.then((result) => {
console.log(result);
return result * 2;
})
.then((result) => {
console.log(result);
return Promise.resolve(result * 3);
})
.then((result) => {
console.log(result);
});
- 최종 반환되는 프로미스는? [[PromiseState]] [[PromiseResult]] 를 고려해서 생각해보기!
return 이 자동으로 붙게되고, return undefined; 이기 때문에, undefined가 들어가게 되는 것.
- 만약 프로미스가 아닌 일반값이 return된다면, [[PromiseState]] [[PromiseResult]]에 어떤 값이 들어갈까? 프로미스이긴 할까??
- 이 경우, [[PromiseState]]에 들어가는 기본값은?
만약 일반 값이 반환된다면 프로미스로 자동 래핑되고, fulfilled가 기본값으로 [[PromiseState]]에 들어가게 된다
rejected를 하려면, 명시적으로 Promise.reject()를 호출해야 함
태스크 큐 => 비동기 함수의 콜백 함수가 저장되는 곳
마이크로 태스크 큐 => Promise의 후속처리 메서드의 콜백 함수가 저장되는 곳
주의! Pending 일 때
45장 프로미스
비동기 처리 → 콜백함수 → 콜백 헬
비동기 처리 → 프로미스
45.1 비동기 처리를 위한 콜백 패턴의 단점
45.1.1 콜백 헬
비동기로 동작하는 코드를 포함하는 비동기 함수가 서버의 응답 결과를 반환하게 하려면??
get() → onload → 응답값 얻기 //2번
→ get 함수 종료 //1번
get함수가 종료되는 시점이 응답 결과를 받는 것보다 빠르다.
기대한 대로 동작하지 않는다
서버의 응답 결과를 반환하도록 수정한다면? 결과는 undefined.
서버의 응답을 상위 스코프의 변수에 할당하면? 마찬가지로 순서를 보장하지 않는다.
xhr.onload 이벤트 핸들러는 load 이벤트가 발생하면 테스크 큐에 저장되어 대기. 이후 콜 스택이 비게 되면 이벤트 루프에 의해 콜 스택으로 푸시되어 실행
이벤트 핸들러 평가 → 이벤트 핸들러의 실행 컨텍스트 생성 → 콜 스택에 푸시 → 이벤트 핸들러 실행 과정을 거친다
따라서, 이벤트 핸들러가 실행되는 시점에는 콜 스택이 빈 상태여야 한다
n번 호출된다 해도, 모든 console.log가 종료된 이후 xhr.onload 이벤트 핸들러가 실행된다.
=> React에서 비동기 코드가 제대로 작동하지 않는 이유^,^
비동기 함수는,
(1) 비동기 처리 결과를 외부에 반환할 수 없다
(2) 상위 스코프의 변수에 할당할 수 없다
∴ 비동기 함수의 처리 결과에 대한 후속 처리는 비동기 함수 내부에서 수행!
=> 내부에서?
일반적으로, 후속 처리를 수행하는 콜백 함수를 전달하지만,
처리 결과를 갖고 또 다시 비동기 함수를 호출해서 콜백 함수의 호출이 중첩되어 복잡도가 높아지면 콜백 헬 발생
45.1.2 에러 처리의 한계
비동기 처리의 콜백 패턴의 문제: 에러 처리가 어렵다
에러를 캐치하지 못하고 함수가 종료되어 버린다.
코드 실행 | 콜 스택 동작 | 테스크 큐 동작 |
비동기 함수 setTimeout 호출 | (1) setTimeout 함수의 실행컨텍스트 생성 (2) setTimeout 함수의 실행 컨텍스트가 콜 스택에 푸시 (3) setTimeout 함수의 코드 실행 |
|
setTimeout 즉시 종료(콜백함수 호출 기다리지 않음) | setTimeout 함수의 실행 컨텍스트가 콜스택에서 제거 | |
타이머 만료 | setTimeout의 콜백 함수는 테스크 큐로 푸시 | |
콜스택 비워짐 | ||
setTimeout의 콜백 함수는 이벤트 루프에 의해 콜 스택으로 푸시되어 실행 |
setTimeout 콜백 함수가 실행될 때 setTimeout 함수는 이미 콜 스택에서 제거된 상태.
만약 setTimeout 콜백 함수의 호출자가 setTimeout 함수라면,
코드 실행 | 콜 스택 동작 |
비동기 함수 setTimeout 호출 | setTimeout 함수의 실행컨텍스트 생성 setTimeout 함수의 실행 컨텍스트가 콜 스택에 푸시 setTimeout 함수의 코드 실행 |
setTimeout 콜백 함수 호출 | setTimeout 콜백 함수의 실행 컨텍스트가 콜 스택에 푸시 |
setTimeout 즉시 종료 | setTimeout 함수의 실행 컨텍스트가 콜스택에서 제거 |
setTimeout 콜백 함수의 호출자가 setTimeout 함수가 아니라는 의미
에러는 호출자 방향으로 전파=> 콜 스택의 아래 방향(실행중인 실행 컨텍스트가 푸시되기 직전에 푸시된 실행 컨텍스트 방향)으로 전파
따라서, catch 블록에서 잡히지 않는다.
콜백 헬, 에러처리를 위해 프로미스 등장
45.2 프로미스의 생성
Promise 생성자 함수는 비동기 처리를 수행할 콜백함수 resolve, reject 함수를 인수로 받는다
resolve('success result')
reject('failure reason')
이후에는?
Promise의 상태 정보 | 의미 | 상태 변경 조건 |
pending | 비동기 처리가 아직 수행되지 않은 상태 | 프로미스가 생성된 직후 기본 상태 |
fulfilled (settled 상태== pending 아님) | 비동기 처리가 수행됨, 결과는 성공 | resolve 함수 호출 |
rejected (settled 상태== pending 아님) | 비동기 처리가 수행됨, 결과는 실패 | reject 함수 호출 |
pending 상태에서 settled 상태로 변한다 (settled 상태가 되면 다른 상태로 변할 수 없다)
const fulfilled = new Promise(resolve => resolve(1));
위의 fulfilled 변수를 출력해보면,
[[PromiseState]] 슬롯은 "fulfilled"
[[PromiseResult]] 슬롯의 값은 1의 결과 값을 갖는다
"프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체"
45.3 프로미스의 후속 처리 메서드
프로미스의 비동기 처리 상태가 변화하면 후속처리를 해야 한다.
프로미스의 후속처리 메서드: then, catch, finally
후속처리 메서드는 프로미스를 반환, 비동기로 동작
45.3.1 Promise.prototype.then
=> 프로미스를 반환
콜백 함수가 3개 들어가게 된다
(1) 성공 또는 실패의 결과
(2) then 메서드의 첫번째 인수인 콜백 함수: 비공기 처리가 성공했을 때 호출되는 성공 처리 콜백 함수
(3) then 메서드의 두번째 인수인 콜백 함수: 비공기 처리가 실패했을 때 호출되는 실패 처리 콜백 함수
new Promise( resolve or reject )
.then( v=> console.log('resolved'), e=> console.error('rejected'));
45.3.2 Promise.prototype.catch
catch 메서드=> 1개의 콜백 함수를 인수로 전달 받는다. catch 메서드의 콜백 함수는 프로미스가 rejected 상태인 경우만 호출
=> 프로미스를 반환
function asyncFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Something went wrong!'));
}, 1000);
});
}
asyncFunction()
.then(result => {
console.log(result);
})
.catch(error => {
console.log(error.name); // Error
console.log(error.message); // Something went wrong!
console.log(error.stack); // Error: Something went wrong! at ...
});
catch 메서드에서 반환되는 프로미스는 이전에 발생한 에러를 포함하는 객체 정보를 갖고 있습니다. 이 객체 정보에는 다음과 같은 프로퍼티들이 포함될 수 있습니다. name: 에러 이름 (문자열) message: 에러 메시지 (문자열) stack: 에러 스택 트레이스 (문자열) |
메서드 | 파라미터 갯수 | 콜백함수 호출 조건 |
Promise.prototype.then | 두 개의 콜백 함수 | (1) 첫 번째 콜백 함수: - 프로미스가 fulfilled 상태일 때 호출 - 콜백함수는 프로미스의 비동기 처리 결과를 인수로 전달 받음 (2) 두 번째 콜백 함수 - 프로미스가 rejected 상태일 때 호출 - 콜백함수는 프로미스의 에러를 인수로 전달 받음 |
Promise.prototype.catch | 한 개의 콜백 함수 | rejected 상태일 때만 호출 |
Promise.prototype.finally | 한 개의 콜백 함수 | fulfilled/rejected 여부와 관계 없이 무조건 한 번 호출 |
'프로미스를 반환한다'는 것의 의미가 궁금했다.
[[PromiseState]]와, [[PromiseResult]]를 갖는 Promise가 반환된다.
resolve, reject 어떤 결과도 내놓지 않았지만 finally 메서드를 호출한 경우가 궁금해서 출력해봤는데, console.log가 동작하지 않는 것처럼 보였다.
const result = new Promise(()=>{}).finally(()=>console.log('finally'))
result
//Promise {<pending>}[[Prototype]]: Promise[[PromiseState]]: "pending"[[PromiseResult]]: undefined
new Promise((resolve,reject)=>{ resolve('resolved')}).finally(()=>console.log('finally'))
//Promise {<fulfilled>: 'resolved'}
finally 메서드는 pending 상태일 때 실행되지 않는 것으로 보인다.
finally() 메서드는 항상 프로미스가 처리되면 실행됩니다. 하지만, new Promise(()=>{})는 resolve()나 reject() 메서드가 호출되지 않으므로 항상 pending 상태로 남아 있습니다. 따라서 finally() 메서드는 실행되지 않고 프로미스가 계속 대기 상태로 남아 있게 됩니다. 따라서, finally() 메서드가 실행되려면 resolve()나 reject() 메서드가 호출되어 프로미스가 처리되어야 합니다. |
여기서 잠깐 정리하고 가는 프로미스
[[PromiseState]]
[[PromiseResult]]
코드에 블록이 몇개이든, 몇 블록 안쪽에서 사용했든 간에 콜백함수로 해당 프로미스 결과값을 전달하는 것
const PromiseGet = ()=>{
return new Promise((resolve, reject)=>{
...
{
resolve( ... )
}
{
reject( ... )
}
...
})
}
new Promise() 즉, Promise 생성자를 통해 생성한 프로미스는 프로미스를 반환한다.
프로미스?
프로미스가 갖고있는 것이 무엇인지 이해하면 조금 쉬울 것 같다.
콜백 | [[PromiseState]]와 [[PromiseResult]]에 들어갈 값 |
(1) resolve 콜백 함수를 사용했을 때 | [[PromiseState]] => fulfilled [[PromiseResult]] => resolve( 안의 값 ) |
(2) reject 콜백 함수를 사용했을 때 | [[PromiseState]] => reject [[PromiseResult]] => reject( 안의 값 ) // new Error()등 |
(3) 둘 다 안 썼을 때 | [[PromiseState]] => pending [[PromiseResult]] => |
이 결과인 프로미스를 then, catch, finally 메소드를 사용해서 넘겨준다!
45.4 프로미스의 에러 처리
콜백 패턴의 곤란한 에러 처리
아래는 다 같은(비슷한) 에러 처리 방법
promiseGet(wrongURL)
.then(resolved => console.log(resolved), rejected => console.error(rejected))
promiseGet(wrongURL)
.then(resolved => console.log(resolved))
.catch(rejected => console.error(rejected))
promiseGet(wrongURL)
.then(resolved => console.log(resolved))
.then(undefined, rejected => console.log(resolved))
catch( err => ... ) 메서드는 내부적으로 then(undefined, onRejected)를 호출한다
하지만, catch를 쓰는 것을 권장
(1) then에서 발생한 에러도 catch 가 받을 수 있고
(2) catch를 쓰는 것이 더 가독성에 좋기 때문
45.5 프로미스 체이닝
then, catch, finally 후속 처리 메서드는 언제나 프로미스를 반환
따라서, 연속적으로 호출 가능 == 프로미스 체이닝
만약 후속 처리 메서드가 프로미스가 아닌 값을 반환하면,
그 값을 암묵적으로 resolve 또는 reject하여 프로미스를 생성해 반환
await를 사용하면 프로미스 후속 처리 메서드 없이 동기 처리하는 것 처럼 프로미스 처리 결과를 반환하도록 구현 가능
45.6 프로미스의 정적 메서드
Promise는 생성자 함수
Promise는 (함수)객체
45.6.1 Promise.resolve / Promise.reject
이미 존재하는 값을 래핑하여 프로미스를 생성하기 위해 사용
const resolvedPromise = Promise.resolve(['hello'])
resolvedPromise.then(console.log)
왜 이렇게 곧바로 프로미스로 생성할 데이터를 주입하는 걸까?
프로미스가 굳이 필요한 상황이 있는걸까? 프로미스를 사용하는 것보다 그냥 쓰는게 편할것 같다는 생각이 드는데..
(1) 이미 값이 결정된 상태에서 프로미스 객체를 생성할 때=> 왜지?
(2) 다른 비동기 작업의 결과값을 프로미스 객체로 바꾸기 위해
: 만약 비동기 함수를 2개 연달아 사용한다면, 첫번째 함수는 반환값으로 일반값을 사용하고 두번째 함수는 반환값이 프로미스라면, 첫번째 함수의 결과값이 두번째 함수의 입력값이 되어야 하는데 두번째 함수는 그 일반 값(프로미스가 아닌값)을 그대로 쓸 수 없기 때문에 프로미스 객체로 변환하는 것이 좋다고 한다
45.6.2 Promise.all
여러개의 비동기 처리를 병렬로 처리..
병렬 ↔ 순차적으로 처리
=> 각 비동기 처리는 서로 의존하지 않고 개별적으로 수행됨
=> 앞선 비동기 처리 결과를 다음 비동기 처리가 사용하지 않는다.
하나의 프로미스를 후속 메소드로 계속 전달하는 것이 아님
(1) then에서 각각 새로운 프로미스를 생성
(2) 새로 생성된 프로미스를 return
(3) then을 통해 또 새로운 프로미스를 생성
(4) 새로 생성된 프로미스를 return
... 반복
여기서 중요한건
then에서 프로미스를 생성하는 것이 목적이 아니다. 프로미스의 결과가 다음 프로미스의 실행에 영향을 미치지 않기 때문에.
단지 여러개의 프로미스 객체를 처리하는 것이 목적.
그렇다면,
Promise.all을 하게 되면 파라미터로 넣는 값의 순서가 프로미스로 만들어지는데 순서에 영향을 미치는건가?
프로미스로 만들어지는 순서가 보장되는건지 만약 그렇지 않으면 어떤 순서로 프로미스가 만들어지는지 궁금하다.
const promise1 = new Promise((resolve) => setTimeout(() => resolve('2000ms'), 2000));
const promise2 = new Promise((resolve) => setTimeout(() => resolve('500ms'), 500));
Promise.all([promise1, promise2]).then((result) => {
console.log(result); // ['2000ms','500ms' ]
});
promise1은 2000ms 이후 생성되고,
promise2는 500ms 이후 생성된다.
promise2가 더 빨리 생성되지만, 결과는 배열에 프로미스를 넣은 순서대로 출력된다.
즉, Promise.all에 들어가는 모든 프로미스의 비동기 처리가 완료된 뒤 결과값을 반환한다. 처리 순서가 보장됨!
=> 전달 받은 모든 프로미스 상태가 fulfilled가 되면 모든 처리 결과를 배열에 저장해 새 프로미스를 반환
따라서, Promise.all 메서드가 종료하는 데 걸리는 시간은? 가장 늦게 fulfilled 상태가 되는 프로미스의 처리 시간보다 조금 더 길다.
이 때, 여러개의 프로미스 중 하나라도 rejected 상태가 된다면, 나머지가 종료하는 것을 기다리지 않고 즉시 종료한다. catch에서는 가장 빨리 rejected된 프로미스의 값을 반환한다.
Promise.all()은 이터러블한 객체를 매개변수로 받는다.
따라서, 매개변수에 배열 메소드를 사용해 비동기 처리로 프로미스를 생성할수도 있다.
만약 인수로 전달받은 이터러블의 요소가 프로미스가 아니라면, 자동으로 Promise.resolve 메서드를 통해 프로미스로 래핑한다.
45.6.3 Promise.race
가장 먼저 상태가 처리된 프로미스가 결과값으로 반환
↔ Promise.all (모든 프로미스의 상태가 fulfilled로 되는 것을 기다림)
=> 결국 정적 메서드 race란, 상태가 가장 먼저 처리되는 것을 반환한다.
what if... 정말로 동시에 끝나면???
보통은 그런 상황이 없지만 예외처리가 필요할 수 있다. 그리고 우선순위가 없기 때문에 순서를 보장하지도 않는다.
45.6.4 Promise.allSettled
전달받은 프로미스가 모두 settled 상태(fulfilled, rejected 상태)가 되면 처리 결과를 배열로 반환
fulfilled 상태, rejected 상태 상관 없이 메서드가 인수로 전달 받은 모든 프로미스들의 처리 결과가 모두 담겨있다.
그렇다면 만약 계속 pending이라면? 결과가 나오지 않는다면?
항상 대기(pending) 상태로 남아있는 프로미스를 반환하면, 모든 프로미스가 이행되지 않기 때문에 결코 이행되지 않는다.
따라서 .then 메서드도 호출되지 않는다. 아무런 결과도 출력되지 않는다. . .
pending이라면 안되고,
fulfilled 상태, rejected 상태 어떤 것이라면 가능하다.
45.7 마이크로태스크 큐
프로미스의 후속 처리 메서드의 콜백 함수는 마이크로 테스크 큐에 저장(테스크 큐 아님)
우선순위 높은 것: 마이크로 태스크 큐 > 태스크 큐
(1) 이벤트 루프는 콜 스텍이 비면 먼저 마이크로 태스크 큐에서 대기하고 있는 함수를 가져와 실행
(2) 마이크로 태스크 큐가 비면 태스크 큐에서 대기하고 있는 함수를 가져와 실행
45.8 fetch
fetch 함수는 HTTP 응답을 나타내는 Response 객체를 래핑한 Promise 객체를 반환
response: Response 객체의 인스턴스
즉, Response.prototype.json 메서드 사용 가능
fetch('URL').then(response=>response.json());
404, 500에러는 reject하지 않는다(일단 서버에서 응답을 받으면 resolve 되는 듯)
오프라인 네트워크 장애, CORS 에러에 의해 요청이 완료되지 못한 경우만 reject
하지만!
axios는 모든 HTTP 에러를 reject하는 프로미스를 반환
(인터셉터, 요청 설정 등 fetch보다 다양한 기능 지원)
'Front-End > JavaScript' 카테고리의 다른 글
27장 배열(~ flatMap) (모던 자바스크립트 Deep Dive) (0) | 2023.04.26 |
---|---|
27장 배열(~slice) (모던 자바스크립트 Deep Dive) (0) | 2023.04.22 |
19장 프로토타입[19.8절~19.14절] (모던 자바스크립트 Deep Dive) (0) | 2023.04.13 |
[헷갈린 내용 정리] 19장 프로토타입 (모던 자바스크립트 Deep Dive) (0) | 2023.04.10 |
19장 프로토타입 (모던 자바스크립트 Deep Dive) (0) | 2023.04.10 |