카테고리 없음
브라우저 기본 기능을 활용한 클라이언트 오류 수집 전략
UCANON
2025. 8. 28. 20:41
프론트엔드 애플리케이션이 복잡해질수록, 사용자 환경에서 발생하는 오류를 빠르게 탐지하고 수집하는 것은 서비스 품질 유지의 핵심입니다. 서버 로그만으로는 확인하기 어려운 클라이언트 단 오류를 어떻게 안정적으로 모을 수 있을까요?
이번 글에서는 외부 라이브러리 없이 브라우저 기본 기능만으로 오류 수집 시스템을 구축하는 방법을 정리해보겠습니다.
1. 수집 대상 정의
브라우저 환경에서 발생하는 오류는 크게 네 가지로 나눌 수 있습니다.
- 실행 오류 (JavaScript Runtime Error)
- 예: TypeError: undefined is not a function
- 수집 방법: window.onerror
- 리소스 로드 오류 (Resource Error)
- 예: 이미지, CSS, JS 파일 로드 실패
- 수집 방법: window.addEventListener('error', …, true)
- ⚠️ 참고: window.onerror로는 리소스 로드 오류를 잡을 수 없고, 반드시 capture=true 옵션을 준 이벤트 리스너에서만 감지됩니다.
- 비처리 프로미스 오류 (Unhandled Promise Rejection)
- 예: fetch() 실패 후 .catch 미처리
- 수집 방법: window.onunhandledrejection
- 성능/네트워크 힌트 (선택 사항)
- 예: 느린 요청, 대용량 자원 로드
- 수집 방법: PerformanceObserver (resource/longtask 관찰)
2. 오류 수집 스니펫 (개선 버전)
아래 코드는 오류를 캡처하고, 일정 간격 또는 언로드 시점에 서버로 전송하는 예시입니다.
<script>
(function () {
const ENDPOINT = "/client-error";
const queue = [];
const MAX_PAYLOAD_SIZE = 60000; // 보수적으로 60KB 이하
const SAMPLE_RATE = 0.1; // 10% 샘플링
const sentErrors = new Set();
function shouldSample() {
return Math.random() <= SAMPLE_RATE;
}
function isDuplicate(error) {
const key = btoa(unescape(encodeURIComponent(
`${error.type}-${error.msg || error.reason}-${error.stack || ''}`
))); // 해시 기반 중복 체크
if (sentErrors.has(key)) return true;
sentErrors.add(key);
setTimeout(() => sentErrors.delete(key), 300000);
return false;
}
function push(error) {
if (!shouldSample()) return;
if (isDuplicate(error)) return;
queue.push({
...error,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: location.href
});
if (queue.length >= 5) flush();
}
function flush() {
if (!queue.length) return;
const payload = JSON.stringify({
ts: Date.now(),
url: location.href,
items: queue.splice(0)
});
if (new Blob([payload]).size > MAX_PAYLOAD_SIZE) {
console.warn('Payload too large, splitting...');
return;
}
if (navigator.sendBeacon) {
const ok = navigator.sendBeacon(ENDPOINT, payload);
if (!ok) fallback(payload);
} else {
fallback(payload);
}
}
function fallback(payload) {
fetch(ENDPOINT, {
method: "POST",
body: payload,
headers: { "Content-Type": "application/json" }
}).catch(() => {
try {
const img = new Image();
const encoded = encodeURIComponent(payload);
if (encoded.length < 2000) {
img.src = `${ENDPOINT}?data=${encoded}`;
}
} catch (e) {}
});
}
window.addEventListener("beforeunload", flush);
window.addEventListener("pagehide", flush);
window.onerror = (msg, src, line, col, err) => {
push({
type: "js-error",
msg: String(msg).slice(0, 500),
src,
line,
col,
stack: err?.stack?.slice(0, 3000)
});
};
window.addEventListener("error", e => {
if (e.target && e.target !== window) {
push({
type: "resource-error",
tag: e.target.tagName,
src: e.target.src || e.target.href
});
}
}, true);
window.onunhandledrejection = e => {
const reason = e.reason instanceof Error ?
e.reason.message :
String(e.reason).slice(0, 500);
push({
type: "unhandled-rejection",
reason,
stack: e.reason?.stack?.slice(0, 3000)
});
};
setInterval(flush, 30000);
})();
</script>
3. navigator.sendBeacon의 특징과 한계
오류 수집에서 가장 중요한 포인트는 언로드 시점에서도 로그 유실 없이 서버에 도착하는 것입니다.
- 장점
- 브라우저 종료/이탈 시에도 안정적으로 전송
- 비동기 처리라 페이지 종료 지연 없음
- POST 전송만 지원
- 제한
- 응답을 받을 수 없음 (fire-and-forget)
- 브라우저별 크기 제한 존재 (보통 64KB, 보수적으로 60KB 이하 권장)
- 지원 현황: Chrome 39+, Firefox 31+, Safari 11.1+, Edge 14+
- 미지원: Internet Explorer
👉 따라서 sendBeacon → 일반 fetch → 이미지 태그 순으로 fallback을 준비하는 것이 최적입니다.
4. 운영 시 반드시 고려할 점
- 소스맵 연계
- 번들된 스택을 소스맵으로 원본 코드에 매핑
- 보안상 소스맵은 프로덕션에 노출하지 않고 별도 서버에 저장
- CORS 정책
- 로그 서버가 다른 도메인이라면 Access-Control-Allow-Origin 필수
- Preflight 최소화를 위해 Content-Type: text/plain 고려
- 민감정보 마스킹
- url.replace(/([?&])(password|token|key|secret)=[^&]*/gi, '$1$2=***');
- 샘플링 & 중복 억제
- 동일 오류는 일정 시간(예: 5분)에 1회만 전송
- 트래픽 기반 동적 샘플링
- 호환성 안내
- navigator.userAgent는 차후 Deprecation 예정 → UA Client Hints도 고려
- performance.memory는 Chrome 실험적 기능 → 지원 제한 있음
5. 서버 수집 예시
app.post('/client-error', (req, res) => {
const errors = req.body.items;
errors.forEach(error => {
if (error.stack) {
error.originalStack = mapStackTrace(error.stack);
}
logger.error({ ...error, ip: req.ip, userAgent: req.get('User-Agent') });
if (error.type === 'js-error') {
sendAlert(error);
}
});
res.status(200).send('OK');
});
6. 결론
- 브라우저 기본 기능만으로도 충분히 프로덕션 수준의 오류 수집이 가능합니다.
- 핵심은 sendBeacon을 우선 사용하면서, 크기 제한·호환성·민감정보·성능을 꼼꼼히 관리하는 것입니다.
- 샘플링과 중복 억제를 통해 효율적으로 운영하면 서비스 품질과 사용자 경험 개선에 큰 도움이 됩니다.