자바스크립트 호스트 환경(자바스크립트가 구동되는 환경)이 제공하는 여러 함수를 사용하면, 비동기 동작을 스케쥴링 할 수 있다. 원하는 때에 동작시킬 수 있다는 말이다. 이중 setTimeout
이 대표적이다.
src
에 있는 스크립트를 읽어오는 함수 loadScript(src)
역시 비동기 처리다.
function loadScript(src) {
// <script> 태그생성, 페이지에 태그를 추가
// 태그가 페이지에 추가되면 src에 있는 스크립트를 로딩하고 실행
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
loadScript(path)
로 실행된 스크립트는 비동기적으로 실행된다.
로딩은 되어도 실행 자체는 함수자체가 끝나야하기 때문이다.
따라서, loadScript(path)
아래에 있는 코드들은 스크립트 로딩이 종료되는 것을 기다리지 않는다.
가령 path = /my/script.js
라고 가정한다면, loadScript(path)
를 호출하자마자 script.js
내부의 함수들을 호출한다면 에러가 발생할 수 있다. 브라우저도 스크립트를 읽어오는 데 시간이 필요하기 때문이다. 따라서, 브라우저가 스크립트를 모두 읽은 후에 내부함수를 호출하는 방식이 되어야 한다.
loadScript
의 두 번째 인수로 스크립트 로딩이 끝난 후 실행될 콜백(callback)
함수를 추가하여 해결할 수 있다.
function loadScript(src,callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
새롭게 불러온 스크립트에 있는 함수를 콜백 함수 안에서 호출하면 원하는 대로 외부 스크립타 안의 함수를 사용할 수 있게 된다.
loacScript(path,function(){
newFunction();
...
});
이런식으로 두 번째 인수로 전달된 함수는 원하는 동작이( 위 예제에서는 외부 스크립트를 불러오는것) 이 완료되었을 때 실행된다.
이러한 방식을 콜백 기반
비동기 프로그래밍이라고 한다. 무언가를 비동기적으로 수행하는 함수는 함수 내 동작이 모두 처리된 후 실행되어야 하는 함수가 들어갈 콜백
을 인수로 반드시 제공해야 한다.
스크립트가 2가지 있는 경우, 2개의 스크립트를 순차적으로 불러오는 방법을 구현해보자.
loadScript('/my/script.js', function(script) {
alert(`${script.src}loaded. next.`);
loadScript('/my/script2.js', function(script) {
alert(`script2 loaded.`);
});
});
이런 방식은, 동작이 많은경우에 비효율적이다.
로딩이 실패할 경우에 에러를 추적할 수 있게 콜백 함수를 구현해보자.
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`error occured while loading ${src}.`));
document.head.append(script);
}
loadScript
는 스크립트 로딩에 성공하면, callback(null,script)
, 실패시 callback(error)
을 호출한다.
if else
구문을 이용하여 다음과 같이 사용한다. 이를 오류 우선 콜백이라고 한다.
loadScript(path, function(error, script) {
if (error) {
// Error
} else {
// success
}
});
callback
의 첫 번째 파라미터는 에러를 위해 남겨둔다. 에러가 발생하면, `callback(error) 호출
두 번째 파라미터는 성공적으로 실행되었을때 사용된다.
콜백을 무작위로 남발하게 되었을 때의 상황을 콜백 지옥이라고 한다. 콜백 지옥을 피하기 위해서는 다음과 같은 방법을 사용할 수 있다.
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
를 사용하는 것이다.
let promise = new Promise(function(resolve,rejevt){
// executor
});
new Promise
에 전달되는 함수를 executor(실행자,실행함수)
라고 부른다. executor
는 new Promise
가 만들어질 때 자동으로 실행되며, 결과를 만들어내는 제작 코드를 포함한다.
executor
의 파라미터 resolve
와 reject
는 자바스크립트가 자체적으로 제공하는 콜백이므로 , 따로 신경쓸 필요 없다.
하지만, 어찌 되었든간에 파라미터로 넘겨준 콜백 중 하나를 반드시 호출해야 한다.
resolve(value)
- 일이 성공적으로 끝난 경우, 그 결과를 나타내는 value
와 함께 호출
reject(error)
- 에러 발생 시 에러 객체를 나타내는 error
와 함께 호출
즉, 위에서 구현한 콜백 처럼 new Promise
의 생성과 함께 executor
가 실행되며 실행 성공 여부에 따라 resolve
나 reject
를 호출한다.
new Promise
가 생성하여 반환하는 promise
객체의 내부 프로퍼티는 다음과 같다.
state
- 디폴트로 "pending"
이었다가 resolve
가 호출되면 "fulfilled"
, reject
가 호출되면 "rejected"
가 할당된다.
result
- 디폴트로 undefined
이었다가, resolve
호출시 value
가, reject(error)
호출시 error
가 할당된다.
즉 executor
는 promise
의 상태를 다음과 같이 변화시키다.
// `resolve(value)`호출
{
state: "fulfilled"
result : value
}
// `reject(error)` 호출
{
state : "rejected"
result : error
}
다음의 예시를 보자.
setTimeout
을 이용하여 executor
를 시간이 걸리게 끔 만들었다.
let promise = new Promise(function(resolve,reject){
setTimeout(() => resolve("done"),1000);
// 1초 뒤 "done"
});
위에서 언급하였듯, new Promise
에 의해 executor
는 자동으로 호출된다.
executor
가 호출되면, 1초 뒤에 resolve("done")
이 호출되며 다음과 promise
객체는 다음과 같은 상태가 된다.
{
state : "fulfilled"
result : "done"
}
resolve
가 호출된 promise
를 fulfilled promise
라고 부른다.
다음은 에러가 발생하는 상황을 보자.
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Error")), 1000);
// 1초 뒤 에러와 함께 실행 종료
});
1초 후 reject(...)
가 호출되면 promise
객체는 다음과 같다.
{
state : "rejected"
result : error
}
이처럼 성공 또는 실패한 promise
를 settled promise
라 부르며, 그와 반대로 성공 또는 실패 어느것도 이루어지지 않은 promise
를 pending promise
라고 한다.
resolve(value)
또는 reject(error)
가 호출되면, 그 이후에 등장하는
executor
의 resolve(value)
,reject(error)
은 무시된다.
Error
객체 사용reject(error)
는 Error
객체 또는 Error
를 상속받은 객체를 인수로 사용하는 것이 좋다.
resolve
, reject
함수 즉시 호출하기executor
는 대개 무언가를 비동기적으로 수행하는데, 즉시 호출 또한 가능하다.
.then
은 프라미스에서 가장 중요하며, 기본적인 메서드이다.
promise.then(
function(result) {...},
function(error){...}
.then
의 첫 번째 파라미터는 프라미스가 이행될 때 실행되는 함수이고 여기서 실행 결과를 받는다.
.then
의 두 번째 파라미터는 프라미스가 거절될 때 실행되는 함수이며, 여기서 에러를 받는다.
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// resolve 함수는 .then의 첫 번째 파라미터(함수)를 실행
promise.then(
result => alert(result), // 1초 후 "done!"을 출력
error => alert(error) // 무시
);
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});
// reject 함수는 .then의 두 번째 파라미터(함수)를 실행
promise.then(
result => alert(result), // 무시
error => alert(error) // 1초 후 "Error: 에러 발생!"를 출력
);
에러처리만 하고 싶을때 .then(null,handleErrorFunc)
의 형태로 구현 할 수 있다. 또, .catch(handleErrorFunc)
을 써도 된다. .then
을 이용하는것과 같은 방식으로 동작하며, 문법적 편의를 위한 용도이다.
결과가 어떻든 마무리가 필요할 때 사용한다.
new Promise((resolve, reject) => {
setTimeout(() => resolve("결과"), 2000)
})
.finally(() => alert("프라미스가 준비되었습니다."))
.then(result => alert(result)); // <-- .then에서 result를 다룰 수 있음
promise
가 "pending"
상태일 때, .then/catch/finally
핸들러는 promise
가 처리되길 기다리고, 이미 처리된 상태이면 핸들러는 즉각 실행된다.
위의 loadScript
를 프라미스로 바꿔보자.
function loadScript(src) {
return new Promise(function(resolve,reject){
let script = document.createElement("script");
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Error occured`));
document.head.append(script);
});
}
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => console.log(`${script.src}`),
error => console.log(`${error.message}`)
);
promise.then(script => console.log(` another handler `))