프로젝트를 진행하다보면 비동기 함수는 무조건 사용하게 되고 꼭 비동기 함수의 상태에 따른 에러처리를 하게 된다.
그리고 나는 항상 위 비동기 처리를 하는 부분이 고민이다.
여러가지 이유로 인해서 그런데...
모든 비동기 api 함수에 try catch를 때려 박으면 되나?
에러가 발생하면 유저의 화면에 어떻게 보여야되나?
에러를 catch하면 console.log로 그냥 다 찍어보나?
위와 같은 고민을 수차례 했지만 비동기 함수에서 특히 api 통신 함수 에러가 발생했을 때 해야하는 만능 지침서는 얻을 수 없었다.
이번 글을 시작? 삼아서 모든 프로젝트에서 에러 핸들링을 잘 할 수 있도록 해보겠다.
기본적으로 js에서는 비동기 통신을 위해서 promise 방식을 사용한다.
promise를 사용해 간단히 특정 data를 1초후에 전달하는 비동기 코드를 만들어보겠다.
function getSomeData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldError) {
return reject("get data Error");
}
resolve(["Get", "Some", "Data"]);
}, 1000);
});
}
뭔지는 모르지만 만약 에러가 있다면 (shouldError)
reject 로 에러를 보내는 것을 했다.
위 예시 getSomeData
를 불러오는 함수에서 에러를 처리하는 방법을 알아보겠다.
가장 보편적인 방법 중 하나이다.
비동기 함수는 메서드로 then과 catch를 가지고 있다. (finally도 있음)
getSomeData()
.then((data) => {
console.log(data);
})
.catch((err) => console.log(err));
getSomeData와 같이 비동기처리를 다루는 함수마다 then , catch를 사용하게 되므로 코드가 지저분해질 가능성이 크다.
가장 보편적인 방법 두번째이다.
주로 async await 를 사용한 곳에서 사용한다.
async function asyncFunction() {
try {
const data = await getSomeData();
console.log(data);
} catch (err) {
console.log(err);
}
}
try-catch block scope 안에 함수나 에러처리부분이 갇혀있어서 try-catch 문 밖에서 data 상태에 대한 처리를 하기위한 추가 작업이 필요하다.
해서 이를 개선한 코드는 다음과 같다.
async function asyncWrap(promise) {
try {
const data = await promise;
return [data, undefined];
} catch (err) {
return [undefined, err];
}
}
이러면 위처럼 내부적으로 특정 promise 함수를 사용하지 않고 외부에서
promise
를 받아 data 나 error 만 전달하기 때문에
위 코드에서 더이상 추가 핸들링을 할 필요가 없고 비동기 함수를 안전하게 다룰 수 있다
allSettled 안에 들어온 비동기 함수들을 병렬적으로 처리하고 처리후 상태에 대한 값을 state, value, reason으로 나눠서 알려준다.
이와 비슷한 메서드로 promise.all 이 있는데 이 함수는 병렬적으로 비동기함수를 처리하되 하나라도 reject가 되면 비동기 함수 전부가 reject되는 특성이 있다.
// getSomeError는 에러가 발생할 함수라 가정
const results = await Promise.allSettled([getSomeData(), getSomeError()]);
// results :
//[{status: 'fulfilled', value: ["Get", "Some", "Data"]},
// {status: 'rejected', reason: "get data Error"}]
이렇게 되면 results
에 value나 reason에 접근해 값을 처리할 수 있다.
위 코드를 좀더 개선하면 다음과 같다.
// 배열형태롤 반환
async function wrapIt(promise) {
return Promise.allSettled([promise]).then(function ([{ value, reason }]) {
return [value, reason];
});
}
const [data, err] = await wrapIt(getSomeData())
// 객체형태로 반환
async function wrapItObj(promise) {
return Promise.allSettled([promise]).then(function ([{ value, reason }]) {
return {value, reason};
});
}
const {data : cheeseDataObj, err : cheeseErrorObj }= await wrapItObj(getSomeData())
이렇게 처리하게 된 error를 react 에서 어떻게 다룰까?
React에서는 여러 컴포넌트에서 각자 비동기로 받아온 데이터를 보여주는 일을 하게 된다.
그러다 특정 컴포넌트에서 받아온 데이터가 문제를 일으켜 해당 컴포넌트에서 에러가 발생해 UI가 깨진다면?
개발자가 발생한 에러가 안나도록 처리하면 되지만 실 서비스에서 불특정 다수의 사용자가 앱을 사용하면서 의도치않게 이상한 에러가 발생하게 되고 앱 전체가 먹통이 된다면 이는 커다란 영업손실을 가져올 수가 있다.
react 에서는 공식문서를 통해 react v16 부터 Error Boundary를 사용해 에러에 대한 UI 처리를 할 수 있도록 해준다.
위 개념은 컴포넌트의 생명주기를 다뤄야하기 때문에 class Component로 만들 수 있다.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 에러 리포팅 서비스에 에러를 기록할 수도 있습니다.
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
return this.props.fallback;
}
return this.props.children;
}
}
특정 컴포넌트 만을 감싸서 핸들링 할 수도 있고
전체 app을 감싸서 사용할 수도 있다.
방식은 마음대로
export default function SomeComponent() {
return (
<div className="SomeGroup">
<ErrorBoundary fallback="Error 발생">
<GetDataComponent1/>
<GetDataComponent2/>
<GetDataComponent3/>
</ErrorBoundary>
</div>
);
}
여기에 suspense를 붙여 사용하고 각각 컴포넌트 별로 분리해서 사용해도 좋다.
export default function SomeComponent() {
return (
<div className="SomeGroup">
<ErrorBoundary fallback="Error 발생">
<Suspense fallback={<Loading>}>
<GetDataComponent1 />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback="Error 발생">
<Suspense fallback={<Loading>}>
<GetDataComponent2 />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback="Error 발생">
<Suspense fallback={<Loading>}>
<GetDataComponent3 />
</Suspense>
</ErrorBoundary>
</div>
);
}
참고로 Error Boundary는 다음의 경우에는 에러를 포착하지 않는다.
- 이벤트 핸들러
- 비동기적 코드 (예: setTimeout 혹은 requestAnimationFrame 콜백)
- 서버 사이드 렌더링
- 자식에서가 아닌 에러 경계 자체에서 발생하는 에러
위와 같은 컴포넌트는 직접 제작하지 않고 react-error-boundary 라이브러리를 사용하는 것이 안전하다.
비동기 함수에서 에러와 data 값을 다루는 여러가지 방법과
react 에서 컴포넌트에서 에러 발생시 UI 처리를 위한 Error Boundary 에 대해서 알아보았다.
글이 길어진 관계로 여기서 (사실 아직 내용정리가 필요해서) 줄이고 다음에 이어서 다뤄보도록 하겠다.
참고
https://www.youtube.com/watch?v=_FuDMEgIy7I&t=236s&pp=ygUXZXJyb3IgaGFuZGxpbmcgaW4gcmVhY3Q%3D
https://www.youtube.com/watch?v=wsoQ-fgaoyQ&t=1s&pp=ygUXZXJyb3IgaGFuZGxpbmcgaW4gcmVhY3Q%3D