[js 문법] 비동기 처리 방법 3가지 - 콜백, 프로미스, async/await
자바스크립트의 기본적인 처리 방식은 비동기 방식이다.
비동기 방식이란 코드가 위에서 아래로 순차적으로 실행되지 않고,
동시에 시작되어 빨리 끝나는 코드가 먼저 실행 완료 될 수 있도록 하는 방식이다.
https://walwaldev.tistory.com/33
[개념 정리] 자바스크립트에서 동기 / 비동기란 무엇일까?
자바스크립트에서 동기(synchronous)와 비동기(asynchronous)는 코드의 실행 방식을 설명하는 용어입니다. 코드의 실행 방식의 두 종류 동기 비동기 코드 순차 실행 코드가 순차적으로 실행되지 않음
walwaldev.tistory.com
비동기 방식은 효율적이지만, 코드 실행에 선후 관계가 필요한 경우가 있다.
예를 들어 아래와 같은 경우 countFruit 함수 정의 후 console.log()를 실행해야 한다.
const countFruit = (fruit) => {
return 3
}
console.log(countFruit('apple'))
이렇게 비동기 환경에서 동기적인 실행이 필요한 경우,
아래와 같은 '비동기 처리 방식'을 사용해주어야 한다.
비동기 처리 방식은 크게 아래의 3가지 방식이 있다.
- 콜백(callback)
- 프로미스(promise)
- async/await
세 가지 비동기 처리 방식은
1부터 3까지 점점 더 편리한 방식으로 발전했다고 볼 수 있다.
세 가지 방식에 대해 알아보자.
1. 콜백(callback)
자바스크립트에서 콜백(callback)은 비동기 처리를 위해 많이 사용되는 방식 중 하나다.
콜백은 함수를 인자로 전달하고, 비동기 작업이 완료되면 해당 함수를 호출하여 작업 결과를 처리한다.
아래는 콜백 함수를 이용한 간단한 비동기 처리 예시이다.
function fetchData(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
if (xhr.status === 200) {
callback(null, xhr.response);
} else {
callback(new Error('Request failed'));
}
};
xhr.onerror = function() {
callback(new Error('Request failed'));
};
xhr.send();
}
fetchData('https://example.com', function(error, data) {
if (error) {
console.error(error);
} else {
console.log(data);
}
});
위 코드에서 fetchData 함수는 인자로 URL과 콜백 함수를 받는다.
XMLHttpRequest 객체를 사용하여 비동기 요청을 보내고, 요청이 완료되면 onload 이벤트 핸들러가 호출된다.
이때, 요청이 성공했을 경우에는 콜백 함수에 null과 요청 결과를 전달하고,
실패했을 경우에는 에러 객체를 전달한다.
이후, fetchData 함수를 호출할 때 콜백 함수를 인자로 전달하여 작업 결과를 처리한다.
콜백을 사용하면 비동기 작업을 순차적으로 처리하기 어렵고, 중첩된 구조로 코드가 복잡해지는 콜백 지옥(callback hell) 문제가 발생할 수 있다.
이러한 문제를 해결하기 위해 Promise나 async/await와 같은 방식을 사용할 수 있다.
* 콜백 지옥(callback hell)
콜백 지옥(callback hell)이란 콜백 함수를 중첩하여 코드가 복잡해지면서 발생하는 문제를 말한다.
콜백 함수를 이용한 비동기 처리에서
하나의 작업이 끝나면 다음 작업을 진행하기 위해 또 다른 콜백 함수를 호출하는 방식.
이렇게 콜백 함수를 중첩하여 사용하면 코드가 복잡해지고 가독성이 떨어질 수 있다.
아래의 코드를 예시로 들어보자.
getUser(function(user) {
getComments(user, function(comments) {
getReplies(comments, function(replies) {
// ...
});
});
});
위 코드에서 getUser 함수가 호출되어 사용자 정보를 가져온 후,
이어서 getComments 함수가 호출되어
사용자의 댓글을 가져온다.
그리고 다시 이어서 getReplies 함수가 호출되어
댓글에 달린 답글을 가져오는 작업을 수행한다.
이렇게 콜백 함수를 중첩하여 사용하면
코드가 길어지고 가독성이 떨어지며, 디버깅이 어렵고 오류 발생 가능성이 높아지므로 주의하자.
2. 프로미스(promise)
promise는 자바스크립트의 비동기 처리를 위한 "객체"이다.
promise는 최종적으로 성공 / 실패, 두 가지 상태를 가진다.
작업이 완료되면 이후 처리를 위해 then() 메서드를 호출할 수 있다.
Promise는 다음과 같은 세 가지 상태를 가진다.
- 대기(pending) : 이행이나 거부가 되지 않은 초기 상태
- 이행(fulfilled) : 작업이 성공적으로 완료된 상태
- 거부(rejected) : 작업이 실패한 상태
아래는 promise 객체 생성의 예시 코드이다.
const promise = new Promise(function(resolve, reject) {
// 비동기 작업 수행
if (/* 작업이 성공적으로 완료됐을 때 */) {
resolve(/* 결과 데이터 */);
} else {
reject(/* 에러 객체 */);
}
});
resolve 함수는 작업이 성공적으로 완료되었을 때 호출되며, 해당 작업의 결과를 인자로 전달한다.
reject 함수는 작업이 실패했을 때 호출되며, 에러 객체를 인자로 전달한다.
이후 then() 메서드를 호출하여 작업이 성공적으로 완료되었을 때의 처리와
catch() 메서드를 호출하여 작업이 실패했을 때의 처리를 수행할 수 있다.
promise
.then(function(result) {
// 작업이 성공적으로 완료되었을 때의 처리
})
.catch(function(error) {
// 작업이 실패했을 때의 처리
});
또한, then() 메서드 내에서 다시 promise 객체를 반환하여 연속된 비동기 작업을 처리할 수 있다.
promise
.then(function(result) {
return new Promise(function(resolve, reject) {
// 다른 비동기 작업 수행
if (/* 작업이 성공적으로 완료됐을 때 */) {
resolve(/* 결과 데이터 */);
} else {
reject(/* 에러 객체 */);
}
});
})
.then(function(result) {
// 다음 작업 수행
})
.catch(function(error) {
// 작업이 실패했을 때의 처리
});
3. async/await
기본적으로 비동기적으로 동작하는 node.js에서 동기적으로 동작하는 함수를 만들기 위해서는
async/await 문을 사용하여야 한다.
async는 항상 함수 선언 앞에 붙는다.
async function myAsyncFunction() {
// 비동기 처리를 수행하는 코드
}
async가 붙은 함수 자체는
선언된 후다른 곳에서 호출되어 사용될 때 비동기적으로 동작한다.
비동기 기반 node.js에서 동기적으로 코드를 실행하는 방식은, async 함수 안에서 await 키워드를 사용하는 것이다.
await는 '기다리다'라는 뜻을 가진 영단어로, async 함수 내에서 특정 실행문 앞에 await 키워드를 붙여주면,
해당 작업이 완료될 때까지 코드 실행이 일시 중지된다.
즉, await가 붙은 함수의 작업이 끝날 때 까지는 다음 코드로 진행되지 않기 때문에,
async 함수 내에서 await가 코드를 동기적으로 실행시킨다고 볼 수 있다.
하나의 async 함수 안에서 await는 여러번 사용될 수 있다.
await가 비동기 함수 내에서 중단점 같은 역할을 한다.
진행되던 코드들이 await를 만나면 일시 중지하고, await로 묶인 실행문이 처리될 때까지 기다리는 것이다.
[callback 함수에서 다음 callback 함수로 넘어가는 과정],
[promise의 .then 메서드]와 await의 역할이 같고,
단계를 거쳐 점점 더 사용하기 편하게 발전했다고 볼 수 있다.
주의할 점은, await는 async 함수 안에서만 동작한다는 점이다.
async 함수 내에 있지 않다면(함수 앞에 async를 붙이지 않았다면) await 문을 사용할 수 없다.
async function myAsyncFunction() {
const result = await myAsyncOperation();
console.log(result);
}
또, await를 사용하는 함수는 promise를 반환해야 한다.
종종 await 사용 순서가 잘못되어 올바른 순서대로 함수가 동작하지 않을 때 특정 변수가 promise.pending으로 찍히는 모습을 볼 수 있다.
(promise는 비동기 실행의 근간이 되는 객체로 node.js 비동기 처리의 근본이다.)
promise가 아닌 값을 반환하면 자동으로 Promise.resolve(value)로 감싸진다.
async function myAsyncFunction() {
return "Hello, world!";
}
myAsyncFunction().then(result => {
console.log(result); // "Hello, world!"
});
아래와 같이 async/await 문에서 try/catch를 사용하여 예외처리를 할 수도 있다.
(try/catch문은 async/await문이 아니더라도, js 내에서 어디서든 사용될 수 있는 예외처리 문법이다.)
async function myAsyncFunction() {
try {
const result = await myAsyncOperation();
console.log(result);
} catch (error) {
console.error(error);
}
}