AI로 개발이 빨라졌습니다. 예전에는 반나절 걸리던 화면이, 지금은 오전에 나오고 오후에 배포됩니다. “지금 밖이라 집에 들어가서 할게요.”라는 말 대신, 모바일로 에이전트에 요청하고 배포하는 시대가 와버렸죠. 코드는 늘어나고, 기능은 확장되고, 배포 주기는 극단적으로 짧아졌습니다. 하지만 안정성은 같은 속도로 따라오지 못합니다.
오히려 반대입니다. 코드가 빨리 쌓일수록 “그 버튼이 안 된다” 같은 VOC는 더 모호해집니다. 서버는 200을 반환하고, DB도 멀쩡하고, 백엔드 로그에는 아무런 흔적이 없습니다. 그런데 유저는 안 된다고 합니다. 외부 본인인증처럼 우리 서버를 거치지 않는 구간이면, 이 문제는 더 어려워집니다. 개발 속도는 구현의 문제고, 안정성은 관측의 문제입니다. 저는 이 둘이 같은 문제인 줄 알았습니다. 개발을 잘하면 안정성도 좋아질 줄 알았습니다. 하지만 실제로 서비스를 운영해 보니 그 둘은 다른 문제였습니다.
그래서 프론트엔드에 두 개의 눈을 심었습니다. 하나는 Sentry, 다른 하나는 Grafana입니다. 깨진 화면은 Sentry로, 핵심 시나리오가 실제로 불렸는지는 Grafana로 봅니다. 이번 글에서는 사이드 프로젝트에서 이 두 도구를 무료 티어로 어떻게 운영하고 개선했는지, 그 경험을 정리해 봤습니다.
서비스를 운영하다 보면 VOC는 매우 단순합니다. “그 버튼이 안 돼요” 혹은 “에러가 떠요”에서 대부분 마무리됩니다. 가끔 현재 상태와 재현 시나리오를 함께 전달해 주는 분도 있지만, 매번 그것을 기대할 수는 없습니다. 그래서 부랴부랴 서버를 열어 보면 200이거나, 요청 자체가 없습니다. DB도 멀쩡합니다. 백엔드 개발자에게 물어봐도 “우리 쪽은 이상없다”는 답만 돌아옵니다. 오직 그 유저만 안 됩니다.
제가 겪은 예시를 들어보겠습니다. 첫 번째는 본인 인증입니다. 가입이나 온보딩 단계에서 외부 API로 휴대폰 번호를 검증합니다. 그런데 카카오톡 인앱 브라우저에서 인증을 마치고 돌아오면, 가입을 이어가는 데 필요한 인증 정보가 사라져 있었습니다. 서버 로그에는 최종 가입 API가 호출되지 않은 채로 끝나 있습니다.
두 번째는 특정 코드에서 발생한 split is not a function 에러입니다. API가 실패한 것이 아니라, 서버가 준 값을 프론트에서 문자열인 줄 알고 자른 것입니다. 타입스크립트를 쓰고 스웨거(Swagger)로 자동 생성한 타입 정보까지 받고 있었는데, 타입은 string인데 실제 값은 문자열이 아니었습니다. 그런데도 유저에게는 실제로 에러가 발생하고 있었습니다. 나중에 원인을 보니 데이터 마이그레이션 과정에서 잘못된 값이 들어가 있었습니다.
이렇게 실패에는 여러 종류가 있습니다. 서버가 아는 실패, 브라우저가 아는 실패, 외부 SDK가 삼킨 실패입니다. 그중 백엔드 로깅은 첫 번째만 커버할 수 있습니다. “그 버튼이 안 된다”에서 난감한 부분은 바로 뒤의 두 가지입니다.
그래서 프론트에도 관측이 필요합니다.
모든 구현에서 가장 좋은 코드는 아무것도 작성하지 않은 코드입니다. 코드 없이 문제를 해결할 수 있다면 그게 가장 좋다는 말입니다. 하지만 이번에는 하나의 도구로 모든 것을 할 수 없어서 두 가지로 나눠 구현했습니다.
Sentry는 “실패를 체크”합니다. 화면이 죽거나, 에러 바운더리에 잡히거나, 서버 5xx가 프론트까지 오면 이슈가 생성됩니다. 여기에 개발자가 볼 수 있는 기본 정보가 함께 붙습니다. 앞서 말한 split is not a function이 바로 여기에 해당합니다. API 호출은 정상이었지만 화면이 깨진 경우죠.

Grafana는 “성공을 체크”합니다. Grafana의 프론트엔드 관측 SDK인 Faro로 화면을 따라가고, 네트워크 요청마다 기록을 남깁니다. 특정 유저의 화면이 정상적으로 로딩되었는지, API 요청의 성공률은 얼마인지 등을 체크합니다. 그리고 핵심 시나리오의 전환율이 어떻게 되고 있는지도 확인할 수 있습니다.

다시 정리하면 Sentry는 실패를, Grafana는 성공을 체크합니다. 따라서 “그 버튼이 안 된다”는 VOC는 Grafana에서 성공을 기본으로 확인하고, 실패했을 때는 Sentry로 원인을 확인하는 식으로 관리해야 합니다. 이것이 제가 두 가지 툴을 모두 도입한 이유입니다.
실패와 성공을 체크해도, 그게 누구의 어떤 요청인지 확인할 수 없으면 VOC는 여전히 모호합니다. 물론 모든 VOC와 에러를 체크해서 고치는 것이 가장 좋습니다. 하지만 서비스는 매우 다양하고 참신한 방식으로 실패합니다.
제 사례를 하나 들어보겠습니다. 2020년에 출근만 하면 특정 API가 안 된다는 VOC를 받았습니다. 당시에는 이런 툴을 도입할 생각을 못 했고, 일주일 동안 원인을 찾아도 해결하지 못해 결국 그 유저의 출근 장소를 직접 찾아갔습니다. 그곳은 지하 3층이었고, 네트워크가 너무 느려 타임아웃 에러가 발생하고 있었습니다. 그 사이드 이펙트로 API가 안 되던 것이었습니다. 지금 생각하면 참 멍청한 일이지만, 실제로 많은 프로젝트가 이런 툴 없이 운영되는 모습을 주변에서 종종 봅니다.
다시 돌아와서, 간단한 구현 예시로 VOC를 추적하는 방법을 알아보겠습니다.
제가 운영하는 서비스의 로그인 타입은 크게 두 가지, 업체 유저와 일반 유저입니다. 그리고 유저 타입에 따라 제공되는 기능이 다릅니다. 그래서 Sentry와 Grafana에 로그인 정보를 심어 봅시다. (이 글의 예시 코드는 Next.js 16, TypeScript 7 기준으로 작성했습니다.)


이렇게 각각의 훅에서 로그인 정보를 처리합니다. 로직은 매우 유사합니다. 그 결과는 대시보드에서 다음처럼 보입니다.

그렇다면 모든 정보를 계속 쌓으면 좋을까요? 그렇지 않습니다. 가장 근본적인 이유는 돈입니다. 많이 쌓을수록 비싼 값을 내야 합니다.

저는 회사에서는 Enterprise를 쓰지만, 사이드 프로젝트에서는 모두 Free Tier를 사용합니다. Grafana뿐 아니라 Sentry도 마찬가지입니다.
Free Tier는 데이터 보존 기간이 짧고 용량과 기능도 제한적이지만, 사이드 프로젝트에는 이 정도로 충분합니다. 서비스가 잘 된 다음에 더 높은 Tier로 옮겨 가는 것이 현명합니다. 티어를 바꾼다고 별도의 마이그레이션이 필요한 것도 아닙니다. 단지 돈을 더 내야 할 뿐이죠. 그렇다면 다시 돌아와서, 어떻게 Free Tier에서 최고의 효율을 뽑아낼 수 있을까요? 소제목처럼 쌓아야 할 데이터와 버려야 할 데이터를 구분하는 것이 그 시작입니다.
로깅은 많이 남기는 것이 능사가 아닙니다. 물론 AI의 발전으로 데이터 정제가 쉬워진 만큼 많아도 문제될 건 없다는 게 최근 제 생각이긴 하지만, 그럼에도 관리는 필요합니다.
우선 저에게 필요한 정보는 현재 서비스의 상태입니다. 예를 들어 Sentry는 다음과 같은 설정값을 갖고 있습니다.

각 환경값과 버전값, 그 외에 필요한 값을 선언해서 관리하는 방식입니다.
그런 다음 버리는 값을 살펴볼까요? 대표적으로 개인식별정보(PII)와 관련된 값은 최대한 제거합니다. 우선 각 서비스에서 제공하는 기본 옵션(Sentry의 경우 위에 있는 sendDefaultPii)으로 1차 방어합니다.
그리고 프론트엔드에서 쓰는 별도의 패턴값으로 2차 제거합니다.

여기까지만 해도 각 툴의 기본 사용은 충분합니다. 쌓인 데이터만으로도 많은 인사이트와 개선 사항을 찾을 수 있을 겁니다. 여기에 더해 몇 가지 운영 팁을 알아보겠습니다.
우선 Local과 Dev 환경에서는 이슈를 쌓지 않습니다. Staging 환경은 예외가 될 수도 있지만, Local과 Dev에서 확인되는 에러는 어차피 개발 단계에서 모두 수정되기 마련입니다. 따라서 Free Tier 용량을 차지하지 않도록 비활성화하는 편입니다.

SampleRate도 Prod 환경에서 100%로 두지 않습니다. 최소한으로 잡아 처리합니다. 그다음은 노이즈 에러를 걸러내는 일입니다. Next.js에서 자주 발생하지만, 실제로는 노이즈인 에러를 미리 찾아내 계속 제거합니다.

실제로 리액트 프로젝트에 Sentry를 붙여 운영 환경에 배포하면 가장 많이 뜨는 에러는 하이드레이션(Hydration) 에러입니다. 이를 적절히 제거하거나 필터링하는 방식으로 운영하면 좋습니다.
이렇게 다양한 필터링을 추가하다 보면 어느 순간 정말 필요한 에러 로그만 쌓이는 것을 확인할 수 있습니다.
혹시 LogQL을 알고 계신가요? Grafana Loki에서 로그를 검색하고 분석할 때 쓰는 쿼리 언어입니다. 여기서 Loki는 오픈소스 로그 집계·모니터링 시스템이라고 보면 됩니다.
따라서 Grafana 대시보드에서 데이터를 보고 싶으면 이 쿼리 언어를 알아야 합니다. 예전에는 이 쿼리 언어 자체가 하나의 장벽이었습니다. 언어를 잘 다뤄야 그 많은 데이터 속에서 원하는 인사이트를 얻을 수 있었지요.
하지만 AI가 발전하면서 양상이 매우 달라졌습니다. 이제는 소스 코드에서 보고 싶은 데이터를 MCP(Model Context Protocol)로 요청하기만 하면 적절한 쿼리를 작성해 주는 시대가 되었습니다.

위 대시보드에서 저는 쿼리를 단 한 줄도 작성하지 않았습니다. 그렇다고 쿼리 언어를 모르는 것도 아닙니다. 단지 그럴 필요가 없었을 뿐이죠. 서비스 운영의 핵심 API부터 마케팅 링크와 알림톡으로 들어오는 세션까지, MCP로 연결하고 무엇을 보고 싶은지 말하면 다 만들어 줍니다.
예를 들면, 이렇게 요청하곤 합니다.
그리고 그 결과가 대시보드에 마법처럼 나타납니다.

그런 다음 Message에 쌓인 로그를 보고 에러를 수정합니다. 지금 보면 console.error: `DialogContent` requires a `DialogTitle` for the component to be… 같은 에러가 반복적으로 발생하고 있습니다.
사용 중인 컴포넌트 라이브러리에서 DialogTitle이 없을 때 발생하는 경고성 에러입니다. 이를 수정해 이후에는 같은 에러가 발생하지 않도록 관리합니다. 그다음에는 아래 대시보드에서 성공률이 낮은 API를 찾아 개선합니다. 가장 상단의 성공률 42.9% API를 분석해 유저가 어떤 이유로 실패를 마주했는지 살펴봅니다.

이 API는 업체 유저만 호출해야 하는 API였는데, 호출 위치가 잘못되어 일반 유저도 호출하는 바람에 백엔드 서버에서 거절한 경우였습니다.
오전에 피그마 기획이 나오고 오후에 구현이 끝나고, 컴퓨터 앞에 없어도 배포하는 삶이 왔습니다. 빨라진 개발 속도에 맞춰 우리는 유저의 VOC에도 더 빨리 대응해야 합니다. 이제는 개발 속도보다 대응 속도가 더 중요한 시기입니다. 이를 위해 프론트엔드에 두 개의 눈을 심었습니다. Sentry는 실패를 체크하고 Grafana는 성공을 체크합니다.
이번에 Free Tier 툴을 도입하고 운영하면서 느낀 점은, 무작정 쌓는 것만이 능사는 아니라는 것이었습니다. 그리고 AI의 발전 덕에 생각보다 쉽게 수준 높은 최적화와 깔끔한 대시보드를 만들어 관리할 수 있었습니다. 이런 툴이 없다면, 제가 버그를 잡으러 지하 3층을 찾아갔던 2020년처럼 여러분도 지하 3층으로 내려가야 할지 모릅니다.
다음 VOC가 들어왔을 때, 여러분은 지하 3층으로 향하는 삶과 대시보드를 여는 삶 중 어떤 것을 선택하고 싶으신가요? 단언컨대 저는 대시보드를 여는 쪽이 훨씬 편리해 보입니다.
ⓒ요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.