자바스크립트의 비동기 처리
callback, Promise, async/await
# 동기, 비동기란?
동기(synchronous)란 동시에 일어나는 것, 비동기(Asynchronous) 동시에 일어나지 않는 것이라는 의미이다. 동기와 비동기란 일을 처리하는 순서라고 해석할 수 있다. 예를 들어, 청소를 한다고 치자. 세탁기와 청소기를 돌려야 한다. 어떻게 해야 하는가? 세탁기 작동 버튼을 눌러놓고 세탁이 끝날 때까지 기다렸다가 청소기를 돌려야 하는가? 그건... 너무 비효율적이지 않은가?🤔 세탁기 작동 버튼을 눌러 놓고 세탁기가 돌아가는 동안 청소기를 돌리면 된다. 세탁기와 청소기는 각자 돌아가니까!
여기서 동기, 비동기 개념이 나온다. 하나가 끝나야 그 다음 일을 하는 것을 동기적 처리라고 한다. 세탁이 끝날 때까지 기다렸다가 청소기를 돌리는 것은 동기적 처리라고 할 수 있겠다. 그리고, 세탁기를 작동시켜 놓고 그 사이에 청소기를 돌리는 것을 비동기적 처리라고 할 수 있다. 동시에 여러 작업을 처리하는 것이다.
동기는 동시에 일어나는 것, 비동기는 동시에 일어나지 않는 것이라는 뜻이라서 헷갈릴 수 있다. 뜻이 실제 개념과 반대되는 것 같잖아😅 이는 요청과 응답 측면에서 보면 이해가 될 것이다. 동기는 요청과 그에 대한 응답이 동시에 일어나야 하는 것이다. 즉, 어떤 일에 대해 요청을 하면 그 응답이 그 자리에서 바로 동시적으로 일어나야 한다. 반대로 비동기는 요청에 대한 응답이 동시에 일어나지 않아도 된다. A라는 요청이 들어왔을 때, 동기적 처리는 A에 대한 응답을 받아야만 다음 요청으로 넘어갈 수 있지만, 비동기적 처리는 A에 대한 응답을 받지 않아도 다음 요청을 처리하러 갈 수 있다. 즉, 비동기적 처리는 특정 요청에 대한 즉각적인(동시적인) 응답을 필요로 하지 않는다.
그리고 위 예시에서도 알 수 있듯이, 비동기적 처리가 훨씬 효율적이다. 웹에서는 수많은 요청과 응답이 오고 가는데, 그 요청 하나하나, 그 응답 하나하나를 모두 기다렸다가 처리할 수 없을 것이다. 따라서 프로그래밍에서는 비동기 처리를 해야할 일이 많다.
그러나 비동기 처리를 하다보면 마주치는 문제가 있다. 동기적 처리에서는 그냥 함수를 순서대로 쭉 적으면 될 텐데 비동기적 처리에서는 그럴 수가 없다. 각 실행이 언제 끝나는지 따로 확인하고 작업을 처리해야 한다. 무슨 말이냐면, 예를 들어 우선 A함수, B함수를 실행시키고, A함수가 끝나면 C함수를 실행시키고 싶다고 하자. 이 상황에서 A함수 언제 끝나는지 어떻게 아는가? 그걸 알아야 C를 실행시킬 수 있는데. 단순히 A함수, C함수, B함수 순서대로 코드를 작성하면 되는가? 그럼 B함수는 C함수의 실행 순서에 영향을 받을 텐데, 그건 비동기가 아닌데?🤔 즉, 'A함수가 끝나면 C함수를 실행시켜라'라는 코드를 작성하는 것이 개발자의 몫이 된다. 그리고 이게 프로그래밍 언어에서 비동기 처리를 이해해야 하는 이유이다.
# 자바스크립트에서의 비동기 처리
자바스크립트에서 비동기 작업을 처리하는 방식은 크게 3가지가 있다. callback 함수, Promise, async/await 이다.
1. callback 함수
자바스크립트에서 함수는 일급 객체라서, 함수는 또 다른 함수의 입력값으로 들어갈 수 있다. 이때 다른 함수의 입력값으로 들어가는 함수를 callback 함수라고 한다. 이 callback 함수를 이용하면 비동기 처리를 할 수 있다. A라는 동작이 끝나면 B라는 동작을 하고 싶다고 치자. 그렇다면 A함수 안에 B함수를 넣으면 된다.
const one = () => {
console.log("one");
two();
};
const two = () => {
console.log("two");
};
one();
one 함수 안에 two 함수를 넣었다. 그럼 one 함수 안에 있는 코드가 실행되고 나서 two 함수 안의 코드가 실행된다. 이렇게 callback 함수로 비동기 작업을 처리할 수 있다. 그런데 만약, callback 함수가 아주 많다면? 위 예시는 함수의 수도 적고 함수 내의 코드 양도 적고 단순해서 괜찮지만, 실제 개발 과정에서는 함수 내의 코드가 매우 복잡할 것이다. 복잡한 코드가 담겨있는 함수들을 모두 callback으로 불러서 작성한다면 코드의 가독성이 매우 떨어지게 되고 유지 보수에 어려움이 생긴다. 이를 callback 지옥이라고 한다.
step1(function(value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
step5(value4, function(value5) {
// value5를 사용하는 처리
});
});
});
});
});
callback 지옥을 해결하기 위해서 또 다른 해결 방법이 등장했는데, 바로 Promise와 async/await이다.
2. Promise
Promise는 ES6부터 채택된 방식으로 new Promsie로 객체를 생성하여 비동기 작업을 수행한다. Promise 생성자 함수는 비동기 작업을 수행할 resolve와 reject 함수를 인자로 전달받는다.
const promise = new Promise((resolve, reject) => {
// 비동기 작업을 수행한다.
if (/* 비동기 작업 수행 성공 */) {
resolve('result');
}
else { /* 비동기 작업 수행 실패 */
reject('failure reason');
}
});
Promise는 내부에서 비동기 처리 작업을 수행한다. 이때 비동기 처리가 성공하면 resolve 함수를 호출하고 Promise의 상태는 ‘fulfilled’가 된다. 비동기 처리가 실패하면 reject 함수를 호출하고 Promise의 상태는 ‘rejected’가 된다.
상태 | 의미 | 상세 내용 |
pending | 비동기 처리가 아직 수행되지 않음 | resolve 혹은 reject가 실행되기 전 |
fulfilled | 비동기 처리 성공 | resolve 실행 |
rejected | 비동기 처리 실패 | reject 실행 |
settled | 비동기 처리가 수행 완료된 상태 (성공 or 실패 완료) | resolve 혹은 reject가 실행됨 |
Promise를 이용한 비동기 처리의 예시는 다음과 같다.
function getData() {
return new Promise(function(resolve, reject) {
const data = 100;
resolve(data);
});
}
getData().then(function(resolvedData) {
console.log(resolvedData); // 100
});
resolve 인자를 이용하여 함수를 실행한다. 함수 실행 후 또 다른 작업을 하고 싶다면 then을 붙여서 사용하면 된다. 위 예시에서는 getData 함수를 이용해서 100이라는 data를 받고, then을 이용하여 받은 data를 출력하는 순서대로 코드가 진행된다.
function getData() {
return new Promise(function(resolve, reject) {
reject(new Error("Request is failed"));
});
}
// reject()의 결과 값 Error를 err에 받음
getData().then().catch(function(err) {
console.log(err); // Error: Request is failed
});
reject 인자를 이용하면 실패 상태를 처리할 수 있다. 실패했을 시 수행하고 싶은 작업은 then이 아닌 catch로 작성한다. then과 catch를 모두 작성한 예시는 다음과 같다.
function getData(num) {
return new Promise(function (resolve, reject) {
if (num === 1) {
resolve("ok");
} else {
reject("error");
}
});
}
getData(1)
.then(function (result) {
console.log(result);
})
.catch(function (result) {
console.error(result);
});
getData의 인자로 들어온 값이 1이면 ok를 출력하고, 아니면 error를 보여주는 코드이다. 제대로 응답을 받아오면 resolve를 호출하고, 잘못된 응답에는 reject를 호출하는 것이다. 호출된 값에 따라 then()이나 catch()로 응답 결과 또는 오류를 출력할 수 있다.
3. async/await
비동기 처리 문법 중 가장 최근에 나온 것이다. Promise 방식보다 더 간단하게 비동기 코드를 처리한다.
3.1. async
함수 앞에 async만 붙여주면 Promise 객체를 리턴하는 함수로 만들어준다.
async function example() {
return "hello";
}
example(); // "hello"
리턴값이 문자열이어서 출력값도 단순 문자열이지만, example 함수 자체는 Promise 객체를 리턴하고 있다.
async function example() {
return "hello";
}
console.log(example());
위 예시처럼 example() 자체를 출력하면 Promise 객체가 리턴된 것을 확인할 수 있다. 함수 앞에 async를 붙이면 자동으로 Promise 객체를 리턴하는 함수가 되고, return 값이 resolve() 값이 되는 것이다.
3.2. async/await
async는 await와 같이 사용된다. 함수 앞에는 async를 붙이고, 비동기로 처리해야 하는 부분에 await를 붙인다. async와 await로 작성된 코드는 다음과 같은 의미를 지닌다.
- async가 붙은 함수를 Promise를 반환하고, Promise가 아닌 것은 Promise로 감싸서 반환한다.
- await는 Promise가 처리(settled)될 때까지 기다린다.
- Promise 처리가 완료되어 resolve 값이 나오면, 값만 따로 추출해서 return한다.
즉, 기존의 Promise 문법을 좀 더 깔끔하게 처리할 수 있도록 해준다.
function delay() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), 1000);
});
}
async function printHello() {
await delay();
return "hello";
}
async function getData() {
let text = await printHello();
console.log(text);
}
getData(); // hello
await는 Promise가 끝날 때까지 기다렸다가, 끝나면 resolve의 값만 추출해서 리턴하는 특징이 있다. 따라서 위 예시에서는 printHello 함수에 있는 Promise를 기다렸다가 printHello의 리턴값인 "hello"를 값으로 추출해서 getData의 text 변수에 할당한다.
3.3. 예외 처리
Promise에서는 비동기 처리가 실패했을 경우 reject로 처리했다. async/await는 이를 try...catch로 처리한다.
function example() {
return new Promise(function (resolve, reject) {
reject("error");
});
}
async function example2() {
try {
await example();
} catch (error) {
console.log(error);
}
}
example2(); // error
Promise에서 reject 될 때까지 await하고, reject되면 error를 출력한다.
참고 자료
https://poiemaweb.com/es6-promise
https://velog.io/@songyi7091/%EB%8F%99%EA%B8%B0-%EB%B9%84%EB%8F%99%EA%B8%B0
https://youtu.be/m0icCqHY39U
https://joshua1988.github.io/web-development/javascript/promise-for-beginners/
'FE > JavaScript' 카테고리의 다른 글
[JavaScript] 이벤트 루프 (0) | 2023.02.21 |
---|---|
[JavaScript] 실행 컨텍스트 (0) | 2023.01.03 |
[JavaScript] 이벤트 (0) | 2022.11.05 |
[JavaScript] for, forEach, map 차이 (0) | 2022.11.02 |
[JavaScript] 메모리와 값의 복사 (0) | 2022.10.27 |