[마이쿠키하우스]가 릴리즈되기 며칠전에 주요 기능 구현을 마치고 사용성 테스트를 하기 위해 팀원들의 지인들 위주로 테스트할 사용자들을 모았다.
그런데 로그인 이후에 페이지 리다이렉트 과정에서 특정 로직에서 문제가 생겼는지 원하는대로 페이지 이동이 안되는 문제가 발생했다. 나 포함 대부분의 사람들은 문제가 없이 의도한대로 잘 동작하지만, 특정 유저 몇몇만 원하는대로 동작하지 않는 것이 문제였다. 내가 개발중에 이런 문제가 발생했다면 개발자 도구를 통해 어렵지 않게 원인을 찾았겠지만 모르는 사람에게 console.log를 찍어달라고 할 수도 없고... 답답했다.
프론트엔드에서의 완벽한 테스트는 어렵다고 한다. 왜냐하면 모든 브라우저/디바이스 환경을 고려하고, 가능한 모든 사용자의 인터렉션을 완벽하게 재현하기 힘들기 때문이다. 아무리 꼼꼼하게 QA와 E2E 테스트를 하더라도 내가 가능한 모든 시나리오를 확인해봤다고 증명할 수 있는 방법은 없다.
위 문제 상황을 어찌저찌 해결했지만, 불안해지기 시작했다. 과연 내가 짠 코드가 수천명의 사람들이 다양한 인터렉션을 해도 더 이상 아무 문제가 없다고 장담할 수 있을까?
클라이언트의 에러를 모니터링 할 수 있는 도구들을 찾다가 if(kakao)2022 영상을 보고 Sentry를 사용해보기로 하였다. 부분 무료이고 레퍼런스가 많아서 도입하기 어렵지 않다고 생각했다. 또한 Sentry는 깃허브, Jira, 슬랙과 같은 도구들과 연동해서 알림을 받을 수 있다. 나는 내가 설정한 기준에 해당하는 에러들만 이메일로 받도록 설정했다. 그 기준은 후술하겠다.
기본 설치와 세팅은 공식문서를 참고하였다. 나는 리액트를 사용하였는데, 리액트와 react router dom을 사용할 때 어떻게 적용하는지 자세하게 설명이 되어있다. 아래부터는 내가 Sentry의 주요 기능과 그것들을 어떻게 적용하고 효과를 봤는지 설명해보겠다.
Sentry를 configure하면, 디폴트 설정으로 에러를 캐치하는 글로벌 핸들러를 심는다. 그렇기 때문에 별도의 설정을 하지 않아도 uncaught exceptions
혹은 unhandled promise rejections
과 같은 이벤트들을 Sentry가 캡쳐해준다.
이와 함께 수동으로 원하는 타이밍에 에러를 캡쳐할 수 있는데, 이를 가능하게 해주는 대표적인 api가 Sentry.captureException
혹은Sentry.captureMessage
이다.
captureException
captureMessage
를 통해 에러 객체를 전달 할 수 있다.
captureException
을 통해 문자열을 전송할 수 있다.
Sentry는 스코프 단위로 발생한 이벤트를 캡쳐한다. [마이쿠키하우스]에서는 다음과 같이 App 최상단에서 configureScope
를 이용해 스코프에 로그인 된 유저 정보를 저장했다. 이렇게 되면 이 정보가 전역에서 사용되어, 내부에서 일어나는 모든 이벤트들을 기록할 때 같이 이 유저 정보가 함께 기록되게 된다.
이를 통해 모니터링 할 때, 어떤 유저에게 발생한 에러인지 빠르게 파악할 수 있다.
나는 서버의 Api 에러의 경우, 요청 내용과 내용과 응답 내용을 빠르게 한눈에 보고 싶었다. Sentry에서 에러가 발생할 경우 기본적으로, 자바스크립트의 기본 Error 객체를 생성하여 전송하는데, 나는 여기에 요청 내용과 응답 내용을 담아 보냈다. 아래와 같이 Error 객체를 상속받는 ApiError 객체를 생성하여 내가 원하는 내용을 Sentry.setContext
와 Sentry.setTag
를 활용하여 담았다.
Sentry.setContext
는 이벤트에 원하는 데이터를 넣고 싶을 때 사용하고, Sentry.setTag
는 키-값 쌍을 이루는 문자열로, 인덱싱 되어 원하는 이벤트를 빠르게 찾고 싶을 때 유용하게 사용된다.
이를 통해 에러의 원인을 빠르게 찾을 수 있을 뿐만 아니라, 단순히 'Error'라는 이름으로 에러가 기록되지 않고 정확한 에러 이름을 나타나게 했다. 그리고 태그로 (상태 코드, api 호출 함수명) 을 설정하여, 같은 에러들끼리 그룹화 되게끔 하였다. 이렇게 하면 필요할 때 태그로 해당 에러들을 검색하거나 한번에 모아볼 수 있고, 해당 에러의 발생 케이스를 쉽게 분석할 수 있다.
Sentry에서는 발생하는 이벤트 마다 Level을 설정할 수 있다. 디폴트로 발생한 모든 에러 이벤트들은 Error
로 Level이 설정되어 알림이 가도록 되어있다. [마이쿠키하우스]에서는 중요하지 않은 에러들에 대해 Level을 따로 설정해 두었다.
예를 들어 아래와 같이, 존재하지 않는 페이지로 이동해서 발생하는 404에러는 굳이 주목할 필요가 없기 때문에 withScope
를 활용하여 Level을 warning
으로 재구성하였다.
configureScope
가 글로벌한 스코프 범위의 이벤트 데이터를 구성하는 것이라면, withScope
는 로컬한 현재 스코프 범위의 이벤트 데이터를 재구성할 때 사용된다.
서비스 개시한지 얼마 지나지 않아 하나의 똑같은 에러가 자주 발생하는 것을 확인할 수 있었다.
미션 수행 과정에서, 가구를 선택한 뒤에 버튼을 클릭하여 제출하는 api에서 계속 에러가 발생했다. POST 요청인데, body에 일부 데이터가 null 로 담겨 제출되는 것이 문제였다. 문제는 요청 성공 이후에 집 내부를 보여주는 페이지로 리다이렉트 되야하는데, 여기서 에러가 발생하면 사용자는 멈춰있는 화면을 계속 보게된다. 사용자가 이상적인 흐름대로 사용하였다면 null 이 담길 수가 없는 구조인데 알 수 없는 이유로 null 이 담기는 것이었다.
Sentry에서 이슈가 발생하기 이전의 과정들을 추적하여 보여주는 breadcrumbs
를 제공한다. 이를 통해 에러가 발생하기 이전에 사용자가 어떤 UI를 조작했고, 어떻게 페이지를 이동했고, 어떤 요청들을 보냈는지 확인 할 수 있다.
이를 통해 해당 에러가 발생할 때, 사용자가 짧은 시간 동안 제출 버튼을 두 번 이상 반복적으로 누른다는 사실을 알 수 있었다.
해당 에러의 원인은 이러했다:
이를 통해 개발과정에서 이러한 경우를 생각하지 않았다는 것을 깨닫고, 해당 mutation이 isPending 상태일 때 버튼을 disabled 상태로 바꾸는 방식으로 코드를 수정하여 해결했다.
Sentry가 아니었다면, 저런 문제를 겪는 사용자가 있다는 것을 몰랐을 수도 있었다고 생각하니 소름이 끼쳤다. 알았더라도 같은 상황을 재연하고 원인 파악하는 데 굉장히 오랜 시간을 걸렸이 것이다. 확실히 Sentry를 도입하고 나서 클라이언트에서 발생하는 모든 에러들을 확인할 수 있으니, 조금은 안정감을 느낄 수 있게 되었다.
또한 생각보다 사용자들은 이상적인 흐름으로 서비스를 사용하지 않는 다는 것을 느꼈고 나도 그에 맞춰서 조금 더 꼼꼼하게 개발을 진행해야겠다고 생각했다. 여담으로 무슨 이유에서인지 도무지 모르겠지만, 존재하지 않는 이상한 URL로 접근하려는 사용자가 몇몇 있었다🤔.
끝으로, 왜 기업들이 '실제 서비스 운영 경험'을 중요시 여기는지도 깨닫게 되었다.