3. Callback -> Promise -> async/await
1. 콜백지옥
다음과 같이 비동기로 작동되는 함수가 있다.
이 비동기 함수는 2초 뒤에 Dain라는 이름을 인자로 받은 콜백함수의 인자로 넘겨준다.
function getName(cb) {
setTimeout(() => {
cb("Dain");
}, 2000);
}
앞선 함수를 실행하려면 다음과 같이 getName 함수에 콜백함수를 넣어서 사용할 수 있다.
getName((name) => {
console.log(name);
})
// 2초 후 Dain
만약 getName 함수를 이용해서 Dain이라는 이름을 3번 출력하려면 어떻게 해야할까?
다음과 같이 getName 함수를 절차적으로 실행시키면 2초 뒤에 Dain 이라는 이름이 나온다.
getName((name) => {
console.log(name);
})
getName((name) => {
console.log(name);
})
getName((name) => {
console.log(name);
})
// 2초 후
// Dain
// Dain
// Dain
하지만 앞선 방법으로 콜백함수를 호출하면 각 함수에 대한 데이터를 사용할 수 없다.
예를들어 이름, 나이, 주소가 저장된 데이터를 비동기적으로 가져와야 한다고 가정해 보자.
function getName(cb) {
setTimeout(() => {
cb("Dain");
}, 2000);
}
function getAge(cb) {
setTimeout(() => {
cb(30);
}, 2000);
}
function getAddress(cb) {
setTimeout(() => {
cb("Seoul");
}, 2000);
}
해당 정보 출력하고자 한다. 하지만 console.log를 한 번만 사용해야한다. 어떻게 해야할까?
getName((name) => {
getAge((age) => {
getAddress((address) => {
console.log(name, age, address)
})
})
})
이렇게 콜백함수 안에 콜백 함수를 반복하여 호출해야 name, age, address에 한번에 접근할 수 있다.
비동기 함수가 3개 쓰이고, 각 2초씩 걸리기 때문에 6초 뒤에 Dain 30 Seoul이라는 log가 출력될 것이다.
바로 이것을 콜백 지옥이라고 부른다. 비동기 함수를 3개만 썼을 뿐인데도, 코드가 정말 복잡해지고 느리다.
비동기 함수가 더 많다면 관리하기 더 힘들겠죠?
2. Promise
Promise를 이용하여 콜백 지옥을 보기 좋게 바꿀 수 있다.
앞서 사용한 비동기 함수인 getName, getAge, getAddress를 가져오고 Promise 객체를 만들어서 다음과 같이 수정 해 볼 수 있다.
function getName() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Dain");
}, 2000);
})
}
function getAge() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(30);
}, 2000);
})
}
function getAddress() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Seoul");
}, 2000);
})
}
각 함수는 Promise 객체를 리턴한다. 그리고 Promise 객체는 항상 2초 후에 resolve 되고 이름, 나이, 주소에 대한 정보를 주도록 했다.
이것을 다음과 같이 호출해서 사용할 수 있다.
getName().then((res) => {
console.log(res);
})
getAge().then((res) => {
console.log(res);
})
getAddress().then((res) => {
console.log(res);
})
하지만 이렇게 사용하면 정보를 하나의 함수에서 제어하기 힘들어진다.
그래서 아래와 같이 다시 수정해 줄 수 있다.
Promise
.all([getName(), getAge(), getAddress()])
.then((res) => {
const [name, age, address] = res;
console.log(name, age, address)
})
Promise.all은 첫번째 인자에 배열을 받는다. 그리고 그 배열의 원소는 모두 프로미스 객체이다.
getName, getAge, getAddress 함수는 모두 프로미스 객체를 반환하기 때문에 Promise.all에서 사용할 수 있다. 또한, Promise.all은 병렬적으로 배열의 원소에 있는 프로미스를 동시에 실행시킨다. 따라서 결과적으로 2초 후에 Dain 30 Seoul을 출력할 수 있다. 즉, 동시에 Promise 객체를 반환하는 함수들을 실행할 수 있는 것이므로 매우 편리하다. 콜백함수로는 할 수 없는 일이다.
3. async/await
위 과정을 더 간단하게 사용하는 방법이 있다.
프로미스를 더 간단하게 사용하려면 즉시실행 함수 형태에 async 화살표 함수를 이용해 코드를 작성한다.
await 키워드에서 프로미스가 resolve 될 때까지 기다린 후 다음 순서로 넘어가기 때문에 6초 후에 Dain 30 Seoul 이 콘솔에 출력된다.
(async () => {
const name = await getName();
const age = await getAge();
const address = await getAddress();
console.log(name, age, address);
})();
결론
Promise와 async/await는 효과적으로 사용될 수 있는 상황이 다르다. 그래서 두 방법 모두 잘 알고 있어야 한다. 메소드 체이닝이 많이 사용되는 코드에서는 Promise가 코드에 일관성을 지켜서 더 깔끔하게 보일 수 있고, 개별 함수를 호출하여 값을 받아오는 경우에는 asyne/await이 효과적이다.
No Comments