function loadScript(src) {
// <script> 태그를 만들고 페이지에 태그를 추가합니다.
// 태그가 페이지에 추가되면 src에 있는 스크립트를 로딩하고 실행합니다.
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
// 해당 경로에 위치한 스크립트를 불러오고 실행함
loadScript('/my/script.js');
// loadScript 아래의 코드는
// 스크립트 로딩이 끝날 때까지 기다리지 않습니다.
// ...
문제점?
js 로딩이 완료 되지 않았을 때, js파일 내에 작성된 함수를 호출할 경우 에러가 발생.
loadScript('/my/script.js'); // script.js엔 "function newFunction() {…}"이 있습니다.
newFunction(); // 함수가 존재하지 않는다는 에러가 발생합니다!
=> 함수의 두번째 함수로, 스크립트 로딩이 끝난 후 실행될 함수를 뜻하는 콜백 함수를 추가해 해결 가능.
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 {
// 스크립트 로딩이 성공적으로 끝남
}
});
=> 오류 우선 콜백('error-first 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가 이러한 멸망의 피라미드를 피할 제대로 된 방법.
문법
let promise = new Promise(function(resolve, reject) {
// executor (제작 코드, '가수')
});
- resolve, reject는 자바스크립트가 자체 제공 하는 콜백. executor 안쪽 코드만 작성하면 된다.
executor가 실행되면, 결과에 따라 Resolve 또는 Reject가 호출된다.
new Promise 생성자가 반환하는 promise 객체가 갖는 내부 프로퍼티
resolve, reject 함수는 미리 정의 되어 있으므로, 호출만 해주면 됨. (resolve나 reject중 하나는 반드시 호출해야 한다.)
- fulfilled promise : successfully executed
- settled promise : revolsed or rejected
- pending promise
promise는 성공 또는실패만 한다. 이때 변경된 상태는 더 이상 변하지 않는다. 처리가 끝난 프라미스에 resolve, reject 호출 시 무시됨.
let promise = new Promise(function(resolve, reject) {
resolve("완료");
reject(new Error("…")); // 무시됨
setTimeout(() => resolve("…")); // 무시됨
});
If something goes wrong.
Error객체나 Error상속 받은 객체를 인수로 reject를 호출. (인수로 다른값도 가능하지만, 가능하면 Error관련 객체 사용)
state, result는 내부 프로퍼티라서 직접 접근 불가.
대신 .then/.catch/.finally 메서드를 사용해 접근 가능.
Then, catch, finally
.then
promise.then(
function(result) { /* 결과(result)를 다룹니다 */ },
function(error) { /* 에러(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: 에러 발생!"을 출력
);
.catch
에러가 발생한 경우만 다룸.
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});
// .catch(f)는 promise.then(null, f)과 동일하게 작동합니다
promise.catch(alert); // 1초 뒤 "Error: 에러 발생!" 출력
.finally
프라미스가 처리되면 반드시 실행됨. 결과가 어떻든 마무리가 필요한 경우 유용하게 사용 가능.
new Promise((resolve, reject) => {
/* 시간이 걸리는 어떤 일을 수행하고, 그 후 resolve, reject를 호출함 */
})
// 성공·실패 여부와 상관없이 프라미스가 처리되면 실행됨
.finally(() => 로딩 인디케이터 중지)
.then(result => result와 err 보여줌 => error 보여줌)
예) loadScript
콜백 기반으로 작성된 함수
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);
}
프라미스를 이용해 다시 작성한 함수.
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(`${src}를 불러오는 도중에 에러가 발생함`));
document.head.append(script);
});
}
사용 방법.
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src}을 불러왔습니다!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('또다른 핸들러...'));
프라미스 사용에 대한 장점
프라미스 체이닝 : result가 .then의 핸들러 체인을 통해 전달된다는 점에서 착안된 아이디어.
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
프라미스 반환하기.
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) { // (**)
alert(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
alert(result); // 4
});
프라미스 .then 사용 시, then에 사용된 핸들러가 프라미스를 생성하거나, 반환하는 경우도 있다.
이 경우 이어지는 핸들러는 프라미스가 처리될 때까지 기다리다가 처리가 완료되면 그 결과를 받음.
//1초의 딜레이를 갖고 얼럿 창이 띄워지며, 1, 2, 4가 차례대로 출력된다.
*** 핸들러 안에서 프라미스를 반환하는 것으로 비동기 작업 체이닝을 가능하게 해주는 방법.
loadScript 예시 개선하기
loadScript를 프라미스 체이닝을 이용해 중첩하여 로딩.
loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
// 스크립트를 정상적으로 불러왔기 때문에 스크립트 내의 함수를 호출할 수 있습니다.
one();
two();
three();
});
*** handler => event handler : promise의 .then같은 것.
fetch와 체이닝 함께 응용하기
프론트 단에서, 네트워크 요청 시 프라미스를 자주 사용.
let promise = fetch(url);
=> url에 네트워크 요청을 보내고 프라미스를 반환한다. 원격 서버가 '헤더+응답'을 보내면, 프라미스는 response객체와 함께 이행.
그러나, 이때 response전체가 완전 다운로드되기 전에 프라미스가 이행 상태가 되어 버린다.
응답이 완전 종료 되고 응답 전체를 읽으려면 response.text() 호출.
fetch('/article/promise-chaining/user.json')
// 원격 서버가 응답하면 .then 아래 코드가 실행됩니다.
.then(function(response) {
// response.text()는 응답 텍스트 전체가 다운로드되면
// 응답 텍스트를 새로운 이행 프라미스를 만들고, 이를 반환합니다.
return response.text();
})
.then(function(text) {
// 원격에서 받아온 파일의 내용
alert(text); // {"name": "Violet-Bora-Lee", "isAdmin": true}
});
response.json()를 쓰면 원격에서 받은 데이터를 읽고 JSON으로 파싱 할 수 있다.
// 위 코드와 동일한 기능을 하지만, response.json()은 원격 서버에서 불러온 내용을 JSON으로 변경해줍니다.
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => alert(user.name)); // Violet-Bora-Lee, 이름만 성공적으로 가져옴
- 프라미스가 거부되면 제어 흐름이 제일 가까운 rejection 핸들러로 넘어감.
.catch절은 여러 .then 뒤에 올 수 있음.
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));
위쪽 프라미스 중 하나라도 거부되면 .catch에서 에러를 잡는다.
암시적 try...catch
예외가 발생하면 암시적 try..catch에서 예외를 잡고 이를 reject처럼 다룬다.
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(alert); // Error: 에러 발생!
다시 던지기
체인 마지막의 .catch는 try..catch와 유사하게, 앞에서 사용된 .then 핸들러들에서 발생한 모든 에러를 처리 가능하다.
// 실행 순서: catch -> catch
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(function(error) { // (*)
if (error instanceof URIError) {
// 에러 처리
} else {
alert("처리할 수 없는 에러");
throw error; // 에러 다시 던지기
}
}).then(function() {
/* 여기는 실행되지 않습니다. */
}).catch(error => { // (**)
alert(`알 수 없는 에러가 발생함: ${error}`);
// 반환값이 없음 => 실행이 계속됨
});
처리되지 못한 Rejections
에러 발생 시 promise는 reject상태가 되고, 가장 가까운 rejection핸들러로 넘어간다. 그러나 만약 에러를 처리해줄 코드가 없으면, 에러가 갇혀 버린다.
이경우 스크립트가 죽어버림.
이런 문제를 해결하기 위해 unhandledreection이벤트를 사용해 처리 가능.
window.addEventListener('unhandledrejection', function(event) {
// unhandledrejection 이벤트엔 두 개의 특수 프로퍼티가 있습니다.
alert(event.promise); // [object Promise] - 에러를 생성하는 프라미스
alert(event.reason); // Error: 에러 발생! - 처리하지 못한 에러 객체
});
new Promise(function() {
throw new Error("에러 발생!");
}); // 에러를 처리할 수 있는 .catch 핸들러가 없음
브라우저 환경에서 에러가 발생했는데 .catch가 없으면 unhandledrejection 핸들러가 트리거 된다.
javascript.info - 프라미스와 async, await(3) (0) | 2023.01.02 |
---|---|
javascript.info - 프라미스와 async, await(2) (0) | 2023.01.02 |
javascript.info - 10.에러 핸들링 (0) | 2023.01.01 |
javascript.info - 9.클래스 (0) | 2022.12.30 |
javascript.info - 프로토타입과 프로토타입 상속 (0) | 2022.12.30 |