본문 바로가기

JavaScript

[ JavaScript ] async/await

반응형

이 글은 promise의 개념을 알고 있어야 이해할 수 있다. 따라서 promise가 아직 뭔지 모른다면 이전 글을 보고 오도록 하자. 등장배경 같은건 생략하고 핵심만 짚어보자. 그럼 바로 시작!

  

 

1. async의 위치는 함수 선언부 앞부분으로 고정된다.
2. 함수 선언시 앞에 async를 붙여주면 그 함수는 async 함수가 된다.
3. async 함수의 반환값은 무조건 promise다.
4. await 키워드는 오직 async 함수 내부에서만 쓸 수 있다.

 

이정도는 암기해놓고 시작하자. 이해하려고 하면 안되고 그냥 받아들여야 한다. 왜냐하면 async/await가 이렇게 만들어졌기 때문이다. '왜?' 라고 질문을 던진다면 '태생이 이래요'라고 답할 수밖에 없다. 1번과 2번은 진짜 있는 그대로 받아들여야 하고, 3번과 4번은 좀 자세히 살펴볼 필요가 있다. 

 

async의 반환값은 무조건 promise다..?

async 키워드는 오직 함수 선언부에만 쓸 수 있다. 함수에 async를 써주는 순간 그 함수는 async 함수가 된다. async 함수가 다른 함수와 가지는 차이점은 반환값에 있다. 소제목과 같이 async의 반환값은 무조건 promise다. 예를 들어 아래의 두 함수는 완전히 동일한 함수다. 

 

async function foo() {
	return 1;
}


function foo() {
	return Promise.resolve(1);
}

 

만약에 async 함수의 반환값이 promise가 아니라면 반환값은 자동으로 Promise.resolve()로 묶이게 된다. 반환값이 promise면 그대로 반환한다. 

 

 

await 사용법과 기능

1. await 사용법

await는 async 내부에만 존재할 수 있다. 사용법은 굉~~장히 간단하다. 그냥 앞에 await만 붙여주면 된다. await 뒤에는 어떤 값도 올 수 있다.

 

function promiseFunc() {
	return Promise.resolve('promise');
}

function non_promiseFunc() {
	return 'non_promise';
}

async function foo() {
    await 1;
    await 'haha';
    const ary = await [1,2,3];
    const promise = await promiseFunc();
    const non_promise = await non_promiseFunc();
    console.log(promise);
    console.log(non_promise);
}

foo();

 

위 코드는 정상적으로 실행된다. await의 사용법은 더이상 알아볼게 없다! 그럼 바로 await의 기능에 대해 알아보자.

2. await 기능

await의 기능은 크게 두 가지가 있다.

1. await 뒤에 오는게 promise 객체면 promise의 resolve 값으로 바꿔준다.
2. async 함수가 비동기적으로 실행되도록 한다. 

 

 

1)  await 뒤에 오는게 promise 객체면 promise의 resolve 값으로 바꿔준다.

 

첫번째 기능을 먼저 살펴보자. await 뒤에 오는게 promise 객체면 promise의 resolve 값으로 바꿔준다는게 무슨 말일까? 아래 코드를 보자.

 

function promiseFunc() {
	return Promise.resolve('promise');
}

function non_promiseFunc() {
	return 'non_promise';
}

async function foo() {
    const promise = await promiseFunc();
    const _promise = promiseFunc();
    const non_promise = await non_promiseFunc();
    const _non_promise = non_promiseFunc()
    console.log(promise);
    console.log(_promise);
    console.log(non_promise);
    console.log(_non_promise);
}

foo();

/**결과**
promise
Promise { 'promise' }
non_promise 
non_promise
*/

 

promiseFunc는 promise 객체를 반환하고, non_promiseFunc는 문자열을 반환하는 함수다. 각 함수 앞에 await를 붙여줬을때와 안 붙여줬을때 어떤 차이가 있는지 확인해보자. 결과값을 보면 문자열을 반환하는 함수는 결과에 차이가 없다. 그러나 promise 객체를 반환하는 함수에는 차이가 있다. await를 붙여주지 않으면 promise 객체 자체가 출력되고, await를 붙여주면 promise의 resolve 값이 출력된다. 이제 await의 첫 번째 기능을 다시 읽어보면 무슨 뜻인지 이해할 수 있을 것이다. 

 

약간 심화된 내용까지 다뤄보자. 사실 await 뒤에 오는 모든 값은 promise 객체화된다. promise 객체가 오면 아무 일도 일어나지 않지만 promise 객체가 아닌 값이 오면 암묵적으로 Promise.resolve()에 묶인다. 아래 코드를 참고하자.

 

await 1         =>  await Promise.resolve(1)
await 'haha'    =>  await Promise.resolve('haha')
await [1,2,3];  =>  await Promise.resolve([1,2,3])

 

 

 

2) async 함수가 비동기적으로 실행되도록 한다. 

 

다음으로 두번째 기능이다. 두번째 기능은 MDN 웹문서 내용을 가져와봤다. MDN 웹문서를 보면 await에 대해 아래와 같이 서술하고 있다.

async 함수 내부는 0개 이상의 await 문으로 분리할 수 있습니다. 첫번째 await 문을 포함하는 최상위 코드는 동기적으로 실행됩니다. 따라서 await 문이 없는 async는 동기적으로 실행됩니다. 하지만 await 문이 있으면 async 함수는 항상 비동기적으로 완료됩니다. 

 

async라도 await 문이 없으면 동기적으로 실행된다고 한다. 그리고 await 문이 있으면 첫번째 await문까지는 동기적으로 실행되고 함수 자체는 항상 비동기적으로 완료된다고 한다. 약간 헷갈리는 문장이다. 아래 코드를 보자. 

 

async function foo() {
  console.log('foo1') // 2
  await 1; // 3
  console.log('foo2'); // 5
}

foo(); // 1
console.log('global'); // 4

/*
foo1
global
foo2
*/

 

위 코드의 출력 결과는 어떻게 될까? 먼저 분석을 해보면 foo 함수는 async 함수고, 내부에 await가 있으니 비동기적으로 완료돼야 한다. foo 내부에서 'await 1'까지는 동기적으로 실행된다. 따라서 콘솔에 'foo1'이 출력된다. 'awiat 1' 하위 코드는 비동기적으로 실행되기 때문에 백그라운드로 넘어가게 되고, 콘솔에 'global' 출력된다. 백그라운드로 넘어간 코드는 다시 콜스텍으로 돌아와 콘솔에 'foo2'가 출력된다. 다른 예제를 보자

 

 

function foo() {
    console.log('inside foo'); // 4
}

function bar() {
    console.log('inside bar'); // 6
}

async function boo() {
    foo(); // 3
    await bar(); // 5
    console.log('inside boo'); // 8
}

console.log(1); // 1
boo(); // 2
console.log(2); // 7


/*
1
inside foo
inside bar
2
inside boo
*/

 

위 코드의 실행 순서를 분석해보자. boo 함수의 첫번째 await까지는 동기적으로 실행된다. 따라서 1~6까지는 순서대로 실행된다. 그런데 awiat 하위 코드는 비동기적으로 실행되므로 백그라운드로 넘어간다. 따라서 7번이 실행 완료된 후에 8번이 실행된다. 그러면 아래와 같이 foo 앞에 await가 붙어 있으면 결과가 어떻게 될까? 

 

function foo() {
    console.log('inside foo'); // 4
}

function bar() {
    console.log('inside bar'); // 7
}

async function boo() {
    await foo(); // 3
    await bar(); // 6
    console.log('inside boo'); // 8
}

console.log(1); // 1
boo(); // 2
console.log(2); // 5


/*
1
inside foo
2
inside bar
inside boo
*/

 

출력 결과가 약간 달라졌다. 이유는 foo 앞에 await를 붙여줘서 bar부터는 비동기적으로 실행되기 때문이다. 만약 여기까지 잘 이해했자면 다시 위로 가서 MDN 웹문서의 내용을 읽어보자. 고개가 끄덕여질 것이다. 

 

에러 헨들링

async/await에서 발생하는 에러는 try/catch 구문으로 간단하게 구현 가능하다. try/catch 구문은 await 뒤에 오는 프로미스 객체가 rejected 일때 reject 결과값을 catch의 인자로 넘겨준다.

async function test() {
    try {
    	const p = Promise.reject(new Error('Oops!'));
    	await p; // await가 p의 reject값을 catch의 인자로 throw
    } catch (error) {
    	console.log(error.message); // "Oops!"
    }
}

 

 

try/catch가 강력한 이유는, 비동기적 코드뿐 아니라 동기적 코드까지 에러를 잡아낼 수 있다는 점이다. 이는 async 함수뿐 아니라 일반 함수에도 똑같이 적용된다. 

async function test() {
    try {
    	const bad = undefined;
        bad.x; // 에러 throw
    	const p = Promise.reject(new Error('Oops!'));
    	await p;
    } catch (error) {
    	// "cannot read property 'x' of undefined"
    	console.log(error.message);
    }
}

 

 

 

<참조>

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/async_function#syntax   

 

 

반응형