JavaScript에서 비동기 프로그래밍을 다루는데 사용되는 여러 가지 개념들이 있는데, 그 중에서도 Promise, Callback, Async, Await는 아주 중요한 개념들이라 한방에 정리해보자!!
이번 내용은 같이 부캠에서 만난 훌륭한 동료들인 지은님, 용현님, 민형님과 함께하는 면접 스터디에서 같이 공부한 내용을 정리한 것이다. 같이 공부하면서 많은 도움이 되었고, 더 나아가서 같이 성장할 수 있는 좋은 동료들을 만나서 너무 행복하다. 앞으로도 같이 성장해보자!!
먼저 가장 핵심적인 개념인 Promise에 대해 알아보자.
Promise는 비동기 작업을 수행할 때 사용되는 객체이다. Promise는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타낸다.
JavaScript는 싱글 스레드 기반의 언어이다. 따라서 한번에 한가지의 일 밖에 하지 못하는데, 서버와 클라이언트간의 통신이나 setTimeout등 시간이 오래 걸리면서, 메인 스레드가 직접적으로 작업에 관여 하는 것이 아닌 작업들은 비동기적으로 처리된다.
비동기적으로 처리된다는 것은 어떤 의미일까? 해당 작업이 끝나기 전에 다음 작업을 수행한다는 의미이다. 하지만 싱글 스레드 기반인 JavaScript는 한번에 한가지의 일 밖에 하지 못한다. 따라서, 비동기적으로 처리되는 작업들은 메인 스레드가 직접적으로 관여하지 않고, Web API에게 위임한다.
Web API에게 위임된 작업들은 해당 작업이 끝나면, Callback Queue에 해당 작업을 담아놓는다. 이 때, 메인 스레드는 다른 작업을 수행하고 있다가, Callback Queue에 담겨있는 작업들을 하나씩 꺼내와서 실행한다.
이때, 비동기 작업의 리턴값을 바로 받아오고자 하면, 비어있는 값을 가지고 오기 때문에 오류가 발생하거나 빈 화면이 뜨게된다. 이를 해결하기 위해 비동기 데이터의 내용을 바로 받아오는 것이 아니라, “미래의 비동기 작업이 끝난 시점에 결과를 제공하겠다는 약속(Promise)”을 대신 반환하게 된다.
Promise는 3가지의 상태를 가진다.
Promise 생성자를 통해 Promise 객체를 생성할 수 있다. 이 때, 인자로 resolve
와 reject
를 전달 받는 executor(실행함수)
를 전달받는다.
executor
는 프로미스 구현에 의해 resolve와 reject 함수를 받아 즉시 실행(실행 함수는 Promise 생성자가 생성한 객체를 반환하기 전에 호출)resolve
를 호출해 프로미스를 이행reject
를 호출해 거부하며, 이 경우 실행 함수의 반환값과 상관없이 Promise는 거부function fetchData() {
return new Promise(function (resolve, reject) {
// 비동기 작업 수행 (보통 서버 통신)
setTimeout(function () {
const success = true;
if (success) {
const data = "Hello, world!";
resolve(data);
} else {
reject("Error occurred");
}
}, 1000);
});
}
fetchData()
// 성공 시 resolve의 결과값을 받아옴
.then(function (result) {
console.log(result);
})
// 실패 시 reject의 결과값을 받아옴
.catch(function (error) {
console.error(error);
});
// Hello, world!
Promise.all()은 인자로 Promise를 요소로 가지는 순환 가능한 객체를 받는다.
Promise.all()이 실행되면, 순환 가능한 객체 내의 모든 Promise를 병렬로 처리하여, 모든 Promise가 성공적으로 처리되거나 객체 내 Promise가 최초로 거부될 때까지 대기
function fetchData() {
return new Promise(function (resolve, reject) {
// 비동기 작업 수행 (보통 서버 통신)
setTimeout(function () {
const success = true;
if (success) {
const data = "Hello, world!";
resolve(data);
} else {
reject("Error occurred");
}
}, 1000);
});
}
Promise.all([fetchData(), fetchData()])
.then(function (result) {
console.log(result);
})
.catch(function (error) {
console.error(error);
});
// ["Hello, world!", "Hello, world!"]
콜백은 함수를 다른 함수에 인자로 전달하여 나중에 호출되도록 하는 패턴으로, 주로 비동기 작업에서 사용되어, 작업이 완료되면 해당 콜백 함수를 호출한다.
사실 Callback은 Promise처럼 특정한 객체나 구조를 의미하는 것은 아니다. 콜백은 함수를 다른 함수의 인자로 전달하여 나중에 호출되도록 하는 것을 의미하며, 이는 비동기적인 작업의 완료나 이벤트 발생 등에 대한 응답을 처리하는 데 사용되는 기법이다.
비동기 함수를 연속적으로 처리하기 위해 Callback과 Promise를 둘 다 사용할 수 있다. 하지만, Callback은 비동기 함수의 연속 처리에 있어서 가독성이 떨어지고, 에러 핸들링이 어렵다는 단점이 있다.
콜백 지옥
(혹은 콜백 피라미드
)가 생성// Callback Hell
fetchData(function (data1) {
process1(data1, function (data2) {
process2(data2, function (data3) {
// ...
});
});
});
// Using Promise
fetchData()
.then(process1)
.then(process2)
.then(function (data3) {
// ...
});
fetchData()
.then(function (data) {
// Success
return processData(data);
})
.catch(function (error) {
// Handle error
console.error(error);
});
async/await
는 Promise
로직을 더 쉽게 사용하기 위해 ES2017 부터 도입된 문법이다. async/await
는 내부적으로 Javascript의 Promise
를 기반으로 동작하는 사용자가 쉽게 비동기 코드를 작성하도록 하는 문법이지, Promise
를 대체하기 위한 새로운 기능은 아니다. (이러한 개념을 보통 Syntactic sugar
라고 한다.)
async
를 붙여주어, “해당 함수에는 await
를 사용할 것이야!”라고 선언function sample(ms) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`${ms} 밀리초가 지났습니다.`);
resolve();
}, ms);
});
}
// Promise.then
function main() {
delay(1000)
.then(() => {
return delay(2000);
})
.then(() => {
return Promise.resolve("끝");
})
.then((result) => {
console.log(result);
});
}
main();
// async/await
async function main() {
await delay(1000);
await delay(2000);
const result = await Promise.resolve("끝");
console.log(result);
}
main();
async를 붙인 함수 내부에 await를 사용하기 위한 선언문
async의 리턴값은 Promise
async 함수 내에 return 1
을 할 경우, 일반 함수와 같이 1을 반환하는 것이 아니라 이행 상태(fulfilled)와 결과값으로 1을 가지는 Promise
를 반환하는 것을 확인
async function func1() {
return 1;
}
const data = func1();
console.log(data); // Promise { 1 }
만약, async
function에 아무런 반환값을 작성하지 않더라도 undefined
의 결과값을 가지는 Promise
를 반환
async function func2() {}
const data2 = func2();
console.log(data2);
async 함수의 반환값은 Promise이기 때문에 then 핸들러를 부착할 수 있음
async function func1() {
return 1;
}
const data = func1();
data.then((result) => {
console.log(result);
}); // 1
await
연산자는async function
내부에서만 사용할 수 있는 연산자로,Promise
를 기다리기 위해 사용
await 문의 Promise가 fulfill/reject 될 때까지 async 함수의 실행을 일시 정지
만약, Promise가 fulfill 되면, async 함수를 일시 정지한 부분부터 실행
await의 값이 Promise가 아닐 경우, 해당 값을 resolved Promise로 변경
async function func1() {
await 1;
}
function func1() {
return Promise.resolve(1).then(() => undefined);
}
// 위의 두 코드는 동일하게 작동
마지막으로 담백하게 Promise와 async/await의 차이점을 정리해보자.
Promise
는 .then()
과 .catch()
를 사용하여 비동기 코드를 처리async/await
는 async
함수 내부에서 await
키워드를 사용하여 비동기 함수를 처리await
는 비동기 작업이 완료될 때까지 기다리며, 이를 통해 코드를 동기적으로 처리Promise
의 .then()
과 .catch()
의 경우 Promise
체인을 통해 중첩된 구조를 가지게 될 경우 코드의 구조를 이해하기에 어려움이 발생할 여지가 있음async/await
코드는 동기적으로 코드를 작성할 수 있어 가독성/유지보수성에 용이함을 가짐Promise
구문은 .catch()
를 사용하여 오류를 처리async/await
구문은 동기 구문과 같이 try/catch
구문을 통해 오류를 처리Promise
는 직접 Promise
객체를 생성하고, 결과를 반환async
함수 내부의 return을 통해 Promise
의 결과를 반환Promise
의 경우Promise
내부 실행 함수 코드가 콜스택에 적재되어 실행.then()
핸들러의 콜백 함수가 이벤트 루프에 의해 Microtask Queue에 적재async/await
의 경우async
함수의 내부 코드가 await
구문을 만날 때까지 콜스택에서 실행await
구문을 만나게 되면 async
함수는 동작을 중단하고, 콜스택에서 빠져 나와 나머지 부분은 Microtask Queue로 이동async
함수는 콜스택으로 이동하여 마저 실행const one = () => Promise.resolve("One!");
async function myFunc() {
console.log("In function!"); // await 구문 전까진 동기적으로 실행
const res = await one(); // 해당 부분부터 Microtask Queue로 이동
console.log(res); // 모든 동기 코드 실행 후, 동작
}
console.log("Before function!");
myFunc();
console.log("After function!");
// 실행 결과
// Before function!
// In function!
// After function!
// One!
Promise
는 비동기 작업을 처리하기 위한 객체async/await
는 Promise
를 더 쉽게 사용하기 위한 문법async/await
는 Promise
를 대체하는 것이 아닌, Promise
를 기반으로 동작하는 문법async/await
는 Promise
의 .then()
과 .catch()
를 대체하는 것이 아닌, try/catch
를 사용하여 오류를 처리이렇게 그동안 헷갈렸던 Promise, Callback, Async, Await에 대해 정리해보았다. 부스트캠프 챌린지 과정의 16개 주제 중에 가장 어려웠던 주제 두가지를 꼽으라면, 첫번째가 함수형 프로그래밍이었고, 두번째가 비동기에 관한 내용이었다.
나는 객체지향과 동기적 프로그래밍에 익숙해져 있었는데, 브라우저에서 비동기적으로 처리되는 작업들을 이해하는데는 상당히 시간이 걸렸던 것 같다. 이번에 면접 스터디를 하면서 한번 더 확실하게 정리 한 것 같다!