프로미스 (Promise)와 async/await에 대하여
ES6 이전: 함수 안의 함수 안의 … 함수: 콜백 지옥
: 콜백의 결과값을 그 다음 함수에 대입하려면 그 값이 살아있는 블록(=지금 함수)안에 또 콜백을 부르는 함수를 넣는 식으로 들어가야 했음. 블록 안의 블록 안의 블록이 계속 이어짐.
ES6의 Promise 객체 도입
: Promise와 .then()의 조합으로 비동기 함수로 받는 콜백 결과값을 선형적(체이닝)으로 연결시키게 됨.
ES6부터는 Promise라는 걸 도입, 콜백 지옥을 해결함.
비동기 수행을 하는 함수는 Promise 객체를 반환.
⇒ 이 Promise는 함수를 매개변수로 받음.
ex)
function 고교_DB_주소_조회_Promise(학생_주민번호, 고교명) {
return new Promise(function (resolve, reject) {
ajax(baseURL + "highschool-db/" + 고교명,
function(response) { resolve([학생_주민번호, response]); });
});
}
어쨌든 이렇게 Promise로 함수를 짜면,
학생정보_조회_Promise(”12345”)
.then(function(…) {
...
return 고교_DB_주소_조회_Promise(…, …)
})
.then(다음_비동기_Promise_함수를_리턴하는_함수 {
...
})
.then(그다음_비동기) {
...
})
하는 식으로 nested 해서 들어가지 않고 체이닝 방식으로 비동기 작업을 순차적으로 할 수 있게 된다.
Promise 자세히 살펴보기:
promise 객체 안에서
resolve
나reject
중 하나는 반드시 호출해야 한다. 아래는resolve
를 호출한 예시:// let promise = new Promise(function(resolve, reject) { // 프라미스가 만들어지면 executor 함수는 자동으로 실행됩니다. // 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result는 '완료'가 됩니다. setTimeout(() => resolve("완료"), 1000); });
1초 후 이 promise 객체의 상태는 이렇게 변화하게 되고, ‘fulfilled/resolved promise’라고 불림:
이번엔 reject를 호출한 예시:
let promise = new Promise(function(resolve, reject) { // 1초 뒤에 에러와 함께 실행이 종료되었다는 신호를 보냅니다. setTimeout(() => reject(new Error("에러 발생!")), 1000); });
1초 후 이 promise 객체의 상태는 이렇게 변화하며 ‘rejected promise’라고 불림:
'대기(pending)'상태의 프라미스 ↔ ‘처리된(settled)’ 프라미스
.then과 .catch, .finally
.then
let promise = new Promise(function(resolve, reject) { setTimeout(() => reject(new Error("에러 발생!")), 1000); }); // reject 함수는 .then의 두 번째 함수를 실행합니다. promise.then( result => alert(result), // 실행되지 않음 error => alert(error) // 1초 후 "Error: 에러 발생!"을 출력 );
보통은 성공 케이스만 처리하기 위해 인자를 하나만 사용한다:
let promise = new Promise(resolve => { setTimeout(() => resolve("완료!"), 1000); }); promise.then(alert); // 1초 뒤 "완료!" 출력
.catch
.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
성공·실패 여부와 상관없이 프라미스가 처리되면 실행된다. 결과가 어떻든 마무리가 필요할 때 사용하게 된다.
finally
는 프라미스 결과를 처리하기 위해 만들어 진 게 아니다. 프라미스 결과는finally
를 통과해서 전달된다.따라서 다음과 같은 코드가 가능하다:
new Promise((resolve, reject) => { throw new Error("에러 발생!"); }) .finally(() => alert("프라미스가 준비되었습니다.")) .catch(err => alert(err)); // <-- .catch에서 에러 객체를 다룰 수 있음
ES8의 async/await 키워드 등장
: Promise와 async, await 키워드의 조합으로 체이닝조차 하지 않고 비동기 함수의 콜백을 쓸 수 있게 됨. 변수명에 그냥 대입하는 식으로..!
그래서 ES8에서는 async/await이라는 키워드 쌍이 등장해서, 비동기 함수를 동기함수처럼 쉽게 작성할 수 있도록 했다.
async function 고3시_수학교사_찾기_Promise(학생_학번) {
⇒ 이렇게 하면 이 함수 안의 코드는 그냥 =으로 할당해도 비동기식으로 읽어라 라는 뜻이 된다. “안에서 ‘await’을 사용하겠다”.
let 학생_정보 = await 학생정보_조회_Promise(학생_학번);
⇒ await이 없으면 ‘학생정보_조회_Promise(학생_학번)’이라는 비동기 함수가 (시간이 오래 걸려) 반환값을 돌려주기 전에 그냥 ‘학생정보_조회_Promise()’이라는 객체 자체를 담아버리고 다음 코드로 진행하게 된다. 그래서 await으로 “시간이 걸려도 프로그램 실행을 멈추고 반환값을 받을 때까지 기다렸다 담아가라”라고 알려줌.
그래서 위의 .then으로 쓴 코드를 더 직관적으로 쓰면:
// Promise와 .then의 조합:
학생정보_조회_Promise(”12345”)
.then(function(…) {
...
return 고교_DB_주소_조회_Promise(…, …)
})
.then(다음_비동기_Promise_함수를_리턴하는_함수 {
...
})
.then(그다음_비동기) {
...
})
// Promise와 async/await 콤비 조합:
async function 고3시_수학교사_찾기_Promise(학생_학번) {
let 학생_정보 = await 학생정보_조회_Promise(학생_학번);
let 고교_DB_주소
= await 고교_DB_주소_조회_Promise(…, …);
let 수강과목_일람
= await 다음_비동기_Promise_함수를_리턴하는_함수( );
let 그다음_얻을_변수
= await 그다음_비동기( );
}
고3시_수학교사_찾기_Promise("12345");
(참고: 비동기 프로그래밍이 뭔가요? https://www.youtube.com/watch?v=m0icCqHY39U)
async/await 자세히 살펴보기:
async 키워드가 하는 일
- function 앞에 붙어 해당 함수가 항상 promise를 반환하도록 만든다.
async function f() { return 1; // => 1을 값으로 가지는 resolved promise(이행된 프라미스)가 반환되게 됨. } f().then(alert); // 1
⇒ 명시적으로 Promise 객체를 반환해주지 않아도 .then으로 받을 수 있어 편리하다 .
await
키워드를 내부에서 동작시킬 수 있도록 한다. (await는 async 함수 안에서만 동작)
await 키워드가 하는 일
- 프라미스 앞에 붙어서 프라미스가 처리될 때까지 함수 실행을 기다리게 만든다. ⇒ 프라미스가 처리되길 기다리는 동안엔 엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않는다.
- 잠깐만… 이게 어떻게 가능하지? await 자체가 동기처럼 멈춰두게 만드는 것 아닌가..
- await이 붙은 코드에서 에러가 발생하지 않으면 promise를 벗겨서(?) result값을 반환한다.
- await이 붙은 코드에서 에러가 발생하면 throw문을 작성한 것 같은 예외가 던져지고, try…catch를 이용해 잡아낼 수 있다.
(참고: https://ko.javascript.info/async-await)
클로저 (Closure)란 (아주 대략적인 개념)
: 외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수를 의미.
자바스크립트에서는 모든 함수가 (별도의 처리 없이도)클로저가 될 수 있다. (다른 언어에서는 특수하게 함수를 정의해야 하거나 불가능할 수 있음.)
핵심은 자바스크립트의 함수가 숨김 프로퍼티인 [[Environment]]
를 이용해 자신이 어디서 만들어졌는지를 기억한다는 것이다.
예시:
var base = 'Hello, ';
function sayHelloTo(name) {
var text = base + name;
return function() {
console.log(text);
};
}
var hello1 = sayHelloTo('승민');
var hello2 = sayHelloTo('현섭');
var hello3 = sayHelloTo('유근');
hello1(); // 'Hello, 승민'
hello2(); // 'Hello, 현섭'
hello3(); // 'Hello, 유근'
⇒ 반환된 익명 함수가 바로 클로저이다.
⇒ text
라는 변수가 여러 번 생성된다. 즉, hello1()
과 hello2()
, hello3()
은 서로 다른 환경을 가지고 있고 그곳에 각자의 text
를 저장해두고 있다.
(참고: https://hyunseob.github.io/2016/08/30/javascript-closure/)
이 ‘환경’이란,
렉시컬 환경
: 자바스크립트에서 실행 중인 함수, 코드 블록 {...}
, 스크립트 전체에 할당되는 ‘이론상의 객체’. ⇒ 자바스크립트 명세서에서 자바스크립트가 어떻게 동작하는지 설명하는 데 쓰이지만 코드를 사용해 직접 렉시컬 환경을 얻거나 조작하는 것은 불가능하다.
렉시컬 환경 객체는 두 가지 부분으로 구성됨:
- 환경 레코드(Environment Record) : 모든 지역 변수를 프로퍼티로 저장하고 있는 객체. ⇒ 변수를 저장하는데 이를 어떤 객체의 프로퍼티 형태로 저장함.
- 외부 렉시컬 환경(Outer Lexical Environment) 에 대한 참조
![](https://blog.kakaocdn.net/dn/lfL6c/btsbBXYUdnW/rxV5vWss4c8bxv02djqa01/img.png)
함수를 호출해 실행하면 새로운 렉시컬 환경이 자동으로 만들어지는데, 본인이 생성된 곳의 렉시컬 환경을 기억한다.
이 렉시컬 환경엔 함수 호출 시 넘겨받은 매개변수와 함수의 지역 변수가 저장된다.
함수는 [[Environment]]
라 불리는 숨김 프로퍼티를 갖는데, 여기에 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.
참고로 클로저를 통해 내부 변수를 참조하는 동안에는 내부 변수가 차지하는 메모리를 GC가 회수하지 않는다. 따라서 클로저 사용이 끝나면 참조를 제거하는 것이 좋다. (참조가 제거된 메모리를 가비지 콜렉터가 회수하므로)
(참조:
![](https://ko.javascript.info/img/favicon/apple-touch-icon-precomposed.png)
![](https://ko.javascript.info/img/site_preview_en_1200x630.png)
![](https://poiemaweb.com/img/poiemaweb.jpg)
![](https://poiemaweb.com/img/poiemaweb.jpg)
※ 위 내용은 스스로 공부하며 적어둔 노트이며 불확실한 내용이 있을 수 있습니다. 학습용으로 적합하지 않음을 유념해주세요. ※
Uploaded by N2T