콜백
setTimeout은 스케줄링에 사용되는 가장 대표적인 함수
스크립트나 모듈을 로딩하는 것 또한 비동기 동작
콜백 기반(callback-based)’ 비동기 프로그래밍
<예시>
- 스크립트 읽어오는 함수
function loadScript(src) {
// <script> 태그를 만들고 페이지에 태그를 추가합니다.
// 태그가 페이지에 추가되면 src에 있는 스크립트를 로딩하고 실행합니다.
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
- 스크립트 로딩이 끝나자마자 이 스크립트를 사용해 무언가를 해야만 한다고 가정
// script.js엔 "function newFunction() {…}"이 있습니다.
loadScript('/my/script.js');
newFunction(); // 함수가 존재하지 않는다는 에러가 발생합니다!
- 에러는 브라우저가 스크립트를 읽어올 수 있는 시간을 충분히 확보하지 못했기 때문에 발생
- loadScript 의 두 번째 인수로 스크립트 로딩이 끝난 후 실행될 함수인 콜백함수를 추가
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
- 새롭게 불러온 스크립트에 있는 함수를 콜백 함수 안에서 호출
loadScript('/my/script.js', function() {
// 콜백 함수는 스크립트 로드가 끝나면 실행됩니다.
newFunction(); // 이제 함수 호출이 제대로 동작합니다.
...
});
→ 원하는 대로 외부 스크립트 안의 함수를 사용가능
무언가를 비동기적으로 수행하는 함수는 함수 내 동작이 모두 처리된 후 실행되어야 하는 함수가 들어갈 콜백을 인수로 반드시 제공해야 합니다. 이렇게 콜백을 사용한 방식은 비동기 프로그래밍의 일반적인 접근법
콜백 속 콜백
스크립트가 두 개 있는 경우, 어떻게 하면 두 스크립트를 순차적으로 불러올 수 있을까요?→ 방법은 콜백 함수안에 loadScript 를 호출하는 것, 이렇게 중첩 콜백을 만들면 바깥에 위치한 loadScript가 완료된 후, 안쪽 loadScript가 실행
!!! 콜백 안에 콜백을 넣는 것은 수행하려는 동작이 많은 경우엔 좋지 않은 방식
loadScript('/my/script.js', function(script) {
alert(`${script.src}을 로딩했습니다. 이젠, 다음 스크립트를 로딩합시다.`);
loadScript('/my/script2.js', function(script) {
alert(`두 번째 스크립트를 성공적으로 로딩했습니다.`);
});
});
에러 핸들링
loadScript에서 로딩 에러를 추적할 수 있게 기능을 개선
이러한 방식으로 에러를 다루는 패턴을 ⇒ 오류 우선 콜백(error-first callback) 이라고 한다
<예시>
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`${src}를 불러오는 도중에 에러가 발생했습니다.`));
document.head.append(script);
}
<사용방식>
loadScript('/my/script.js', function(error, script) {
if (error) {
// 에러 처리
} else {
// 스크립트 로딩이 성공적으로 끝남
}
});
오류 우선 콜백 의 관례
- callback의 첫 번째 인수는 에러를 위해 남겨둡니다. 에러가 발생하면 이 인수를 이용해 callback(err)이 호출됩니다.
- 두 번째 인수(필요하면 인수를 더 추가할 수 있음)는 에러가 발생하지 않았을 때를 위해 남겨둡니다. 원하는 동작이 성공한 경우엔 callback(null, result1, result2...)이 호출됩니다.
⇒ 오류 우선 콜백 스타일을 사용하면, 단일 콜백 함수에서 에러 케이스와 성공 케이스 모두를 처리할 수 있습니다.
멸망의 피라미드
- 꼬리에 꼬리를 무는 비동기 동작이 많아지면 깊은 중첩이 있는 코드가 만들어진다
- 이렇게 깊은 중첩 코드가 만들어내는 패턴은 소위 ‘콜백 지옥(callback hell)’ 혹은 '멸망의 피라미드(pyramid of doom)'라고 불립니다
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// 모든 스크립트가 로딩된 후, 실행 흐름이 이어집니다. (*)
}
});
}
})
}
});
각 동작을 독립적인 함수로 만들어 위와 같은 문제를 완화하는 방법?
loadScript('1.js', step1);
function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', step2);
}
}
function step2(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', step3);
}
}
function step3(error, script) {
if (error) {
handleError(error);
} else {
// 모든 스크립트가 로딩되면 다른 동작을 수행합니다. (*)
}
};
- 각 동작을 분리해 최상위 레벨의 함수로 만들었기 때문에 깊은 중첩이 없습니다.
- 코드가 여기저기 흩어져 보이는 문제가 있다.
- 게다가 step*이라고 명명한 함수들은 '멸망의 피라미드’를 피하려는 용도 만으로 만들었기 때문에 재사용이 불가능
→ 이를 해결할 수 있는 가장 좋은 방법 중 하나는 “프라미스” 를 사용하는 것이다.
참고자료
'프로그래밍 > JavaScript' 카테고리의 다른 글
[ JS ] 프라미스와 async / await : 프라미스 체이닝 (0) | 2024.06.04 |
---|---|
[ JS ] 프라미스와 async / await : promise (0) | 2024.06.04 |
[ JS ] 원시값, 참조값, 얕은 복사, 깊은 복사 (0) | 2024.05.20 |
[ JS ] 불변객체 immutable object (1) | 2024.05.11 |
[ JS ] Promise.allSettled 사용하는 이유 (0) | 2024.05.06 |