[if.kakao_frontend] sentry 적용기

봄도둑·2023년 1월 2일
5

if.kakao_2022

목록 보기
1/1

최근 회사 프로젝트에서 모니터링 툴로 sentry를 적용했고, 에러 처리를 위해 react errorBoundary를 추가하면서 많은 문제를 마주치면서 다른 이들은 어떻게 이러한 문제를 해결했을까라는 생각이 들었습니다. 그러던 찰나 if.kakao에서 react에서 이러한 문제를 어떻게 적용하고 해결했는지 흥미 있게 지켜본 주제여서 블로그에 기록으로 남겨봅니다. (영상 링크 : if.kakao의 sentry 적용기)

1. Sentry를 이용한 에러 추적기

이슈 해결까지 장시간 소요

  • 이슈 보고 → FE에서 BE 에러 인지 확인 요청 → BE의 확인 보고 → 테스트 환경 재현

Front에서 발생할 수 있는 이슈

  • 데이터 영역에서의 에러
  • 화면 영역에서의 에러
  • 외부요인에 의한 에러
  • 런타임 에러

→ 이러한 에러를 트래킹 할 수 있다면…

에러 추적 프로세스 개선을 위한 과정

  • 에러 데이터 쌓기 → serverity 기준 설정 및 모니터링 → 에러 데이터 분석 → 분석 결과에 따른 개선

에러 데이터 쌓기

  • Sentry 선정
  • 실시간 로그 취합 및 분석 도구이자 모니터링 플랫폼
  • 로그에 대한 다양한 정보를 제공하고 시각화 도구를 통해 발생한 이벤트들을 쉽게 분석할 수 있도록 도와줌
  • sentry는 기본적으로 2가지 api를 이용해 에러 데이터를 전송함
    • captureException
      • 에러 객체나 문자열 전송
    • captureMessage
      • 문자열만 전송
  • 그런데 에러 데이터는 모았어…. 어떻게 관리할까???

sentry에서는 에러 데이터를 관리할 수 있는 기능들이 많이 있음

Scope

  • configureScope는 공통 정보(사용자 정보 등등), withScope는 각 에러 상황 시의 추가 정보를 전송(localScope와 비슷) → 잘 관리하면 이벤트마다 추가 정보를 확보할 수 있음

context

  • 이벤트에 임의의 데이터를 연결할 수 있는 context를 통해 추가 정보를 전송
  • 단, context를 통해 적재된 데이터는 검색할 수 없고 이벤트가 발생한 이벤트 로그에서 확인할 수 있음

customized tags

  • tag는 indexing이 가능하기 때문에 적재된 데이터에 빠르게 접근할 수 있음 → 이슈 검색 및 트래킹을 빠르게 할 수 있음 ⇒ 이것만 잘 정의해도 sentry 이벤트 분석이 훨씬 쉬울 듯…???

level

  • 이벤트 마다 중요도 레벨을 설정할 수 있음
  • fetal, error, warning, log, info, debug, critical 등급으로 나눌 수 있음
  • 이슈 그룹핑, 알람 조건을 세분화하는데 도움이 됨

issue grouping

  • 모든 이벤트는 fingerprint를 가지고 있음
  • fingerprint가 동일한 이벤트는 하나의 이슈로 그룹화 ⇒ 얘도 결국 이슈 그룹핑을 해주는 조건 중 하나
  • 센트리의 분석 강점은 여러 이슈 그룹핑이 가능하다는 점일 수 도..????

Severity 기준 설정 및 모니터링

  1. 알람 조건 설정하기
    1. When : 처음 보는 이슈가 생길 경우, 해결된 이슈가 다시 발생한 경우, 무시하고 있던 이슈가 해제될 경우
    2. If : 이벤트의 level이 조건에 맞는 경우, 이벤트의 attribute가 조건에 맞는 경우, 이벤트의 tag가 조건에 맞는 경우, 이슈가 중복 발생할 경우(횟수 조건) ⇒ 필터링 조건
    3. Then : 알림 발송 (slack, jira etc…)
  2. when → if → then 순으로 알람의 조건이 설정
    1. ex) 해결된 이슈가 다시 발생할 경우, 이벤트 태그 === api 일 경우, 이벤트의 level === Error일 경우, 동일한 이슈가 1시간 기간 동안 5번 이상 발생할 경우 → slack으로 알림을 받겠다 라는 형태로 조건 지정

에러 데이터 분석

  • 카카오측에서 에러 데이터 수집 전 세웠던 가설
    • QA 과정을 거치기 때문에 화면 영역에서 발생하는 에러는 많이 않을 것
    • 데이터 영역에서 발생하는 API에 대한 에러가 대부분일 것
    • 특정 기기 혹은 브라우저에서 발생하는 에러가 있을 것(크로스 브라우징 이슈)
  • 데이터 분석 결과 API 에러 (ApiNotFoundError)가 대부분으로 가설은 적중했으나 이 중 약속된 에러도 포함되어 있었기 때문에 에러 데이터 수집 시 무분별한 에러 수집은 데이터 분석에 어려움을 준다고 판단

분석 결과에 따른 개선

  • 에러 데이터를 언제 저장해야 하는가? → 유의미한 데이터를 수집하자
  • chunk load 에러, network 에러는 수집 제외(단, timeout 에러는 수집)
  • 트래픽이 많은 서비스의 경우 api 에러를 수집하는 것은 도움이 되지 않음 → 500 에러의 경우 의미 있는 케이스에 한해 수집
  • 에러 데이터 뿐만 아니라 디버깅과 분석에 필요한 추가적인 정보 수집
  • 이슈 그룹핑을 통해 에러 세분화 → 특정 서비스의 진입점을 명확하게 분리할 수 있었음
  • 프론트엔드는 대부분 API를 통해 받은 데이터를 화면에 그려줌 → 서버와 약속된 custom header를 추가하여 요청 → 로그 분석 시 데이터의 정합성을 맞춰볼 수 있게 됨(서버 로그와 프론트엔드에 남은 데이터를 서로 비교해서 분석의 정합성을 높일 수 있음)
    • tag 기능을 이용해 custom header를 추가하면 cs와 장애 상황 시 서버 로그를 더 빠르게 추적할 수 있음

개선 후 좋아진 점

  • 브라우저 버전 문제나 빌드 설정과 같은 문제로 발생한 예상하지 못했던 에러들을 발견하여 사용자 경험을 개선 시킬 수 있었음
    • 사용자의 웹뷰 브라우저 버전을 확인하고 대응하기 위하여 polyfill 추가
  • 장애 탑지 시간, 원인 파악, 해결 까지의 시간이 줄어듬
  • cs 인입 시 사용자의 환경에서 재현하지 않아도 에러 원인을 파악하고 이전보다 정확하게 안내할 수 있게 됨
  • 개발자의 경험이 좋아짐 → 특정 상황 재현에 오랜 시간을 들이지 않아도 에러 추적이 가능

2. 에러 처리를 개선해보자!

사용자 경험을 어렵게 만드는 기존의 에러 처리

  • 하나의 api에서 에러가 발생해도 에러 페이지로 이동
  • 카카오페이의 프론트엔드는 api gateway를 통해 api 호출 → 그렇기 때문에 api gateway는 에러 마다 다양한 케이스의 에러 화면을 가지고 있음 ex) 일반적인 에러, 네트워크 에러, 강제 업데이트, 서비스 점검, 계정 이상 상태 등…
  • 기존의 에러 처리는 axios interceptor 의 reject callback으로 처리 → 요거 우리가 처리하는 방식이랑 같네..???
function onResponseRejected(error : error) {
	return new Promise((_, reject) =>{
		/*네트워크 에러 처리 로직*/
		const errorCode = error.response?.data?.errorCode;
		
		if(errorCode) {
			switch (errorCode) {
				case KPAY_COMMON_ERROR_CODES.계정이상 :
					return handleKitckOutError(error);
			}
		}
	}); 
}
  • reject callback을 사용할 때의 문제점
    • axios interceptor에서 예외 케이스는 어떻게 처리하지? 전역에서 처리하는 흐름이 맞는가?
    • 프로그램 에러 처리를 명령형으로 처리
    • 에러 화면을 보여주려면 라우트 이동으로 처리해야만 함 ex) history.push('error');
    • 에러가 발생하는 API를 호출하는 곳에서 일일이 에러 발생 여부를 확인하고 에러를 표시해야했음
    • axios interceptor를 사용하면 전역에서 발생하는 에러를 처리하는 것은 수월하나 화면의 일부만 에러 상태로 대응하는 것은 어려움 → 부분적 에러 처리
  • 이러한 어려움을 해결하고자 react의 ErrorBoundaries를 도입 고려 (오????)
  • 명령형 에러 처리
    • 에러를 어떻게 화면에 보여줄 것인가에 집중

      const componentWithPossibleError = () => {
      	const {error: err1} = useSomeQuery();
      	const {error: err2} = useAnotherQuery();
      
      	if (err1 || err2) {
      		return <ErrorFallback />;
      	}
      	
      	return <NormalComponent />;
      
      }
  • 선언형 에러 처리
    • 무엇을 보여줄지 코드를 통해 결정할 수 있음

    • 가장 가까운 에러 바운더리로 에러를 넘김

      const componentWithPossibleError = () => {
      	return (
      		<ApiErrorBoundary>
      			<NormalComponent />
      		</ApiErrorBoundary>
      	);
      }

React errorboundary

  • getDerivedStateFromError : 다음 렌더링에서 fallback UI가 보이도록 상태 업데이트
  • componentDidCatch : 에러 리포팅 서비스에 에러 기록 → 요 부분이 sentry가 들어가면 좋을 듯
  • render : 커스텀한 fallback UI 렌더링
  • errorboundary는 react 컴포넌트를 상속 받아서 처리하 수 있지만 react-error-boudary 라이브러리를 통해 쉽게 만들 수 있음
import {ReactErrorBoundary} from 'react-error-boundary';

const FallbackComponent = () => {
	return (
		<div>에러!!!!!</div>
	);
}
export const App = () => {
	return (
		<ReactErrorBoundary fallback={<FallbackComponent>}>
			<Routes>
				{/*중략*/}
			<Routes/>
		</ReactErrorBoundary>
	)

}
  • 요렇게 써주면 서비스 성격이나 에러 상황에 적절한 fallback component 구현 가능
  • react의 error boundaries는 본래 이벤트 핸들러의 에러를 포착하지 않음 → 그래서 우리 프로젝트의 에러가 errorCatcher로 감싸놓고도 잡지 못했음
    • 요놈 잡으려면 react-query의 useErrorBoundary라는 옵션을 사용해 API에서 에러가 발생하면 이를 Error Boundaries가 캐치할 수 있음
    • 카카오 페이의 대부분 서비스들이 react-query를 사용하기 때문에 useErrorBoundary를 사용해 api 호출 시 발생하는 에러를 잡아냄

선언적 에러 처리를 적용해보자

  • axios interceptor 코드 줄이기 → Error Boundaries의 관심사 분리 → Error Boundaries 및 Fallback Component 작성

axios interceptor 코드 줄이기(로직 줄이기)

  • 네트워크 관련 에러만 처리하고 나머지는 error Boundaries에서 처리하도록 위임
function onResponseRejected(error : error) {
	return new Promise((_, reject) =>{
		/*네트워크 에러 처리 로직*/
	}); 
}

Error Boundaries의 관심사 분리

  • Error Boundaries를 관심사별로 분리하기 위해 중첩으로 구성
export const AppLayout = () => {
	return (
		<RootErrorBoundary>
			<ApiErrorBoundary>
				<Outlet />
			</ApiErrorBoundary>
		</RootErrorBoundary>

	)
}
  • ApiErrorBoundary에서는 api에서 발생하는 error를, RootErrorBoundary에서는 그 외 발생하는 Frontend Error를 잡도록 처리

  • 중첩으로 처리하면 하위 에러 바운더리부터 우선적으로 적용하고 관심사의 분리가 가능

    Error Boundaries 및 Fallback Component 작성

const ApiErrorBoundary = ({ children }) => {
	const { reset } = useQueryErrorResetBoundary();
	const { key } = useLocation();

	retrun(
		<ReactErrorBoundary
			FallbackComponent={FallbackComponent}
			onReset={reset}
			resetKeys={[keys]}
		>
			{children}
		</ReactErrorBoundary>
	);

}
  • useQueryErrorResetBoundary : react-query에서 query를 reset하는 custom hook → 필요에 따라 API를 재호출 할 수 있음
  • 필요한 경우 location 객체로 라우터 화면이 일어났을 때 오류 화면이 남아있는 것을 방지할 수 있음 (key가 일종의 reset 트리거로 사용됨)
  • 만약 fallbackComponent에서 처리할 에러가 아니라면 상위의 error boundaries로 위임
function FallbackComponent({ error, resetErrorBoundary }) {
	useEffect(() => {
		//여기서 sentry 같은 에러 모니터링 서비스에 에러 전송
		captureAipError(props.error);
	}, [])
	
	if(!isAxiosError(error)) {
		//API 에러가 아니라면 여기서 상위 error boundary로 위임 처리
		throw error;
	}

	/*다른 에러 처리 로직*/
	
	return (
		<CommonErrorHandler
			resetErrorBoundary={resetErrorBoundary}
		/>
	);
}
  • 에러 바운더리의 장점은 에러를 상위로 전파하지 않고 핸들링할 수 있다는 것이 강점
  • 이러한 에러 바운더리는 local 레벨에서 일부만 보여주도록 사용할 수 도 있음 → 오 꽤 괜찮은데…????? 이게 부분 로딩보다 훨씬 더 좋을 것 같다는 생각이 드네 ⇒ 그런데 이러한 localErrorBoudary의 경우 공통 에러라면 상위의 error boundaries로 넘겨주는 로직이 필요함
const ProductList = () => {
	return (
		<>
			<Container>
				<Banner />
				<Title />
				<LocalApiErrorBoundary height={300}>
					<AllProductList />
				</LocalApiErrorBoundary>
			</Container>
	)
}
  • 공통 에러 처리가 필요한 경우
    • ios에서 unload event가 발생하면 axios에서 호출 중이던 API에서 Network error가 발생 → 이 때 발생한 에러는 Error Boundaries가 포착하면 안됨 → 에러 처리를 200 ms 정도 지연 시키면 error boundary가 포착하지 않게 됨 ⇒ 코드를 강제적으로 지연 시킨다는 건데 이 부분에 대한 해답은 카카오페이도 아직 명확하게 찾지 못한 듯

에러 처리 개선 후…

  • 비즈니스 로직에 집중한 에러 처리가 가능함 → 명령형 에러 처리의 경우 에러가 발생할 수 있는 모든 경우에 대해 Fallback UI 작업이 필요해짐. 선언적 에러 처리의 경우 에러 처리는 error boundary가 위임받아 처리하기 때문에 비즈니스 로직에만 집중할 수 있음
  • UI 일부에서 발생하는 에러를 전역으로 전파시키지 않고 처리할 수 있음
    • 에러가 발생한 컴포넌트의 상위 error boundaries로 이동하기 때문에 더 높은 상위 레벨에서 error를 처리하지 않아도 됨
    • 다만 전역에서 처리되어야 하는 에러는 구분해야 함 → 카카오페이에서는 처리할 에러가 아니라면 throw error를 발생 시켜 상위 error boundaries로 위임
profile
배워서 내일을 위해 쓰자

0개의 댓글