본문 바로가기

Frontend/JavaScript

[JavaScript] Promise 문법과 async/await

 

 


자바스크립트의 Promise 문법 알아보기


 

 

 

1. Promise 문법이란?


Promise 는 비동기 작업의 상태 및 결과를 나타내는 객체다.

자바스크립트에서 ① 비동기 작업을 처리하고, ② 성공(resolve) 또는 실패(rejected) 상태를 처리할 수 있는 구조를 제공한다.

 

라고 하면 처음 봤을 때 이해가 하나도 안 되니까 쉽게 풀면...

 

① 비동기 작업 → 연산이 시작되고 끝나는 순서가 정해진 건 아니다.

② 성공 또는 실패 상태 처리 → 성공하면 resolve() 가 호출, 실패하면 reject() 가 호출된다.

③ 객체 → 익명 함수를 품고 있어서 그렇다.

 

이정도가 되겠다.

 

정리하면...

익명함수를 품고 있는데, 그 익명함수를 실행했을 때 성공하면 resolve 호출, 실패하면 reject 함수를 호출하는 객체 라고 할 수 있겠다. 근데 이제 딱히 연산 순서가 정해진 작업은 아니라서 실행순서와 관계없이 빨리 연산된 순서대로 결과가 나오기도 하는.

 

 

 

2. Promise 객체의 생성구조


① Promise 객체가 익명함수 하나를 품고 있다.

new Promise( ()=>{} );

 

② 생성된 객체를 참조변수(pm)에 저장한다.

const pm = new Promise ( ()=>{} );

- 이러면 pm 안에는 익명함수에 대한 실행 결과 값이 저장되는 것이다.

 

③ 안에 있는 익명함수는 resolve, reject 라는 매개변수가 존재한다.

const pm = new Promise( (resolve, reject)=>{} );

 

매개변수에는 함수가 전달되어서 매개변수 이름으로 전달된 함수를 호출할 수 있다.

const pm = new Promise( (resolve, reject)=>{
	resolve(); // 또는 reject();
} );

 

 

⑤ 대개 익명함수가 정상실행 되었다면 resolve() 가 호출되고, 그렇지 않으면 reject() 가 호출된다.

 

 

이를 모두 종합한 일반적인 예시는 아래와 같다.

const pm = new Promise( (resolve, reject) =>{
    // 명령1, 명령2...
    if( /* 조건 */ true /* or false */ ) {
        resolve();
    }else{
        reject();
    }
});

 

 

 

 

3. Promise 객체의 결과를 받아 사용하는 방법


const pm = new Promise( (resolve, reject) =>{
    if( true ) {
        resolve('성공');
    }else{
        reject('실패');
    }
});

 

위와 같은 Promise 객체가 존재한다고 해보자.

물론 예시의 경우 if 조건이 true 이기 때문에 reject('실패'); 함수가 호출되는 일은 없긴하다.

 

결과를 사용하는 순서는 다음과 같다.

① Promise 객체 안에 있는 익명 함수의 실행 결과를 pm 변수에 담는다. (선택)

② 결과를 지닌 참조변수 pm 에 .then() 함수와 .catch() 함수를 붙여준다.

이때 then() 은 성공(resolve) 했을 때 호출되고, catch() 는 실패(reject) 했을 때 호출된다.

 

 

▶ 형태1

pm.then() // resolve() 가 호출되었을 때 사용할 함수
pm.catch() // reject() 가 호출되었을 때 사용할 함수

 

직관적으로 보면 이런 형태다.

그런데 보통은 pm 을 각각 참조하지 않고 아래와 같이 체이닝 방식을 통해 연결해서 사용한다.

pm
.then( ()=>{} ) // resolve() 가 호출되었을 때 사용할 함수
.catch( ()=>{} ); // reject() 가 호출되었을 때 사용할 함수

 

 

Promise 객체를 사용할 때는 이런 형태를 만들어 사용한다.

 

이렇게 실행결과가 담긴 참조변수 pm 의 결과에 따라 .then() 혹은 .catch() 함수를 실행되는 것이다.

 

다시 말해 Promise 객체에서 resolve() 가 호출되면 .then() 함수가 실행되고, reject() 가 호출되면 .catch() 함수가 실행되는 방식이다.

 

 

▶ 형태2

따로 객체에 받지 않고도 실행할 수 있다.

new Promise((resolve, reject)=>{
    if(true){
        resolve('성공');
    }else{
        reject('실패');
    }
})
.then((msg)=>{ console.log('pnm 없이', msg) } )
.catch((msg)=>{ console.log('pnm 없이', msg) } );

 

 

▶ 예시1

const pm = new Promise( (resolve, reject) =>{
    if( true ) {
        resolve('성공'); // 실행
    }else{
        reject('실패');
    }
});

pm
.then( (message)=>{
    console.log(message); // 실행
})
.catch( (message)=>{
    console.log(message);
});

 

처음에 예시로 든 Promise 객체를 가져와서 전달 받은 메시지를 출력하는 구조로 짜봤다.

 

이와 같은 형태라면

pm 에는 Promise 의 익명함수 실행 결과가 담기게 되는데

  if 문의 조건이 true 이니 resolve('성공'); 이 담기게 될 것이고

resolve() 가 실행됐으니 pm.then() 함수가 실행된다.

④ then 함수에서 message 를 받아 console 에 출력하면? 성공 이란 메시지가 뜬다.

 

 

▶ 예시2

const con = false; // 특정 상황을 만들기 위한 상태값
const pm5 = new Promise( (resolve, reject)=>{
    if( con ){
        resolve("condition 값은 true 입니다.");
    }else{
        reject("condition 값은 false 입니다."); // 실행
    }
});

pm5
.then( (msg)=>{ console.log(msg); } )
.catch( (msg)=>{ console.log(msg); } ) // 실행
.finally( ()=>{ console.log('Promise 가 종료되었습니다.'); } ); // 실행

 

비슷한 방식이다.

 

 pm5 에는 Promise 의 익명함수 실행 결과인 reject(); 가 담기게 되고

pm5.catch() 로 reject() 함수를 실행한다.

③ catch 함수가 실행되어 msg 를 console 에 출력한다.

함수가 종료되면 무조건 실행되는 .finally 함수도 catch() 실행 이후 실행된다.

 

 

 

4. Promise의 특징 '비동기실행'


동기실행, 비동기실행이 뭐냐고 물어본다면 '실행한 순서대로 결과가 반환되는지'의 여부라고 할 수 있겠다.

실행 순서대로 결과 반환까지 진행되면 동기, 실행 순서와 관계없이 빨리 연산되는 순서대로 결과가 반환되면 비동기실행이다.

 

이를 간단한 예시로 알아보자.

 

▶ 동기실행

/* 동기실행 */
console.log('====== 작업 시작 ======');
console.log('작업1 시작');
const wakeUpTime = Date.now() + 1000; // 현재시간을 얻어서 1초를 더한 시간 계산
// 반복실행동안 연속해서 현재 시간을 얻으면서 아무것도 안 하는 반복 실행을 진행
while(Date.now() < wakeUpTime){ } // 작업 1에 1초간 긴 작업시간 포함
console.log('작업1 종료');
console.log('작업2 시작');
console.log('작업2 종료');
console.log('====== 모든 작업 종료 ======');

 

작업 1이 시작되고 1초의 텀을 둔 뒤 '작업1 종료', '작업2 시작', '작업2 종료' 를 순차적으로 출력한 코드다.

이렇게 실행 순서대로 결과가 반환되는 것이 동기실행이다.

 

 

▶ 비동기실행

/* 비동기실행 */
// setTimeout, setInterval 등 전체 시작과 상관없이 별도로 진행
function longRunningTask(){
    console.log('작업1 시작');
    const wakeUpTime = Date.now() + 1000;
    while(Date.now() < wakeUpTime){ } // 1초 딜레이
    console.log('작업1 종료');
}

console.log('====== 작업 시작 ======');
setTimeout( longRunningTask, 0 ); // 작업2 보다 작업1 을 먼저 실행
console.log('작업2 시작');
console.log('작업2 종료');
console.log('====== 모든 작업 종료 ======');
// 그럼에도 불구하고 작업2의 시작과 종료가 먼저 이루어짐

 

비동기 실행의 대표적인 setTimeout 함수를 사용했다.

(비동기실행의 대표적인 예로 setTimeout, setInterval, Promise 가 있다.)

마찬가지로 '작업1 시작' → '작업1 종료'  → '작업2 시작'  → '작업2 종료' 순서대로 실행을 했다.

 

 

함수를 실행하는 게 연산이 더 오래걸렸는지 어쨌는지 모르겠으나 결과는 뒤죽박죽이 되었다.

이게 비동기실행이다.

 

 

다시 돌아와서,

Promise 도 실행순서와 상관없이 결과를 반환하는 비동기 실행이다.

 

 

▶ Promise 의 비동기실행

/* Promise 를 이용한 비동기실행 */
let pm = new Promise((resolve, reject)=>{
    console.log('====== 작업 시작 ======');
    resolve('작업1'); // 비동기 실행할 내용의 호출
    console.log('작업2 시작');
    console.log('작업2 종료');
    console.log('====== 모든 작업 종료 ======');
});
// 다른명령
// 다른명령
// 다른명령
pm
.then((msg)=>{
    console.log(msg, '시작');
    const wakeUpTime = Date.now() + 1000; // 1초 딜레이
    while(Date.now() < wakeUpTime){ }
    console.log(msg, '종료');
});

 

작업 시작과 동시에 resolve 함수의 매개변수로 '작업1' 텍스트를 보냈다.

그리고 pm.then 이 실행되면서 '작업1' 을 받아

'작업1 시작'  →  (1초 딜레이 이후)  → '작업1 종료' 문자를 console 에 남긴다.

 

실행 순서 자체는 마찬가지로  '작업1 시작' → '작업1 종료'  → '작업2 시작'  → '작업2 종료'  다.

 

 

하지만 역시나, 비동기실행이라 실행순서와 관계없이 결과를 반환한다.

 

 

 

5. 동기 실행처럼 보이게 하는 async / await


이런 비동기 실행을 하는 Promise 를 가지고 동기 실행을 해야할 때가 있을 수 있다.

물론, 기존의 방법으로도 가능은 하다.

 

/* promise 를 이용한 비동기실행에서 두 작업의 실행순서를 맞춰야 할 때 */
let pm = new Promise((resolve, reject)=>{
    console.log('===== 작업 시작 =====');
    resolve();
});

pm
.then(()=>{
    console.log('작업1 시작');
    const wakeUpTime = Date.now() + 3000;
    while (Date.now() < wakeUpTime){}
    console.log('작업1 종료');

    return new Promise((resolve, reject)=>{
        resolve();
    });
})
.then(()=>{
    console.log("작업2 시작");
    console.log("작업2 종료");
})

 

단지 이정도까지? 란 생각이 들 뿐.

 

 

resolve 안에 resolve 를 넣어 .then 함수를 두번 호출하는 형태다.

결과가 동기 실행처럼 나오기는 했다.

 

이런 방법 말고, Promise 에는 async / await 를 이용해 동기처럼 보이도록 실행하는 방법이 존재한다.

정확히는, await 를 통해 함수 실행이 마무리될 때까지 기다리는 방식이다.

 

 

▶ async / await 방식

async / await 는 자바스크립트에서 Promise 를 더 간결하고 읽기 쉬운 방식으로 처리할 수 있도록 도와주는 문법이다.

순서없이 끝나는 다중 작업을 원하는 순서대로 맞춰 종료하고자 한다면(= 동기실행 하고 싶다면)

async 로 생성한 비동기 함수에 비동기 호출을 await 로 호출하고, 다른 명령들을 쭉 - 작성해준다.

 

await 키워드가 붙으면 Promise 가 해결될 때까지 기다렸다가 결과를 반환하기 때문에 결과적으로는 비동기 실행이라도 순차적으로 실행되는 것이다.

 

function funcPromise(){ // 실행순서 #3
    const pm = new Promise( (resolve, reject)=>{
        console.log('작업1 시작'); // 실행순서 #4
        resolve('작업1 종료'); // 실행순서 #5
    });
    return pm; // Promise 반환
}

async function func1(){
    try{
    	// await 사용 시, then 은 사라지고 변수에 넣을 수도 있음
        let result = await funcPromise(); // 실행순서 #2 (Promise 결과 반환 기다림) -> 실행순서 #6 
        console.log(result); // 실행순서 #7 -> resolve()
        console.log('작업2 시작'); // 실행순서 #8
        console.log('작업2 종료'); // 실행순서 #9
    }catch(err){
        console.error(err);
    }
}

func1(); // 실행순서 #1

 

async - await 를 활용한 예시다.

 

이를 간단히 요약하자면...

 비동기 실행이 이루어지는 func1 앞에 async 키워드가 붙는다.

 동기적으로 처리할 funcPromise 함수를 await 키워드를 붙여 호출한다.

결과를 반환할 때가지 다른 작업은 진행되지 않는다.

이정도겠다.

 

본래라면 Promise 는 비동기 실행이기 때문에 result 가 나오기도 전에 밑에 코드들이 실행되었겠지만.

funcPromise() 앞에 await 키워드가 붙어있어 funcPromise() 가 결과를 반환할 때까지 모든 작업을 멈춘다.

funcPromise() 에서 반환된 결과는 변수에 담을 수 있게 되며, result 에 값이 들어오고 나서야 비로소 console.log(result); 가 실행된다.

 

 

▶ await 반환과 예외처리

async/await 방식으로 동기처리를 하면서 await 로 호출한 함수를 변수에 담았다.

await 는 비동기 작업이 끝난 후 '결과를 반환' 한다고 했으니 호출한 함수의 반환값은 Promise 의 결과값일텐데,

여기에 담겨진 함수는 resolve 일까, reject 일까?

 

결과부터 말하면 resolve 다.

 

async function func1(){
    try{
    	let result = await funcPromise(); // 비동기 호출을 await 로 호출 
        // result 가 resolve 반환 시 실행
    }catch(err){
        // result 가 reject 반환 시 실행(예외처리)
    }
}

 

await 를 사용할 때 try-catch 문으로 감싸준 이유가 여기서 나온다.

await 는 reject 를 예외로 간주하기 때문에, reject 가 반환되면 catch 가 실행되면서 에러 처리가 된다.

그렇기 때문에 await 에서 예외 처리는 필수다!

 

await 밑으로 존재하는 연산을 처리하기 위해서는 Promise 가 resolve(성공) 를 반환해야 하는 것이다.

 

 

▶ async 반환값

비동기 실행이 이루어지는 함수 앞에 async 가 붙는다는 것은 async는 비동기 함수임을 나타내는 키워드라는 말이된다.

이렇게 비동기 함수 임이 선언된  async 가 붙은 함수는 반드시 Promise 를 반환한다.

 

async function func1(){ // 순서대로 맞춰 종료하고자 하는 함수를 async 로 생성
    try{
        let result = await funcPromise(); // 비동기 호출을 await 로 호출 
        console.log(result);
        console.log('작업2 시작');
        console.log('작업2 종료');
    }catch(err){
        console.error(err);
    }
}

const returnValue = func1();
console.log(returnValue); // Promise { <pending> }

returnValue.then(() => console.log('func1 완료')); // 'func1 완료' 출력

전체코드

 

const returnValue = func1();
console.log(returnValue); // Promise { <pending> }

async 함수 반환

 

async 함수로 선언된 func1 의 반환값을 받아 출력하면 Promise { <pending> }

즉, Promise 가 반환된다.

위와 같이 Promise 가 반환됐다는 것은 아직 Promise 가 resolve 되지 않았음을 뜻한다.

 

returnValue.then(() => console.log('func1 완료')); // 'func1 완료' 출력

async 함수에서 반환된 Promise 를 해결

 

이렇게 반환된 Promise 의 반환값을 보기 위해 .then() 을 붙이면

Promise 가 해결(resolve) 되면서 resolve() 가 호출되어 'func1 완료' 가 출력된다.