2023년 2월 13일 내용이 추가 개편되었습니다 :-)
Promise 개념에 대해서 다시 짚고 넘어가기
Promise
는 비동기 작업의 단위를 말합니다. 그리고async/await
문법에서 반환하는Promise
란pending, fulfilled, reject라는 각각의 상태에 따른 값을 가지는 Promise 객체
를 말합니다.
- Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
- Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
- Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태
비동기 코드를 동기식으로 표현해서 간단하게 표현하기 위해 사용하는 async / await는 가장 최근에 나온 문법입니다. 콜백 헬, 프러미스 헬이 생길 수 있는 기존 방식의 단점을 보완하고 개발자가 읽기 좋은 코드를 작성할 수 있게 도와줍니다.
async
키워드는 함수를 선언할 때 붙여주 수 있습니다. 함수 선언시 앞에 async
키워드를 붙인 함수를 Async 함수라고 하고, 이 Async 함수 내부에서는 비동기 처리할 로직(함수) 앞에await
키워드 사용하는 방식으로 비동기 작업을 손쉽게 구현할 수 있습니다.
async function asyncFunc() {
// await 비동기_처리_메서드_명();
}
// or
const asyncFunc = async () => {
// await 비동기 처리할 함수 or 메서드 명
}
1) 먼저 함수의 앞에 async
라는 예약어를 붙인다.
2) 함수의 내부 로직중 HTTP 통신을 하는 비동기 처리 코드 앞에 await
를 붙인다.
🔥 일반적으로 await
의 대상이 되는 비동기 처리 코드는 프로미스를 반환하는 API 호출함수입니다.
function fetchDoodles() {
return new Promise(function(resolve, reject) {
const dogs = ["위로","호두","바오"];
resolve(dogs)
});
}
async function logDoodles() {
const result = await fetchDoodles();
console.log(result); //
}
fetchDoodles()
함수는 프러미스 객체를 반환하는 함수이다.fetchDoodles()
함수를 실행하면 프러미스가 resolved되며 결과 값은dogs
배열이 된다.logDoodles()
함수를 실행하면 fetchDoodles()
함수의 결과값인 dogs
배열이 result
변수에 담긴다. 콘솔에는 ["위로","호두","바오"]가 출력된다.
async
함수의 리턴값은 무조건 Promise
객체입니다. 따라서, async
함수 내부에서 then
과 catch
를 활용해 제어된 값이 Promise
에 담겨 반환됩니다.
모든 비동기 동작을 async 함수로 만들 수 있는게 아닙니다.async
함수를 만들때 resolve(value);
를 return value;
로 변경할 수 있습니다. 따라서, Promise
를 반환하는 것에 한해 해당 키워드를 사용할 수 있습니다. Promise
가 아닌 것은 Promise
로 감싸 반환하도록 방식으로 사용할 수 있습니다.
async func foo() {
return Promise.resove(1);
}
foo().then(alert) // 1
await
는 Promise
를 받아 처리하는 키워드입니다. wait
라는 문맥 상 기다리라는 의미를 가지고 있는 것 같는데, 얼추 맞습니다. await
는 Promise가 fulfilled(이행)
되든지, rejected(거부)
되든지, 비동기 처리 로직이 끝 날때까지 기다리도록 요청하는 함수입니다.
await
키워드를 사용하려면 함수가 async
함수로 선언되어야 합니다. async
함수는 화살표 함수로도 가능하고, 함수 표현식으로도 가능합니다.
const functionExpression = async function() {
console.log('함수 표현식');
};
const arrowFunction = async () => {
console.log('화살표 함수'); // 전 이 방식을 선호합니다.
};
const IIFE = (async () => {
console.log('즉시 실행 함수 표현식');
})();
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x)
}, 2000)
})
}
async function add1(x) {
console.log('start')
const a = await resolveAfter2Seconds(20)
console.log('a', a);
return x + a
}
add1(10).then(v => {
console.log(v)
})
console.log('finish')
다음과 같이 async function 내부에서 await
키워드를 사용하였고,
이는 resolveAfter2Seconds(20)
으로 리턴받는 Promise객체가 fulfilled(이행)
된 상태가 될 때까지 기다렸다가 다음 코드로 넘어가게 됩니다.
add1(10)
함수 호출.then()
은 async 함수의 리턴값이 프로미스 객체이다. 따라서, async 함수가 return되어야 then()
의 콜백함수가 실행됩니다. x+a
값이 30 리턴 .then()
의 콜백함수가 실행되면서 콘솔에 10 출력된다.
await 프로미스 객체
는 완료된 값을 반환한다.
async 함수는 return 또는 예외처리되어 throw 된 값이 담긴 Promise 객체를 반환합니다.
Promise 객체를 리턴하기 때문에 코드의 then(성공) 또는 catch(실패)여부에 따라 다시 await로 연결할 수 있습니다. Promise
에서 reject
가 발생한다면 예외가 발생해, 예외 처리를 위해 try-catch
구문을 사용할 수 있습니다. 에러는 catch
절로 넘어가, 에러를 처리하게 됩니다.
async fuction another() {
try {
let result = await returnPromise();
} catch (err) {
console.error(err);
}
}
코드를 async/await
문을 사용해 비동기 프로그래밍을 동기적으로 작동하는 것처럼 보이게 한다는 점에서 코드의 흐름을 잘 보여준다는 점에서 나름의 의미를 갖습니다. 하지만 중요한 것은 async/await
문을 사용해해 코드의 흐름을 분절해서 실행하려는 습관에 갇히지 않기를 바랍니다.
비동기 작업의 의의는 어떤 함수를 실행하는 동안, 여러가지 처리를 동시에 할 수 있다는데 의의가 있습니다. 따라서 실제 작업이 끝난 다음(분절) 그 후속조치를 수행한다
가 아니라, 실제 작업이 끝나는걸 기다린 다음 코드를 수행한다
라는 느낌을 가지는 것이 중요합니다.
직렬 처리
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x)
}, 2000)
})
}
async function add1(x) {
const a = await resolveAfter2Seconds(20)
console.log('a', a) // 2초 뒤 a 20 출력
const b = await resolveAfter2Seconds(30)
console.log('b', b) // 4초 뒤 a 30 출력
return x + a + b
}
add1(10).then(v => {
console.log(v) // 50 출력
})
console.log('finish')
병렬 처리
await
키워드를 호출하기 전에 Promise 객체를 생성해서 미리 비동기 작업을 pending 시켜놓고 이 pending 상태의 프로미스 객체를 await
하면 병렬적으로 처리할 수 있습니다.
function resolveAfter2Seconds(x) {
return new Promise(function foo(resolve) {
setTimeout(() => {
resolve(x)
}, 2000)
})
}
// Async/Await Example #3
async function add2(x) {
const a = resolveAfter2Seconds(20)
const b = resolveAfter2Seconds(30)
return x + (await a) + (await b)
}
add2(10).then(v => {
console.log(v)
})
Promise
가 도입되었음에도 여전히 콜백을 사용하는 것처럼, async/await
모두 Promise
나 callback
의 완벽한 대체품이 아닙니다. 경우에 따라 사용할 수 있는 것이죠.
비동기 작업은 동작 특성상 실제 작업과 그 후속조치를 따로 분리시킬 수 밖에 없습니다. 하여, then
과 catch
키워드를 사용하기도 합니다.
function setTimeoutPromise(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
async function startAsync() {
await setTimeoutPromise(1000).then(() => {
console.log("1초 지났습니다.");
});
}
console.log("시작입니다.");
startAsync();
다만, 위의 방식대로 await
와 then
을 동시에 사용하면 문법적으로는 틀리지 않았지만,
코드의 가독성 및 의도가 불분명해지는 문제가 있습니다.
await
문을 사용하는 이유 중 가장 주된 이유는 코드를 조금 더 직관적으로 볼 수 있게 하기 위함입니다.
가능하다면, async/await
를 사용하고, 불가피하다면 어쩔 수 없이 resolve/reject -then-catch
를 사용하는 것을 추천합니다. 위 예제에서 then
이하는 불필요해보입니다. 따라서, 아래와 같이 수정해볼 수 있겠죠
function setTimeoutPromise(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
async function startAsync() {
await setTimeoutPromise(1000);
console.log("1초 지났습니다.");
}
console.log("시작입니다.");
startAsync();
.then()
에 async
함수를 넣는다면?function setTimeoutPromise(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
async function startAsync() {
await setTimeoutPromise(1000).then( async () => {
await setTimeoutPromise(1000);
console.log("A");
});
console.log("B");
}
startAsync();
코드는 의도한 대로 순차적으로 잘 작동하겠지만, 코드가 상당히 난잡합니다.
따라서, 다음과 같이 작성해볼 수 있습니다.
function setTimeoutPromise(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
async function startAsync() {
await setTimeoutPromise(1000);
await setTimeoutPromise(1000);
console.log("A");
console.log("B");
}
startAsync();
.then
으로 구현하고 싶다면 다음과 같이 작성할 수 있겠네요
function setTimeoutPromise(delay) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
function startAsync() {
setTimeoutPromise(1000)
.then(() => setTimeoutPromise(1000))
.then(() => console.log("A"))
.then(() => console.log("B"));
}
startAsync();
Promise
를 생성할 때는 resolve
, reject
함수를 사용합니다. 이후 작업은 try/catch
문 블록 안에서 후속 작업 로직을 작성할 수 있습니다. new Promise(...)
는 async
키워드를 사용해 async/await
문으로 변환할 수 있습니다. async
함수 내에서 Promise에 대해 await
를 걸어 작업을 기다릴 수 있다. async/await
혹은 resolve-reject-then-catch
를 사용합시다. zerocho 블로그
캡틴 판교 블로그
[Javascript] 비동기, Promise, async, await 확실하게 이해하기