React에서 Sentry로 에러 대응 시스템 구축하기

10tacion·2025년 8월 3일

Library

목록 보기
2/2
post-thumbnail

개요

최근 진행한 사이드 프로젝트 Morib을 릴리즈하며 많은 에러와 마주하게 되었는데, 갑자기 발생한 에러를 모두 추적해서 대응하기란 쉽지 않았다.

왜냐하면 에러가 발생한다고 해도 개발자는 이를 모를 수도 있고, 이를 확인했더라도 당시에 어떤 상황에서 에러가 발생했는지 확인할 수 없기 때문이다.

그렇기 때문에 에러가 발생했을 때는 일단 개발자에게 에러가 발생한 상황을 알릴 수 있어야 했고, 발생한 에러를 적재하여 어떤 상황에서 발생한 에러인지 확인 할 수 있게 해야했다.

이 프로세스를 만들기 위해서 에러 바운더리Sentry를 활용했는데, 당시 이에 대해서 고민했던 내용을 기록하고자 한다.


에러를 어떤 기준을 갖고 분류를 해야할까?


에러 바운더리와 Sentry에 대해서 소개하기 전에 먼저 에러 분류에 기준에 대한 내용을 먼저 소개하고자 한다.

에러를 처리하기 위한 프로세스를 고민하면서 가장 먼저 든 생각은 에러를 어떻게 분류할까이다.

분류한 에러의 종류에 따라 어떤 개발자어떤 대응을 해야할지를 미리 염두해두고 코드를 작성한다면 에러를 처리함에 있어서 더욱 편리할 것 같았기 때문이다.

그래서 나는 에러 분류에 대한 기준을 잡기 위해 3가지 측면에서 정리를 해보았다.

  • 종류
  • 예측 가능성
  • 에러를 대응하는 주체

먼저 3가지 기준에 대해서 살펴보자

종류에 따른 에러

  • 클라이언트 에러 (400~)
  • API 서버 에러 (500~)
  • 웹 애플리케이션 외부 요인에 의한 에러 (cors, network error, …)

예측 가능성에 따른 에러

에러를 대응할 때는 우리가 예측가능한 에러가 있고 예측 불가능한 에러가 있다.

예를 들면 사용자가 입력값을 잘못 입력한 경우 우리는 이를 미리 예측하여, 발생한 에러 코드(400)에 따라 대응할 수 있다. 반면 갑자기 서버에서 500에러가 터지는 경우는 우리가 이를 예측하여 대응하기란 쉽지 않을 것이다.

  • 예측 가능한 에러
    • 클라이언트 에러 (400~)
  • 예측 불가능한 에러
    • API 서버 에러 (500~)

에러를 대응하는 주체에 따른 에러

에러가 발생한다면 누군가 대응을 해야한다. 예를 들어서 웹 애플리케이션에서 400에러가 발생했다면 프론트엔드 개발자가 즉시 대응을 해야할 것이다. 반면 서버에서 500에러가 발생했다면 서버 개발자가 즉시 대응을 해야할것이다.

그렇다면 에러를 분류할 때도 이에 따라 분류해볼 수 있을 것이다.

  • 대응 주체가 서버 개발자인 경우
    • API 서버 에러 (500~)
  • 대응 주체가 프론트엔드 개발자인 경우
    • 클라이언트 에러 (400~)
  • 모두 대응 하여 확인해봐야하는 경우
    • 웹 애플리케이션 외부 요인에 의한 에러

내가 에러를 분류하기 위해 채택한 기준

여기서 내가 채택한 기준은 에러를 대응하는 주체에 따라서 에러를 분류하는것이다.

에러의 종류가 어떻든 간에 에러를 대응할 누군가와 매칭이 되어야한다. 대응 주체로 하여금 본인에게 대응해야할 일이 생겼다고 알려주는것이 가장 중요하다고 생각했기 때문에 위 기준을 채택하게되었다.

그래서 위 기준을 바탕으로 에러 바운더리센트리가 제공하는 API를 활용하여 코드를 작성하기 시작했다..!


센트리에 대해서 알아보자


센트리란?

https://sentry.io/

센트리는 실시간으로 로그를 취합하고 수집할 수 있는 모니터링 플랫폼이다. 로그에 대한 다양한 정보를 제공한다.

로그를 통해서 디바이스, OS, 이벤트 로그, 요청을 보낸 주체, 보낸 시간 등에 대한 정보를 파악할 수 있다.

센트리에 로그가 전달되면 이메일로 알림을 받을 수 있고, 웹에서 자세한 정보를 확인할 수 있다. 아래에서 어떤 정보를 제공하는지 확인해보자.

센트리에서 제공하는 주요 데이터

웹에서 센트리를 통해 주로 확인할 수 있는 정보는 아래와 같다.

  • Stack Trace: 발생한 ****에러 메시지와 에러 발생 지점까지 함수 호출 경로
  • Session Replay: 에러가 발생한 시점의 스냅샷
  • Breadcrumbs: 이벤트 발생 과정
  • HTTP Request: 에러가 발생한 URL 경로와 User Agent
  • Tags: 여러 태그에 따른 정보 (Browser, Device, Environment, OS, …)
  • Context: 여러 카테고리에 따른 데이터 블록 (태그와 비슷함)

이러한 정보들을 바탕으로 어떤 상황에서 에러가 발생했는지 확인하고 대응을 할 수가 있다.

필요한 정보가 딱 정리되어있고, 러닝커브없이 바로 사용할 수 있어서 정말 잘만든 툴이라는 생각이 든다.

센트리 초기 세팅하는 방법은 너무 간단해서 생략한다.

가입 → 프로젝트 생성 → 웹에 나와있는 가이드대로 세팅하면 OK 이다.

센트리는 기본적으로 초기 세팅만 해도 대부분의 에러를 로그를 적재하고 모니터링할 수 있다.

하지만 나는 센트리가 제공하는 API를 통해서 로그에 풍부한 에러 정보를 추가하기 위해서 커스텀을 했다.

추가적인 정보를 넣기 위해 에러 바운더리와 앱의 최상위에서 센트리를 초기화 해주는곳에 코드를 작성했는데, 아래에서는 이에 대해 설명하고자 한다.


에러 바운더리를 통해 센트리로 로그를 보내자


에러 바운더리는 에러 바운더리 하위 컴포넌트 트리 발생한 에러를 캐치하고 fallback을 보여주기 위해 작성하는 리액트 컴포넌트이다. 직접 작성하기 위해서는 class 로 작성해야한다.

나는 에러바운더리에서 에러를 캡쳐하고, 추가적인 정보를 더하여 센트리로 로그를 전송하는 코드를 작성했다.

단순히 센트리만을 위해 작성한 것은 아니지만 센트리 API를 사용한 쪽에 초점을 두어 설명하고자한다.

에러 바운더리에서 비동기 에러를 캐치할 수 있을까?

정말 중요한 점이다. 에러바운더리에서는 비동기 에러를 캐치할 수 없다. 왜냐하면 에러 바운더리는 렌더링 중에 발생한 에러만 캐치할 수 있기 때문이다.

이벤트 루프 모델의 구조를 생각해보면 된다.

리액트에서 컴포넌트가 렌더링 되는 동안 동기 코드는 콜스택안에서 즉시 실행된다.

컴포넌트 렌더링을 위해 콜스택에 형성되는 스택을 렌더링 스택이라고 해보자.

Promise 기반 비동기 작업은 완료/실패 시 그 콜백이 마이크로태스크 큐에 들어가고,
현재 작업이 끝나 리액트의 렌더링 콜스택이 비워진 뒤에 실행된다.

Promise에서 오류가 나면 해당 Promise는 즉시 rejected 상태가 되고,

catch에서 작성한 에러를 throw하는 코드는 이벤트 루프에 의해 마이크로태스크큐로 옮겨져, 에러바운더리 컨텍스트를 포함한 렌더링 콜스택이 모두 비워진 후(동기 코드가 모두 실행된 후)에 에러가 throw 된다.

이 시점에는 에러 바운더리의 컨텍스트가 존재하는 렌더링 스택이 모두 pop된 상태이기 때문에 에러를 throw 하더라도 전파될 스택 자체가 존재하지 않아 비동기 에러를 catch할 수 없는것이다.

그렇다면 에러 바운더리에서비동기 에러를 어떻게 처리할 수 있을까?

unhandledrejection 구독

unhandledrejection 이벤트란 어떤 Promise의 reject 되었는데도 이를 처리할 핸들러가 없을 때 전역 스코프(Window)에서 발생하는 이벤트이다.

unhandledRejection 이벤트와 상태 변경을 활용하면 에러 바운더리에서 비동기 에러를 캐치하여 처리할 수 있다. 간단하게, 에러 바운더리 컴포넌트가 마운트 될 때 이벤트 리스너를 등록해주고, 콜백 함수를 인자에 넣어주면 된다.

그렇다면 이게 대체 어떤 이벤트길래 비동기 에러를 캐치할 수 있는것일까?

에러 전파는 리액트 렌더링 스택에 의존적이다. 반면 unhandleRejetion 이벤트를 구독해서 리액트의 상태를 변경해준다면 렌더링을 유발할 수 있다. 즉 렌더링 스택을 호출할 수 있는것이다. 따라서 이를 활용하면 비동기 에러를 에러바운더리가 캐치해서 처리할 수 있다.

componentDidMount() {
    window.addEventListener('unhandledrejection', this.실행할함수);
  }
componentWillUnmount() {
  window.removeEventListener('unhandledrejection', this.실행할함수);
}

tanstack query를 활용한다면?

나는 프로젝트에서 tanstack query를 사용하고 있는데, 이를 활용하면 unhandledRejection을 활용할 필요가 없었다.

Tanstack Query의 throwOnError 옵션

tanstack query의 throwOnError 옵션을 true해주면 리액트의 다음 렌더링 사이클에서 동기적으로 에러를 throw 할수있다. 이를 통해서 에러바운더리가 에러를 캐치할 수 있도록 할수 있다.

이 때 queries에만 true 옵션을 주었는데, mutation에서 발생한 에러는 mutation 함수를 사용하는곳에서 처리 해주었기 때문에 따로 옵션을 주지 않았다.

export const queryClient = new QueryClient({
	defaultOptions: {
		queries: {
			throwOnError: true,
		},
	},
});

자 이렇게 비동기 에러를 에러 바운더리에서 처리하는 방법에 대해 알아보았다.

이제 포착한 에러 로그를 센트리에 전달해보자

에러 바운더리 내부에서 센트리로 로그를 전달하는 코드 작성하기

센트리에서 제공하는 API를 통해서 작성해주면 된다.

크게 Sentry.withScropeSentry.captureException 을 위주로 확인해보자.

Sentry.withScrope 를 통해서 현재 에러가 전송될 때만 태그/컨텍스트 등을 적용해줄 수 있다.

즉, 에러가 여러번 캡쳐 됐을 때 이전 메타 정보가 전달되는것을 방지한다.

Sentry.captureException 는 에러를 센트리에 전달하는 역할을 한다.

이 둘을 활용하면 센트리에 적절하게 에러 로그를 전달할 수 있다.

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
	constructor(props: ErrorBoundaryProps) {
		super(props);
		this.state = {
			hasError: false,
			caughtError: null,
			showFallbackUI: false,
		};
	}

	static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
		...
	}

	componentDidCatch(error: Error, errorInfo: ErrorInfo) {
		const errorCategory = getErrorCategory(error);
		
		// ✅ 현재 에러가 전송 될 때만 태그/컨텍스트를 적용
		Sentry.withScope((scope) => {
			scope.setLevel('error');
			scope.setTag('errorCategory', errorCategory);

			scope.setExtras({ componentStack: errorInfo.componentStack });
			// ✅ 센트리로 에러 로그 전달!
			Sentry.captureException(error);
		});
	}

	resetErrorBoundary = () => {
		...
	};

	render() {
		...
}

export default ErrorBoundary;

센트리에서 비동기 에러를 캡쳐할 때 주의 할점

비동기 에러가 발생할 경우 주의할 점이있다.

비동기 에러가 에러 바운더리에서 센트리에 캡쳐되기 전에, 센트리의 글로벌 캡쳐에 의해서 먼저 비동기 에러가 캡쳐된다는것이다.

따라서 비동기 에러와 관련해서 작성할 코드가 있다면, 에러바운더리가 아닌 앱 최상위에 센트리를 초기화 해주는 부분에 코드를 작성해야한다.

Sentry.init({
	dsn: import.meta.env.VITE_SENTRY_DSN,
	integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
	...
	
	/*
	 * ✅ 비동기 오류의 경우 센트리 글로벌 캡쳐가 error boundary 도달하기 전에 캡쳐하기 때문에,
	 * tag나 context를 설정하기 위해서 beforeSend 활용
	 */
	beforeSend(event, hint) {
		const error = hint.originalException;
		if (error instanceof Error || isAxiosError(error)) {
			const category = getErrorCategory(error);
			event.tags = { ...event.tags, errorCategory: category };
		}
		if (isAxiosError(error) && error.config) {
			const { method, url, params, data: requestData, headers } = error.config;
			event.contexts = {
				...event.contexts,
				'API 요청 디테일': { method, url, params, requestData, headers },
			};
			if (error.response) {
				const { data, status } = error.response;
				event.contexts['API 응답 디테일'] = { data, status };
			}
		}
		return event;
	},
});

에러 로그 쉽게 추적하기 위한 추가 메타 정보 전달하기


Tag를 활용해서 필터링을 용이하게 하자

센트리에서는 아래와 같이 Tag를 통해서 에러를 필터링 할 수 있다. 센트리에서는 Tag를 커스텀해서 로그 정보에 추가할 수 있다

나는 이 Tag를 활용해서 에러를 대응 주체에 따라 로그를 필터링해서 보고자 했다.

에러를 분류하는 함수 작성

에러를 받아서 에러 코드에 따라서 아래와 같이 분류해주었다.

  • 대응 주체가 서버 개발자인 경우: server
    • API 서버 에러 (500~)
  • 대응 주체가 프론트엔드 개발자인 경우: client
    • 클라이언트 에러 (400~)
  • 모두 대응 하여 확인해봐야하는 경우: network, cors, other
    • 웹 애플리케이션 외부 요인에 의한 에러

대응 주체에 따라 구분해주었고, network / cors는 편의를 위해 따로 분리해두었다.

이를 통해서 센트리에 보낼 태그를 작성하는데 활용하였다.

export const getErrorCategory = (error: AxiosError | Error): string => {
	if (isAxiosError(error)) {
		const axiosError = error as AxiosError;
		if (axiosError.response) {
			const status = axiosError.response.status;
			if (status >= 400 && status < 500) {
				return 'client';
			} else if (status >= 500) {
				return 'server';
			}
		} else {
			if (error.message.toLowerCase().includes('cors')) {
				return 'cors';
			}
			return 'network';
		}
	}
	return 'other';
};

위 함수를 활용해서 간단하게 scope.setTag 를 통해서 태그를 전달해주었다.

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
		const errorCategory = getErrorCategory(error);

		Sentry.withScope((scope) => {
			scope.setLevel('error');
			...
			
			// ✅ 에러 카테고리를 태그로 설정하여 Sentry 대시보드에서 로그 필터링에 도움을 준다!
			scope.setTag('errorCategory', errorCategory);

			...
			Sentry.captureException(error);
		});
	}

Context를 활용해서 API 요청, 응답 정보를 추가하자

API 에러가 발생한 경우에는 요청, 응답 정보를 추가했다.

이를 통해 API 에러가 발생했을 때 혹시 잘못된 요청 데이터가 전달되지는 않았는지 쉽게 파악할 수 있게 하고자 했다.

이 때는 scope.setContext 또는 beforeSend 내부에서 event.contexts 를 활용하면 된다.

Sentry.init({
	beforeSend(event, hint) {
			const error = hint.originalException;
			
			if (isAxiosError(error) && error.config) {
				const { method, url, params, data: requestData, headers } = error.config;
				
				// ✅ AxiosError인 경우 API 요청 세부 정보를 추가 컨텍스트로 설정!
				event.contexts = {
					...event.contexts,
					'API 요청 디테일': { method, url, params, requestData, headers },
				};
				if (error.response) {
					const { data, status } = error.response;
				// ✅ AxiosError인 경우 API 응답 세부 정보를 추가 컨텍스트로 설정!
					event.contexts['API 응답 디테일'] = { data, status };
				}
			}
			return event;
		},
})

센트리 웹에 접속하면 아래와 같이 API 요청 디테일 / API 응답 디테일 에서 에러가 발생한 경우의 요청 응답 데이터를 확인해볼 수 있다.

에러가 발생한 컴포넌트 트리 위치 정보 추가

scope.setExtras를 이용하여 componentStack에 대한 정보를 전달하면,

센트리 웹의 Additional Data 카테고리에서 어떤 컴포넌트 트리에서 에러가 발생했는지 확인할 수 있다.

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
		const errorCategory = getErrorCategory(error);

		Sentry.withScope((scope) => {
			scope.setLevel('error');
			scope.setTag('errorCategory', errorCategory);

			// ✅ 에러 발생 시점의 컴포넌트 스택을 전달하자!
			scope.setExtras({ componentStack: errorInfo.componentStack });
			Sentry.captureException(error);
		});
	}

추가적으로 슬랙 알림 연동하기

센트리 웹에 접속하여 Alerts 탭에 들어가면 우리팀의 슬랙과 연동이 가능하다.

이를 통해서 에러가 발생했을 때 슬랙을 통해서 바로 알아차릴수 있다!!


마치며

프로젝트를 진행하면서 에러는 개발과 떼어놓을 수 없는 부분이다.

에러를 미리 예측하여 코드 내에서 미리 대응할 UI를 작성해 둘 수도 있지만, 에러가 항상 예측이 가능한 것은 아니다. 최근 프로젝트에서 후자의 상황이 많이 발생하였기 때문에 에러를 추적하고 모니터링하는것의 중요성을 느끼게 되었다.

이 에러를 추적하고 대응하는 일은 우리 프로덕트의 발전을 위해, 그리고 프로덕트를 사용하는 사용자를 위해서도 아주 중요한 부분이라고 생각한다.

에러 분류, 에러 바운더리 작성, 센트리 커스텀 등 많은 작업들을 진행하면서 에러 대응이란 주제로 많은 고민을 하였는데, 그 결과 에러는 우리 팀의 대응 체계에 적합하게 분류하여 대응하는것이 중요하다는것을 느꼈다.

다른 개발자 분들이 에러를 다루는데 이 글이 많은 조금이나마 되었으면 한다.

profile
늦게 자고 일찍 일어나기

1개의 댓글

comment-user-thumbnail
2025년 8월 20일

도움이 되었습니다.

답글 달기