콜백(Callback)
비유로 이해하는 콜백함수
콜백 함수의 동작 방식은 유명 맛집 자리 예약과 같습니다. 유명한 맛집을 가면 자리가 없을때가 있어서 대기자 명단에 이름과 연락처를 쓰고 자리가 날 때까지 주변을 돌아다니죠. 만약 식당에 자리가 생기면 전화로 자리가 났다고 연락이 옵니다. 그 전화를 받는 시점이 콜백 함수가 호출되는 시점과 같습니다. 손님 입장에서는 자리가 날 때까지 식당에서 기다리지 않고 근처 가게에서 잠깐 쇼핑을 할 수도 있고 아니면 다른 식당 자리를 알아볼 수도 있습니다. 자리가 났을 때만 연락이 오기 때문에 미리 가서 기다릴 필요도 없고, 직접 식당 안에 들어가서 자리가 비어 있는지 확인할 필요도 없습니다. 자리가 준비된 시점, 즉 데이터가 준비된 시점에서만 우리가 원하는 동작(자리에 앉는다 / 특정 값을 출력한다 등)을 수행할 수 있습니다.
콜백 에러 핸들링(Error handling)
const getDataFromFile = function (filePath, callback) {
fs.readFile(filePath, "utf8", function (err, file) {
if (err) {
callback(err, null);
} else {
callback(null, file.toString());
}
});
};
위 코드는 fs.readFile을 통해 파일을 읽어올 때, 파일을 읽는데 실패할 경우(error) callback(err,null)을 호출하고, 성공했을 경우 callback(null, file.toString())를 호출합니다. 위와 같은 패턴으로 에러를 처리하는 방식을 오류 우선 콜백(error-first callback)이라고 합니다.
오류 우선 콜백은 다음 관례를 따릅니다.
- callback의 첫 번째 인수는 에러를 위해 남겨둡니다. 에러가 발생하면 이 인수를 이용해 callback(err)이 호출됩니다.
- 두 번째 인수(필요하면 인수를 더 추가할 수 있음)는 에러가 발생하지 않았을 때를 위해 남겨둡니다. 원하는 동작이 성공한 경우엔 callback(null, result1, result2...)이 호출됩니다.
콜백 지옥
콜백 기반 비동기 처리는 언뜻 봤을 때 꽤 쓸만해 보이고, 실제로도 그렇지만 꼬리에 꼬리를 무는 비동기 동작이 많아지면 아래 그림과 같은 코드 작성이 불가피해집니다.
[그림] 콜백 지옥의 예위 그림과 같이 콜백에 콜백에 콜백....으로 이루어진 코드처럼 깊은 중첩 코드가 만들어내는 패턴은 소위 ‘콜백 지옥(callback hell)’ 혹은 '멸망의 피라미드(pyramid of doom)'라고 불립니다.
이러한 콜백 지옥을 벗어나기 위해서는 Promise나 async, await을 이용하여 코드를 작성하는 것이 좋습니다.
프로미스(Promise)
프로미스의 3가지 상태(states)
여기서 말하는 상태란 promise의 처리 과정을 의미합니다. new Promise()로 promise를 생성하고 종료될 때까지 아래와 같은 3가지 상태를 갖습니다.
- Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
- Fulfilled(이행) : 비동기 처리가 완료되어 promise가 결과 값을 반환해준 상태
- Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태
new Promise(); //new Promise() 메서드를 호출하면 대기(Pending) 상태가 된다.
new Promise(function(resolve, reject) {
// ...
}); //new Promise() 메서드를 호출할 때 콜백 함수를 선언할 수 있고, 콜백 함수의 인자는 resolve, reject 이다.
new Promise(function(resolve, reject) {
resolve();
}); //콜백 함수의 인자 resolve를 아래와 같이 실행하면 이행(Fulfilled) 상태가 된다.
new Promise(function(resolve, reject) {
reject();
}); // reject를 아래와 같이 호출하면 실패(Rejected) 상태가 된다.
function getData() {
return new Promise(function(resolve, reject) {
var data = 100;
resolve(data);
});
} // promise 예시
Promise chaining
promise에서는 then 메소드를 이용하여 여러 개의 promise를 연결하여 사용할 수 있습니다. then() 메서드를 호출하고 나면 새로운 promise 객체가 반환됩니다.
function getData() {
return new Promise({...});
}
// then() 으로 여러 개의 프로미스를 연결한 사용자 로그인 인증 로직 예시
getData(userInfo)
.then(parseValue)
.then(auth)
.then(display);
위 예시처럼 유저 정보를 받고, 파싱하고, 인증을 받고, 화면에 나타내는 등의 작업을 then()을 통해 연결하여 차례대로 진행할 수 있습니다.
Promise.all
promise.all은 요소 전체가 promise인 배열(엄밀히 따지면 이터러블 객체이지만, 대개는 배열)을 받고 새로운 promise를 반환합니다.
배열 안 promise가 모두 처리되면 새로운 promise가 이행되는데, 배열 안 promise의 결괏값을 담은 배열이 새로운 promise의 result가 됩니다. 주어진 프로미스 중 하나가 거부하는 경우, 첫 번째로 거절한 프로미스의 이유를 사용해 자신도 거부합니다.
Promise.all은 여러 프로미스의 결과를 집계할 때 유용하게 사용할 수 있습니다.
반환한 프로미스의 이행 결과값은 (프로미스가 아닌 값을 포함하여) 매개변수로 주어진 순회 가능한 객체에 포함된 모든 값을 담은 배열입니다.
- 빈 객체를 전달한 경우, (동기적으로) 이미 이행한 프로미스를 반환합니다.
- 전달받은 모든 프로미스가 이미 이행되어 있거나 프로미스가 없는 경우, 비동기적으로 이행하는 프로미스를 반환합니다.
문법과 예시:
let promise = Promise.all([...promises...]);
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
Promise error handling
promise의 에러 처리 방법에는 다음과 같이 2가지 방법이 있습니다.
getData().then(
handleSuccess,
handleError
); // 1. then()의 두 번째 인자로 에러를 처리하는 방법
getData().then().catch(); // 2.catch()를 이용하는 방법
이처럼 2가지 방법이 있지만, 가급적 catch()로 에러를 처리하는 게 더 효율적입니다.
async await
async와 await는 자바스크립트의 비동기 처리 패턴 중 가장 최근에 나온 문법으로, 이 특별한 문법을 사용하면 promise를 좀 더 편하게 사용할 수 있습니다.
async 함수
async function f() {
return 1;
}
f().then(alert); // 1
function 앞에 async라는 예약어를 붙이면 해당 함수는 항상 promise를 반환합니다. promise가 아닌 값을 반환하더라도 이행 상태의 promise(resolved promise)로 값을 감싸 이행된 promise가 반환되도록 합니다.
await
await는 async 함수 안에서만 동작합니다. javascript는 await 키워드를 만나면 promise가 처리(settled)될 때까지 기다립니다.
결과는 그 이후 반환됩니다. promise가 처리되길 기다리는 동안엔 엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않습니다.
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});
let result = await promise; // promise가 이행될 때까지 기다린다.
alert(result); // "완료!"
}
f();
에러 핸들링
promise가 정상적으로 resolve되면 await promise는 promise 객체의 result에 저장된 값을 반환합니다.
반면 promise가 reject되면 마치 throw문을 작성한 것처럼 에러가 던져집니다.
async function f() {
await Promise.reject(new Error("에러 발생!"));
}
await가 던진 에러는 throw가 던진 에러를 잡을 때처럼 try..catch를 사용해 잡을 수 있습니다.
async function f() {
try {
let response = await fetch('http://유효하지-않은-주소');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
출처 : https://velog.io/@pizzahand/TIL-20210623-Callback-Promise-async-await
'자바스크립트 > 기초' 카테고리의 다른 글
Math.trunc() (0) | 2023.06.28 |
---|---|
번들링과 웹팩 (0) | 2021.11.30 |
비동기 (0) | 2021.07.30 |
JSON (0) | 2021.07.21 |
객체 지향 프로그래밍(OOP, Object-oriented programming) (1) | 2021.07.19 |