JS 호스트 환경이 제공하는 여러 함수를 사용하면 비동기 동작을 스케줄링 할 수 있다
실무에서 맞닥뜨리는 비동기 동작은 아주 다양한데 스크립트나 모듈을 로딩하는 것 또한 비동기 동작이다
function loadScript(src) {
// <script> 태그를 만들고 페이지에 태그를 추가
// 태그가 페이지에 추가되면 src에 있는 스크립트를 로딩하고 실행
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
loadScript(src)
는 <script src="..">
를 동적으로 만들고 이를 문서에 추가한다
브라우저는 자동으로 태그에 있는 스크립트를 불러오고, 로딩이 완료되면 스크립트를 실행한다
📝 loadScript(src) 사용법
// 해당 경로에 위치한 스크립트를 불러오고 실행함
loadScript('/my/script.js');
이 때 스크립트는 비동기적으로 실행된다 👉 로딩은 당장 시작되더라도 실행은 함수가 끝난 후에야 되기 때문
loadScript('/my/script.js');
// loadScript 아래의 코드는
// 스크립트 로딩이 끝날 때까지 기다리지 않는다
// ...
따라서 loadScript(...)
아래에 위치한 코드들은 스크립트 로딩이 종료되는 걸 기다리지 않는다
loadScript('/my/script.js');
// script.js엔 "function newFunc() {…}"이 있다
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('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
// 세 스크립트 로딩이 끝난 후 실행됨
});
})
});
콜백 안에 콜백을 넣는 것은 수행하려는 동작이 단 몇 개 뿐이라면 괜찮지만, 동작이 여러개인 경우엔 좋지 않다
스크립트 로딩에 실패했을 경우를 고려하여 로딩 에러를 추적할 수 있게 기능을 개선하자면
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 {
// 스크립트 로딩이 성공적으로 끝남
}
});
loadScript
는 스크립트 로딩에 성공하면 callback(null, script)
을, 실패하면 callback(error)
을 호출한다
👉 이런 패턴을 오류 우선 콜백(error-first callback) 이라고 한다
오류 우선 콜백은 다음 관례를 따른다
callback
의 첫 번째 인수는 에러를 위해 남겨둔다. 에러가 발생하면 이 인수를 이용해 callback(err)
을 호출한다.
두 번째 인수는 에러가 발생하지 않았을 때를 위해 남겨둔다. 원하는 동작이 성공한 경우엔 callback(null, result1, result2, ...)
을 호출한다.
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 {
// ...
}
};
각 동작을 독립적인 함수로 만들어 각각의 동작을 분리해 최상위 레벨의 함수로 만들었기 때문에 깊은 중첩을 피할 수 있다
하지만 이렇게 작성하면 코드가 분산되어 가독성이 떨어지고 각각의 함수들은 재사용이 불가능 하므로 Promise를 이용해서 개선시킬 필요가 있다