Async 와 Await의 등장
Promise를 사용하여 콜백지옥에서 벗어난 줄 알았지만, then도 여러번 반복될 경우 콜백지옥과 비슷한 현상이 발생한다.
이러한 현상에서 벗어나기 위해 ES2017에서 비동기 처리를 더욱 간결하게 구현할 수 있는 async와 await라는 문법이 도입되었다.
'async'라는 키워드를 함수 앞에 붙여 비동기 함수라 선언하고, 'await'라는 키워드를 Promise 앞에 붙여 결과가 나올때까지 기다리도록 만든다.
즉, 동기적 처리방식인 자바스크립트에서
비동기 처리를 위해 async를 사용해 비동기 함수를 병렬적으로 실행하고
이를 await로 동기적으로 출력하는 것이다.
await는 async 함수 안에서만 사용되며, Pormise가 이행될 때까지 기다리는 것이기 때문에 실행 기간동안 바깥의 코드는 실행가능하다.
이는 await가 비동기 처리를 위해 동기적으로 기다리는 것처럼 보이지만, 실제로는 비동기적으로 처리된다는 것을 말한다.
async 함수가 반환하는 Promise는 마이크로태스크 큐에 들어가 대기하지만, 만약 await를 만난다면 await가 사용된 Promise가 처리되기 전까지 그 뒤의 코드는 마이크로태스크 큐에 대기하다가 await의 Promise가 이행되면 그 이후에 추가된다.
async function asyncFnc(){
const exPromise = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve("작업 성공");
}, 1000);
});
console.log(exPromise); // 작업 성공
}
// 예제 출처 F-Lab
async function
async는 정확히 말하면 async function이 한 키워드로, AsyncFunction 객체를 반환한다.
AsyncFunction은 이벤트 루프를 통해 비동기적으로 작동하는 함수로, 이 또한 '마이크로태스크 큐'에 들어간다.
작성하는 방식은 표준 동기함수와 비슷하나 async 함수는 항상 Promise를 반환한다는 차이점을 가지고있다.
만약 async 함수의 반환값이 명시적으로 promise가 아니라면 암묵적으로 promise로 감싸진다.
// async로 만든 함수 foo()는 promise값을 리턴하는 함수 foo()와 같다.
async function foo() {
return 1;
}
function foo() {
return Promise.resolve(1);
}
await
await는 async funciton 내부에서만 사용할 수 있는 연산자로, Promise를 기다리기 위해 사용된다.
(외부에서 사용할 경우 SyntaxError 발생)
async function foo() {
const result = await promiseName;
console.log(result);
}
await 문은 프로미스가 fulfuill 되거나 reject 될 때까지 다음 코드를 대기 상태로 만든다.
위의 예시에선 promiseName이라는 프로미스의 작업이 종료된 후 result 변수에 그 결과값이 저장되고, 이후 콘솔에 result 값이 출력된다.
1) await 문의 실행순서 이해하기
async function fetchData() { console.log("예시1"); const data = await fetch("https://api.example.com"); console.log("예시2"); return data; } async function main() { console.log("예시3"); const result = fetchData(); console.log("예시4"); await result; console.log("예시5"); } main(); // 콘솔 출력 결과 // 예시3 // 예시1 // 예시4 // 예시2 // 예시5
main() 함수 실행
→ "예시3" 출력
→ fetchData 비동기 함수 호출
→ "예시1" 출력
→ data 변수가 await로 인해 호출하는 동안 main의 "예시4"가 먼저 출력
→ 이후 예시5 이전에 fetchData 가 담겨있는 result를 기다리라는 await문이 있기 때문에 먼저 "예시2" 출력
→ fetchData() 함수가 작업 완료하였기 때문에 "예시5" 출력
2) 만약 await 뒤에 Promise 이외의 값이 들어오면?
await는 프로미스가 아닌 값이 들어오면 자동으로 Promise.resolve()처럼 처리된다.
아래와 같이 await 5라고 작성하면 이를 Promise.resolve(5)로 변환하고 그 값을 반환하는 것이다.
let result = await 5;
console.log(result); // 5가 출력됨
왜 이렇게 자동으로 변환되면 다음과 같은 장점이 있다.
- 일관성 : await는 항상 Promise를 기다리므로 코드 흐름이 일관된다. Promise가 아닌 값이 오더라도 자동으로 처리하여 그 뒤에 어떤 값이 오든 상관없게 만들 수있다. 개발자는 추가적인 Promise.resolve()를 호출하지 않아도 된다.
- 간결성: Promise인지 아닌지 구분할 필요 없이 동기식처럼 코드 작성이 가능하다. 모든 값을 Promise로 처리할 수 있기 때문에, 비동기 코드에서 여러 상태를 처리할 때 코드가 간결해진다.
- 유연성: 비동기 코드가 아닌 동기적인 값을 처리할 때, 동기적인 값을 Promise로 변환하는 것처럼 보이므로, 동기적인 작업을 비동기 방식으로 처리하는 데 유리하다.
예시를 통해 await 가 자동변환됐을 때의 장점을 알아보자.
async function data() { const data = await getData(); const processed = await step(data); return processed; } async function step(data) { if (data.condition) { return await data.some(); } return 42; // 동기적 값 반환 }
위의 예시에선 함수 setp()의 리턴값이 if문에 의해 달라진다.
data.condition가 true인지 false인지에 따라 리턴값이 달라지기 때문이다.
이 경우 data()의 processed변수에는 Promise가 들어갈수도 단순 숫자가 들어올 수도 있다.
이때, await가 어떠한 값이 들어와도 promise로 값을 변환하여 결과를 저장하기 때문에 비동기 작업이 포함되어 있어도 코드가 복잡해지지 않는다.
3) async 함수에서 Promise를 return 할 때 await가 없어도 된다?
위에서 async function을 설명할 때 async 함수가 항상 프로미스를 반환한다는 것을 배웠다.
이는 async function의 반환값이 항상 Promise.resolve()로 감싸진다는 것을 의미한다.
// async로 만든 함수 foo()는 promise값을 리턴하는 함수 foo()와 같다.
async function foo() {
return 1;
}
function foo() {
return Promise.resolve(1);
}
이때, Promise.resolve()와 await의 역할에 공통점이 존재한다.
Promise.resolve()는 주어진 값을 프로미스로 감싸는 역할로, 주어진 값이 이미 프로미스라면 그 프로미스를 그대로 반환하고, 그렇지 않다면 주어진 값을 해결된 상태의 프로미스로 래핑한다.
const promise = Promise.resolve(42);
promise.then(result => {
console.log(result); // 42
});
Promise.resolve(42)는 42라는 해결된 상태의 값을 프로미스로 감싼 것이다. 이때, 42는 해결된 값이기 때문에 then()에서 바로 출력된다.
const promise = Promise.resolve(Promise.resolve(42));
promise.then(result => {
console.log(result); // 42
});
Promise.resolve(Promise.resolve(42))는 해결된 값이 아니라 한번 프로미스로 감싸진, 그러니까 프로미스 객체를 전달받은 것이다. 이때, 외부의 Promise.resolve()는 내부의 프로미스가 해결될 때까지 기다린 후 최종적으로 값을 반환한다.
이렇게 프로미스가 해결될 때까지 기다린다는 점이 await와 비슷하다.
다만, await는 프로미스가 해결될 때까지 다음의 코드를 일시정지 상태로 만드는 차이가 있으나 return 하는 순간 다음의 코드는 취소되기 때문에 의미가 없다.
** 두 방식의 비교
1. 명시적 대기
await 는 someAsyncFunction()에서 반환하는 프로미스가 해결될 때까지 기다리고, 해결된 값을 반환한다.
foo()는 result를 바로 반환하는 대신, someAsyncFunction()의 결과가 해결된 후 그 값을 반환한다.
async function foo() { const result = await someAsyncFunction(); // Promise가 해결될 때까지 기다림 return result; // 해결된 값 반환 } foo().then(result => { console.log(result); // someAsyncFunction()이 해결된 후 출력 });
2. 간단환 반환
someAsyncFunction()이 이미 프로미스를 반환하므로, async 함수에서 프로미스를 그대로 반환한다.
foo()가 반환하는 프로미스는 someAsyncFunction()의 프로미스이기 때문에, 결과적으로 foo()도 동일하게 그 프로미스가 해결될 때까지 기다린다.async function foo() { return someAsyncFunction(); // 바로 Promise 반환 } foo().then(result => { console.log(result); // someAsyncFunction()이 해결된 후 출력 });
=> 결론
두 방식 모두 비슷한 방식으로 동작하지만 await는 내부에서 프로미스가 해결될 때까지 기다리면서 기다리면서 코드 흐름을 제어할 수 있게 한다.
따라서, async 함수에서 프로미스를 반환하는 방식은 결국 프로미스가 해결될 때까지 기다린다는 점에서 결과는 같 고, 주로 코드 흐름을 제어하는 방식에서 차이가 있을 뿐이다.
만약 반환(return)하지 않고 await 만 작성할 경우 undefined가 반환된다.
async function foo() {
await 1;
}
function foo() {
return Promise.resolve(1).then(() => undefined);
}
그렇기 때문에 보통 프로미스 객체를 가져와서 사용할 때는 await 문을 통해 사용하고,
return 문으로 반환할 때
async function fnc1(url) {
let v;
try {
v = await fnc2(url);
} catch (e) {
v = await fnc3(url);
}
return fnc4(v);
}
try...catch 문
기존 Promise에서는 then()과 catch(), finally() 메서드를 사용해서 프로미스 체인을 구성하였다.
async function 에서는 대부분 try...catch 문을 사용하여 구성된다.
try...catch 문은 try블록과 catch 블록 그리고 finally 블록으로 구성되어있다.
try {
// try 블록에서 실행될 구문
} catch (예외변수) {
// try 블록에서 예외가 발생했을 때 실행될 구문
} finally {
// finally 블록에서 실행될 구문
}
다른 구조인 if 나 for 와 달리 try블록, catch 블록, finally 블록은 반드시 중괄호 {} 로 감싸진 블럭이어야 한다.
try functionName(); // SyntaxError
catch (e) console.log(e); // SyntaxError
try {
functionName();
} catch (e) {
console.log(e);
} // correct!
try 블록 내의 코드가 먼저 실행되고, 만약 예외(오류)가 발생한다면 catch 블록으로 넘어가 실행된다.
finally 블록은 finally()메소드와 비슷하게 프로미스의 성공 여부와 상관없이 항상 실행되며, 전체 구문을 종료하기 전에 실행된다.
1) try 블록
try 문은 항상 try 블록으로 시작하는데, 이후 catch 블록 또는 finally 블록 중 하나가 반드시 존재해야한다.
* try...catch
* try...finally
* try...catch...finally
try 블록은 하나 이상의 try 문을 중첩해서 사용할 수 있다.
중첩된 try 문에서 오류가 발생할 경우 catch과 finally가 동작하는 방식에 대해선 각각의 블록의 설명과 함께하겠다.
2) catch 블록
catch 블록은 try 블록에서 예외가 발생했을 때 실행된다.
만약 try구문에서 예외가 발생하지 않으면 catch 블록은 실행되지 않는다.
try 블록에서 발생한 예외는 바로 예외 변수에 값이 저장되며, 이 예외변수를 바인딩(binding) 하면 발생한 예외에 대한 정보를 얻을 수 있다.
** catch 블록의 예외변수
1. 예외변수는 try 블록에서 발생한 예외에 관한 정보가 담겨있는 변수이다.
만약 예외 값을 실제로 사용하지 않는다면, 이 변수과 괄호는 생략 가능하다.
2. 여러개의 예외 변수를 한번에 할당할 수 있다.
예외변수는 반드시 단일 식별자일 필요는 없으며, 구조 분해 할당을 사용해 여러 개의 식별자를 한번에 할당 할 수 있다.
try { throw new TypeError("oops"); } catch ({ name, message }) { console.log(name); // "TypeError" console.log(message); // "oops" }
3. 예외변수에 저장된 정보는 catch 블록의 scope 내에서만 사용할 수 있다.
catch 절에 의해 생성된 바인딩은 catch 블록과 동일한 범위를 가진다.
따라서 catch 블록 내에서 예외 변수와 동일한 이름의 변수는 생성할 수 없다.try { throw new TypeError("oops"); } catch ({ name, message }) { var name; // SyntaxError: Identifier 'name' has already been declared let message; // SyntaxError: Identifier 'message' has already been declared }
try 문이 중첩되어 사용되었을 경우 catch 블록의 작동 방식을 알아보겠다.
** try 문에서 예외가 발생하면 가장 가까운 catch 블록이 사용된다.
아래와 같이 try 문이 중첩되어 있을 경우 외부 try 문이 오류를 발생하면 외부 catch 문이 실행되고,
내부 try 문이 오류를 발생하면 내부 catch 문이 실행된다.try { // 외부 try 문 console.log("외부 try 시작"); throw new Error("에러1"); try { // 내부 try 문 console.log("내부 try 시작"); throw new Error("에러2"); } catch (innerError) { // 내부 catch 문: 내부 try에서 발생한 예외를 처리 console.log("내부 catch 블록에서 예외 처리:", innerError); } console.log("내부 try 종료"); throw new Error("에러3"); } catch (outerError) { // 외부 catch 문: 외부 try에서 발생한 예외를 처리 console.log("외부 catch 블록에서 예외 처리:", outerError); } /* < 에러1만 발생했을 경우 콘솔창 > 외부 try 시작 내부 try 시작 내부 catch에서 예외 처리: innerError 내부 try 종료 < 에러2만 발생했을 경우 콘솔창 > 외부 try 시작 외부 catch 블록에서 예외 처리: outerError < 에러3만 발생했을 경우 콘솔창 > 외부 try 시작 내부 try 시작 내부 try 종료 외부 catch 블록에서 예외 처리: outerError */
만약 내부 try 문에 catch 블록이 없는 경우, 외부 try 문의 catch 블록이 대신 사용된다.
즉, 중첩된 try 문 내에서 예외가 발생하면 가장 가까운 상위 catch 블록이 해당 예외를 처리하게 되는 것이다.
catch 블록을 if 문과 함께 사용할 경우 " 조건부 catch 블록 " 을 만들 수 있다.
** 조건부 catch 블록
보통 일부 오류만 포착하고 억제한 후, 다른 경우에는 오류를 다시 던지는(throw) 경우가 일반적이다.
try { myroutine(); // may throw three types of exceptions } catch (e) { if (e instanceof TypeError) { // statements to handle TypeError exceptions } else if (e instanceof RangeError) { // statements to handle RangeError exceptions } else if (e instanceof EvalError) { // statements to handle EvalError exceptions } else { throw e; // re-throw the error unchanged } }
3) finally 블록
finally 블록은 try 블록과 catch 블록이 실행된 후에 실행할 구문을 말한다.
openMyFile();
try {
// tie up a resource
writeMyFile(theData);
} finally {
closeMyFile(); // always close the resource
}
finally 블록이 존재할 경우 제어 흐름은 항상 finally 블록으로 진입하는데, 위와 같이 파일을 연 다음 try 블록에서 오류가 발생했더라도 파일이 항상 닫히도록 할 수 있다.
** finally 블록의 실행시점
finally 블록의 실행시점은 try 블록이나 catch 블록에서 제어 흐름 구문(return, throw, break, continue)이 실행되어 해당 블록을 벗어나기 직전이다.
정확히는 return 문 안의 구문이 실행되고 return이 되기 직전이다.
function example() { try { console.log("Try block 실행"); return console.log("Return 후 실행"); } finally { console.log("Finally block 실행"); } } example(); // < 출력결과 > // Try block 실행 // Return 후 실행 // Finally block 실행 // ( 이후 return )
** finally 블록과 .finally() 메소드 차이점
프로미스 체인을 만들 때 finally() 메소드는 앞선 인자값에 영향을 주지 않았다.
인자값을 2로 받아 finally() 에서 인자값을 77로 만들어도 그 다음 then()에서 사용하는 인자값은 2였다.
하지만 finally 블록에서는 블록 내에서 반환값이 변할 수 있다.
function doIt() { try { return 1; } finally { return 2; } } doIt(); // returns 2
이와 같이 finally 블록 내에서는 제어 흐름 구문(return, throw, break, continue)을 사용하는 것은 원하지 않는 결과를 가져올 수 있으므로 정리 작업을 위한 코드만 사용하는것을 권장한다.
'자바스크립트(JavaScript) > 자바스크립트' 카테고리의 다른 글
[JavaScript] Generator Function / 제너레이터 함수 (0) | 2024.12.11 |
---|---|
[JavaScript] Class (2) | 2024.12.10 |
함수를 인라인으로 만든다 / 인라인 함수 (2) | 2024.12.09 |
[JavaScript] Promise, 자바스크립트 비동기 처리기 (0) | 2024.12.06 |
[JavaScript] 비동기 처리 개념 (2) | 2024.12.04 |