현업에서 웹 개발자로 일한지 어느덧 2년이 다 되가는 시점에서 비동기라는 개념을 애매하게 이해하고 있다고 생각되었습니다.
최근 봤던 저의 지식 밑천이 탈탈 털린(...) 면접에서 비동기에 대해 물었을 때 정말 자신없게 대답해서 너무 부끄러웠습니다ㅠ
이제는 비동기에 대한 개념을 제 것으로 확실하게 만들고 싶습니다.
1. 비동기/동기의 차이점
코어 자바스크립트에서 설명한 '비동기'는 A함수에서 B함수로 제어권을 넘겨 A함수는 B함수를 실행하고 바로 다음 작업을 처리하는 동시에 B함수에서 작업이 완료되면 A함수로 그 결과값을 리턴합니다.
A함수는 그동안 신나게 B와 관련없는 작업들을 하고 있다가 B함수로부터 결과값을 받으면 그제서야 B함수 다음으로 해야 할 작업을 실행합니다.
스타벅스 온 김에 real world에서의 상황을 예로 들어보자면
- 동기
직원: 커피기계로 커피 내려야지
(커피 내리는 중 ...)
직원: (커피 다 내릴 때까지 기다리는 중....)
(커피 내리기 완료)
직원: 커피 다 내렸으니까 우유 스팀 시작해야지
(우유 스팀하는 중...)
직원: (또 기다리는 중)
(우유 스팀 완료)
직원: 이제 라떼 만들어야지
(라떼 만드는 중)
(라떼 만들기 완료)
직원: 이제 케이크 꺼내야지
(케이크 꺼내는 중)
(케이크 꺼내기 완료)
직원: 이제 세팅해야지
(세팅 중)
(세팅 완료)
직원: 주문하신 음료 나왔습니다!
이렇게 일하면 짤리겠죠?
- 비동기
직원: 커피기계로 커피 내릴 동안 우유 스팀도 켜두고, 케이크도 꺼내놔야지
(커피 내리는 중...)
(우유 스팀 중...)
(케이크 꺼내는 중...)
(케이크 꺼내기 완료)
직원: 미리 세팅하고 있어야지
(세팅 중)
(커피 내리기 완료)
(우유 스팀 완료)
직원: 어 다 내렸네, 이제 라떼 만들어야지
(라떼 만드는 중)
(라떼 만들기 완료)
(세팅 완료)
직원: 주문하신 음료 나왔습니다!
따라서 B함수에서 어떤 결과값을 받기 전까지는 비동기와 관련없는 다른 Task를 처리하고 있다가 비동기 작업이 완료되면 다음 Task를 수행하는 방식이 곧 비동기식 처리입니다.
2. Promise
Promise는 비동기 처리에 사용되는 객체로, 비동기 작업을 완료 또는 실패하는 결과값을 반환하여 그 결과값을 받아 다음 Task를 실행할 수 있는 방식으로 사용됩니다. 상태에 따라 pending, fullfilled, rejected로 구분합니다.
또한 Promise는 다음 그림처럼 여러 개의 Promise를 연결하여 실행하는 체인 구조를 가집니다.
pending: Promise를 호출하고 아직 결과값을 반환하지 않은 상태
1
|
const result = new Promise();
|
fullfilled: 비동기 작업이 완료된 상태. then()으로 결과값을 받아 다음 작업을 처리할 수 있는 상태
1
2
3
|
const result = new Promise(function (resolve, reject){
resolve(true);
});
|
rejected: 비동기 작업이 실패한 상태. catch()로 실패한 결과값을 받을 수 있습니다.
1
2
3
4
5
|
const result = new Promise(function (resolve, reject) {
reject(false);
}).catch(function (e) {
console.log(e);
});
|
2-1. Promise 에러 핸들링
Promise로 에러를 처리하는 방법은 두 가지가 있습니다.
- catch로 받기
위의 rejected상태에서 볼 수 있듯 catch()로 실패 결과값을 받아서 처리할 수 있습니다.
catch()는 reject로 명시적인 에러 반환 뿐만 아니라 코드에서 발생할 수 있는 모든 에러 및 throw로 넘긴 에러 또한 처리합니다.
.- then()의 두번째 인자로 에러 핸들링
then()에서 에러를 받아서 처리할 수도 있습니다.
then()의 두번째 인자의 콜백함수가 곧 에러 핸들링 함수입니다.
1
2
3
4
5
6
7
|
const result = new Promise(function (resolve, reject) {
reject(false);
}).then(function(res) {
console.log("res: ", res);
}, function(error) {
console.log("error: ", error);
});
|
Q. 둘 중에 어떤 에러 핸들링을 지향해야 할까?
가급적 catch()를 통해 에러 핸들링할 것을 권장합니다.
다음 Promise에서 에러를 핸들링하는 상황을 가정합니다.
then에서 throw한 에러는 어디에서 처리될까요?
1
2
3
4
5
6
7
8
9
10
|
const promise1 = new Promise(function(resolve, reject) {
resolve("yes");
}).then(function(res){
console.log(res);
throw new Error("error in then");
}, function(e){
console.log("catch error in then(): ", e)
}).catch(function(e) {
console.log("catch error in catch(): ", e);
});
|
정답은 catch문에서 처리됩니다.
즉 then에서 발생한 에러를 잡지 못하는 문제가 있습니다.
다음과 같이 23~28번째 줄에서 내가 예상한 건 "This is Resolved"인데 Promise에서 에러를 처리하지 못해 스크립트가 중단되고, 브라우저 콘솔창에 에러가 찍힙니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
function task1 (fn) {
var result = fn();
return new Promise(function(resolve, reject){
if (result) resolve(result);
else reject({ message: "This is Rejected" });
});
}
function closure (a, b) {
return function() {
return a > b;
}
}
task1(closure(1, 2)).then(function(res){
if (res)
throw new Error("This is Resolved");
}, function(err){
console.log(err.message);
}); // This is Rejected
task1(closure(10, 2)).then(function(res) {
if (res)
throw new Error("This is Resolved");
}, function(err){
console.log(err.message);
}) // Uncaught (in promise) Error: This is Resolved
|
catch로 에러를 처리할 경우에는 Promise 체인의 어떤 위치에서 발생한 에러든 잡을 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
function task1 (fn) {
var result = fn();
return new Promise(function(resolve, reject){
if (result) resolve(result);
else reject({ message: "This is Rejected" });
});
}
function closure (a, b) {
return function() {
return a > b;
}
}
task1(closure(1, 2)).then(function(res) {
if (res)
throw new Error({ message: "This is Resolved" });
}).catch(function(err) {
console.log(err.message);
}); // This is Rejected
task1(closure(10, 2)).then(function(res) {
if (res)
throw new Error("This is Resolved");
}).catch(function(err) {
console.log(err.message);
}); // This is Resolved
|
Q. 그렇다면 catch() 다음의 then()은 어떻게 실행될까?
아래의 결과를 예측해봅니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
new Promise((resolve, reject) => {
console.log('Initial');
resolve();
})
.then(() => {
throw new Error('Something failed');
console.log('Do this');
})
.catch(() => {
console.error('Do that');
})
.then(() => {
console.log('Do this, no matter what happened before');
});
|
Promise 체이닝에서는 catch()문또한 정상적으로 종료되었다고 판단될 경우 다음 then으로 제어 흐름이 넘어가 에러 핸들링 이후의 Task를 계속 실행할 수 있습니다.
따라서 결과값은 다음과 같습니다.
"Initial"
"Do that"
"Do this, no matter what happend before"
It's possible to chain after a failure, i.e. a catch, which is useful to accomplish new actions even after an action failed in the chain. Read the following example:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
만약 catch()에서 에러를 해결하지 못한 경우에는 다음으로 가까운 에러 핸들러로 에러를 throw할수도 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 실행 순서: catch -> catch -> then
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(function(error) { // (*)
if (error instanceof URIError) {
// 에러 처리
} else {
alert("처리할 수 없는 에러");
throw error; // 에러 다시 던지기
}
}).then(function() {
/* 여기는 실행되지 않습니다. */
}).catch(error => { // (**)
alert(`알 수 없는 에러가 발생함: ${error}`);
// 반환값이 없음 => 실행이 계속됨
});
|
"처리할 수 없는 에러"
"알수 없는 에러가 발생함: ..."
Q. Promise Chain에서 각각 다른 에러를 처리하고 싶은 경우
위의 catch()로 에러를 처리하는 외에 then()에서 발생하는 에러를 각각 핸들링하고 싶은 경우도 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
const task = new Promise(function (resolve, reject) {
resolve(1);
});
task.then(function(res) {
if (res >= 1) return res+1;
else {
throw new Error(res)
}
}).then(function(res){
if (res >= 3) return res+1;
else {
throw new Error("last");
}
}).catch(function(e) {
if (e.message == "last") {
console.log("마지막에서 실패함");
} else if (e.message < 1) {
console.log("중간에서 실패함");
}
});
|
만약 then이 엄~청 많고 에러도 각각 다르게 처리해야 한다면 catch()에서 조건을 무한정 늘릴수도 없는 노릇입니다.
따라서 then()의 각 콜백함수에서 try catch문으로 에러를 핸들링할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
const task = new Promise(function (resolve, reject) {
resolve(1);
});
task.then(function(res) {
try {
if (res >= 1) return res+1;
else {
throw new Error(res)
}
} catch(e) {
console.log("중간에서 실패함");
throw e; // throw 하지 않으면 정상 종료로 판단하고, 다음 then이 실행됨
}
}).then(function(res){
try {
if (res >= 3) return res+1;
else {
throw new Error("last");
}
} catch (e) {
console.log("마지막에서 실패함");
throw e; // throw 하지 않으면 정상 종료로 판단하고, 다음 then이 실행됨
}
}).catch(function(e) {
console.log(e.message);
});
|
또한 일반적으로 try catch에서 발생한 에러를 해당 catch에서 해결할 수 없다고 판단할 경우, 다음 에러 핸들러로 에러를 던지는 것처럼 Promise의 catch또한 다음 catch로 에러를 throw할 수 있습니다.
Q. Promise의 마지막에 catch를 하지 않으면 어떻게 될까?
Promise에 catch문으로 에러를 핸들링하지 않으면 Promise에서 에러가 발생할 때 그 에러는 갇히게 되고, 결국 스크립트가 비정상적으로 종료되고 콘솔에 에러가 출력됩니다.
전역적으로 Promise에러를 잡는 방법으로 unhandledrejection 이벤트를 지원합니다.
1
2
3
|
window.addEventListener("unhandledrejection", event => {
console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
|
콜백함수의 event 인자는 두 속성을 가집니다.
- event.promise: reject된 promise객체
- evnet.reason: reject된 promise에서 리턴한 에러 객체
또한 브라우저에서 해당 에러 로그를 출력하게 하고 싶지 않다면 event.preventDefault();로 이벤트를 취소할 수 있습니다.
문제 1.
다음의 상황에서 catch()문은 에러를 처리할 수 있을까요? 정답은 '더보기'에 있습니다.
1
2
3
4
5
6
7
|
new Promise(function(resolve, reject) {
setTimeout(_ => {
throw new Error("에러 발생!");
}, 1000);
}).catch(function(e){
console.log(e.message);
});
|
정답.
처리할 수 없습니다.
콜스택에서 이미 Promise 콜백함수가 pop된 후 setTimeout 콜백함수가 콜스택에 쌓여 실행되기 때문에 catch문이 트리거될 수 없습니다.
이벤트 루프를 익히고 난 후 더욱 쉽게 이해할 수 있습니다.
마지막으로 Promise 이해를 돕는 문제를 풀 수 있는 사이트들입니다.
https://danlevy.net/javascript-promises-quiz/
codingame.com/playgrounds/347/javascript-promises-mastering-the-asynchronous/its-quiz-time
[참고한 자료]
https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event
https://ko.javascript.info/promise-error-handling
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://joshua1988.github.io/web-development/javascript/promise-for-beginners/
'Today I Learn > 프론트엔드' 카테고리의 다른 글
[리액트]크로스 브라우징 이슈 해결 경험기 (0) | 2020.04.03 |
---|---|
[리액트] Toast 알람 구현 (1) (0) | 2020.03.31 |
[리액트] 리액트 훅 기반 프론트엔드 개발 시 주의할 점 (0) | 2020.03.21 |
[리액트] requestAnimationFrame으로 custom hook 만들기 (0) | 2020.03.03 |
[리액트] 모바일 웹 브라우저에서 가상 키보드 오픈 시 스크롤 내리기 (0) | 2020.02.25 |