면접 답변 예시
- Promise란 무엇인가요?
자바스크립트에서의 Promise 비동기 작업을 처리하기 위한 객체입니다. 비동기 작업의 결과로 대기(panding), 이행(fulfilled), 거부(rejected) 상태를 가집니다. - Promise의 장점은 무엇인가요?
비동기 작업의 성공 또는 실패와 관련된 로직을 명확히 나타낼 수 있습니다.
여러개의 비동기 작업을 조합하거나 순차적으로 실행할 수 있는 유용한 메서드를 제공합니다.
비동기 작업을 간결하고 가독성 있게 처리할 수 있습니다. - Promise의 상태는 어떤 종류가 있나요?
대기, 이행, 거부 의 3가지 상태를 가지고 있습니다.
대기 상태는 비동기 작업이 아직 완료되지 않은 상태를 나타냅니다.
거부 상태는 비동기 작업이 실패한 상태를 나타냅니다.
이행 상태는 비동기 작업이 성공적으로 완료된 상태를 나타냅니다. - Promise의 then 과 catch 메서드는 무엇을 하는데 사용되나요?
then으로도 에러를 받을 수 있지만, cath는 에러만 발생한 경우를 다룹니다.
then은 결과와 에러를 다룰 수 있는 두가지의 인수를 가집니다.
첫번째 인수는 실행 결과를 받고, 두번째 인수는 프라미스가 거부되었을때의 에러를 받습니다. - Promise의 all 과 race 메서드는 어떤 역할을 하나요?
Promise.race 메서드는 여러 개의 Promise를 동시에 실행하고, 가장 먼저 이행되는 Promise의 결과를 반환합니다. Promise.all 메서드는 여러 개의 Promise를 동시에 실행하고, 모든 Promise가 이행된 후에 결과를 반환합니다. - Promise와 async/await의 차이점은 무엇인가요?
Promise는 비동기 작업의 결과를 다루는 객체이고, then()과 catch()를 사용하여 결과를 처리합니다. async/await는 Promise를 기반으로 한 구문적인 편의 기능으로, 비동기 작업을 동기적으로 작성할 수 있게 해줍니다.
컴퓨터 과학과 Future & Promise
Futures와 promises는 비동기 프로그래밍의 인기 있는 추상화 방식으로, 특히 분산 시스템의 문맥에서 자주 사용됩니다.
JS에서 ES6에 등장한 객체의 이름이기도 하지만, 해당 개념은 CS의 ‘퓨처와 프로미스’ (Future, Promise)의 개념을 구현한 것입니다.
컴퓨터에게 있어 동기와 비동기란
간단한 컴퓨터 프로세서는 인간과 달리, 병렬 처리가 없으며 한번에 하나의 작업과 프로세스를 완료할 수 있습니다. 프로세스는 처리해야 할 작업들에 수시로 차단당합니다. 처리해야 할 작업들은, 디스크 읽기와 쓰기, CPU를 이용한 여러 계산 작업부터 네트워크를 통한 패킷 송수신과 같은 I/O(Input, Output / 입출력) 작업 등.. 여러 종류가 있습니다.
그리고 일반적으로 CPU 바운드 작업보다 I/O 바운드와 같은 작업이 더 많은 시간이 소요될 가능성이 높습니다.
CPU Bound / I/O Bound가 무엇인가요?
CPU Bound
프로세스가 진행 될 때 CPU 사용 기간이 I/O Wating 보다 많은 경우를 의미 행렬 곱이나 고속 연산 시 나타남 CPU 성능에 의존적인 작업 속도
I/O Bound
프로세스가 진행될 때 I/O Wating 시간이 많은 경우를 의미 파일 쓰기, 디스크 작업, 네트워크 통신 등에서 나타남 작업에 의한 병목에 의해 작업 속도가 결정
프로세서는 두가지 방법으로 차단 호출을 처리할 수 있습니다.
- 동기식: 프로세서는 블로킹 호출이 작업을 완료하고 결과를 반환할때까지 기다립니다. 해당 호출이 끝날때까지 다른 작업을 하지 못하기에, CPU가 효율적으로 활용되지 않을 수 있으며, 시간이 많이 걸릴 수 있습니다.
- 비동기식: 작업이 비동기적으로 처리되면, CPU는 언제 끝날지 모르는 작업들 (I/O 작업 등)이 끝나길 기다리는 대신 다른 작업을 처리하는데 사용됩니다.
Future와 Promise 추상화는 동기식 비동기식 프로그래밍을 모두 수행하는데 널리 사용되는 개념입니다.
기본 아이디어
넓은 의미에서 Future와 Promise는 미래 또는 약속을 사용할 수 있는 값으로 생각하는 것입니다.
이 컨셉을 사용하면 작업을 요청한 시점에 작업의 결과가 여러 상태를 가질 수 있다고 가정합니다.
- completed/determined (완료됨 / 결정됨 )
- 계산이 완료되었으며 Future/Promise 값을 사용 가능
- incomplete/undetermined ( 불완전/ 미결정 )
- 계산이 아직 완료되지 않음
중요한 것은 future/promise는 어느 정도의 동시성(concurrency)을 가능하게 한다는 것입니다. 아래는 future 의 첫번째 정의 중 하나입니다.
💡 (future X)라는 구조는 식 X의 값을 나타내는 future를 즉시 반환하고, 동시에 X의 평가를 시작합니다. X의 평가가 값을 반환할 때, 해당 값이 future를 대체합니다. - (Halstead, 1985)
즉, future는 X의 평가 결과를 나중에 얻을 수 있는 대리자(Proxy) 역할을 수행하는 것 입니다.
future/promise에 대한 더 일반적인 해석 중 일부는 작업을 연결하거나, 계산이 완료된 후에 호출될 작업 파이프라인*을 future/promise로 표현할 수 있도록 한다는 것입니다. 이는 콜백 함수를 많이 사용하거나 보다 명령적인 차단 접근 방식과 대비됩니다.
이러한 개념은 비동기적인 작업의 처리를 간소화하고 조립 가능한 작업 단위로 표현할 수 있게 해줍니다. future는 아직 계산되지 않은 값을 나타내며, 이 값을 얻기 위해 필요한 계산이 완료될 때까지 기다릴 수 있습니다. promise는 future의 값을 생성하고, 이 값을 완료된 계산 결과로 설정할 수 있는 메커니즘입니다.
이러한 방식을 통해 future/promise를 사용하여 비동기 작업을 조율하고, 작업 간의 의존성을 처리하며, 비동기 코드를 보다 명확하고 구조화된 형태로 작성할 수 있게 됩니다.
작업 파이프라인이 무슨 뜻인가요?
연속적인 작업들을 연결하여 데이터나 결과를 순차적으로 처리하는 구조를 말합니다.
각 작업은 이전 작업의 출력을 입력으로 받아 처리하고, 그 결과를 다음 작업의 입력으로 전달합니다. 이러한 방식으로 작업들이 연결되어 데이터가 여러 단계를 거쳐 가공되거나 처리되는 것을 의미합니다.
용도
- Request-Response Patterns (요청 - 응답 패턴)
Future는 HTTP를 통한 웹 서비스 호출의 응답 값을 나타내는데 사용할 수 있습니다. - Input/Output (입력 / 출력)
Future는 IO 호출 및 결과 값 (EX. 터미널 입력, 읽은 파일의 바이트 배열)을 나타내는데 사용 가능합니다. - Long-Running Computations (오래걸리는 계산)
Future는 복잡하고 고비용의 알고리즘과 같은 계산 결과로 사용될 수 있습니다. - Database Queries (DB 쿼리)
- RPC (원격 프로시저 호출 / Remote Procedure Call)
- Reading Data from a Socket (소켓에서 데이터 읽기)
- Timeouts (시간 초과)
원격 프로시저 호출이란 무엇인가요? (remote procedure call, 리모트 프로시저 콜, RPC)
네트워크를 통해 다른 컴퓨터나 시스템에 있는 프로시저 또는 함수를 호출하는 기술입니다. 간단히 말하면, 로컬 컴퓨터에서 원격 컴퓨터에 있는 함수를 마치 로컬에서 실행하는 것처럼 호출할 수 있는 방법을 제공합니다.
(아래는 친절한 AI의 설명을 첨부합니다…)
예를 들어, 클라이언트가 사용자의 이름을 서버에 전달하고 싶다고 가정해봅시다. 클라이언트는 RPC를 사용하여 서버의 "saveUser"라는 함수를 호출할 수 있습니다. 이 함수는 사용자의 이름을 받아서 데이터베이스에 저장하는 역할을 합니다. 클라이언트는 로컬 함수를 호출하는 것처럼 "saveUser" 함수를 호출하고, RPC는 이 호출을 서버로 전달하여 서버에서 해당 함수를 실행하고 결과를 클라이언트에게 반환합니다.
이렇게 RPC는 네트워크를 통해 다른 시스템과 상호작용하는 데 사용되며, 분산 시스템 및 원격 서비스 호출 등 다양한 상황에서 유용하게 활용될 수 있습니다.
Futuer/Promise와 JS
퓨처(Futer), 프로미스(Promise), 딜레이(Delay), 디퍼드(Defferd) 는 일반적으로 거의 동일한 동기화 메커니즘을 가리키는 용어로, 객체가 아직 알려지지 않은 결과를 대행하는 역할을 하는 것을 의미합니다.
해당 개념은 여러 컴퓨터 언어로 구현되었지만 이 글에서는 JavaScript의 경우만 다루도록 하겠습니다.
조금 혼동스럽게도, JavaScript 커뮤니티에서는 Promise라고 불리는 단일한 구조를 표준화했습니다. 이는 다른 언어에서의 future 개념과 유사하게 사용할 수 있습니다.
JS의 Promises 사양 (Promises/A+, 2013)은 단일 인터페이스만 정의하고, promise를 완료하거나 수행하는 세부 사항은 사양을 구현하는 사람(개발자)에게 맡기고 있습니다. JavaScript의 Promises는 비동기적이며 파이프라인으로 사용할 수도 있으며, ECMAScript 6 (ES6)를 지원하는 브라우저에서 기본적으로 활성화되어 있거나 Bluebird, Q와 같은 여러 라이브러리에서 사용할 수 있습니다.
Promise in JS
Promise의 등장 이유 : 콜백 지옥
콜백이란, 나중에 호출할 함수를 의미합니다. 역시 비동기 작업을 처리하기 위해 사용되는 함수로써 일반적으로 다른 함수에 인자로 전달되어 특정 조건이 충족되거나, 비동기 작업이 완료된 이후 호출되는 함수를 의미합니다.
이런 방식을 ‘콜백 기반(callback-based)’ 비동기 프로그래밍이라고 합니다. 무언가를 비동기적으로 수행하는 함수는 함수 내 동작이 모두 처리된 후 실행되어야 하는 함수가 들어갈 콜백을 인수로 반드시 제공해야 합니다.
그러나 여러 비동기 작업을 처리해야 하는 경우, 아래와 같이 콜백 속에 콜백을 넣어 전달해야 합니다. loadScript는 스크립트의 경로와 콜백 함수를 인자로 받아 해당 경로에 위치한 스크립트를 불러오고 실행하는 비동기 함수입니다.
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
// 세 스크립트 로딩이 끝난 후 실행됨
});
})
});
이런 상태에서 각 콜백의 에러 핸들링을 추가한다면…
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// 모든 스크립트가 로딩된 후, 실행 흐름이 이어집니다. (*)
}
});
}
})
}
});
‘콜백 지옥(callback hell)’ 혹은 '멸망의 피라미드(pyramid of doom)'가 만들어지게 됩니다. 이런 코드는 깊은 중첩을 가지고 있어 가독성이 매우 좋지 않습니다.
Promise
Promise는 이런 문제 해결을 위한 대리자로써 비동기 메서드를 마치 동기 메서드처럼 다룰 수 있게 해줍니다. 다만 최종 결과를 반환하는 것이 아닌 미래의 ‘약속’ 을 반환합니다.
Promise는 다음 중 하나의 상태를 가집니다.
- 대기(pending): 이행하지도, 거부하지도 않은 초기 상태.
- 이행(fulfilled): 연산이 성공적으로 완료됨.
- 거부(rejected): 연산이 실패함.
이행이나 거부될 때, 프로미스의 then 메서드에 의해 대기열(큐)에 추가된 처리기들이 호출됩니다.
문법
생성자 : Promise()
새로운 Promise 객체를 생성합니다. 주로 프로미스를 지원하지 않는 함수를 감쌀 때 사용합니다.
let promise = new Promise(function(resolve, reject) {
// executor (집행자)
});
executor의 인수 resolve와 reject는 자바스크립트에서 자체 제공하는 콜백입니다. 개발자는 resolve와 reject를 신경 쓰지 않고 executor 안 코드만 작성하면 됩니다.
대신 executor에선 결과를 즉시 얻든 늦게 얻든 상관없이 상황에 따라 인수로 넘겨준 콜백 중 하나를 반드시 호출해야 합니다.
- resolve(value) — 일이 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출
- reject(error) — 에러 발생 시 에러 객체를 나타내는 error와 함께 호출
executor는 자동으로 실행되는데 여기서 원하는 일이 처리됩니다. 처리가 끝나면 executor는 처리 성공 여부에 따라 resolve나 reject를 호출합니다.
new Promise 생성자가 반환하는 Promise객체의 내부 프로퍼티
state
초기 호출 시 pending resolve 호출되면 fulfilled reject가 호출되면 rejected 로 변화
result
처음엔 undefined resolve(value)가 호출되면 value로 reject(error)가 호출되면 error로 변화
⚠️ 하나의 Promise는 하나의 성공 또는 하나의 실패만 합니다.
let promise = new Promise(function(resolve, reject) {
resolve("완료");
reject(new Error("…")); // 무시됨
setTimeout(() => resolve("…")); // 무시됨
});
⚠️ 비동기가 아닌 동기적으로 즉시 호출도 가능합니다.
아래와 같이 resolve나 reject를 즉시 호출할 수도 있습니다.
let promise = new Promise(function(resolve, reject) {
// 일을 끝마치는 데 시간이 들지 않음
resolve(123); // 결과(123)를 즉시 resolve에 전달함
});
어떤 일을 시작했는데 알고 보니 일이 이미 끝나 저장까지 되어있는 경우, 이렇게 resolve나 reject를 즉시 호출하는 방식을 사용할 수 있습니다.
이렇게 하면 프라미스는 즉시 이행 상태가 됩니다.
⚠️ state와 result는 내부에 있습니다.
프라미스 객체의 state, result 프로퍼티는 내부 프로퍼티이므로 개발자가 직접 접근할 수 없습니다. .then/.catch/.finally 메서드를 사용하여 접근 가능합니다.
소비함수 : then, catch, finally 메서드
then
.then은 프라미스에서 가장 중요하고 기본이 되는 메서드입니다.
promise.then(
function(result) { /* 결과(result)를 다룹니다 */ },
function(error) { /* 에러(error)를 다룹니다 */ }
);
.then의 첫 번째 인수는 프라미스가 이행되었을 때 실행되는 함수이고, 여기서 실행 결과를 받습니다. .then의 두 번째 인수는 프라미스가 거부되었을 때 실행되는 함수이고, 여기서 에러를 받습니다.
작업이 성공적으로 처리된 경우만 다루고 싶다면 .then에 인수를 하나만 전달하면 됩니다.
let promise = new Promise(resolve => {
setTimeout(() => resolve("완료!"), 1000);
});
promise.then(alert); // 1초 뒤 "완료!" 출력
catch
.catch는 .then에 null을 전달하는 것과 동일하게 작동합니다.
.catch(f)는 문법이 간결하다는 점만 빼고 .then(null,f)과 완벽하게 같습니다.
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});
// .catch(f)는 promise.then(null, f)과 동일하게 작동합니다
promise.catch(alert); // 1초 뒤 "Error: 에러 발생!" 출력
finally
프라미스가 처리되면(이행이나 거부) f가 항상 실행된다는 점에서 .finally(f) 호출은 .then(f, f)과 유사합니다. 결과가 어떻든 마무리가 필요하면 finally가 유용합니다.
new Promise((resolve, reject) => {
/* 시간이 걸리는 어떤 일을 수행하고, 그 후 resolve, reject를 호출함 */
})
// 성공·실패 여부와 상관없이 프라미스가 처리되면 실행됨
.finally(() => 로딩 인디케이터 중지)
.then(result => result와 err 보여줌 => error 보여줌)
그런데 finally는 .then(f, f)과 완전히 같진 않습니다. 차이점은 다음과 같습니다.
- finally 핸들러엔 인수가 없습니다. finally에선 프라미스가 이행되었는지, 거부되었는지 알 수 없습니다.
- finally에선 절차를 마무리하는 ‘보편적’ 동작을 수행하기 때문에 성공·실패 여부를 몰라도 됩니다.
- finally 핸들러는 자동으로 다음 핸들러에 결과와 에러를 전달합니다.
이런 특징을 통해 보면, finally는 프라미스 결과를 처리하기 위해 만들어 진 게 아닙니다. 프라미스 결과는 finally를 통과해서 전달되죠.
프라미스 체이닝
프라미스 체이닝은 result가 .then 핸들러의 체인(사슬)을 통해 전달된다는 점에서 착안한 아이디어입니다.
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
위 예시는 아래와 같은 순서로 실행됩니다.
- 1초 후 최초 프라미스가 이행됩니다. – (*)
- 이후 첫번째 .then 핸들러가 호출됩니다. –(**)
- 2에서 반환한 값은 다음 .then 핸들러에 전달됩니다. – (***)
- 이런 과정이 계속 이어집니다.
result가 핸들러 체인을 따라 전달되므로, alert 창엔 1, 2, 4가 순서대로 출력됩니다.
프라미스 체이닝이 가능한 이유는 **promise.then을 호출하면 프라미스가 반환되기 때문**입니다. 반환된 프라미스엔 당연히 .then을 호출할 수 있습니다.
한편 핸들러가 값을 반환할 때엔 이 값이 프라미스의 result가 됩니다. 따라서 다음 .then은 이 값을 이용해 호출됩니다.
⚠️ 프라미스 하나에 .then을 여러 개 추가한 후, 이를 체이닝이라고 착각하는 경우가 있습니다. 하지만 이는 체이닝이 아닙니다.
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
예시의 프라미스는 하나인데 여기에 등록된 핸들러는 여러 개 입니다. 이 핸들러들은 result를 순차적으로 전달하지 않고 독립적으로 처리합니다. 동일한 프라미스에 등록된 .then 모두는 동일한 결과(프라미스의 result)를 받습니다. 따라서 위 예시를 실행하면 얼럿 창엔 전부 1이 출력됩니다.
이런 식으로 한 프라미스에 여러 개의 핸들러를 등록해서 사용하는 경우는 거의 없습니다. 프라미스는 주로 체이닝을 해서 씁니다.
💡 thenable 핸들러는 프라미스가 아닌 thenable이라 불리는 객체를 반환하기도 합니다.
.then이라는 메서드를 가진 객체는 모두 thenable객체라고 부르는데, 이 객체는 프라미스와 같은 방식으로 처리됩니다. 아래는 thenable 객체 예시입니다.
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve); // function() { 네이티브 코드 }
// 1초 후 this.num*2와 함께 이행됨
setTimeout(() => resolve(this.num * 2), 1000); // (**)
}
}
new Promise(resolve => resolve(1))
.then(result => {
return new Thenable(result); // (*)
})
.then(alert); // 1000밀리 초 후 2를 보여줌
자바스크립트는 (*)로 표시한 줄에서 .then 핸들러가 반환한 객체를 확인합니다. 이 객체에 호출 가능한 메서드 then이 있으면 then이 호출됩니다. then은 resolve와 reject라는 네이티브 함수를 인수로 받고(executor과 유사함), 둘 중 하나가 호출될 때까지 기다립니다. 위 예시에서 resolve(2)는 1초 후에 호출됩니다((**)). 호출 후 결과는 체인을 따라 아래로 전달됩니다.
이런 식으로 구현하면 Promise를 상속받지 않고도 커스텀 객체를 사용해 프라미스 체이닝을 만들 수 있습니다.
Error handling - 암시적 try..catch
프라미스 executor와 프라미스 핸들러 코드 주위엔 '보이지 않는(암시적) try..catch'가 있습니다.
예외가 발생하면 암시적 try..catch에서 예외를 잡고 이를 reject처럼 다룹니다.
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(alert); // Error: 에러 발생!
new Promise((resolve, reject) => {
reject(new Error("에러 발생!"));
}).catch(alert); // Error: 에러 발생!
위 두 예시는 똑같이 동작합니다.
executor 주위의 '암시적 try..catch'는 스스로 에러를 잡고, 에러를 거부상태의 프라미스로 변경시킵니다.
이런 일은 executor 함수뿐만 아니라 핸들러에서도 발생합니다. .then 핸들러 안에서 throw를 사용해 에러를 던지면, 이 자체가 거부된 프라미스를 의미하게 됩니다. 따라서 제어 흐름이 가장 가까운 에러 핸들러로 넘어갑니다. 즉, 내부에서 발생한 에러는, 다음에 .catch 가 있다면 해당 .catch의 인수로 내려갑니다. 따라서 마지막에 .then 핸들러를 원하는 만큼 사용하다 .catch하나만 붙이면 .then 핸들러에서 발생한 모든 에러를 처리할 수 있습니다.
.catch 안에서 throw를 사용하면 제어 흐름이 가장 가까운 곳에 있는 에러 핸들러로 넘어갑니다. 여기서 에러가 성공적으로 처리되면 가장 가까운 곳에 있는 .then 핸들러로 제어 흐름이 넘어가 실행이 이어집니다.
// 실행 순서: catch -> then
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(function(error) {
alert("에러가 잘 처리되었습니다. 정상적으로 실행이 이어집니다.");
}).then(() => alert("다음 핸들러가 실행됩니다."));
Promise API : Promise.all
문법
let promise = Promise.all([...promises...]);
- Promise.all은 요소 전체가 프라미스인 배열(엄밀히 따지면 이터러블 객체이지만, 대개는 배열임)을 받고 새로운 프라미스를 반환합니다.
- 배열 안 프라미스가 모두 처리되면 새로운 프라미스가 이행되는데, **배열 안 프라미스의 결괏값을 담은 배열이 새로운 프라미스의 result**가 됩니다.
- 배열 result의 요소 순서는 Promise.all에 전달되는 프라미스 순서와 일치합니다. Promise.all의 첫 번째 프라미스는 가장 늦게 이행되더라도 처리 결과는 배열의 첫 번째 요소에 저장됩니다.
- 주어진 프라미스 중 하나라도 실패하면 Promise.all는 거부되고, 나머지 프라미스의 결과는 무시됩니다.
Promise API : Promise.allSettled
⚠️ 최근에 추가됨
스펙에 추가된 지 얼마 안 된 문법입니다. 구식 브라우저는 폴리필이 필요합니다.
Promise.allSettled는 모든 프라미스가 처리될 때까지 기다립니다. 반환되는 배열은 다음과 같은 요소를 갖습니다.
- 응답이 성공할 경우 – {status:"fulfilled", value:result}
- 에러가 발생한 경우 – {status:"rejected", reason:error}
Promise.allSettled를 사용하면 이처럼 각 프라미스의 상태와 값 또는 에러를 받을 수 있습니다.
Promise API : Promise.race
문법
let promise = Promise.race(iterable);
Promise.race는 Promise.all과 비슷합니다. 다만 가장 먼저 처리되는 프라미스의 결과(혹은 에러)를 반환합니다.
(이외에 romise.resolve와 Promise.reject는 async/await 문법이 생긴 후로 거의 사용하지 않습니다.)
Promise와 마이크로태스크
비동기 작업을 처리하려면 적절한 관리가 필요합니다. 이를 위해 ECMA에선 PromiseJobs라는 내부 큐(internal queue)를 명시합니다. V8 엔진에선 이를 '마이크로태스크 큐(microtask queue)'라고 부르기 때문에 이 용어가 좀 더 선호됩니다.
- 마이크로태스크 큐는 먼저 들어온 작업을 먼저 실행합니다(FIFO, first-in-first-out).
- 실행할 것이 아무것도 남아있지 않을 때만 마이크로태스크 큐에 있는 작업이 실행되기 시작합니다.
요약하자면, .then/catch/finally 핸들러는 항상 현재 코드가 종료되고 난 후에 호출됩니다.
어떤 코드 조각을 .then/catch/finally가 호출된 이후에 실행하고 싶다면 .then을 체인에 추가하고 이 안에 코드 조각을 넣으면 됩니다.
브라우저와 Node.js를 포함한 대부분의 자바스크립트 엔진에선 마이크로태스크가 '이벤트 루프(event loop)'와 '매크로태스크(macrotask)'와 깊은 연관 관계를 맺습니다.
async / await
async와 await라는 특별한 문법을 사용하면 프라미스를 좀 더 편하게 사용할 수 있습니다.
async
async는 function 앞에 위치합니다.
async function f() {
return 1;
}
function 앞에 async를 붙이면 해당 함수는 항상 프라미스를 반환합니다. 프라미스가 아닌 값을 반환하더라도 이행 상태의 프라미스(resolved promise)로 값을 감싸 이행된 프라미스가 반환되도록 합니다.
async function f() {
return 1;
}
f().then(alert); // 1
예시의 함수를 호출하면 result가 1인 이행 프라미스가 반환됩a니다.
await
// await는 async 함수 안에서만 동작합니다.
let value = await promise;
자바스크립트는 await 키워드를 만나면, 단어가 가진 뜻처럼 프라미스가 처리될 때까지 기다립니다
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});
let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)
alert(result); // "완료!"
}
f();
(*)로 표시한 줄에서 실행이 잠시 '중단’되었다가 프라미스가 처리되면 실행이 재개됩니다. 이때 프라미스 객체의 result 값이 변수 result에 할당됩니다. 따라서 위 예시를 실행하면 1초 뒤에 '완료!'가 출력됩니다.
await는 말 그대로 프라미스가 처리될 때까지 함수 실행을 기다리게 만듭니다. 프라미스가 처리되길 기다리는 동안엔 엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않습니다.
await는 promise.then보다 좀 더 세련되게 프라미스의 result 값을 얻을 수 있도록 해주는 문법입니다. promise.then보다 가독성 좋고 쓰기도 쉽습니다.
⚠️ await는 최상위 레벨 코드에서 작동하지 않습니다.
// 최상위 레벨 코드에선 문법 에러가 발생함
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
하지만 익명 async 함수로 코드를 감싸면 최상위 레벨 코드에도 await를 사용할 수 있습니다.
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
에러 핸들링
프라미스가 정상적으로 이행되면 await promise는 프라미스 객체의 result에 저장된 값을 반환합니다. 반면 프라미스가 거부되면 마치 throw문을 작성한 것처럼 에러가 던져집니다.
async function f() {
await Promise.reject(new Error("에러 발생!"));
}
async function f() {
throw new Error("에러 발생!");
}
실제 상황에선 프라미스가 거부 되기 전에 약간의 시간이 지체되는 경우가 있습니다. 이런 경우엔 await가 에러를 던지기 전에 지연이 발생합니다.
await가 던진 에러는 throw가 던진 에러를 잡을 때처럼 try..catch를 사용해 잡을 수 있습니다.
💡 async/await와 promise.then/catch
async/await을 사용하면 await가 대기를 처리해주기 때문에 .then이 거의 필요하지 않습니다. 여기에 더하여 .catch 대신 일반 try..catch를 사용할 수 있다는 장점도 생깁니다. 항상 그러한 것은 아니지만, promise.then을 사용하는 것보다 async/await를 사용하는 것이 대개는 더 편리합니다. 그런데 문법 제약 때문에 async함수 바깥의 최상위 레벨 코드에선 await를 사용할 수 없습니다.그렇기 때문에 관행처럼 .then/catch를 추가해 최종 결과나 처리되지 못한 에러를 다룹니다.
💡 async/await는 Promise.all과도 함께 쓸 수 있습니다.
여러 개의 프라미스가 모두 처리되길 기다려야 하는 상황이라면 이 프라미스들을 Promise.all로 감싸고 여기에 await를 붙여 사용할 수 있습니다.
// 프라미스 처리 결과가 담긴 배열을 기다립니다.
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);
실패한 프라미스에서 발생한 에러는 보통 에러와 마찬가지로 Promise.all로 전파됩니다. 에러 때문에 생긴 예외는 try..catch로 감싸 잡을 수 있습니다.
출처
Futures and Promises
futures and promises Futures and Promises By Kisalaya Prasad, Avanti Patil, and Heather Miller Futures and promises are a popular abstraction for asynchronous programming, especially in the context of distributed systems. We'll cover the motivation for and
dist-prog-book.com
프라미스와 async, await
ko.javascript.info
프라미스와 async, await
ko.javascript.info
제 글의 내용은 아래의 글을 번역 및 요약 한 것입니다. 잘못된 부분이 있으면 언제든 피드백 주세요.
읽어주셔서 감사합니다.
'Study > FE-Study' 카테고리의 다른 글
[HTML] <head> tag (0) | 2023.06.09 |
---|---|
[WEB] HTTP / HTTPS (0) | 2023.06.02 |
[CS] 시간복잡도와 공간복잡도 (0) | 2023.04.21 |
[JS] Event Loop : JavaScript의 비동기 처리 방법 (0) | 2023.04.15 |
[Web] 웹 퍼포먼스 최적화 (0) | 2023.04.07 |