시작하며
🔗 if(kakao) 발표 영상 보러 가기: Sentry를 이용한 에러 추적기, React의 선언적 에러 처리
안녕하세요. 카카오페이 투자FE유닛 클로이입니다.
많은 개발자들에게 장애라는 단어는 겪을 때마다 적응이 안되는 단어이면서 상황일 것입니다. 서비스를 제공하는 기업의 입장에서 대형 서비스의 장애는 매출 하락으로 이어질 수도 있고 사용자와 쌓아왔던 신뢰를 잃을 수도 있습니다. 그만큼 장애 탐지와 대응은 신속하고 정확하게 이루어져야 합니다. 장애는 원인을 파악하기 전까지 인프라 이슈인지 데이터 이슈인지 알 수 없습니다. 또한 백엔드, 프론트엔드 중 어디에서 발생한 이슈인지 파악이 어렵기 때문에 장애가 발생하면 관련된 모든 개발자들은 장애의 원인을 찾기 위해 고군분투합니다. 아무리 QA를 꼼꼼하게 마치고 배포된 시스템이라 하더라도 예상치 못한 원인으로 장애를 겪게 될 수도 있습니다.
프론트엔드에서의 오류
프론트엔드에서의 오류는 크게 데이터 영역, 화면 영역 두 가지 영역에서의 오류, 그리고 예상할 수 없는 네트워크 이슈나 특정 브라우저 버전, 단말기 OS 업데이트 같은 외부 요인에 의한 오류나 예상치 못한 런타임 오류로 나눌 수 있을 것 같습니다.
위 오류 분류를 기준으로 생각해보면 데이터 영역, 화면 영역에서 발생하는 오류는 우리가 충분히 조심하고 방어할 수 있을 것 같습니다. 대부분의 경우에는 QA나 Staging 환경에서도 충분히 재현되기 때문에 사용자들이 오류 사항을 경험하기 전에 해결하는 것도 가능합니다. 그런데 외부 요인에 의한 오류나 런타임 오류는 어떨까요? 크롬의 쿠키 정책 변경, Safari 특정 버전의 Indexed DB API 버그, 또는 정말 특이한 케이스에서만 발생하는 런타임 오류같은 친구들은 QA나 Staging 환경에서 잘 재현되지 않는 경우가 대부분입니다. 언제든지 발생할 수 있는 오류이지만 개발자에게 이런 오류를 예측하는것은 거의 불가능한 일에 가깝습니다.
클라이언트에서 발생하는 오류는 오류가 발생하는 브라우저의 개발자 도구 콘솔에서 확인하거나, 오류가 발생하는 사용자의 디바이스와 동일한 조건의 디바이스에서 재현하며 원인을 파악할 수 있습니다. 그러나 사용자가 가지고 있는 디바이스나 버전은 매우 다양하고, 오류가 발생할 때마다 디바이스를 확보하는 것은 현실적인 어려움이 많고 번거롭습니다. 그렇다고 개발자가 아닌 일반 사용자에게 아웃바운드를 통해 개발자 도구 콘솔에서 발생하는 오류를 알려달라고 할 수도 없을 것입니다. 또한 개발자 도구만으로 파악하기 어려운 오류도 충분히 발생할 수 있고 이러한 모든 과정들은 장애 파악과 해소를 더디게 하여 장애 발생 시간이 길어지게 만들 수도 있습니다.
클라이언트에서 발생하는 오류를 트래킹할 수 있다면 오류를 신속하게 탐지하고 정확하게 원인을 파악하여 빠르게 대응할 수 있지 않을까요? 카카오페이 프론트엔드 개발자들은 이런 오류를 어떻게 대응하고 있을까요?
Sentry
오류를 탐지하기 위한 프론트엔드 모니터링 툴은 여러 가지가 존재합니다. 그 중 카카오페이 프론트엔드에서 채택하여 사용하고 있는 Sentry에 대해서 소개해보고자 합니다.
Working Code, Happy Customers
Your code is telling you more than what your logs let on. Sentry’s frontend monitoring gives you full visibility into your code, so you can catch issues before they become downtime.
Sentry는 실시간 로그 취합 및 분석 도구이자 모니터링 플랫폼입니다. 로그에 대해 다양한 정보를 제공하고 이벤트별, 타임라인으로 얼마나 많은 이벤트가 발생하는지 알 수 있고 설정에 따라 알림을 받을 수 있습니다. 그리고 로그를 수집하는데서 그치지 않고 발생한 로그들을 시각화 도구로 쉽게 분석할 수 있도록 도와주며 다양한 플랫폼을 지원합니다.
그러면 Sentry의 대표적인 특징에 대해서 간략하게 알아보도록 하겠습니다.
▪ 이벤트 로그에 대한 다양한 정보 제공
Sentry는 발생한 이벤트 로그에 대하여 다양한 정보를 제공합니다.
- Exception & Message: 이벤트 로그 메시지 및 코드 라인 정보 (source map 설정을 해야 정확한 코드라인을 파악할 수 있습니다.)
- Device: 이벤트 발생 장비 정보 (name, family, model, memory 등)
- Browser: 이벤트 발생 브라우저 정보 (name, version 등)
- OS: 이벤트 발생 OS 정보 (name, version, build, kernelVersion 등)
- Breadcrumbs: 이벤트 발생 과정
Context 기능으로 기본적으로 제공되는 정보 외에 특정 이벤트에 대한 추가 정보를 수집할 수도 있습니다. Context에 대한 내용은 이 아티클의 하단에서 자세히 다룹니다.
▪ 비슷한 오류 통합
Sentry는 Issue Grouping 기능으로 비슷한 이벤트 로그를 하나의 이슈로 통합합니다. 이는 비슷한 오류를 파악하고 추적하는 데 큰 도움이 됩니다.
▪ 다양한 플랫폼 지원
프론트엔드 뿐만 아니라 .NET, Android, Apple(Cocoa), Go, Java, Kotlin, Python 등의 다양한 플랫폼을 지원합니다.
▪ 다양한 알림 채널 지원
발생한 이슈에 대해 실시간으로 알림을 받을 수 있도록 Slack, Teams, Jira, GitHub 등 다양한 채널을 지원합니다.
기본적인 데이터 쌓기
카카오페이에서는 프론트엔드 기술 스택 중 React
를 중점적으로 사용하고 있습니다. React 기준으로 Sentry를 설정하고 데이터를 쌓을 수 있는 기본 기능에 대해서 살펴보도록 하겠습니다.
▪ install & configure
-
install
Sentry 사용에 필요한 패키지를 설치합니다. Sentry는 애플리케이션 런타임 내에서 SDK를 사용하여 데이터를 캡쳐하기 때문에@sentry/react
,@sentry/tracing
패키지를 설치해야 합니다.# using npm npm install --save @Sentry/react @Sentry/tracing # using yarn yarn add @Sentry/react @Sentry/tracing
@Sentry/browser
에서 사용 가능한 모든 메소드는@Sentry/react
에서 가져올 수 있습니다. -
configure
import React from 'react'; import ReactDOM from 'react-dom'; import * as Sentry from '@Sentry/react'; import { BrowserTracing } from '@Sentry/tracing'; import App from './App'; Sentry.init({ dsn: 'dsn key', release: 'release version', environment: 'production', normalizeDepth: 6, integrations: [ new Sentry.Integrations.Breadcrumbs({ console: true }), new BrowserTracing(), ], }); ReactDOM.render(<App />, document.getElementById('root'));
Sentry 설정에 필요한 기본 정보는 다음과 같습니다.
- dsn: 이벤트를 전송하기 위한 식별 키
- release: 애플리케이션 버전 (보통 package.json에 명시한 버전을 사용합니다. 이는 버전별 오류 추적을 용이하게 합니다.)
- environment: 애플리케이션 환경 (dev, production 등)
- normalizeDepth: 컨텍스트 데이터를 주어진 깊이로 정규화 (기본값: 3)
- integrations: 플랫폼 SDK별 통합 구성 설정 (React의 경우 react-router integration 설정 가능)
hooks 설정도 지원하고 있는데, Sentry에 이벤트를 전송하기 전에 이벤트를 선택적으로 수정해서 데이터를 보낼 수 있는
beforeSend
와 같은 옵션도 제공하고 있습니다. 이 밖에 다양한 기본 설정 옵션은 공식 홈페이지에서 확인할 수 있습니다.추가적으로 React SDK는 자동으로 JavaScript 오류를 탐지하고 Sentry로 전송할 수 있도록 Error Boundary 컴포넌트를 제공하며 다음과 같이 사용할 수 있습니다.
import React from 'react'; import * as Sentry from '@Sentry/react'; <Sentry.ErrorBoundary fallback={<p>에러가 발생하였습니다. 잠시 후 다시 시도해주세요.</p>} > <Example /> </Sentry.ErrorBoundary>;
▪ Capture errors
Sentry는 captureException
과 captureMessage
두 가지 이벤트 전송 API를 제공합니다. 두 API는 다음과 같은 특성을 가지고 있습니다.
-
captureException: error 객체나 문자열 전송 가능
import * as Sentry from '@Sentry/react'; try { aFunctionThatMightFail(); } catch (err) { Sentry.captureException(err); }
-
captureMessage: 문자열 전송 가능
Sentry.captureMessage('에러가 발생하였습니다!');
여기까지의 설정만으로도 Sentry를 기본적으로 사용하는 데 있어 큰 어려움은 없을 것입니다.
풍부한 데이터 쌓기
Sentry는 다양하고 강력한 기능들을 제공합니다. 기본적인 설정에 조금의 설정을 더해준다면 오류를 파악할 수 있는 데이터를 풍부하게 쌓을 수 있고, 오류 추적에도 큰 도움이 됩니다. 데이터를 쌓는 몇 가지 기능들을 더 알아보겠습니다.
▪ Scope
Sentry는 scope
단위로 이벤트 데이터를 관리합니다. 이벤트가 전송되면 해당 이벤트의 데이터를 현재 scope
의 추가 정보와 병합합니다. 일반적으로 SDK에서 자동으로 scope를 관리하므로 이에 대해 크게 신경쓰지 않아도 되지만 scope에 대해서 자세히 알게된다면 각 이벤트마다 의미 있는 정보를 추가적으로 전송할 수 있습니다.
Sentry에서의 scope는 configureScope
와 withScope
두 가지로 나누어 설정할 수 있습니다.
-
configureScope
configureScope
설정은 글로벌 scope와 비슷한 맥락으로 현재 범위 내에서 데이터를 재구성하는데 사용합니다. 이벤트 전송에 있어 공통적으로 사용되는 정보가 있다면 이 설정을 사용하여 다음과 같이 구성할 수 있습니다.import * as Sentry from '@Sentry/react'; Sentry.configureScope((scope: Sentry.Scope) => { scope.setUser({ id: 42, email: 'chloe.ykim@example.com', }); });
-
withScope
withScope
설정은 로컬 scope로 한 번의 범위 내에서 데이터를 재구성할 때 사용합니다. 이 scope를 사용하게 되면 현재 범위의 복제본이 생성되고 설정한 추가적인 정보를 병합하게 되며 함수 호출이 완료될 때까지 격리된 상태로 유지합니다. API로 발생한 오류, 문법 오류, library로 발생하는 오류 등 오류는 다양합니다. 이 기능은 오류에 따라 서로 다른 데이터를 추가적으로 전송해야 할 필요가 있을 때 유용하게 사용할 수 있습니다.카카오페이 프론트엔드 개발자들은 각 서비스마다 추적이 필요한 오류 상황(pdf viewer, 인증 등)에 따라
withScope
기능을 적절히 사용하여 데이터를 추가적으로 전송하고 오류 탐지 및 분석을 하고 있습니다.import * as Sentry from '@Sentry/react'; Sentry.withScope((scope: Sentry.Scope) => { scope.setTag('my-tag', 'my value'); scope.setLevel(Sentry.Severity.Warning); Sentry.captureException(new Error('my 에러')); }); Sentry.captureException(new Error('일반 에러'));
마지막 라인의
captureException
에서는withScope
에서 설정한 태그 정보가 전송되지 않습니다.
▪ Context
context
는 이벤트에 임의의 데이터를 연결 할 수 있는 기능입니다. 검색은 할 수 없고 해당 이벤트가 발생한 이벤트 로그에서 확인할 수 있습니다. API 오류의 경우 요청 데이터와 오류 응답 데이터를 제일 확인하고 싶을 것입니다. 이런 정보는 SDK에서 기본적으로 제공하지 않기 때문에 context를 이용하여 추가적인 정보를 전송해 애플리케이션이 어떤 영향을 받았는지 쉽게 확인할 수 있습니다. context도 위에서 언급한 scope에 따라 설정할 수 있습니다.
다음과 같이 애플리케이션 내에서 axios를 사용하는 경우 error 객체에 담겨있는 다양한 정보를 context에 추가적으로 설정할 수 있습니다.
import * as Sentry from '@Sentry/react';
const { method, url, params, data, headers } = error.config; // axios의 error객체
const { data, status } = error.response; // axios의 error객체
Sentry.setContext('API Request Detail', {
method,
url,
params,
data,
headers,
});
Sentry.setContext('API Response Detail', {
status,
data,
});
▪ Customized Tags
Sentry의 강력한 기능 중 하나인 tag
는 키와 값이 쌍으로 이루어진 문자열입니다. tag의 일반적인 용도로는 호스트 네임, 플랫폼 버전, 운영체제 버전, 사용자 언어 등이 있습니다. tag는 인덱싱 되는 요소이기 때문에 관련한 이벤트에 빠르게 접근할 수 있고 이슈 검색이나 트래킹을 신속하게 진행할 수 있습니다. 또한 이슈의 이벤트에 대한 tag 분포를 확인할 수도 있습니다.
API 오류를 예로 들어보겠습니다. API 오류는 network, timeout, not found, internal server error 등 종류가 다양합니다. network와 일반 API 오류를 구분하여 tag를 설정한다면 두 가지를 구분하여 이슈를 파악할 수 있고 이슈 빈도 확인 등의 분석을 할 수 있습니다. 또한 이슈 알람을 받을 수 있는 조건에 특정 tag 조건을 설정하여 원하는 알람을 생성할 수도 있습니다.
import * as Sentry from '@Sentry/react';
Sentry.setTag('api', 'network');
Sentry.setTag('api', 'internalServerError');
tag의 키는 32 글자이고, 값은 200자까지 가능하므로 주의하여 사용해야 합니다.
▪ Level
Sentry에서는 이벤트마다 level
을 설정하여 이벤트의 중요도를 식별할 수 있습니다. 기본적으로 다음과 같은 에러 level을 정의하고 있습니다.
export declare enum Severity {
Fatal = 'fatal',
Error = 'error',
Warning = 'warning',
Log = 'log',
Info = 'info',
Debug = 'debug',
Critical = 'critical',
}
Issue Grouping이나 앞서 설정한 tag
기능과 마찬가지로 모니터링 알람을 설정하는 데에도 유용하게 사용할 수 있습니다.
import * as Sentry from '@Sentry/react';
Sentry.setLevel(Sentry.Severity.Warning);
이벤트의 level
을 Warning
으로 설정하면 이벤트의 중요도는 높지 않지만 오류 추적 및 분석이 필요한 이벤트로 분류할 수 있을 것입니다.
▪ Issue Grouping
Sentry에서 각 이벤트는 fingerprint
를 가지고 있습니다. fingerprint는 이벤트 내에 수집된 stacktrace, exception, message와 같은 정보들을 기반으로 내재되어 있는 그룹화 알고리즘으로 생성되며 fingerprint가 동일한 이벤트들은 자동으로 하나의 이슈로 그룹화 됩니다. 내재화된 알고리즘에 기반하여 자동으로 그룹화되기 때문에 이벤트들이 예상한 것과 다른 이슈로 보여질 때가 있습니다.
계속해서 API 오류를 예로 들어보겠습니다. 하나의 API에서 발생할 수 있는 오류는 400, 404, 500 등 다양합니다. 요청 uri가 같으면 Sentry는 HTTP status가 다르다 하더라도 같은 이슈로 그룹화합니다. 이렇게 되면 각 HTTP status별로 발생하는 이벤트 분포 확인, 이벤트 검색, 이슈 분석이 어려워집니다.
다음 예제와 같이 HTTP method(GET, POST), status(400, 404, 500), url을 fingerprint 조건으로 설정해주면 해당 조건에 부합하는 이벤트들을 하나의 이슈로 그룹화할 수 있습니다.
import * as Sentry from '@Sentry/react';
const { method, url } = error.config; // axios의 error객체
const { status } = error.response; // axios의 error객체
Sentry.setFingerprint([method, status, url]);
오류 확장하기
Sentry에서 이슈 타이틀은 자바스크립트에 내장되어 있는 빌트인 에러 객체로 생성되어 전송된 에러 객체의 이름에 기반하고 있습니다. 물론 위에서 소개한 captureMessage
를 이용하여 문자열만 이벤트로 전송하게 되면 빌트인 에러 객체를 사용하지 않아도 됩니다. 그러나 stack trace등 오류에 대한 다양한 정보를 얻으려면 빌트인 에러 객체를 이용하여 오류를 생성 및 핸들링하고 Sentry에 전송하는 것이 좋습니다.
앞서 언급했지만 클라이언트에서는 다양한 오류가 발생합니다. 어떠한 오류들이 있을까요?
- 문법 오류
- API 관련 오류
- third-party library에서 발생하는 오류
- 비즈니스 로직 내에서 발생하는 오류
- 예상하지 못한 오류
자바스크립트 빌트인 에러 객체의 유형에는 RangeError
, ReferenceError
, SyntaxError
, TypeError
등 몇 가지가 있습니다. 위에 언급된 오류들을 이러한 빌트인 에러 객체로 생성하여 전송하게 되면 몇 가지 문제점이 발생할 수 있습니다.
예를 들어 인증과 관련해 추적하고 싶은 오류가 있습니다. 일반 빌트인 에러 객체로 생성된 오류를 Sentry에 전송한다면 동일한 에러 객체를 이용하여 생성된 이벤트들과 그룹화 되어 하나의 이슈로 보여지게 됩니다. stack trace 정보를 이용하여 그룹화하는 Sentry의 그룹화 알고리즘 덕분에 그룹화가 되지 않을 수도 있지만 여전히 이슈 타이틀은 같은 Error
로 보여질 수 있으며 이슈 파악과 그룹화, 이슈의 중요도 식별에 방해가 될 수 있습니다.
이런 문제점을 해결하기 위해서는 빌트인 에러 객체를 상속한 새로운 에러 객체를 생성하여 오류를 핸들링하고 Sentry에 전송하면 됩니다. 다음 예제와 같이 확장된 에러 객체를 이용하여 Sentry로 이벤트를 전송하게 되면 이슈 타이틀에는 Error
대신 AuthError
라는 타이틀로 보여지게 됩니다. 또한, 동일한 에러 객체로 생성되어 전송된 이벤트들끼리 그룹화 되어 해당 이슈의 빈도나 발생 케이스를 파악할 수 있게 됩니다.
class AuthError extends Error {
constructor(message: string) {
super(message);
this.name = 'AuthError';
}
}
Sentry.captureError(new AuthError('인증 에러 발생'));
만약, 애플리케이션에서 axios와 타입스크립트를 사용하고 있고 API 오류를 별도의 에러 객체로 핸들링하고 싶다면 다음과 같이 에러 객체를 확장할 수도 있습니다.
class ApiError<T = unknown> extends Error implements AxiosError<T> {
config: AxiosRequestConfig;
code?: string;
request?: any;
response?: AxiosResponse<T>;
isAxiosError: boolean;
toJSON: () => any;
constructor(error: AxiosError<T>, message?: string) {
super(message ?? error.message);
const errorStatus = error.response?.status || 0;
let name = 'ApiError';
switch (errorStatus) {
case HTTP_STATUS.BAD_REQUEST: // 400
name = 'ApiBadRequestError';
break;
case HTTP_STATUS.UNAUTHORIZED: // 401
name = 'ApiUnauthorizedError';
break;
case HTTP_STATUS.FORBIDDEN: // 403
name = 'ApiForbiddenError';
break;
case HTTP_STATUS.NOT_FOUND: // 404
name = 'ApiNotFoundError';
break;
case HTTP_STATUS.INTERNAL_SERVER_ERROR: // 500
name = 'ApiInternalServerError';
break;
}
}
this.name = name;
this.stack = error.stack;
this.config = error.config;
this.code = error.code;
this.request = error.request;
this.response = error.response;
this.isAxiosError = error.isAxiosError;
this.toJSON = error.toJSON;
}
알람 설정하기
지금까지 이벤트 데이터를 Sentry에 전송할 수 있는 대표적인 기능들을 알아보았습니다. 풍부하게 쌓은 데이터는 오류를 추적하고 분석하는데 사용할 수 있고 알람을 받기 위한 조건 설정에도 사용할 수 있습니다.
발생하는 모든 이벤트를 모니터링하고 알람을 받는 것은 매우 비생산적인 일입니다. 따라서 서비스의 성격에 따라 알람을 받기 원하는 조건과 임계치(threshold)를 잘 설정해야 합니다. 위에서 소개했던 기능들은 알람에서 알람 조건을 설정할 때 유용하게 사용될 수 있습니다.
API 오류 중 500 에러에 대해서만 알람을 받고 싶다면 tag
과 level
기능을 이용하여 설정해볼 수 있습니다. 예를 들어 API 500 에러에 대해 level
을 Error
로, 이벤트의 tag
를 api
로 설정하면 관련한 이벤트를 추려낼 수가 있게 됩니다. 이 알람 조건은 이벤트나 이슈 검색 조건으로 활용될 수도 있습니다.
추적하고자 하는 이벤트에 대한 알람 조건과 임계치를 설정하고 모니터링 한다면, 의도치 않은 오류에 대한 신속한 탐지와 발빠른 대응이 가능하고 오류를 분석하여 사용자 경험을 개선시킬 수 있을 것입니다.
언제 어떤 오류를 쌓는 것이 좋을까요?
그렇다면 언제 어떤 오류를 쌓는 것이 좋을까요? 아마도 서비스 성격에 따라 다를 수 있습니다. CS가 빈번하게 인입되는 부분에 대한 오류 추적이 필요하거나 추가된 특정 기능의 Side Effect 유무에 대한 모니터링을 하고자 할 경우에도 이벤트 수집이 필요할 것입니다. 또한 경우에 따라 백엔드에서 발생하는 오류 빈도를 고려하여 이벤트를 수집해야할 수도 있습니다.
API 오류의 경우 사용량이 많은 애플리케이션에서는 HTTP status가 400, 401, 404, 204인 이벤트 수집은 큰 도움이 되지 않을 수 있습니다. 그러나 사용자 경험을 감지하는데 유용하다고 생각된다면 internal server error(500) 이벤트를 수집하고 분석하는 것이 많은 도움이 될 수 있습니다.
어떤 이벤트를 추적하고 분석할지 설계를 먼저하고 이벤트를 수집한다면 개발자 경험과 사용자 경험 개선에 도움이 될 것입니다.
마치며
Sentry는 위에서 소개한 내용보다 더 강력하고 편리한 기능들을 제공하고 있습니다. 오류를 추적하고 분석하여 개선하는 일은 사용자에게는 신뢰를 주고 더불어 개발자에게는 견고한 서비스를 만들어 낼 수 있다는 자신감을 줍니다.
Sentry로 오류 관련 데이터를 수집하고 분석하면서 카카오페이 프론트엔드 개발자들은 다음과 같은 변화를 느꼈습니다.
오류를 추적할 수 있는 데이터 수집으로 든든함을 느꼈습니다. API 때문에 발생한 에러가 대부분이라는 예상도 적중하고 확인할 수 있었습니다. 그리고 QA 과정에서 발생하지 않았던 특정 기기와 특정 브라우저 버전에 대한 에러를 발견할 수 있었습니다. 폴리필을 추가하는 등의 대응이 가능했으며 이로 인해 사용자 경험을 증대시킬 수 있다는 자신감이 생겼습니다. 또한 오류 추적 시 디바이스 디버깅으로 특정 상황을 재현하지 않고도 로그를 통해 빠른 파악이 가능해졌습니다. 이는 개발자 경험도 좋아졌으며 특히 QA 이슈를 해결할 때 많은 도움이 되었습니다.
이 밖에도 오류 추적을 통해 조금 더 고민이 필요한 부분에 대해서도 배우고 있습니다. 빈번하게 발생하는 오류에 대한 분석의 필요성을 느끼게 되었고 **임계치(threshold)**에 대한 고민도 생겼습니다. 임계치는 어느 정도의 수치가 적절하며 어느 수치 이상부터 장애 상황으로 보아야 할지에 대해서 크루들과 고민하고 있습니다. 또한 프론트엔드와 백엔드의 오류 로그를 연결해 추적할 수 있는 방안을 마련하고자 여러 가지 시도와 고민을 해보고 있습니다.