Sentry를 바로 도입하지 않고 200줄 에러 트래커를 만든 이유
안녕하세요. 서비스웹개발팀의 헨비입니다.
Sentry를 바로 도입하지 않고 200줄 에러 트래커를 만든 이유

안녕하세요. 서비스웹개발팀의 헨비입니다.
이번 글에서는 Sentry 같은 상용 도구를 바로 도입하는 대신, 200줄 수준의 작은 클라이언트 에러트래커를 만든 이유와 후기를 정리해보겠습니다.
현재는 직접 만든 작은 도구와 클라이언트 에러 수집·집계·Slack 알림이라는 핵심 기능으로 운영 중에 있습니다. 이 과정에서 무엇을 얻고 무엇을 포기했는지, 그리고 앞으로 어떤 기준으로 이런 간단한 솔루션을 선택해야 하는지 공유하고자 합니다.
왜 직접 도구를 만들기 시작했나?
처음에는 “필요한 건 결국 Sentry 같은 표준 솔루션이 아닐까”라는 생각도 했지만, 실제 운영에서는 도입 비용과 승인 절차를 밟는 시간보다, 당장 할 수 있는 간단한 에러트래킹 툴로 최소한의 운영 신뢰성을 가져가는 것이 중요하다고 생각했습니다.
결론적으로 저희 조건에서는 Sentry 도입보다 Edge Function 기반의 작은 자체 도구가 더 현실적인 선택이었습니다.
초기 설계: Sentry에서 핵심 기능만 뽑기
자체구축 도구는 관리 포인트를 늘리는 안티 패턴입니다. 원본의 좋은 기능을 하나씩 따라 만들기 시작하다가, 어느새 본업보다 도구 유지보수에 더 많은 시간을 쓰게 됩니다. 자체 도구가 커지는 걸 막으려면, 처음부터 필요한 기능을 줄여야 했습니다. 그래서 먼저 Sentry 기능 중 실제로 쓸 것만 정리해 보았습니다.
- 클라이언트 에러 수집 —
window.error, Promise rejection, React Query 실패, Axios 5xx - 누적 집계 — 같은 에러가 며칠간 몇 번 발생했는지 추적
- Slack 알림 — 일정 임계값을 넘으면 Slack 에러 채널로 전송
- 스팸 방지 — 한 번 알린 에러는 일정 기간 다시 알리지 않기
Source map symbolication, breadcrumbs, User ID 자동 주입, 자체 대시보드 UI 같은 기능은 제외했습니다. 좋은 기능인 건 맞지만, 당시 저에겐 꼭 필요한 기능은 아니었습니다.
필요했던 건 Sentry 전체가 아니라 에러 수집, 중복 집계, Slack 알림 정도였습니다. 이 정도는 Supabase Edge Function, Postgres, Slack Webhook 조합으로 하루 이틀이면 만들 수 있는 범위였습니다.

브라우저나 WebView에서 발생한 에러는 reportError로 모은 뒤, fingerprint 생성과 중복 전송 방지를 거쳐 Edge Function으로 보냅니다. Edge Function은 DB에 에러를 저장하고, 같은 fingerprint가 일정 횟수 이상 쌓였을 때만 Slack으로 알림을 보냅니다.
임계값과 쿨다운은 클라이언트가 아니라 DB에서 관리했습니다. 운영 중에 알림 기준을 바꿔야 할 수 있기 때문에, 재배포 없이 조정할 수 있게 하기 위해서였습니다.
후보에 올랐지만 제거된 기능
미니 센트리를 만들기로 했으므로 공수가 들어가거나 관리 포인트가 생기는 기능들은 최대한 버리거나 대체 가능한 방법으로 해결했습니다.
- Source map symbolication — 빌드 SHA만 저장, 필요해지면 그때 검토
- Breadcrumbs (사용자 행동 이력) — 구현 비용 대비 디버깅 패턴엔 우선순위 낮음
- User ID 자동 주입 — 필요한 호출부에서 metadata로 수동 주입
- 자체 대시보드 UI — DB Table Editor로 충분
- 이슈 트래커 자동 생성 — Slack 알림에서 사람이 직접 판단
중간중간 넣고 싶은 기능은 계속 생겼습니다. 예를 들어 user ID를 자동으로 붙이는 로직도 잠깐 고민했습니다. 하지만 처음에 정한 기준으로는 지금 할 일이 아니었습니다.
만들면서 중요했던 결정 두 가지
결정 1. Stack을 그대로 쓰지 않고 fingerprint로 해싱한 이유
같은 에러를 “같은 에러”로 인식하려면 식별자가 필요합니다. 처음에는 stack trace 첫 줄을 그대로 식별자로 쓰려 했지만, 문제는 빌드마다 minified 함수 이름이 바뀌면 같은 버그가 매번 새 에러로 잡힌다는 점이었습니다. 배포가 자주 일어나면 7일 5회를 못 채울 수도 있습니다.
그래서 stack에서 라인·컬럼·번들 해시처럼 배포 때마다 달라질 수 있는 부분을 먼저 정규화한 뒤 FNV-1a 32비트 해시를 적용했습니다.
export function createFingerprint(input: {
errorName: string;
message: string;
stack?: string;
}): string {
const normalized = (input.stack ?? '')
.split('
').slice(0, 3)
.map(line =>
line
.replace(/:\d+:\d+/g, '') // line:col 제거
.replace(/\?[a-z0-9]+/gi, '') // ?hash 제거
)
.join('|');
let hash = 2166136261;
const seed = `${input.errorName}|${input.message}|${normalized}`;
for (let i = 0; i < seed.length; i++) {
hash ^= seed.charCodeAt(i);
hash = (hash * 16777619) >>> 0;
}
return hash.toString(16);
}
이렇게 하면 배포가 바뀌어도 같은 버그를 같은 fingerprint로 묶을 수 있습니다. 이후 집계와 알림은 모두 이 값을 기준으로 동작합니다.
결정 2. 임계값/쿨다운을 클라이언트가 아닌 DB에 둔 이유
알림 기준은 단순했습니다. “7일 동안 5번 이상 발생하면 알림을 보낸다. 한 번 알림을 보낸 에러는 7일 동안 다시 알리지 않는다.”
문제는 이 숫자가 운영하면서 계속 바뀔 수 있다는 점이었습니다. 알림이 너무 많으면 5를 10으로 올리고 싶고, 너무 조용하면 5를 3으로 낮추고 싶어집니다.
이 값을 클라이언트에 두면 수정할 때마다 재배포가 필요합니다. WebView 환경에서는 사용자 캐시가 언제 갱신될지도 신경 써야 합니다. 그래서 임계값과 쿨다운값은 DB의 RPC 함수 안에 뒀습니다.
CREATE OR REPLACE FUNCTION upsert_error(...)
RETURNS jsonb AS $$
DECLARE
v_threshold int := 5; -- ← 여기만 바꾸면 즉시 반영
v_cooldown interval := '7 days';
...
SQL Editor에서 값만 바꾸면 바로 반영됩니다. 알림 기준처럼 운영하면서 조정할 가능성이 큰 값은, 배포 없이 바꿀 수 있는 위치에 두는 편이 낫다고 봤습니다.
3주 적용기: 생각보다 대부분은 일회성 에러였다.

운영 전에는 dev/stage 환경에서 QA 기간 동안 1달 이상 먼저 검증했고, 운영 배포는 최근에 진행했습니다. 운영 표본은 아직 충분하지 않아, 이 절의 관찰은 dev/stage 데이터를 기준으로 정리합니다.
가장 의미 있었던 관찰은 3주 동안 7일 5회 임계값을 넘긴 fingerprint가 단 1건이었다는 점입니다.
처음에는 임계값 5가 너무 높아 유의미한 에러를 놓치는 것이 아닌지 점검했습니다. 이를 확인하기 위해 임계값을 1로 낮춰 raw 데이터를 직접 살펴봤습니다. 결과는 대부분이 한두 번 발생하고 사라지는 일회성 에러였습니다. 네트워크 일시 단절, 광고 SDK의 일시적 실패, 사용자가 빠르게 페이지를 이탈한 경우가 여기에 포함됐습니다.
결과적으로 “7일 5회”는 불필요한 알림을 줄이면서도 반복되는 에러는 놓치지 않는 임계값으로 동작하고 있었습니다. 우리가 추적해야 했던 것은 여러 사용자에게 반복적으로 나타나는 에러였고, 이 임계값은 그 신호를 비교적 정확하게 분리해냈습니다. 정책을 DB에 둔 덕분에 임계값 1로 잠깐 내려보는 실험도 재배포 없이 가볍게 수행할 수 있었습니다.
유일하게 임계값을 넘긴 사례도 흥미로웠습니다. 특정 OS 버전 + WebView 조합에서만 발생하는 에러였는데, device/browser/platform 메타데이터를 함께 저장해둔 덕분에 원인을 빠르게 좁힐 수 있었습니다. UA를 raw string으로만 저장했다면 원인 파악 속도는 훨씬 느렸을 것입니다.
반면 운영 관점에서 아쉬웠던 결정도 있습니다. 빌드 SHA를 처음부터 주입하지 않은 것입니다. 회귀 버그를 추적할 때는 “어느 배포부터 시작됐는가”가 가장 빠른 단서가 되는데, 도입 시점에 이 항목을 뒤로 미뤘습니다. 운영 데이터가 쌓이기 시작한 지금은 우선순위가 더 높아졌고, 곧 보완할 예정입니다.
마무리
이번 작업을 하면서 느낀 건, 간단한 서비스일수록 최소한의 에러 트래킹이 더 중요할 수 있다는 점이었습니다.
요즘은 AI 도구의 도움을 받아 작은 서비스를 빠르게 만들고 배포하는 일이 많아졌습니다. 하지만 빠르게 만든 서비스일수록 운영 가시성은 뒤로 밀리기 쉽습니다. 화면은 배포됐고 API도 정상인데, 정작 사용자의 브라우저 안에서 어떤 에러가 나고 있는지는 모르는 상태가 될 수 있습니다.
특히 CSR 기반 서비스는 이 문제가 더 잘 드러납니다. 많은 로직이 클라이언트에서 실행되기 때문에, 서버 로그만으로는 실제 사용자 환경에서 발생한 문제를 파악하기 어렵습니다. 특정 브라우저, OS, WebView, 외부 SDK 조합에서만 나는 에러는 더더욱 그렇습니다.
이번 도구도 그 정도의 역할을 기대하고 만들었습니다. 모든 에러를 완벽하게 보는 도구는 아니지만, 적어도 반복되는 클라이언트 에러를 모르고 지나치는 상태에서는 벗어났습니다.
감사합니다.
메타데이터
- post_id
- 2b1c3fc5413d
- slug
- sentry를-바로-도입하지-않고-200줄-에러-트래커를-만든-이유-2b1c3fc5413d
- url
- https://techblog.gccompany.co.kr/sentry%EB%A5%BC-%EB%B0%94%EB%A1%9C-%EB%8F%84%EC%9E%85%ED%95%98%EC%A7%80-%EC%95%8A%EA%B3%A0-200%EC%A4%84-%EC%97%90%EB%9F%AC-%ED%8A%B8%EB%9E%98%EC%BB%A4%EB%A5%BC-%EB%A7%8C%EB%93%A0-%EC%9D%B4%EC%9C%A0-2b1c3fc5413d
- canonical_url
- https://techblog.gccompany.co.kr/sentry%EB%A5%BC-%EB%B0%94%EB%A1%9C-%EB%8F%84%EC%9E%85%ED%95%98%EC%A7%80-%EC%95%8A%EA%B3%A0-200%EC%A4%84-%EC%97%90%EB%9F%AC-%ED%8A%B8%EB%9E%98%EC%BB%A4%EB%A5%BC-%EB%A7%8C%EB%93%A0-%EC%9D%B4%EC%9C%A0-2b1c3fc5413d
- author_url
- https://medium.com/@henby
- status
- ok
- fetched_at
- 2026-06-09 15:37:30