Slack에서 '@bot 하루 요약' 한마디면 끝 — Claude Agent SDK로 일간 리포트 자동화하기
Slack에서 '@bot 하루 요약' 한마디면 끝 — Claude Agent SDK로 일간 리포트 자동화하기#
매일 아침 이런 루틴이 있었습니다.
- Google Analytics 열어서 어제 DAU, 페이지뷰, 이탈률 확인
- Sentry 열어서 새로운 에러 있는지 확인
- Microsoft Clarity 열어서 데드클릭, 레이지클릭 확인
- DB 접속해서 어제 신규 가입자 수 확인
각각 3분씩만 잡아도 12분. 매일 반복하면 한 달에 4시간입니다. 데이터를 "보는 것"에만 4시간을 쓰고 있었습니다. 보고 나서 "판단"하는 시간은 별도입니다.
이 4개 도구를 하나로 합쳐서, Slack에서 @bot 하루 요약 한마디면 전체 리포트가 나오는 시스템을 만들었습니다.
기술 스택: 왜 이 조합인가#
| 구성 요소 | 선택 | 이유 |
|---|---|---|
| AI | Claude Agent SDK | MCP 네이티브 지원, 도구 호출 자율 판단 |
| 메신저 | Slack Bolt (Socket Mode) | 팀이 이미 쓰는 도구, WebSocket 기반이라 서버 노출 불필요 |
| 데이터 소스 | MCP 서버 4개 | GA, Sentry, Clarity, PostgreSQL을 표준화된 인터페이스로 연결 |
| 런타임 | TypeScript + tsx | 타입 안전성 + 컴파일 없이 바로 실행 |
핵심은 **MCP(Model Context Protocol)**입니다. MCP는 AI 모델이 외부 도구와 통신하는 표준 프로토콜입니다. GA용 MCP 서버, Sentry용 MCP 서버, Clarity용 MCP 서버, PostgreSQL용 MCP 서버 — 이렇게 4개를 연결하면 Claude가 필요한 도구를 알아서 선택해서 호출합니다.
"GA 데이터 가져와"라고 프롬프트에 쓰지 않아도 됩니다. "하루 요약해줘"라고만 하면 Claude가 어떤 MCP 서버에서 어떤 데이터를 가져올지 스스로 판단합니다.
아키텍처: 256줄로 완성되는 에이전트#
전체 코드가 src/index.ts 단일 파일 256줄입니다. 구조는 이렇습니다.
사용자: "@bot 하루 요약"
↓
Slack (Socket Mode, WebSocket)
↓
app.event("app_mention") → runAgent()
↓
Claude Agent SDK query()
├─→ analytics-mcp (GA4 데이터)
├─→ sentry (에러 모니터링)
├─→ clarity-mcp (UX 데이터)
└─→ postgres (DB 직접 조회)
↓
응답 → splitMessage() → Slack Thread로 전송
Socket Mode를 선택한 이유#
Slack 봇 연동 방식은 크게 두 가지입니다. HTTP 웹훅과 Socket Mode.
HTTP 웹훅은 Slack이 우리 서버에 요청을 보내는 구조입니다. 서버가 외부에 노출되어야 하고, HTTPS 인증서가 필요하고, 방화벽 인바운드 포트를 열어야 합니다.
Socket Mode는 반대입니다. 우리 서버가 Slack에 WebSocket으로 연결하는 아웃바운드 구조입니다. 서버 노출이 필요 없고, 로컬 환경에서도 바로 동작합니다. 소규모 팀에서 운영하는 내부 도구에는 Socket Mode가 훨씬 실용적입니다.
const app = new App({
token: SLACK_BOT_TOKEN,
appToken: SLACK_APP_TOKEN,
socketMode: true, // WebSocket 연결
});
토큰 3개만 세팅하면 끝입니다. SLACK_BOT_TOKEN(메시지 전송 권한), SLACK_APP_TOKEN(Socket Mode 전용), SLACK_SIGNING_SECRET(요청 서명 검증).
MCP 서버 4개 연결하기#
MCP 서버는 AI 모델에게 외부 도구를 제공하는 프로세스입니다. Claude Agent SDK에서는 mcpServers 옵션으로 선언하면 에이전트가 자동으로 연결합니다.
const mcpServers = {
"analytics-mcp": {
command: "pipx",
args: ["run", "analytics-mcp"],
env: {
GOOGLE_APPLICATION_CREDENTIALS: "/path/to/ga-service-account.json",
},
},
sentry: {
command: "npx",
args: ["-y", "@sentry/mcp-server@latest", `--access-token=${SENTRY_TOKEN}`],
},
"clarity-mcp": {
command: "npx",
args: ["@microsoft/clarity-mcp-server", `--clarity_api_token=${CLARITY_TOKEN}`],
},
postgres: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-postgres", POSTGRES_URL],
},
};
주목할 점은 GA MCP 서버는 Python 기반(pipx)이고 나머지는 Node.js 기반(npx)이라는 것입니다. MCP의 장점이 여기에 있습니다. 서버가 어떤 언어로 구현되었든 표준 프로토콜로 통신하기 때문에, 에이전트 입장에서는 차이가 없습니다.
PostgreSQL 서버의 경우 읽기 전용 계정을 별도로 만들어서 연결했습니다. 에이전트가 실수로 데이터를 변경할 가능성을 원천 차단하기 위해서입니다.
시스템 프롬프트: "하루 요약"의 정의#
에이전트에게 "하루 요약"이 뭔지 정확하게 알려줘야 합니다. 우리는 시스템 프롬프트에 7개 항목을 정의했습니다.
| # | 항목 | 데이터 소스 |
|---|---|---|
| 1 | GA 핵심 지표 (페이지뷰, 세션수, 평균 세션 시간) | analytics-mcp |
| 2 | Clarity UX 지표 (데드클릭, 레이지클릭, 스크롤 깊이) | clarity-mcp |
| 3 | Sentry 에러 요약 (미해결 이슈, 신규 이슈, Top 3 에러) | sentry |
| 4 | DAU/WAU/MAU 추이 (전일/전주/전월 대비) | analytics-mcp |
| 5 | 신규 회원가입 수 | postgres |
| 6 | 인기 페이지 Top 5 | analytics-mcp |
| 7 | 비인기 페이지 Top 5 | analytics-mcp |
여기서 중요한 설계 결정이 하나 있습니다.
Typefy 분리 — 서비스별 요약의 필요성#
우리 서비스 wegglePlus는 8개의 하위 서비스를 운영합니다 (Blog, Typefy, Versus, DFC, JobSkill, Nomad's 등). 초기에는 전체를 한꺼번에 요약했는데, Typefy(유형 테스트)의 트래픽이 다른 서비스보다 압도적으로 많아서 전체 데이터가 Typefy에 묻혀버리는 현상이 발생했습니다.
해결 방법은 단순했습니다. 서비스 목록을 코드에 SSOT로 정의하고, 일반 요약에서는 Typefy를 제외합니다.
const SERVICE_LIST = [
{ name: "Blog", path: "/blog/*" },
{ name: "Typefy", path: "/typefy/*" },
{ name: "Versus", path: "/versus/*" },
// ... 총 8개
];
const SERVICE_LIST_WITHOUT_TYPEFY = SERVICE_LIST.filter(
(service) => service.name !== "Typefy",
);
Typefy는 전용 요약 명령어(@bot Typefy 요약)로 분리했습니다. 테스트 완료율, 인기 테스트 랭킹, 결과 조회 이벤트 분석 등 Typefy에 특화된 6개 항목을 별도로 정의했습니다.
키워드 라우팅과 maxTurns 전략#
에이전트가 하나의 요약을 완성하려면 여러 MCP 서버를 여러 번 호출해야 합니다. Claude Agent SDK에서는 이 왕복 횟수를 maxTurns로 제한합니다.
문제는 요약의 복잡도에 따라 필요한 턴 수가 다르다는 것입니다.
const DEFAULT_MAX_TURNS = 20; // 일반 질문
const TYPEFY_MAX_TURNS = 25; // Typefy 전용 요약 (6개 항목)
const INSIGHT_MAX_TURNS = 30; // 인사이트 요약 (가장 복잡)
키워드 매칭으로 요청 유형을 판별합니다.
const TYPEFY_SUMMARY_KEYWORDS = [
"Typefy 요약", "오늘 Typefy 요약", "타입파이 요약"
];
function runAgent(prompt: string) {
const isTypefySummary = TYPEFY_SUMMARY_KEYWORDS.some(
(keyword) => prompt.includes(keyword)
);
const maxTurns = isTypefySummary ? TYPEFY_MAX_TURNS : DEFAULT_MAX_TURNS;
// ...
}
maxTurns를 너무 낮게 잡으면 에이전트가 데이터를 다 수집하기 전에 중단됩니다. 너무 높게 잡으면 불필요한 API 호출이 발생합니다. 초기에 기본값 10으로 시작했다가, 일간 요약이 항목 5번째에서 잘리는 문제를 겪고 20으로 올렸습니다.
40KB 벽 — Slack 메시지 제한 대응#
Slack 메시지에는 약 40,000자 제한이 있습니다. 일간 요약이 이 제한을 초과하는 경우가 종종 발생합니다. 특히 Sentry 에러 스택트레이스가 포함되면 길이가 급격히 늘어납니다.
해결은 줄 단위 분할입니다.
const SLACK_MESSAGE_LIMIT = 40000;
function splitMessage(text: string, limit: number): string[] {
if (text.length <= limit) return [text];
const chunks: string[] = [];
let remaining = text;
while (remaining.length > 0) {
const splitIndex = remaining.lastIndexOf("\n", limit);
const cutAt = splitIndex >= 1 ? splitIndex : limit;
chunks.push(remaining.slice(0, cutAt));
remaining = remaining.slice(cutAt).trimStart();
}
return chunks;
}
핵심은 lastIndexOf("\n", limit)입니다. 제한 위치에서 가장 가까운 줄바꿈을 찾아서 거기서 잘릅니다. 표나 코드 블록 중간에서 잘리는 것을 최소화하기 위해서입니다.
Thread 기반 응답 — 채널을 어지럽히지 않기#
모든 응답은 원본 메시지의 Thread로 전송됩니다.
app.event("app_mention", async ({ event, say }) => {
await say({ text: "작업을 시작합니다.", thread_ts: event.ts });
const result = await runAgent(userMessage);
const chunks = splitMessage(result, SLACK_MESSAGE_LIMIT);
for (const chunk of chunks) {
await say({ text: chunk, thread_ts: event.ts });
}
});
thread_ts: event.ts가 핵심입니다. 이 값을 지정하면 응답이 원본 메시지 아래 Thread로 들어갑니다. 채널 타임라인이 리포트로 도배되는 것을 방지하면서도, Thread를 펼치면 전체 리포트를 볼 수 있습니다.
실제 일간 요약 결과 예시#
@bot 하루 요약을 치면 이런 형태의 리포트가 Thread로 옵니다.
📊 하루 요약 — 2026년 3월 16일 (일)
1. GA 핵심 지표 (Typefy 제외)
- 페이지뷰: 1,247 (전일 대비 ↑12%)
- 세션수: 842
- 평균 세션 시간: 2분 34초
- 이탈률: 43.2%
2. Clarity UX
- 데드클릭: 23건 (주로 /blog 페이지)
- 레이지클릭: 8건
- 평균 스크롤 깊이: 62%
3. Sentry
- 미해결 이슈: 3건
- 신규 이슈: 0건
- Top 에러: TypeError in AdBannerSlider (2건)
4. DAU/WAU/MAU
- 어제 DAU: 312 (전일 대비 ↑8%)
- 이번주 WAU: 1,847
- 이번달 MAU: 6,234
5. 신규 회원가입: 14명
6. 인기 페이지 Top 5
1. /blog/posts/16 (배너 시스템 글) — 234 views
2. /digital-fixed-cost — 189 views
...
7. 비인기 페이지 Top 5
1. /job-skill — 3 views
...
4개 도구를 직접 열어볼 때는 12분 걸리던 작업이, Slack 한 줄로 약 30초 만에 완료됩니다.
실전에서 마주친 문제들#
1. MCP 서버 시작 시간#
4개 MCP 서버가 모두 프로세스로 실행됩니다. npx로 실행하는 서버는 패키지 다운로드가 선행되어야 해서 첫 실행에 5~10초가 걸립니다. 두 번째부터는 캐시되어 빠릅니다.
이 문제는 에이전트가 "작업을 시작합니다" 메시지를 먼저 보내는 것으로 사용자 경험을 보완했습니다. 로딩 중이라는 피드백이 있으면 30초 대기도 참을 수 있습니다.
2. GA 데이터 지연#
Google Analytics 4의 데이터는 실시간이 아닙니다. 보통 4~8시간의 지연이 있습니다. "오늘 요약"을 오전 9시에 요청하면 어제 자정까지의 데이터만 포함되는 게 아니라, 어제 오후 4시까지의 데이터만 포함될 수 있습니다.
시스템 프롬프트에 "어제 데이터를 기준으로 요약"이라고 명시해서, 완전한 하루 데이터를 기반으로 리포트가 생성되도록 했습니다.
3. Clarity API 토큰 만료#
Microsoft Clarity의 API 토큰은 JWT 형태로 만료 기한이 있습니다. 토큰이 만료되면 Clarity MCP 서버가 실패하고, 에이전트는 UX 데이터 없이 불완전한 리포트를 생성합니다.
현재는 토큰 갱신을 수동으로 하고 있습니다. 자동 갱신은 추후 개선 사항입니다.
다음 단계 — 자동 스케줄링#
현재는 사용자가 @bot 하루 요약을 직접 타이핑해야 합니다. 매일 같은 시간에 자동으로 전송하는 기능은 아직 구현되지 않았습니다.
계획하고 있는 구조는 node-cron을 사용한 스케줄러입니다.
// 구현 예정
import cron from "node-cron";
cron.schedule("0 9 * * 1-5", async () => {
// 평일 오전 9시에 자동 실행
const result = await runAgent("하루 요약");
await app.client.chat.postMessage({
channel: DAILY_REPORT_CHANNEL,
text: result,
});
});
이 기능이 추가되면 아침에 Slack을 열기만 하면 어제 리포트가 이미 와 있는 상태가 됩니다. "데이터를 보러 가는" 행위 자체가 사라집니다.
마무리 — 256줄로 4개 도구를 하나로#
돌이켜보면 이 에이전트의 핵심은 "AI가 얼마나 똑똑한가"가 아니었습니다. MCP로 도구를 표준화하고, 시스템 프롬프트로 "무엇을 요약할지" 정확히 정의한 것이 전부였습니다.
Claude Agent SDK + MCP 서버 조합은 "AI에게 도구를 주고 알아서 쓰게 만드는" 패턴을 매우 적은 코드로 구현할 수 있게 해줍니다. 256줄이면 4개 외부 도구와 연동되는 Slack 에이전트를 만들 수 있습니다.
만약 여러분도 매일 여러 대시보드를 순회하며 데이터를 확인하는 루틴이 있다면, 이 패턴을 추천합니다. MCP 서버는 공식/커뮤니티 생태계가 빠르게 성장하고 있어서, 여러분이 사용하는 도구의 MCP 서버가 이미 있을 가능성이 높습니다.
비슷한 고민을 하고 계시거나 같이 실험해보고 싶은 분은 커피챗 하고 싶습니다 ☕️ 📩 ksy90101@gmail.com
함께 읽으면 좋은 글
댓글
0의견을 남겨보세요. 로그인하면 닉네임이 자동으로 입력됩니다.
댓글을 불러오는 중...