리액트를 사용하든, JS를 사용하든 api 통신을 할 때
axios나 fetch 등을 활용하게 된다.
사용법의 이해를 위해 예제들을 찾아보면 정말 많은 예시들이
throw, try, catch, async, await를 섞어서 사용한다.
이는 도대체 무슨 의미이며, 어떤 효과를 가지는지 알아보자.
기초적인 내용을 학습하기 위한 내용이므로 흐름에 따라 한번에 몰아서 작성했습니다
function testThrow(x, y) {
if (typeof x !== "number" || typeof y !== "number") {
throw "좀 이상한데요?";
}
return x + y;
}
console.log(testThrow(1, 3));
위와 같은 코드를 실행시켜보자.
보아하니 문제 없이 작동할 것 같다.
그렇다. 잘 작동한다.
throw
를 실험하기 위해 일부러 에러를 내보자.
function testThrow(x, y) {
if (typeof x !== "number" || typeof y !== "number") {
throw "좀 이상한데요?";
}
return x + y;
}
console.log(testThrow("1", 3));
숫자형이 아닌 문자형을 넣어 에러를 발생시켜봤다.
throw
에 넣어준 에러 메세지가 콘솔에 뜨는 것을 볼 수 있다.
참고로 필자는 codesandbox를 사용했는데,
크롬 브라우저에서 개발자 도구로 살펴보면 아래와 같이 나온다.
이번에는 아래와 같은 코드로 실험을 해보자.
function f2() {
console.log("f2 start");
console.log("f2 end");
}
function f1() {
console.log("f1 start");
f2();
console.log("f1 end");
}
console.log("will : f1");
f1();
console.log("did: f1");
아래와 같이 실행된다.
여기에 일부러 에러를 발생시켜보겠다.
function f2() {
console.log("f2 start");
throw "에러";
console.log("f2 end");
}
아래와 같이 결과가 나온다.
이제 예외 처리를 해보자.
try
, catch
를 사용하여 에러가 발생할 수 있는 부분을 처리해주자.
function f1() {
console.log("f1 start");
try {
f2();
} catch (err) {
console.log(err);
}
console.log("f1 end");
}
만약 f2()
에서 에러가 발생하면 catch
를 해서 콘솔에 보여주겠다는 것이다.
결과를 살펴보자.
오! 이번에는 에러가 발생했지만 코드가 중지되지않았다.
예외 처리한 부분에서 에러에 대한 반응을 수행하며 f2()
를 종료한 것을 제외하면,
그대로 나머지 코드를 진행했다.
이번엔 예외 처리 부분을 바꿔보자.
function f2() {
console.log("f2 start");
throw "에러";
console.log("f2 end");
}
function f1() {
console.log("f1 start");
f2();
console.log("f1 end");
}
console.log("will : f1");
try {
f1();
} catch (err) {
console.log(err);
}
console.log("did: f1");
f1()
수행 중 에러가 발생하는 것은 f2()
부분이다.
따라서 f1()
내에 있는 f2()
에서 에러를 catch
해서 예외처리를 하므로,
에러를 마주하기 전까지는 실행하고, 에러를 발견한 뒤부터는 catch
가 실행된다.
따라서 다음과 같은 결과가 나온다.
그런데 보통 throw
를 사용할 때 위 예시처럼 문자열을 적지않고
Error
객체를 사용한다.
function f2() {
console.log("f2 start");
throw new Error("에러"); // Error 객체 사용
console.log("f2 end");
}
function f1() {
console.log("f1 start");
f2();
console.log("f1 end");
}
console.log("will : f1");
try {
f1();
} catch (err) {
console.log(err);
}
console.log("did: f1");
뭐가 달라질까?
결과를 살펴보자.
짠! 에러가 발생한 곳의 정보가 적혀있는 것을 볼 수 있다.(call stack의 정보)
다음과 같은 함수를 실행시켜보자.
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("error!");
}, sec * 1000);
});
}
wait(3);
예상되는 결과는 3000ms 뒤 에러가 reject
되는 것이다.(여기서는 의도적으로 에러 만든 것임)
실제로 3초 뒤 아래와 같은 결과가 나왔다.
코드에서는 의도적으로 에러를 발생시킨 것이지만,
결국 이는 예외 처리를 하지 않았기 때문에 발생한 것이라고 생각할 수 있겠다.
in promise
라고 적혀있다. 이를 이용해서 예외 처리를 해보도록 하자.
위에서 배운 try
, catch
를 활용하겠다.
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("error!");
}, sec * 1000);
});
}
try {
wait(3);
} catch (err) {
console.log(err);
}
자, 과연 예외 처리가 될까?
아니다. 도대체 왜 처리가 안된걸까?
wait()
는 비동기적으로 실행되고 있기 때문이다.
예외가 발생하는 타이밍이 try
가 감싸고 있는 코드가 실행되는 타이밍과 달라,
콜스택이 비었을 때 예외가 발생해버려서 처리가 안되는 것이다.
그러면...Promise
에서 발생하는 예외는 어떻게 처리해요?
Promise.catch()
를 여기서 사용한다.
try
, catch
문을 아래와 같이 수정했다.
wait(3).catch((err) => console.log(err));
과연 reject
로 발생한 에러를 처리할 수 있을까?
예외 처리가 된 것을 확인할 수 있다.
여담으로 Promise
는 then
을 활용하여, 성공 시 추가적으로 작동할 코드를 연결해줄 수 있다.
그래서 then
은 여러번 사용이 가능하다.
wait(3)
.then(() => console.log("1st then"))
.then(() => console.log("2st then"));
이런 식의 코드가 가능하다는 뜻이다.
그런데, catch
는 이게 불가능하다.
wait(3)
.catch((err) => console.log("1st err", err))
.catch((err) => console.log("2nd err", err));
대충 보면 두 번의 콘솔이 찍힐 것 같지만,
실행되는건 첫 번째 catch
뿐이다.
이유가 뭘까?
보통 이런 체인 구조에서는 첫 번째 then
이나 두 번째 then
이나 계속 같은 객체를 리턴하는데 반해,
Promise
는 이런 구조에서 then
이나 catch
에서 전부 다른 객체를 리턴하기 때문이다.
정확히 어디서 어떤 객체가 리턴되는건지 알아보자.
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("error!");
}, sec * 1000);
});
}
위 코드에서 Promise
내부에 있는 것들이 wait()
를 실행했을 때,
즉, 아래 코드에서 리턴된다.
wait(3)
이제 이 Promise
에서 예외가 발생하면
.catch((err) => console.log("1st err", err))
를 실행한다.
그런데 이 친구는 방금 실행했던 wait()
에서 발생한 Promise
에서의 객체와는 관련이 없고,
잘 작동을 했는지에 대해서만 예외처리를 담당하는 것이다.
즉, 첫 번째 catch
가 반환하는 것은 catch
자체에서 동작이 제대로 됐는지에 해당하는 Promise
인 것이다.
그러면 얘는 뭘까?
.catch((err) => console.log("2nd err", err));
두 번째 catch
는 첫 번째 catch
가 잘 동작했는지에 대한 예외 처리를 담당하는 것이 되기 때문에
이미 잘 작동한 첫 번째 catch
에 대해서 처리할 것이 없으므로,
아무 일도 발생하지 않는 것이다.
정리하면 이렇다.
처음 함수 실행에서의 Promise
가 계속해서 넘겨지는게 아니라
각 단계에서의 Promise
가 넘겨지는 것이므로,
then
, catch
가 같은 Promise
를 기준으로 작동하는 것이라고 생각하면 안된다!!!!
그냥 본인이 실행되기 바로 직전 단계에 있는 Promise
를 기준으로 작동하는 것이다.
그러면 이런 의문이 생길 것이다.
"그럼 첫 번째 catch
에서의 에러를 계속해서 에러 상황으로 봐야하고,
또 사용해야될 때는 어떻게 해야하나요?"
그럴 때는, 아래와 같이 사용하면 된다.
wait(3)
.catch((err) => {
console.log("1st err", err);
throw err;
})
.catch((err) => console.log("2nd err", err));
throw
를 통해 에러를 다시 던져주면, 아래와 같이 실행된다.
또 다른 방법이 있는데, 바로 then
을 이용하는 방법이다.
then
에 대한 설명을 보면 다음과 같다.
onfulfilled
와 onrejected
를 입력해줄 수 있다는 것이다.
전자는 then
에서 어떤 것을 성공했을 때의 경우 실행되는 것이고,
후자는 어떤 것을 실패했을 경우 실행되는 것이다.
이를 활용하여 예시 코드를 아래와 같이 수정해볼 수 있겠다.
wait(3)
.then(
() => {
console.log("done!!!");
},
(err) => {
console.log("1st err in then", err);
}
)
.catch((err) => console.log("2nd err", err));
오! onrejected
에 넣어놓은 함수가 실행된 것을 볼 수 있다.
그런데, 두 번째 catch
는 실행되지 않았다.
왜? 이미 then
에서 예외처리마저 성공적으로 해냈기 때문에
catch
는 then
이 Promise
를 성공적으로 완료됐다고 판단해서 그런 것이다.
그럼에도 불구하고 then
에서의 에러를 이어 가져가서 catch
에서 사용하고자 한다면,
wait(3)
.then(
() => {
console.log("done!!!");
},
(err) => {
console.log("1st err in then", err);
throw new Error("throw in then");
}
)
.catch((err) => console.log("2nd err", err));
짠! then
에서의 예외 처리가 성공되었음에도,
catch
까지 그 에러를 끌고 가서 처리해줄 수 있다!
우선 async
에 대해서 기초적인 것을 살펴보고 가자.
async function myAsyncFun() {
return "done!";
}
const result = myAsyncFun();
console.log(result);
result
는 myAsyncFun()
이 반환하는 done!
을 보여줄 것으로 예상된다.
그러나...
콘솔에 찍힌 것은 Promise
이다!
즉, async
를 사용하면 Promise
가 반환된다는 것이다.
이는,
function myPromiseFun() {
return new Promise((resolve, reject) => {
resolve("done!");
});
}
const result2 = myPromiseFun();
console.log(result2);
위 코드와 동일하게 작동하는 것이다.
위 코드도 콘솔에 완전히 동일한 결과가 출력된다.
async
함수에서의 return
이 Promise
함수에서의 resolve
에 해당한다고 생각하면 된다.
이번엔 에러를 발생시켜보자.
function myPromiseFun() {
return new Promise((resolve, reject) => {
reject("myError!");
});
}
const result2 = myPromiseFun();
console.log(result2);
myAsyncFun()
에서도 에러를 발생시키고 싶다면 어떻게 해야할까?
async function myAsyncFun() {
throw "myAsyncError!";
}
const result = myAsyncFun();
console.log(result);
throw
를 사용하면 된다. 그러면 동일하게 Promise
에러가 발생한다.
이 에러를 잡아서 처리하고자 한다면 catch()
를 사용하면 된다.
async function myAsyncFun() {
throw "myAsyncError!";
}
const result = myAsyncFun().catch((err) => {
console.log(err);
});
function myPromiseFun() {
return new Promise((resolve, reject) => {
reject("myError!");
});
}
const result2 = myPromiseFun().catch((err) => {
console.log(err);
});
짠! 에러를 잡아서 처리한 것을 볼 수 있다.
이번에는 await
를 알아보자. await
는 async
내에서 사용할 수 있다.
await
는 뭐하는 녀석일까?
바로 Promise
를 기다리는 녀석이다!
Promise
가 완전히 fulfilled
되거나 rejected
되기를 기다리는 것이다.
음...와닿지 않는다.
예시를 살펴보자.
function wait(sec) {
return new Promise((resolve) => {
setTimeout(() => {
resolve("done!");
}, sec * 1000);
});
}
async function myAsyncFun() {
console.log(new Date());
wait(3);
console.log(new Date());
}
const result = myAsyncFun();
어라..? 3초를 기다릴 거라고 생각했는데 그냥 바로 new Date()
가 실행되어버렸다.
이는 wait()
가 비동기이기 때문에 발생한 것이다.
wait()
를 놔두고 바로 다음 코드를 실행한다는 것이다.
이제 await
를 사용해보자. 기다려!
function wait(sec) {
return new Promise((resolve) => {
setTimeout(() => {
resolve("done!");
}, sec * 1000);
});
}
async function myAsyncFun() {
console.log(new Date());
await wait(3);
console.log(new Date());
}
const result = myAsyncFun();
짠! 기다려!를 외쳤더니 3초를 기다리고 실행이 되었다. 착하네요.
await
는 Promise
를 기다린다는 것을 증명했다!
이번에는 reject
로 실험해보자.
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("wait Error!");
}, sec * 1000);
});
}
async function myAsyncFun() {
console.log(new Date());
await wait(3);
console.log(new Date());
}
const result = myAsyncFun();
예상되는 동작은 new Data()
가 실행되고, 3초 기다렸다가 reject
가 되는 것이다. 확인해보자.
짠! 예상대로 작동했다.
reject
에 들어있던 값이 throw
된 것도 확인했다.
이왕 throw
까지 해봤으니 try catch
를 사용해보자.
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("wait Error!");
}, sec * 1000);
});
}
async function myAsyncFun() {
console.log(new Date());
try {
await wait(3);
} catch (err) {
console.log(err);
}
console.log(new Date());
}
const result = myAsyncFun();
첫 번째 new Date()
가 실행되고,
3초 기다린 뒤 wait()
가 실행되며 reject
된 Promise
가 예외 처리가 된다.
그 후, 두 번째 new Date()
가 실행된다.
try catch
를 했기 때문에 예외 처리가 되서, 두 번째 new Date()
가 실행된 것이다.
catch
를 안 썼다면 예외 처리 없이 그냥 에러만 발생했을 것이다.
물론 코드를 아래와 같이 사용해도 된다.
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("wait Error!");
}, sec * 1000);
});
}
async function myAsyncFun() {
console.log(new Date());
await wait(3).catch((err) => {
console.log(err);
});
console.log(new Date());
}
const result = myAsyncFun();
완전히 동일하게 작동한다.
그런데, 이 방법은 return
되는 것이 있다면 주의해야한다.
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("done!");
}, sec * 1000);
});
}
async function myAsyncFun() {
console.log(new Date());
const result = await wait(3).catch((err) => {
console.log(err);
});
console.log(result);
console.log(new Date());
}
const result = myAsyncFun();
정상적으로 resolve
에 들어있던 값이 return
이 되었다.
그 값은 result
에 저장이 되어서 콘솔에 찍힌 것이다.
그런데 만약에 reject
상황에서는 어떻게 될까?
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("wait Error");
}, sec * 1000);
});
}
async function myAsyncFun() {
console.log(new Date());
const result = await wait(3).catch((err) => {
console.log(err);
});
console.log(result);
console.log(new Date());
}
const result = myAsyncFun();
어라, undefined
가 찍히는 것을 확인 할 수 있다.
왜 그럴까?
await
가 기다리고 있었던 Promise
는 wait()
에서의 Promise
가 아니라
catch()
를 통해 return
된 Promise
이기 때문이다.
현재 catch()
에서는 어떤 것도 return
하지 않는다.
return
을 하는게 있어야 그게 바로 catch()
의 resolve
값이 되는데,
return
을 하지 않기 때문에 undefined
가 콘솔에 찍힌 것이다.
지금까지는 우리가 의도적으로 발생시킨 에러에 대해서만 살펴봤는데,
이번에는 의도한게 아닌(문법, 오타 등등...) 에러에 대해서는 어떻게 되는지 살펴보자.
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("wait Error");
}, sec * 1000);
});
}
async function myAsyncFun() {
consoooooole.log(new Date());
const result = await wait(3).catch((err) => {
console.log(err);
});
console.log(result);
console.log(new Date());
}
const result = myAsyncFun();
console.log()
부분에 아주 명확하게 잘 보이는 오타 에러가 존재하고 있다.
음~ 그러면 try catch
하면 예외 처리 할 수 있겠네~
try {
myAsyncFun();
} catch (err) {}
그런데...예외 처리가 안된다...똑같이 저 에러가 나온다.
왜?
myAsyncFun()
이 Promise
를 리턴했기 때문이다.
따라서 이 에러를 잡고자한다면
try {
myAsyncFun().catch((err) => {});
} catch (err) {}
이런 식으로 작성해야한다.
그랬더니 예외 처리가 잘된다! Uncaught
에러가 사라졌다.