선언적 에러처리 : Vue ErrorBoundary 구현 (feat : Sentry를 곁들인...)
ErrorBoundary가 뭘까
에러 바운더리는 선언적으로 에러를 처리할 수 있게 컴포넌트 형태로 사용되는 에러 헨들링 방법 중 하나입니다.
상세내용 참고
https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
vue 기반의 서비스에서 저 좋은 에러 헨들링 기법을 적용하고 싶다는 강렬한 생각이 들었습니다...🔥
Vue에서 ErrorBoundary 구현하기
먼저 Vue에는 다행이도 에러를 감지해주는 라이프사이클 함수가 존재합니다.
이를 적극적으로 이용해 리액트에서와 같이 컴포넌트 형태로 구현이 가능해집니다.
Vue2 : errorCaptured 이용하기
먼저 ErrorBoundary와 같은 프로바이더 개념의 컴포넌트를 구현해봅니다.
ErrorBoundary.vue
<script>
export default {
name: "ErrorBoundary",
props: {
fallback: {
type: Object
}
},
data() {
return {
hasError: false
};
},
errorCaptured(error,compInst,errorInfo) {
this.hasError = true;
console.log("error: ", error);
console.log("compInst: ", compInst);
console.log("errorInfo: ", errorInfo);
},
render(createElement) {
return this.hasError
? createElement(this.fallback)
: this.$slots.default[0];
}
};
</script>
이제는 해당 에러바운더리에 감지되었을때 리턴해줄 fallback ui를 구현해봅니다.
ErrorFallBack.vue
<template>
<div id="error-page">
<p class="text">서비스를 불러오는데 실패했습니다.</p>
<p class="text">새로고침 버튼을 눌러주세요.</p>
<button class="button" @click="refreshPage()">새로고침 하기</button>
</div>
</template>
<script>
export default {
methods: {
refreshPage(){
window.location.reload();
},
},
};
</script>
<style scoped>
... 원하는 스타일
</style>
해당 구현된 내용을 vue 앱의 최상단인 App.vue에 적용해봅니다.
App.vue에 ErrorBoundary 적용하기
<template>
<ErrorBoundary>
<div>
...
</div>
</ErrorBoundary>
</template>
<script>
import ErrorBoundary from './~~/ErrorBoundary.vue'
import ErrorFallBack from './~~/ErrorFallBack.vue'
export default {
components: {
ErrorBoundary,
ErrorFallBack
},
computed: {
fallbackUI() {
return ErrorFallBack;
},
}
</script>
위와 같이 적용하면 ErrorBoundary 하위에서 발생하는 에러를 감지하여 fallbackUI를 보여주고 앱이 멈추어 작동하지 않는 모습을 보여주지 않을 수 있습니다.
Vue3 : onErrorCaptured 이용하기
위와 마찬가지의 순서로 구현해봅니다.
ErrorBoundary.vue
<template>
<Error v-if="isError"/>
<slot v-else/>
</template>
<script setup lang="ts">
import {
onErrorCaptured,
ref,
} from 'vue';
import Error from './Error.vue';
const isError = ref(false);
onErrorCaptured(() => {
isError.value = true;
});
</script>
ErrorFallBack.vue
<template>
<div id="error-page">
<p class="text">
서비스를 불러오는데 실패했습니다.
</p>
<p class="text">
새로고침 버튼을 눌러주세요.
</p>
<button
class="button"
@click="refreshPage()"
>
새로고침 하기
</button>
</div>
</template>
<script setup lang="ts">
const refreshPage = () => {
window.location.reload();
};
</script>
<style lang="scss" scoped>
... 원하는 스타일
</style>
App.vue에 ErrorBoundary 적용하기
<template>
<ErrorProvider>
<Suspense>
<router-view/>
</Suspense>
</ErrorProvider>
</template>
위처럼 적용하면 위에서 언급했듯 오류 발생시 원하는 오류화면을 보여줄 수 있게 됩니다.
Sentry를 곁들이자...
오류가 났을때 왜 오류가 났는지 추적하려고 로컬에서 켜서 디버깅하는 경험 많았을 것입니다.
sentry를 통해서 프론트 페이지에서 발생하는 오류를 수집하고 미리 어떠 오류인지 보고
로컬에서 접근하여 수정할 수 있어 디버깅시간을 단축시켜줄 수 있습니다.
Sentry가 뭔데...?
Sentry는 실시간 로그 취합 및 분석 도구이자 모니터링 플랫폼입니다.
참조 👉 https://sentry.io/welcome/
Sentry를 이용해 오류를 수집하고 분석할 수 있어 ErrorBoundary와 같이 에러 헨들링에 녹이면 아주 좋겠다는 생각이 들어 적용해보았습니다.
바로 적용해봅시다.
Vue 프로젝트 내 main.js에 적용하기
Sentry.init({
Vue,
dsn: "본인 센트리 프로젝트에서 제공하는 url",
initialScope: (scope) => {
// 객체 형태로 센트리 내 태그를 선택적으로 커스텀하여 넣어줄 수 있습니다.
const customInfo = {
key : value
}
scope.setTags(customInfo);
return scope;
},
})
Axios Error에 Sentry 추가하기
위 설정만 가지고는 네트워크에서 발생하는 오류를 상세히 모니터링하기 어렵기 때문에
axios의 interceptor를 사용하여 추가해줍니다.
Axios interceptor 추가 코드
import * as Sentry from "@sentry/vue";
import axios from "axios";
axios.interceptors.response.use(
async (response) => {
/*
http status가 200인 경우
응답 성공 직전 호출됩니다.
.then() 으로 이어집니다.
*/
return response;
},
async (error) => {
/*
http status가 200이 아닌 경우
응답 에러 직전 호출됩니다.
.catch() 으로 이어집니다.
*/
if(error.response){
const {data, status} = error.response
const {method, url} = error.config
// 이슈 하위 문서 내용에 정보 추가
Sentry.setContext('Api Response Detail', {
status,
data
});
// 커스텀 태그 형성
Sentry.withScope((scope)=>{
scope.setTag('type', 'api')
scope.setTag('api-status', status || 'no value')
scope.setTag('api-data', data ? JSON.stringify(data) : 'no value')
// 이슈의 레벨을 설정
scope.setLevel('error');
// 이슈를 그룹화 시켜줌
scope.setFingerprint([method, status, url])
// 센트리로 오류 전송
Sentry.captureException(new Error('API Internal Server Error'))
})
} else {
const {method, url, params, data, headers} = error.config
// 이슈 하위 문서 내용에 정보 추가
Sentry.setContext('Api Request Detail', {
message:'네크워크 요청은 갔으나 응답이 오지 않음',
method,
url,
params,
data,
headers
});
// 커스텀 태그 형성
Sentry.withScope((scope)=>{
scope.setTag('type', 'api')
scope.setTag('api-method', method || 'no value')
scope.setTag('api-url', url || 'no value')
scope.setTag('api-params', params ? JSON.stringify(params) : 'no value')
scope.setTag('api-data', data ? JSON.stringify(data) : 'no value')
scope.setTag('api-headers', headers ? JSON.stringify(headers) : 'no value')
// 이슈의 레벨을 설정
scope.setLevel('error');
// 이슈를 그룹화 시켜줌
scope.setFingerprint([method, url])
// 센트리로 오류 전송
Sentry.captureException(new Error('API Not Found Error'))
})
}
return await Promise.reject(error);
}
);
export default axios
이로써 Sentry에서 서비스 내 js 오류 및 네트워크 오류를 전부 추적할 수 있게 되었습니다.
참조
https://if.kakao.com/2022/session/84
https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
https://vuejs.org/api/options-lifecycle.html#errorcaptured
https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured
https://engineer-mole.tistory.com/369