콜백(callback)은 '되돌아 호출해달라'는 명령이다. 즉 어떤 함수 X를 호출하면서 '특정 조건일 때 함수 Y를 실행해서 나에게 알려달라'는 요청을 함께 보내는 것. 이 요청을 받은 함수 X는 해당 조건이 갖춰졌는지 스스로 판단하고 Y를 직접 호출한다.
콜백함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다. 콜백함수를 위임받은 코드는 자체적인 내부 로직으로 콜백함수를 적절한 시점에 시행한다.콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가진다.
비동기(asynchronous) 동작은 원하는 때에 동작을 시작하도록 할 수 있으며 setTimeout
이 대표적인 예시다. 실무에서 사용하는 비동기 동작은 매우 다양하다(ex. 스크립트나 모듈 로딩)
src
에 있는 스크립트를 읽어오는 함수 loadScript(src)
를 예시로 살펴보자
function loadScript(src) {
// <script> 태그를 만들고 페이지에 태그를 추가
// 태그가 페이지에 추가되면 src에 있는 스크립트 로딩 및 실행합
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
// 해당 경로에 위치한 스크립트 불러오고 실행하기
loadScript('/my/script.js');// script.js엔 "function newFunction() {…}"이 있다
newFunction()
함수 loadScript(src)
는 <script src="…">
를 동적으로 만들고 이를 문서에 추가한다. 브라우저는 자동으로 태그에 있는 스크립트를 불러오고, 로딩이 완료되면 스크립트를 실행한다.
여기서 스크립트는 '비동기적으로' 실행된다. 로딩은 지금 시작되더라도 실행은 함수가 끝난 후에 되기 때문이다. 따라서 loadScript('/my/script.js')
아래 코드들은 스크립트 로딩이 종료되는 걸 기다리지 않는다.
스크립트 안에 다양한 함수가 정의되어있다고 할 때 loadScript(..)
를 호출하자마자 내부 함수를 호출하면 원하는대로 작동하지 않는다. 따라서 loadScript
에서 스크립트 로딩이 완료되었는지 여부를 알 수 있어야 한다.
loadScript
의 두 번째 인수로 스크립트 로딩이 끝난 후 실행될 함수인 콜백함수를 추가해보자
콜백 함수: 나중에 호출할 함수, 콜백 함수를 넘겨받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 시행하게 됨
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
이렇게 두 번째 인수로 전달된 함수(대개 익명 함수)는 원하는 동작(외부 스크립트 불러오기)이 완료되었을 때 실행된다.
이런 방식을 ‘콜백 기반(callback-based)’ 비동기 프로그래밍이라고 한다. 무언가를 비동기적으로 수행하는 함수는 함수 내 동작이 모두 처리된 후 실행되어야 하는 함수가 들어갈 콜백을 인수로 반드시 제공해야 한다.
두 개의 스크립트를 순차적으로 불러오고 싶을 때는 어떻게 해야할까? 가장 자연스러운 방법은 아래와 같이 콜백 함수 안에서 두 번째 loadScript
를 호출하는 것이다.
loadScript('/my/script.js', function(script) {
alert(`${script.src}을 로딩완료. 다음 스크립트 로딩 시작.`);
loadScript('/my/script2.js', function(script) {
alert(`두 번째 스크립트 로딩 성공.`);
});
});
그러나 만약 여기에 스크립트 개수가 추가된다면 계속 중첩을 시켜야한다.
// 콜백 지옥
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
loadScript('/my/script4.js', function(script) {
//.....
});
});
})
});
콜백 함수를 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상 => 가독성이 떨어지고 코드 수정도 어려움
콜백 지옥의 또 다른 예시
setTimeout(function (name){
var coffeeList = name;
console.log(coffeeList);
setTimeout(function(name) {
coffeeList += ', ' + name;
console.log(coffeeList);
setTimeout(function(name) {
coffeeList += ', ' + name;
console.log(coffeeList);
setTimeout(function(name){
coffeeList += ', ' + name;
console.log(coffeeList);
}, 500, '카페라떼');
}, 500, '카페모카');
}, 500, '아메리카노');
}, 500, '에스프레소');
가독성 문제와 어색함을 해결하기 위한 방법으로 익명의 콜백 함수를 모두 기명 함수로 전환할 수 있다.
var coffeeList = "";
var addEspresso = function (name) {
coffeeList = name;
console.log(coffeeList);
setTimeout(addAmericano, 500, "아메리카노");
};
var addAmericano = function (name) {
coffeeList += ", " + name;
console.log(coffeeList);
setTimeout(addMocha, 500, "카페모카");
};
var addMocha = function (name) {
coffeeList += ", " + name;
console.log(coffeeList);
setTimeout(addLatte, 500, "카페라떼");
};
var addLatte = function (name) {
coffeeList += ", " + name;
console.log(coffeeList);
};
setTimeout(addEspresso, 500, "에스프레소");
new Promise(function(resolve) {
setTimeout(function() {
var name = '에스프레소';
console.log(name);
resolve(name);
}, 500);
}).then(function(prevName) {
return new Promise(function(resolve) {
setTimeout(function() {
var name = prevName + ', 아메리카노';
console.log(name);
resolve(name);
}, 500);
});
}).then(function(prevName) {
return new Promise(function(resolve) {
setTimeout(function() {
var name = prevName + ', 카페모카';
console.log(name);
resolve(name);
}, 500);
});
}).then(function(prevName) {
return new Promise(function(resolve) {
setTimeout(function() {
var name = prevName + ', 카페라떼';
console.log(name);
resolve(name);
}, 500);
});
})
반복적인 내용을 함수화해서 짧게 표현
var addCoffee = function (name) {
return function (prevName) {
return new Promise(function (resolve) {
setTimeout(function () {
const newName = prevName ? (prevName + ', ' + name) : name;
console.log(newName);
resolve(newName);
}, 500);
});
}
};
addCoffee('에스프레소')()
.then(addCoffee('아메리카노'))
.then(addCoffee('카페모카'))
.then(addCoffee('카페라떼'))
함수 앞에 async
키워드를 추가하면 함수는 언제나 Promise를 반환하며 함수 안에서 await
를 사용할 수 있다.
비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고 함수 내부에서 실질적 비동기 작업이 필요한 위치마다 await
를 표기. Promise의 then
과 흡사한 효과를 얻을 수 있음
자바스크립는 await
키워드를 만나면 Promise가 처리될 때까지 기다린다. 결과는 그 이후 반환된다. Promise가 처리되길 기다리는 동안 엔진이 다른 일을 할 수 있으므로 CPU 리소스가 낭비되지 않는다.
await
는 promise.then
보다 좀 더 세련되게 프라미스의 result 값을 얻을 수 있도록 해주는 문법이다. promise.then
보다 가독성 좋고 쓰기도 쉽다.
var addCoffee = function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(name);
}, 500);
});
};
var coffeeMaker = async function () {
let coffeeList = '';
const _addCoffee = async function (name) {
coffeeList += (coffeeList ? ',' : '') + await addCoffee(name);
};
await _addCoffee('에스프레소');
console.log(coffeeList);
await _addCoffee('아메리카노');
console.log(coffeeList);
await _addCoffee('카페모카');
console.log(coffeeList);
await _addCoffee('카페라떼');
console.log(coffeeList);
};
coffeeMaker();
큰 도움이 되었습니다, 감사합니다.