이전포스팅에서는 자바스크립트에서 비동기를 처리하는 방식 중 하나인 Promise에 대해서 소개했다. Promise는 Promise가 나온 이전에 사용했던 콜백 함수 패턴의 콜백 지옥을 해결하고 에러처리의 단점을 해소할 수 있었다. 하지만 Promise는 후속처리 메서드 인터페이스에 종속되는 단점이 존재하고 이에 따라 가독성이 떨어져 구현하고자 하는 로직의 의도를 한번에 파악하기 어렵다는 문제가 있다.
이 문제를 해결할 수 있는 문법이 바로 ES2017에 도입된 async/await이다. 그렇다고해서 async/await가 Promise를 대체하는 것은 절대 아니다. async/await는 간단한 문법으로 동기적인 코드처럼 보이는 가독성을 가질 뿐 내부적으로는 Promise를 사용해서 비동기를 처리하고 있음을 명심하자.
async/await 사용방법
function foo() {
return new Promise((resolve) => {
setTimeout(() => resolve("완료!"), 1000);
});
}
async function bar() {
const result = await foo();
console.log(result); // 완료!
}
bar();
다음은 async/await를 사용한 간단한 예제이다. function 키워드 앞에 async 키워드를 붙여 비동기 함수임을 선언하고, 함수 내부에서 비동기를 처리하는 Promise 객체 앞에 await 키워드를 붙인다 await 키워드는 "나 얘 비동기 처리 완료될 때까지 기다릴거야." 라는 의미로 보자. 여기서 async/await를 사용할 때 주의할 점은, await를 사용하기 위해서는 반드시 function 앞에 async를 붙여 비동기 함수임을 선언해야 한다. 즉, await 키워드는 반드시 async 함수 내부에서만 사용할 수 있다.
async 함수의 리턴값
async 함수는 항상 Promise를 반환한다. 만약 명시적으로 Promise가 아닌 값을 반환하더라도 async 함수는 암묵적으로 반환값을 resolve 하는 Promise를 반환한다. 다음 예제를 보자.
async function foo() {
return 1;
}
const result = foo();
console.log(result);
async 함수 내에서 명시적으로 1을 리턴했음에도 불구하고 Promise 객체 형태로 반환된 것을 볼 수 있다. 그렇기 때문에 Promise 값을 담은 result 변수에 후속처리 메서드인 then과 catch, finally를 사용하는 것이 가능하다. 하지만 Promise의 후속처리 메서드 인터페이스를 벗어나고자 async/await를 사용하면서 또 다시 후속처리 메서드를 사용한다는 것은 웃긴일이다. 되도록 await 키워드를 통해 비동기 작업을 처리하자.
async/await 에러처리
기존 Promise에서는 후속처리 메서드인 catch 메서드를 통해 에러 처리를 했지만, async/await에서의 에러처리는 try/catch문을 사용한다. 다음 예제를 보자.
async function foo() {
try {
const response = await fetch("https://www.xxx.xxx");
const data = await response.json();
console.log(data);
} catch (e) {
console.log("에러발생: ", e); // 에러발생: TypeError: Failed to fetch
}
}
foo();
try 코드 블록에 포함된 코드 중에서 에러가 발생한다면 발생한 에러는 catch 문의 e 변수에 전달되고 catch 코드 블록이 실행된다.
async/await 사용 시 주의할 점
async/await에서 await 키워드는 Promise가 fulfilled 또는 rejected 상태가 될 때까지 대기하고 이후 Promise가 resolve 또는 reject한 결과가 반환된다. 이처럼 await 키워드는 다음 실행을 일시 중지시켰다가 Promise가 settled(fulfilled 또는 rejected) 상태가 되면 다시 재개한다.
async function foo() {
const a = await new Promise((resolve) => setTimeout(() => resolve(1), 1000));
const b = await new Promise((resolve) => setTimeout(() => resolve(2), 2000));
const c = await new Promise((resolve) => setTimeout(() => resolve(3), 3000));
console.log(a, b, c); // 1, 2, 3
}
foo();
위의 예제를 보면 결과를 보는데까지 약 6초의 시간이 걸린다. 위의 예제는 비동기 처리의 순서를 보장할 필요가 없는 예제이다. a는 b와 c에 전혀 영향을 주지 않는 비동기 처리이고, b 또한 c에 전혀 영향을 주지 않는 비동기 처리이다. 따라서 첫 번째 Promise가 fulfilled 상태가 될 때까지 대기하고 이후에 두 번째 비동기 처리를 수행할 필요가 없다.
async function foo() {
const a = new Promise((resolve) => setTimeout(() => resolve(1), 1000));
const b = new Promise((resolve) => setTimeout(() => resolve(2), 2000));
const c = new Promise((resolve) => setTimeout(() => resolve(3), 3000));
const resolvedA = await a;
const resolvedB = await b;
const resolvedC = await c;
console.log(resolvedA, resolvedB, resolvedC); // 1, 2, 3
}
foo();
그렇다면 어떻게 async/await를 적절한 방식으로 사용할 수 있을까? 위의 예시를 보자. 비동기 처리 요청과 await를 분리한다. await 키워드를 통해 Promise의 상태가 settled로 변경되어 비동기 처리 결과를 반환할 때까지 기다리지 않고 여러 비동기 작업을 먼저 실행해놓고 이 후에 비동기 처리 결과를 await 키워드를 통해 받도록 한다. 결과적으로 약 3초가 소요된다.
async function foo() {
const a = new Promise((resolve) => setTimeout(() => resolve(1), 1000));
const b = new Promise((resolve) => setTimeout(() => resolve(2), 2000));
const c = new Promise((resolve) => setTimeout(() => resolve(3), 3000));
const result = await Promise.all([a, b, c]);
console.log(result); // [1, 2, 3]
}
foo();
한 가지 방법을 더 소개하자면 Promise 포스팅에서 다루었던 Promise.all 메서드를 사용하여 모든 Promise를 병렬로 처리하도록한다. Promise가 모두 fulfilled 상태가 되면 모든 처리결과를 배열에 저장한 새로운 Promise를 반환한다. 이 또한 위에서 약 6초 걸렸던 작업에서 약 3초로 줄어들었다.
async/await의 활용
Promise의 특징을 살려서 async/await가 활용될 수 있는 방안을 생각해보자. Promise는 비동기 작업이 완료될 때까지 현재 스레드의 실행을 차단하지않고 계속 실행된다. 즉, Promise의 상태가 settled 상태가 아니라면 Promise는 종료되지 않는다. 따라서 Alert, Toast, Modal, Popup과 같이 우리가 직접 Promise의 상태를 변경해 줄 수 있는 경우에도 Promise가 활용될 수 있다.
See the Pen Untitled by 신건우 (@huhydupg-the-reactor) on CodePen.
위 예제는 Promise를 통해 리스트 삭제시 모달을 띄우도록 구현한 것이다. foo 함수를 호출하면 Promise를 반환하고 버튼을 클릭하기 전까지는 Promise의 resolve가 호출되지 않아 비동기 처리가 종료되지않는다. 콘솔에서 버튼을 누르고 확인해보면 모달창이 닫히기 전까지는 버튼 클릭 종료가 출력되지 않는 것을 확인할 수 있다.
'개발 지식 정리 > Javascript' 카테고리의 다른 글
Promise (0) | 2023.09.09 |
---|---|
requestAnimationFrame (0) | 2023.08.30 |
이벤트 루프 (0) | 2023.07.21 |
타이머 (0) | 2023.07.18 |
DOM (0) | 2023.07.14 |