
Sentry 알림을 끄는 대신, AI 에이전트를 상시 멤버로 — 프로덕션 에러 이슈 90% 정리한 2주
Sentry 알림을 끄는 대신, AI 에이전트를 상시 멤버로 — 프로덕션 에러 이슈 90% 정리한 2주
<!-- 발행 메모: 상태=본문 완성(실측 반영), 남은 건 캡처 7개뿐 / 초점=자동화("일회성 분석"이 아니라 "상시 멤버") / 대상=FE·풀스택, 에이전트 실무 도입 검토 팀 -->TL;DR — 프로덕션 Sentry에 쌓인 유니크 이슈 1,463개를 AI 에이전트(Claude Code + Sentry MCP)로 분석·정리했다. 90%가 단일 원인(서드파티 GTM 에러의 파편화)임을 규명하고, 노이즈 필터링·user context·단일 이슈 그루핑을 설계·배포. 나아가 매일 에러를 자동 분류·보고하고 매주 수정 PR까지 올리는 클라우드 에이전트를 구축해 2주간 운영 검증했다.
- 내 역할: 문제 진단 · 해결 설계(설계 3안 비교 후 결정) · 코드 수정·배포 · 에이전트 자동화 파이프라인 구축·운영
- 스택: Claude Code, Sentry MCP / Sentry REST API, Slack Incoming Webhook, Claude Code 스케줄 routine(클라우드 cron), Next.js(
@sentry/nextjs)- 결과: 유니크 이슈 -90% (1,463 → 150) · 영향 사용자 측정 가능화(Users 0 → 측정) · 봇이 "신규 노이즈 발견 → 검증 → PR" 루프를 스스로 도는 상태로 정착
0. 훅
우리 서비스의 Sentry에는 일주일에 에러 이벤트가 9,381건 쌓인다. 유니크 이슈로는 1,463개.
이 숫자를 보고 어디서부터 손대야 할지 막막하다면, 아마 당신의 팀에도 익숙한 사이클이 있을 것이다: 알림이 너무 많다 → "어차피 대부분 노이즈"라는 학습된 무력감 → 알림을 끈다 → 진짜 버그를 놓친다 → 사고가 터지고 나서야 Sentry를 연다.
이 글은 그 사이클을 AI 에이전트(Claude Code + Sentry MCP)로 끊은 기록이다. 결론부터: 유니크 이슈의 90%가 단 하나의 원인이었고, 그걸 찾아 고쳐 배포하는 데 반나절, 그 뒤 에이전트가 매주 스스로 에러를 정리하는 체계로 자리 잡기까지 2주가 걸렸다.
엄밀히 말하면 이건 "에러를 줄이는 프로젝트"라기보다, 개발자 한 명이 AI 에이전트를 실무 워크플로에 어디까지 밀어넣을 수 있는지 직접 부딪혀 본 실험에 가깝다. 그래서 잘된 것만큼 삐걱댄 것도 그대로 적었다.

1. 시작은 질문 하나였다
Claude Code에 Sentry MCP를 연결해두면, Claude Code에서 Sentry에 자연어로 질문할 수 있다. 이 프로젝트는 정말로 이 한 줄에서 시작됐다:
"센트리 mcp 연결됐을거야 가장 최근에러는 뭐야?"
에이전트가 이슈 목록을 가져왔고, 이어서 "현재 상황과 에러 개수, 빈도, 종류를 전부 기록해줘"라고 하자 몇 분 만에 베이스라인 리포트가 나왔다:
| 지표 | 24시간 | 7일 |
|---|---|---|
| 에러 이벤트 | 955건 | 9,381건 |
| 유니크 이슈 | 291개 | 1,463개 |
대시보드를 클릭하며 돌아다니는 대신, 에이전트가 search_events로 집계 쿼리를 만들어 던지고 표로 정리한다. 여기까지는 "편리한 조회 도구" 수준이다. 진짜는 다음부터다.
2. 진단: 데이터가 말해준 4가지 사실
에이전트가 타입별/페이지별/브라우저별로 쪼개서 본 결과:
- 에러의 70%가 상품 상세 한 페이지(
/product-detail/[productNo])에서 발생 - 상위 25개 이슈 중 17개가
google_tag_manager["rm"][...](61)[0]꼴의 동일 패턴 — 인덱스 숫자만 다른 채 별개 이슈로 파편화. 패턴 전체로 세어보니 주 3,364건, 유니크 이슈 1,311개 — 전체 이슈의 90% - 남은 것 중 최대 건수는
Cannot read properties of undefined (reading '0')— 7일간 3,061건. "이게 진짜 앱 버그구나" 싶었다 (스포일러: 아니었다) - 모든 이슈의 영향 사용자 수가
Users: 0—Sentry.setUser()를 안 해둬서, 3,000건짜리 에러가 한 명이 무한루프를 도는 건지 3,000명이 한 번씩 겪는 건지 구분할 수조차 없었다
3. 원인 분석: 두 번의 반전
반전 1 — "최대 앱 버그"도 GTM이었다
reading '0' 이슈의 근본원인을 파기로 했다. Sentry의 AI 분석(Seer)을 돌렸지만 self-hosted 환경이라 500 에러. 막히자 에이전트가 보인 행동이 이번 작업에서 가장 인상 깊었다:
- 이슈의 스택트레이스에서 minified 위치(
main-d82e88461da85a1b.js:19:171045)를 확보 - 프로덕션 CDN에서 해당 번들을 직접 다운로드
- line 19, col 171045 부근의 코드를 추출
나온 코드는 우리 비즈니스 로직이 아니라 Sentry SDK의 wrap 함수였다. 즉 최상단 프레임은 Sentry가 콜백을 감싼 래퍼일 뿐이고, 실제 발화점은 그 아래 스택 — 전부 gtm.js(vtp_gtmOnSuccess)였다. 결국 최대 "앱 버그" 3,061건도 GTM 태그 코드에서 터진 거였다. 유력한 트리거는, 상품 데이터(items) 없이 dataLayer에 push되는 이벤트를 GTM 태그가 널가드 없이 items[0]으로 읽는 것이었다.
반전 2 — 필터는 이미 있었다. 죽어 있었을 뿐
더 어이없는 발견. sentry.client.config.ts에는 이미 GTM 에러 필터가 있었다:
const externalScriptPatterns = ['googletagmanager.com', ...]; if (frames.some(f => patterns.some(d => f.filename?.includes(d)))) return null;
그런데 @sentry/nextjs의 RewriteFrames가 beforeSend보다 먼저 실행되면서 프레임 filename을 https://www.googletagmanager.com/gtm.js → app:///gtm.js로 치환한다. 도메인 문자열이 사라지니 이 필터는 단 한 번도 매치된 적 없는 죽은 코드였다. 에러가 쏟아진 건 "필터가 없어서"가 아니라, "필터가 조용히 무력화돼 있어서"였다.
4. 수정: 그리고 세 번의 설계 변경
수정 자체는 단순하다. 흥미로운 건 "GTM 에러를 어떻게 처리할 것인가"를 두고 설계가 세 번 바뀐 과정이다.
| 안 | 내용 | 기각 사유 |
|---|---|---|
| 1안 | GTM 에러 전량 차단 | "운영자가 GTM에 잘못된 태그를 넣어 사이트가 죽어도, GTM 때문인지 알 수 없잖아" |
| 2안 | 2%만 샘플링 수집 | "98%에 위험한 버그가 있어도 무시될 수 있잖아" |
| 최종 | 100% 수집 + fingerprint 단일 이슈 그루핑 | 이벤트는 전부 보존(스파이크 = 운영 사고 감지), 이슈 목록 오염만 제거 |
if (isExternalScriptError) { event.fingerprint = ['third-party-gtm']; // 1,311개 파편 → 1개 이슈 event.tags = { ...event.tags, third_party: 'gtm' }; return event; // 드랍 없음 }
두 번의 기각은 모두 내 판단이었다. 에이전트가 1안을 내놨을 때 "GTM 사고가 나도 못 잡는다"고 막았고, 2안(샘플링)에는 "98%에 위험한 버그가 묻힐 수 있다"고 막았다. 에이전트가 초안을 만들고 내가 운영 리스크를 따져 방향을 트는 이 루프가, 이 프로젝트에서 가장 생산적인 협업 형태였다. 결과적으로 작업의 목표도 정정됐다: 에러 이벤트 수를 줄이는 게 아니라 신호 대 잡음비를 높이는 것.
함께 나간 수정:
- 레거시 에디터 콘텐츠의 인라인 스크립트 에러(
$ is not defined등, 주 ~1,800건) — 코드가 아니라 DB 콘텐츠가 던지는 에러라 ignoreErrors로 분리, 콘텐츠 정리는 운영팀 백로그로 Sentry.setUser()— memberNo를 SHA-256 해시(앞 16자)로 가명화해서 전송. Users 집계는 살리고 원본 회원번호는 Sentry에 남기지 않음
배포 20분 후 검증: 서로 다른 메시지의 GTM 에러들이 단일 이슈로 모이기 시작했고, 이슈에 Users: 1 — 프로젝트 사상 처음으로 0이 아닌 사용자 카운트가 찍혔다.
한 가지 분명히 해두자. 이 "최대 3,061건"을 내가 코드로 고친 건 아니다 — 발화 코드는 GTM 컨테이너 안(태그 설정)에 있어 우리 레포에서 손댈 수 없다. 내가 한 건 그 에러를 단일 센티널 이슈로 묶어 추이를 관측 가능하게 만든 것이고, 근본 수정(GTM 태그에 items 널가드)은 GTM 운영사 몫으로 백로그에 넘겼다. "줄였다"가 아니라 "보이게 만들고 책임 소재를 분리했다"가 정확하다.

5. 본론: 에러 관리를 에이전트의 "업무"로 만들기
여기까지는 일회성 대청소다. 문제는 다음 주에 또 쌓인다는 것. 그래서 1장에서 손으로 했던 분석을 에이전트가 매일 스스로 하게 만들었다.
구성
Claude Code의 스케줄 routine(클라우드 cron) — 내 맥이 꺼져 있어도 Anthropic 클라우드에서 독립 실행된다.
평일 10:00 ─ 데일리 트리아지 봇
├─ 24h 신규 이슈 수집 → 앱버그/서드파티/환경 3분류
├─ GTM 센티널 점검 (양적: 전일 대비 배수 / 질적: 신규 메시지 패턴)
└─ Slack: "신규 N건 / 노이즈 M건 / 사람 확인 필요 K건"
매주 월 10:30 ─ 주간 리포트 + 수정 PR
├─ 베이스라인과 동일 쿼리 재실행 → 전주 대비
├─ 반복 노이즈 / 앱 버그 → 검증 후 draft PR (최대 1건, base main)
└─ Slack (+ PR 링크)
자동화의 선을 어디에 긋는가
처음 설계에는 "노이즈 자동 ignore"가 있었지만 v1에서는 뺐다. 읽기 전용 토큰으로 시작해서, 봇이 한 주간 분류 정확도를 증명하면 쓰기 권한을 준다. 같은 이유로 "수정 PR 자동 생성"도 보류 — 에이전트는 분석과 보고까지, 판단과 머지는 사람이.
맹점을 설계에 포함하기
단일 이슈 그루핑의 부작용: 새로운 유형의 GTM 에러가 생겨도 새 이슈로 안 뜨고 센티널 이슈에 묻힌다. 그래서 봇의 일일 점검에 "센티널 이슈 내부의 처음 보는 메시지 패턴 탐지"를 넣었다. 무시가 아니라 위임 + 감시다.

운영 기록 — 첫 주 (W24, 6/15)

봇을 돌린 지 며칠 만에, 자동화를 하길 잘했다 싶은 일이 있었다. 봇이, 내가 놓친 노이즈를 찾아낸 것이다.
주간 봇이 6/12 초기 청소 때 내가 놓친 패턴 2개를 잡아냈다: Can't find variable: webkit(주 354건, Safari/iOS 확장 주입)과 Can't find variable: EmptyRanges(주 55건, 레거시 인라인 스크립트). 둘 다 우리 코드가 쓰지 않는 외부 노이즈임을 grep으로 검증한 뒤, 봇이 직접 draft PR을 올렸다. 봇이 작성한 PR 본문이 이렇다:
목적
주간 Sentry 리포트 봇이 이번 주 반복 노이즈 패턴 2건을 발견하여
ignoreErrors에 추가합니다.1.
Can't find variable: webkit— Sentry #1803, 주간 354건, 재발(regressed)
항목 내용 주간 이벤트 354건 (7d) 브라우저 Mobile Safari 9.0 / iOS 26.x 메커니즘 auto.browser.browserapierrors.addEventListener근거: Safari/iOS WebView 확장이
webkit변수를 직접 참조하나 브릿지가 노출되지 않은 환경에서 발생하는 외부 주입 노이즈. 우리 코드는window.webkit(선택적 타입 선언)만 존재. 검증:grep -rn "webkit" src/→window.d.ts의 타입 선언 및-webkit-CSS 프리픽스만 존재. 직접 JS 변수 참조 없음 확인.2.
Can't find variable: EmptyRanges— Sentry #339, 주간 55건근거: 구버전 에디터 상품 설명 HTML의 인라인 스크립트가 호출. 기존
renderCategoryProducts와 동일 성격의 레거시 콘텐츠 노이즈. 검증:grep -rn "EmptyRanges" src/결과 없음.확신 수준
- webkit: 높음 / EmptyRanges: 높음
총 절감 예상
주간 ~409건 노이즈 제거
Generated by Claude Code


스택트레이스 근거, grep 검증 결과, 확신 수준, 절감 예상까지 — 이 정도면 사람이 따로 더 파보지 않고 바로 리뷰하고 머지를 판단할 수 있었다. (실제 머지 전에 내가 PR의 base 브랜치를 손봐야 했는데, 그 얘기는 회고에서 따로 적었다.)
운영 기록 — 둘째 주 (W25, 6/22)
두 번째 주간 리포트는 봇이 이제 "상시 멤버"로 자리 잡았음을 보여줬다.
- 또 다른 레거시 노이즈를 자율 발견:
initMainLaunchSwiper기획전 배너 인라인 스크립트 누수(주 ~3,027건) → 검증 후 draft PR #526 자동 생성 - 첫 주 webkit/EmptyRanges가 필터된 효과로 비-GTM 노이즈 순위가 재편됨 (봇이 "이미 필터됨 — 중복 PR 금지"를 지켜 재탕 PR을 만들지 않음)
- GTM 센티널은 단일 이슈로 정상 그루핑 유지(주 2,924건, 54명) — 추이 모니터링만
운영하며 다듬은 것 하나: 데일리 봇이 주말에도 돌던 걸 평일(월~금)만으로 바꿨다. "매일"이 항상 좋은 게 아니라, 받는 사람의 업무 리듬에 안 맞으면 알림은 금세 무시당하기 때문이다.
6. 결과 (Before / After)
배포 후 첫 정제된 한 주(6/15~6/22) 실측 — 베이스라인과 동일 쿼리(count(), count_unique(issue), 7d):
| 지표 | Before (6/12) | After (6/22) | 변화 |
|---|---|---|---|
| 주간 유니크 이슈 | 1,463개 | 150개 | -90% ← 핵심 |
| 주간 에러 이벤트 | 9,381건 | 7,370건 | -21% (GTM은 의도적 100% 보존) |
| GTM 노이즈 이슈 | 1,311개 (전체의 90%) | 1개 (센티널 응집) | 파편화 해소 |
| 영향 사용자 측정 | 불가 (Users: 0) | 가능 (센티널만 주 54명) | 신규 확보 |
| 신규 이슈 인지→분류 | 수동·불규칙 | 평일 10:00 자동 | — |
핵심은 이벤트 수(-21%)가 아니라 **유니크 이슈 -90%**다. GTM 에러는 한 건도 안 버렸는데도(추이·스파이크 100% 보존) 이슈 목록이 1,463개→150개로 줄었다. "에러를 줄였다"가 아니라 신호 대 잡음비를 높였다는 게 정확한 표현이다. 그리고 Users가 0에서 측정 가능으로 바뀌면서, 처음으로 "이 에러를 몇 명이 겪는가"를 우선순위 근거로 쓸 수 있게 됐다.

7. 회고
잘된 것
- 진단
수정배포까지 반나절 (자동화 정착·운영 검증은 그 후 2주). 병목은 분석이 아니라 사람의 의사결정이었고, 그건 좋은 병목이다 - Seer(전용 AI 기능)가 막혔을 때 에이전트가 번들 다운로드라는 우회로를 스스로 찾은 것 — 도구가 아니라 문제 해결자로 동작한 순간
한계 / 실패
- self-hosted Sentry는 Seer 미지원 — 에이전트의 직접 분석으로 대체 가능했지만 소스맵이 없어 더 어려웠다. 소스맵 업로드는 백로그 1순위
- 조사 중 발견한 GA4 데이터 품질 버그를 "겸사겸사" 고치려다 스코프 이탈 지적을 받고 원복 — 에이전트는 발견을 잘하는 만큼 스코프가 새기 쉽다. 사람이 "그건 이 작업이 아니야"라고 끊어주는 게 중요했다
- 운영에서 다듬은 것들: ① 클라우드 routine 첫 실행이 네트워크 egress 차단으로 실패(허용 도메인 미등록) — 봇이 "조용히 종료 금지" 지시대로 transcript에 원인을 남겨 빠르게 잡음 ② rolling 7d 통계라 같은 주를 다른 시각에 돌리면 숫자가 미세하게 달라짐 → Before/After는 고정 구간으로 측정 ③ 주말 발송 제외(평일만)로 알림 리듬 조정
사람이 개입해 교정한 케이스들 (자동화의 한계를 정직하게):
- 봇이 노이즈로 분류한
SyntaxError: Unexpected identifier 'https'(특정 상품 인라인 스크립트)는 — 메시지로 필터하면 다른 진짜 SyntaxError까지 삼킬 위험이 있어, "필터 부적합 → 해당 상품 콘텐츠 점검" 케이스로 사람이 재분류. 봇 프롬프트에 이 판단 기준을 추가했다. - webkit 필터를 처음엔 내가 손으로 추가했다가, "봇이 PR로 올리는 모습을 검증하자"는 목적으로 원복하고 봇에게 양보 → 봇이 PR을 만들었는데 PR base를 내가 처음에 stage로 잘못 잡아(stage가 production보다 뒤처져 있어 그대로 머지하면 직전 작업이 회귀됨) main 기준으로 다시 만들었다. 자동화는 매끄러운 직선이 아니라, 사람이 게이트마다 개입해 교정하는 협업 과정이었다.
다음 단계
- 봇 분류 정확도 검증 후 자동 ignore 권한 부여 (v2)
- GTM 운영 대행사에 태그 널가드 요청 — 근본 수정
8. 직접 구축하기 (설정 가이드)
이 시스템은 크게 두 부분이다 — (1) 로컬에서 대화로 분석하는 부분과, (2) 클라우드에서 매일 자동으로 도는 봇. 이 둘은 Sentry에 접근하는 방식이 다르다는 게 핵심 함정이다.
8-1. 로컬: Claude Code에 Sentry MCP 연결
대화형 분석(1~3장에서 한 것)은 Claude Code에 Sentry MCP 서버를 붙이면 된다. 프로젝트 루트의 .mcp.json에 등록:
// .mcp.json { "mcpServers": { "sentry": { "command": "npx", "args": ["-y", "@sentry/mcp-server@latest"], "env": { "SENTRY_HOST": "sentry.example.com", // self-hosted 호스트 (SaaS면 생략) "SENTRY_ACCESS_TOKEN": "<사용자 auth 토큰>" } } } }
self-hosted는
SENTRY_HOST(또는 버전에 따라--url)로 인스턴스를 가리킨다. 연결 후/mcp로 도구 목록이 뜨면 성공. 이후 "최근 에러 뭐야?"처럼 자연어로 질문하면 된다.
8-2. 첫 질문은 가볍게 — 베이스라인부터
연결되면 가장 먼저 시킬 일: "현재 에러 현황을 전부 기록해줘." 이걸 먼저 해두면 나중에 Before/After 비교가 공짜로 생긴다. 이 글의 9,381건/1,463개가 그 산물이다.
8-3. Slack Incoming Webhook 만들기
봇이 결과를 보낼 채널이 필요하다. api.slack.com/apps → Create New App → From a manifest 로 만들면 가장 확실하다:
display_information: name: Sentry 트리아지 봇 features: bot_user: # ← 이게 없으면 "봇 사용자 없음" 에러 display_name: sentry-triage-bot oauth_config: scopes: bot: - incoming-webhook # ← 이게 없으면 "봇 스코프 필요" 에러
함정:
features.bot_user와oauth_config.scopes.bot은 반드시 세트다. 한쪽만 있으면 "봇 사용자 없음" / "봇 스코프 필요" 두 에러를 번갈아 만나며 막힌다 — manifest 방식으로 한 번에 선언하면 피할 수 있다.
생성 후 Incoming Webhooks → 토글 On → Add New Webhook to Workspace → 게시 위치 선택(개인 DM 또는 채널) → URL 복사. 수신자를 늘리려면 webhook을 추가로 만들어 봇이 여러 URL에 동시 POST하게 하거나(2~3명), 처음부터 채널 1개에 붙이는 게 관리가 편하다(채널 초대만으로 확장).
8-4. 클라우드 routine 등록 (/schedule)
매일 자동 실행은 Claude Code의 /schedule로 클라우드 routine을 만든다. 내 맥이 꺼져 있어도 Anthropic 클라우드에서 독립 실행된다. 단 함정 3가지:
- 클라우드는 로컬 MCP 설정을 못 본다. 그래서 routine 프롬프트에는 MCP 대신 Sentry REST API 호출 + 읽기 전용 토큰을 직접 넣는다:
토큰 스코프는 읽기 전용(Project/Issue&Event/Organization = Read)만. 봇은 분석·보고만 하고 Sentry 상태는 안 바꾼다.curl -H "Authorization: Bearer <읽기전용 토큰>" \ "https://sentry.example.com/api/0/organizations/<org>/issues/?project=<id>&query=is:unresolved firstSeen:-24h&sort=freq" - cron은 UTC. 한국시간 10:00 =
0 1 * * *, 평일만이면0 1 * * 1-5(10시는 UTC로 날짜가 안 밀려 요일 그대로). 주간은 월 10:30 =30 1 * * 1. - 네트워크 egress 허용목록. 클라우드 환경 설정에서 봇이 호출할 외부 도메인(self-hosted Sentry 호스트,
hooks.slack.com)을 미리 허용해야 한다. 안 그러면 첫 실행이 조용히 차단된다.
PR까지 만들게 하려면 routine에 GitHub 레포를 연결하고, 프롬프트에 base 브랜치를 production(main)으로 고정 + draft PR만(머지 금지)으로 제한한다.
8-5. 비용
- Slack 웹훅: 무료 / self-hosted Sentry: 추가 비용 없음
- 유일한 비용은 routine 실행분(데일리+주간, 경량 조회 작업이라 Pro 플랜 한도 내에서 미미)