사용자가 알려주는 에러
WICHI를 론칭하고 첫 주, 에러 모니터링이 없었습니다. 기능은 잘 작동하는 것 같았고, 로컬에서 테스트도 통과했습니다.
문제를 알게 된 건 사용자 피드백이었습니다.
| # | 사용자 피드백 | 실제 원인 | 발견까지 걸린 시간 |
|---|---|---|---|
| 1 | ”결과 페이지가 안 떠요” | 특정 쿼리에서 API 타임아웃 | ~18시간 |
| 2 | ”분석이 중간에 멈춰요” | LLM 응답 파싱 실패 | ~12시간 |
| 3 | ”결제했는데 크레딧 안 올라가요” | Webhook 처리 중 예외 | ~6시간 |
| 4 | ”어제까지 됐는데 오늘 안 돼요” | 의존 서비스 API 변경 | ~24시간 |
graph TD
A["에러 발생"] --> B{"모니터링\n있나?"}
B -->|"있음"| C["즉시 알림\n(분 단위)"]
B -->|"없음"| D["사용자가 발견"]
D --> E["사용자가 연락"]
E --> F["개발자 확인"]
F --> G["원인 파악"]
C --> G
H["모니터링 없음:\n평균 15시간"] --> I["모니터링 있음:\n평균 5분"]
style D fill:#ffebee,stroke:#f44336
style C fill:#e8f5e9,stroke:#4caf50
style H fill:#ffebee,stroke:#f44336
style I fill:#e8f5e9,stroke:#4caf50
사용자가 에러를 제보하는 순간, 이미 3가지가 잘못된 겁니다: (1) 에러가 발생했고, (2) 모니터링이 없어서 몰랐고, (3) 사용자 경험이 나빠졌습니다.
모니터링의 3가지 층
모니터링은 한 가지가 아닙니다. 최소 3개 층이 필요합니다.
graph TD
subgraph L1["Layer 1: Uptime 모니터링"]
U1["서비스가 살아있는가?"]
U2["응답 시간이 정상인가?"]
U3["SSL 인증서가 유효한가?"]
end
subgraph L2["Layer 2: 에러 추적"]
E1["런타임 에러 캡처"]
E2["에러 빈도와 영향 범위"]
E3["스택 트레이스"]
end
subgraph L3["Layer 3: 로그 관리"]
L3a["구조화된 로그"]
L3b["로그 검색/필터"]
L3c["로그 보존 기간"]
end
L1 --> L2 --> L3
style L1 fill:#e8f5e9,stroke:#4caf50
style L2 fill:#e3f2fd,stroke:#2196f3
style L3 fill:#fff3e0,stroke:#ff9800
| 층 | 답하는 질문 | 도구 예시 | 우선순위 |
|---|---|---|---|
| Uptime | ”서비스가 죽었나?” | Betterstack, UptimeRobot | 1순위 |
| 에러 추적 | ”어떤 에러가 발생했나?” | Sentry, Bugsnag | 1순위 |
| 로그 관리 | ”왜 발생했나?” | Betterstack Logs, Datadog | 2순위 |
| APM | ”어디가 느린가?” | Sentry Performance, New Relic | 3순위 |
| 사용자 분석 | ”무엇을 하고 있나?” | PostHog, Mixpanel | 3순위 |
1인 SaaS에서 1순위는 Uptime + 에러 추적입니다. “서비스가 죽었나?”와 “에러가 났나?”에 답할 수 있으면 80%는 커버됩니다.
Sentry: 에러 추적
왜 Sentry인가
| 도구 | 무료 티어 | 에러 추적 | 소스맵 | 성능 | 알림 |
|---|---|---|---|---|---|
| Sentry | 5K 이벤트/월 | ✅ | ✅ | ✅ | ✅ |
| Bugsnag | 7.5K 이벤트/월 | ✅ | ✅ | ❌ | ✅ |
| Rollbar | 5K 이벤트/월 | ✅ | ✅ | ❌ | ✅ |
| LogRocket | 1K 세션/월 | ✅ | ✅ | ✅ | ✅ |
| 자체 구축 | 무료 | 수동 | 수동 | 수동 | 수동 |
Sentry를 선택한 이유:
- 무료 티어가 충분: 5,000 이벤트/월. 초기 SaaS에는 넉넉함
- SDK가 모든 프레임워크 지원: Next.js, Express, Python, React 등
- 소스맵 자동 업로드: minified 코드에서도 원본 위치 확인
- 릴리즈 추적: 어떤 배포에서 에러가 시작됐는지 확인
최소 설정
// sentry.config.js (Next.js 예시)
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
// 프로덕션에서만 활성화
enabled: process.env.NODE_ENV === 'production',
// 에러 샘플링 100% (초기에는 모든 에러 캡처)
sampleRate: 1.0,
// 성능 샘플링 10% (무료 티어 절약)
tracesSampleRate: 0.1,
});
에러에 맥락 추가
// 사용자 컨텍스트
Sentry.setUser({
id: user.id,
email: user.email, // 주의: GDPR 고려
});
// 추가 태그
Sentry.setTag('plan', user.plan); // free, pro, enterprise
Sentry.setTag('feature', 'analysis'); // 기능 구분
// 수동 에러 캡처
try {
await processAnalysis(query);
} catch (error) {
Sentry.captureException(error, {
extra: {
queryId: query.id,
engineCount: query.engines.length,
},
});
throw error;
}
Sentry 무료 티어 최적화
5,000 이벤트/월을 효율적으로 쓰려면:
| 전략 | 방법 | 효과 |
|---|---|---|
| 중복 제거 | Sentry가 자동으로 같은 에러 그룹화 | 이벤트 수 절약 |
| 에러 필터링 | 404, 봇 트래픽 무시 | 노이즈 제거 |
| 샘플링 | tracesSampleRate: 0.1 | 성능 이벤트 90% 절약 |
| 환경 분리 | 프로덕션만 전송 | 개발 이벤트 제외 |
// 불필요한 에러 필터링
Sentry.init({
beforeSend(event) {
// 404는 무시
if (event.exception?.values?.[0]?.type === 'NotFoundError') {
return null;
}
// 봇 트래픽 무시
if (event.request?.headers?.['user-agent']?.includes('bot')) {
return null;
}
return event;
},
});
Betterstack: Uptime + 로그
왜 Betterstack인가
| 도구 | 무료 티어 | Uptime | 로그 | 상태 페이지 | 알림 채널 |
|---|---|---|---|---|---|
| Betterstack | 5 모니터, 로그 1GB | ✅ | ✅ | ✅ | Slack, Email, SMS |
| UptimeRobot | 50 모니터 | ✅ | ❌ | ✅ | |
| Pingdom | 유료 | ✅ | ❌ | ✅ | 다양 |
| Datadog | 유료 | ✅ | ✅ | ❌ | 다양 |
Betterstack을 선택한 이유:
- Uptime + 로그가 한 곳에: 별도 도구 2개 대신 1개로 해결
- 무료 상태 페이지: 사용자에게 서비스 상태를 보여줄 수 있음
- 깔끔한 UI: 1인 개발자가 빠르게 파악 가능
- Heartbeat 모니터: cron job, 백그라운드 작업 감시
Uptime 모니터 설정
Betterstack에서 설정할 모니터:
| # | 모니터 대상 | 체크 방법 | 주기 | 알림 |
|---|---|---|---|---|
| 1 | 메인 사이트 | HTTP 200 확인 | 3분 | Slack + Email |
| 2 | API 헬스체크 | /api/health 엔드포인트 | 1분 | Slack + Email |
| 3 | 결제 Webhook | Heartbeat (주기적 ping) | 5분 | Email + SMS |
| 4 | SSL 인증서 | 만료일 확인 | 1일 | Email (30일 전) |
헬스체크 엔드포인트
// /api/health - 서비스 상태 확인용
app.get('/api/health', async (req, res) => {
const checks = {
server: 'ok',
database: 'unknown',
cache: 'unknown',
};
try {
await db.query('SELECT 1');
checks.database = 'ok';
} catch {
checks.database = 'error';
}
try {
await redis.ping();
checks.cache = 'ok';
} catch {
checks.cache = 'error';
}
const healthy = Object.values(checks).every(v => v === 'ok');
res.status(healthy ? 200 : 503).json({
status: healthy ? 'healthy' : 'degraded',
checks,
timestamp: new Date().toISOString(),
});
});
헬스체크 엔드포인트는 단순히 200을 반환하는 게 아니라, DB와 주요 의존성의 상태도 확인해야 합니다. 서버는 살아있는데 DB 연결이 끊어진 경우를 잡아내려면 이 구분이 필요합니다.
상태 페이지
Betterstack 무료 티어에서 상태 페이지를 제공합니다.
status.myapp.com
├── API ● Operational
├── Website ● Operational
├── Database ● Operational
└── Payments ● Operational
Uptime: 99.95% (last 90 days)
상태 페이지가 중요한 이유:
| 상태 페이지 없을 때 | 상태 페이지 있을 때 |
|---|---|
| 사용자: “왜 안 돼?” → 이메일/DM 폭주 | 사용자: 상태 페이지 확인 → “아 점검 중이구나” |
| 개발자: 문의 대응에 시간 소모 | 개발자: 수정에 집중 |
| 신뢰 하락: “이 서비스 불안한데?” | 신뢰 유지: “투명하게 운영하네” |
Sentry + Betterstack 조합: WICHI 적용 사례
모니터링 아키텍처
graph TD
subgraph APP["WICHI Application"]
FE["Frontend\n(Next.js)"]
BE["Backend\n(Express)"]
CRON["Cron Jobs"]
end
subgraph SENTRY["Sentry (에러 추적)"]
SE["에러 캡처"]
SP["성능 모니터링"]
SR["릴리즈 추적"]
end
subgraph BETTER["Betterstack (Uptime + 로그)"]
BU["Uptime 모니터"]
BL["로그 수집"]
BS["상태 페이지"]
end
subgraph ALERT["알림"]
SLACK["Slack"]
EMAIL["Email"]
end
FE --> SE
BE --> SE
BE --> BL
CRON --> BL
BU -->|"3분마다 체크"| BE
SE --> SLACK
BU --> SLACK
BU --> EMAIL
style SENTRY fill:#e3f2fd,stroke:#2196f3
style BETTER fill:#e8f5e9,stroke:#4caf50
역할 분담
| 상황 | 감지 주체 | 알림 내용 |
|---|---|---|
| 런타임 에러 (500) | Sentry | 에러 메시지 + 스택 트레이스 + 사용자 컨텍스트 |
| 서비스 다운 | Betterstack Uptime | ”API 응답 없음” + 다운타임 기록 |
| 백그라운드 작업 실패 | Betterstack Heartbeat | ”마지막 heartbeat 5분 전” |
| 응답 시간 저하 | Sentry Performance | ”p95 응답 시간 3초 초과” |
| SSL 만료 임박 | Betterstack | ”SSL 인증서 30일 후 만료” |
실제 에러 대응 사례
모니터링 도입 후, 같은 종류의 에러가 발생했을 때의 차이:
graph LR
subgraph BEFORE["Before: 모니터링 없음"]
B1["에러 발생\n(오전 2시)"] --> B2["사용자 발견\n(오전 10시)"]
B2 --> B3["피드백 전달\n(오전 11시)"]
B3 --> B4["원인 파악\n(오후 1시)"]
B4 --> B5["수정 배포\n(오후 3시)"]
end
subgraph AFTER["After: Sentry + Betterstack"]
A1["에러 발생\n(오전 2시)"] --> A2["Slack 알림\n(오전 2시 1분)"]
A2 --> A3["아침에 확인\n(오전 9시)"]
A3 --> A4["원인 파악\n(오전 9시 15분)\n스택 트레이스 즉시 확인"]
A4 --> A5["수정 배포\n(오전 10시)"]
end
style BEFORE fill:#ffebee,stroke:#f44336
style AFTER fill:#e8f5e9,stroke:#4caf50
| 지표 | Before | After |
|---|---|---|
| 에러 인지 시간 | ~8시간 | ~1분 (알림) |
| 원인 파악 시간 | ~2시간 | ~15분 (스택 트레이스) |
| 총 대응 시간 | ~13시간 | ~1시간 |
| 사용자 영향 시간 | ~13시간 | ~8시간 (야간 발생 시) |
무료 티어 비용 계산
1인 SaaS 초기에는 모니터링에 돈을 쓸 필요가 없습니다.
| 도구 | 무료 범위 | 초과 시 비용 | 초기 SaaS 충분? |
|---|---|---|---|
| Sentry | 5,000 이벤트/월 | $26/월 (50K) | ✅ 충분 |
| Betterstack Uptime | 5 모니터 | $24/월 (20개) | ✅ 충분 |
| Betterstack Logs | 1GB/월 | $24/월 (5GB) | ✅ 충분 |
| 합계 | $0/월 | - | - |
무료 티어를 오래 쓰는 팁
| 전략 | Sentry | Betterstack |
|---|---|---|
| 환경 분리 | 프로덕션만 전송 | 프로덕션 엔드포인트만 모니터 |
| 필터링 | 404, 봇 에러 제외 | 불필요한 로그 레벨 제외 |
| 샘플링 | 성능 이벤트 10%만 | - |
| 보존 기간 | 기본 90일 | 기본 3일 (무료), 핵심 로그만 |
론칭 전 모니터링 체크리스트
| # | 항목 | 도구 | 소요 시간 |
|---|---|---|---|
| 1 | Sentry 프로젝트 생성 + SDK 설치 | Sentry | 15분 |
| 2 | 에러 알림 → Slack 연동 | Sentry | 5분 |
| 3 | 에러 필터링 설정 (404, 봇 제외) | Sentry | 10분 |
| 4 | Uptime 모니터 설정 (사이트 + API) | Betterstack | 10분 |
| 5 | 헬스체크 엔드포인트 구현 | 직접 | 20분 |
| 6 | 상태 페이지 설정 | Betterstack | 10분 |
| 7 | Heartbeat 모니터 (cron job용) | Betterstack | 10분 |
| 합계 | ~80분 |
graph LR
A["80분 투자"] --> B["에러 인지 시간\n8시간 → 1분"]
A --> C["원인 파악 시간\n2시간 → 15분"]
A --> D["사용자 불만\n감소"]
A --> E["비용\n$0/월"]
style A fill:#e3f2fd,stroke:#2196f3
style E fill:#e8f5e9,stroke:#4caf50
80분이면 설정이 끝납니다. 모니터링 없이 론칭하면, 첫 번째 에러에서 그 80분의 100배를 쓰게 됩니다.
모니터링 → 체크리스트 항목
MMU의 534개 체크리스트 중 모니터링 관련은 38개입니다. 그 중 론칭 전 필수 항목 10개:
| # | 항목 | MMU 카테고리 |
|---|---|---|
| 1 | 에러 추적 도구가 프로덕션에 설치되어 있는가 | Monitoring |
| 2 | Uptime 모니터가 설정되어 있는가 | Monitoring |
| 3 | 에러 알림이 Slack/Email로 전달되는가 | Monitoring |
| 4 | 헬스체크 엔드포인트가 있는가 | Monitoring |
| 5 | 상태 페이지가 있는가 | Monitoring |
| 6 | 소스맵이 에러 추적 도구에 업로드되는가 | Monitoring |
| 7 | 에러에 사용자 컨텍스트가 포함되는가 | Monitoring |
| 8 | 백그라운드 작업에 heartbeat 모니터가 있는가 | Monitoring |
| 9 | SSL 인증서 만료 알림이 설정되어 있는가 | Security |
| 10 | 로그에 민감 정보(비밀번호, 토큰)가 포함되지 않는가 | Security |
이 10개는 모두 무료 도구로, 총 80분 안에 설정할 수 있습니다. “론칭 후에 하자”는 “에러가 터진 후에 하자”와 같습니다.
정리
| 핵심 | 내용 |
|---|---|
| 모니터링 없이 론칭한 결과 | 에러를 사용자 제보로 알게 됨 (평균 15시간 후) |
| 최소 조합 | Sentry (에러 추적) + Betterstack (Uptime + 로그) |
| 비용 | $0/월 (무료 티어로 충분) |
| 설정 시간 | ~80분 |
| 효과 | 에러 인지 8시간 → 1분, 원인 파악 2시간 → 15분 |
| 원칙 | 론칭 전에 모니터링이 먼저. 기능보다 먼저 |
관련 글

Solo SaaS 보안 - 최소한 이것만은 하자
1인 SaaS 빌더를 위한 필수 보안 체크리스트. OWASP Top 10 중 1인 개발에 치명적인 5가지 취약점 방어와 WICHI에서 보완한 실제 보안 설정 사례.

게이트 조건이 다이어그램 안에 문자열로 박혀 있다 - BPMN을 거버넌스 척추로 쓰기
사람 개입 게이트를 코드 안 조건문으로 두는 것과 프로세스 정의 안에 두는 것은 다르다. 흐름 객체 18개짜리 BPMN 안에 코드 에이전트를 노드 하나로 넣고, 게이트 조건을 다이어그램에 그대로 적었을 때 무엇이 달라지는지, 그리고 플랫폼에서 실제로 걸린 문제 세 가지를 정리한다.

가장 위험한 노드는 자동 승인이었다 - 조용한 과신을 뺄셈으로 잡는 법
도메인이 다른 두 워크플로를 같은 채점표로 돌렸더니 최고점 노드의 성격이 같았다. 사람 손을 떼는 노드다. 확신도 뺄셈 하나로 조용한 과신을 잡는 방법, 3.6점 노드가 4.9로 올라가는 계산, 그리고 색이 빨간 것과 사람에게 가는 것이 왜 다른지 정리한다.