๐ Silent Failures: ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์๋ฌ ๋ชจ๋ํฐ๋ง ๋ถ์ฌ
Summary
ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์๋ฌ ๋ชจ๋ํฐ๋ง ์์คํ
์ ๋ถ์ฌ๋ ์ฌ์ฉ์ ๊ฒฝํ ์ ํ, ์ ์ง๋ณด์ ๋ณต์ก์ฑ ์ฆ๊ฐ, ๋น์ฆ๋์ค ์์ค ๋ฑ ์ฌ๊ฐํ ๋ฌธ์ ๋ฅผ ์ด๋ํฉ๋๋ค. window.onerror
, unhandledrejection
๊ณผ ๊ฐ์ ์ ์ญ ์๋ฌ ํธ๋ค๋ฌ์ Sentry์ ๊ฐ์ ์ ๋ฌธ ์๋ฌ ๋ชจ๋ํฐ๋ง ์๋น์ค๋ฅผ ํ์ฉํ์ฌ ์๋ฌ๋ฅผ ์ ์ ์ ์ผ๋ก ๊ฐ์งํ๊ณ ํด๊ฒฐํจ์ผ๋ก์จ ์ ํ๋ฆฌ์ผ์ด์
์ ์์ ์ฑ๊ณผ ์ฌ์ฉ์ ๋ง์กฑ๋๋ฅผ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ต๋๋ค.
Why Wrong?
์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ฉ์ ๊ธฐ๊ธฐ, ๋คํธ์ํฌ ํ๊ฒฝ, ๋ธ๋ผ์ฐ์ ๋ฒ์ ๋ฑ ์์ธก ๋ถ๊ฐ๋ฅํ ๋ค์ํ ํ๊ฒฝ์์ ์คํ๋๋ฏ๋ก, ๊ฐ๋ฐ์๊ฐ ์์์น ๋ชปํ ์๋ฌ๊ฐ ๋ฐ์ํ๊ธฐ ์ฝ์ต๋๋ค. ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์๋ฌ ๋ชจ๋ํฐ๋ง ์์คํ ์ด ๋ถ์ฌํ๋ฉด, ์ฌ์ฉ์๋ค์ ๋ถํธ์ ๊ฒช๊ฑฐ๋ ๊ธฐ๋ฅ ์ค์๋์ ๊ฒฝํํด๋ ๊ฐ๋ฐ์๋ ์ด๋ฌํ ๋ฌธ์ ๋ฐ์ ์ฌ๋ถ๋ฅผ ์ ์ ์์ต๋๋ค. ์ด๋ ๋ค์๊ณผ ๊ฐ์ ์น๋ช ์ ์ธ ๋ฌธ์ ๋ฅผ ์ผ๊ธฐํฉ๋๋ค:
- ์ฌ์ฉ์ ๊ฒฝํ ์ ํ: ์๋ฌ๊ฐ ๋ฐ์ํด๋ ์ฌ์ฉ์์๊ฒ ์ ์ ํ ํผ๋๋ฐฑ(์: ์ค๋ฅ ๋ฉ์์ง, ๋์ฒด UI)์ ์ ๊ณตํ์ง ๋ชปํ๊ณ , ๊ธฐ๋ฅ์ด ์กฐ์ฉํ ์คํจํ๊ฑฐ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ฉ์ถ๋ ์ํฉ์ด ๋ฐ์ํ์ฌ ์ฌ์ฉ์ ๋ง์กฑ๋๊ฐ ํฌ๊ฒ ๋จ์ด์ง๋๋ค.
- ์์ฐ์ฑ ๋ฐ ์ ์ง๋ณด์์ฑ ์ ํด: ๋ฌธ์ ๋ฐ์ ์ ๊ฐ๋ฐ์๋ ์ฌํํ๊ธฐ ์ด๋ ต๊ณ , ์์ธ์ ํ์ ํ๊ธฐ ์ํ ๋๋ฒ๊น ์ ๋ง๋ํ ์๊ฐ์ ์๋ชจํ๊ฒ ๋ฉ๋๋ค. ์ด๋ ๋ฒ๊ทธ ์์ ์ฃผ๊ธฐ๋ฅผ ๋๋ฆฌ๊ณ ๊ฐ๋ฐ ํจ์จ์ ๋จ์ด๋จ๋ฆฝ๋๋ค.
- ๋ฐ์ดํฐ ์์ค ๋ฐ ๋ณด์ ์ํ: ์ค์ํ ์ฌ์ฉ์ ๋ฐ์ดํฐ๊ฐ ์ ์ค๋๊ฑฐ๋, ํน์ ์๋ฌ๊ฐ ์ ์ฌ์ ์ธ ๋ณด์ ์ทจ์ฝ์ ์ ์ ํธ์ผ ์ ์์์๋ ์ด๋ฅผ ์ธ์งํ์ง ๋ชปํ์ฌ ๋ ํฐ ๋ฌธ์ ๋ก ์ด์ด์ง ์ ์์ต๋๋ค.
- ๋น์ฆ๋์ค ์์ค: ํต์ฌ ๊ธฐ๋ฅ์ ์ค๋ฅ๋ ์ง์ ์ ์ธ ๋งค์ถ ์์ค์ด๋ ์ฌ์ฉ์ ์ดํ๋ก ์ด์ด์ง ์ ์์ต๋๋ค. ์๋ฌ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํ์ฌ ์๋น์ค ์์ ์ฑ์ ๊ฐ์ ํ๋ ๊ธฐํ๋ฅผ ์์คํ๊ฒ ๋ฉ๋๋ค.
How to Fix?
ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์๋ฌ๋ฅผ ์ ์ ์ ์ผ๋ก ๊ฐ์งํ๊ณ ๋์ํ๊ธฐ ์ํ ์ฒด๊ณ์ ์ธ ๋ชจ๋ํฐ๋ง ์์คํ ์ ๊ตฌ์ถํด์ผ ํฉ๋๋ค. ๋ค์ ๋ฐฉ๋ฒ๋ค์ ๊ณ ๋ คํ ์ ์์ต๋๋ค:
- ์ ์ญ ์๋ฌ ํธ๋ค๋ฌ ๊ตฌํ: ๋ธ๋ผ์ฐ์ ์์ ๋ฐ์ํ๋ ์บ์น๋์ง ์์(uncaught) JavaScript ์๋ฌ์ Promise ๊ฑฐ๋ถ๋ฅผ ์ ์ญ์ ์ผ๋ก ๊ฐ์งํฉ๋๋ค.
window.onerror
: ๋๊ธฐ์ ์ผ๋ก ๋ฐ์ํ๋ ์บ์น๋์ง ์์ ์คํฌ๋ฆฝํธ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค.window.addEventListener('unhandledrejection')
: ๋น๋๊ธฐ์ ์ผ๋ก ๋ฐ์ํ๋, ์ฒ๋ฆฌ๋์ง ์์ Promise ๊ฑฐ๋ถ๋ฅผ ๊ฐ์งํฉ๋๋ค.
- ์ ์ฉ ์๋ฌ ๋ชจ๋ํฐ๋ง ์๋น์ค ํ์ฉ: Sentry, Bugsnag, New Relic, Datadog ๋ฑ ์ ๋ฌธ ์๋ฌ ๋ชจ๋ํฐ๋ง ์๋ฃจ์ ์ ๋์ ํฉ๋๋ค. ์ด ์๋น์ค๋ค์ ์๋ฌ ๋ฐ์ ์ ์ฝ ์คํ, ๋ธ๋ผ์ฐ์ ์ ๋ณด, ์ฌ์ฉ์ ์ ๋ณด, ํ๊ฒฝ ๋ณ์ ๋ฑ ํ๋ถํ ์ปจํ ์คํธ ๋ฐ์ดํฐ๋ฅผ ์๋์ผ๋ก ์์งํ์ฌ ๊ฐ๋ฐ์์๊ฒ ๋ณด๊ณ ํ๊ณ , ์๋ฌ ํธ๋ ๋ ๋ถ์ ๋ฐ ์๋ฆผ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ฌ ํจ์จ์ ์ธ ๋ฌธ์ ํด๊ฒฐ์ ๋์ต๋๋ค.
- React Error Boundaries (React ์ ํ๋ฆฌ์ผ์ด์
์ ๊ฒฝ์ฐ): React ์ปดํฌ๋ํธ ํธ๋ฆฌ ๋ด์์ ๋ฐ์ํ๋ JavaScript ์๋ฌ๋ฅผ ์บ์นํ๊ณ , ์๋ฌ๊ฐ ๋ฐ์ํ ์ปดํฌ๋ํธ ๋์ ํด๋ฐฑ(fallback) UI๋ฅผ ๋ ๋๋งํ์ฌ ์ ํ๋ฆฌ์ผ์ด์
์ ์ฒด๊ฐ ๋ง๊ฐ์ง๋ ๊ฒ์ ๋ฐฉ์งํฉ๋๋ค.
componentDidCatch
๋ผ์ดํ์ฌ์ดํด ๋ฉ์๋ ๋๋static getDerivedStateFromError
๋ฅผ ์ฌ์ฉํ๋ฉฐ, ์ด๊ณณ์์ ์ ์ญ ์๋ฌ ๋ชจ๋ํฐ๋ง ์๋น์ค๋ก ์๋ฌ๋ฅผ ์ ์กํ ์ ์์ต๋๋ค. - ๋ก๊ทธ ๋ฐ ์ปจํ ์คํธ ๊ฐํ: ์๋ฌ ๋ฐ์ ์ ์ถฉ๋ถํ ๋ก๊ทธ ์ ๋ณด(์ฌ์ฉ์ ID, ํ์ด์ง ๊ฒฝ๋ก, ์ก์ ๊ธฐ๋ก ๋ฑ)๋ฅผ ํจ๊ป ์ ์กํ์ฌ ์๋ฌ ๋ฐ์ ์ํฉ์ ์ฌ๊ตฌ์ฑํ๊ณ ์์ธ์ ๋น ๋ฅด๊ฒ ํ์ ํ ์ ์๋๋ก ํฉ๋๋ค.
- ๊ฐ๋ฐ ๋จ๊ณ์์์ ์๋ฌ ๊ฒ์ถ ๊ฐํ: ESLint, TypeScript ๋ฑ์ ํ์ฉํ์ฌ ๊ฐ๋ฐ ๋จ๊ณ์์ ์ ์ฌ์ ์ธ ์๋ฌ๋ฅผ ์ค์ด๊ณ , ์ฒ ์ ํ ํ ์คํธ(๋จ์ ํ ์คํธ, ํตํฉ ํ ์คํธ, E2E ํ ์คํธ)๋ฅผ ํตํด ์๋ฌ๊ฐ ํ๋ก๋์ ์ ๋ฐฐํฌ๋๊ธฐ ์ ์ ๋ฐ๊ฒฌ๋ ์ ์๋๋ก ํฉ๋๋ค.
Before Code (Bad)
// index.js ๋๋ ์ ํ๋ฆฌ์ผ์ด์
์ง์
์
console.log("์ ํ๋ฆฌ์ผ์ด์
์์.");
function fetchData() {
// ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์๋ฎฌ๋ ์ด์
์ฝ๋
// ์๋์น ์๊ฒ null์ด ๋๋ ๊ฒฝ์ฐ๊ฐ ์๋ค๊ณ ๊ฐ์ ํฉ๋๋ค.
const config = null;
// config๊ฐ null์ผ ๋ ์์ฑ์ ์ ๊ทผํ๋ฉด TypeError๊ฐ ๋ฐ์ํฉ๋๋ค.
// ์ด ์๋ฌ๋ ์ ์ญ์ ์ผ๋ก ์ฒ๋ฆฌ๋์ง ์์ผ๋ฉด ๋ธ๋ผ์ฐ์ ์ฝ์์๋ง ํ์๋๊ณ , ๊ฐ๋ฐ์๋ ์ ์ ์์ต๋๋ค.
console.log(config.apiUrl); // Uncaught TypeError: Cannot read properties of null (reading 'apiUrl')
}
function performAsyncTask() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// ๋น๋๊ธฐ ์์
์ค ์คํจ๊ฐ ๋ฐ์ํ์ง๋ง, catch()๋ก ์ฒ๋ฆฌ๋์ง ์์ ๊ฒฝ์ฐ
reject(new Error("๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์คํจ!")); // Unhandled Promise Rejection
}, 500);
});
}
// ํ์ด์ง ๋ก๋ ํ ํน์ ์์ ์ ์คํ๋๋ ํจ์๋ค
setTimeout(fetchData, 1000);
performAsyncTask();
console.log("์ ํ๋ฆฌ์ผ์ด์
์ด ์คํ ์ค์
๋๋ค. ๋ง์ฝ ์๋ฌ๊ฐ ๋ฐ์ํด๋ ๊ฐ๋ฐ์๋ ์๊ธฐ ์ด๋ ต์ต๋๋ค.");
After Code (Good)
// index.js ๋๋ ์ ํ๋ฆฌ์ผ์ด์
์ง์
์
console.log("์ ํ๋ฆฌ์ผ์ด์
์์.");
// 1. ์ ์ญ JavaScript ์๋ฌ ํธ๋ค๋ฌ (๋๊ธฐ ์๋ฌ ์ฒ๋ฆฌ)
// 'message', 'source', 'lineno', 'colno', 'error' ๊ฐ์ฒด๋ฅผ ์ธ์๋ก ๋ฐ์ต๋๋ค.
window.onerror = function(message, source, lineno, colno, error) {
console.error('--- ์ ์ญ JS ์๋ฌ ๊ฐ์ง (window.onerror) ---');
console.error('๋ฉ์์ง:', message);
console.error('์์ค:', source);
console.error('์์น:', lineno, '์ค,', colno, '์ปฌ๋ผ');
console.error('์๋ฌ ๊ฐ์ฒด:', error); // ์๋ฌ ์คํ ํธ๋ ์ด์ค ๋ฑ ์์ธ ์ ๋ณด
// ์ฌ๊ธฐ์ ์ค์ ์๋ฌ ๋ชจ๋ํฐ๋ง ์๋น์ค(์: Sentry.captureException(error))๋ก ์๋ฌ๋ฅผ ์ ์กํ๋ ๋ก์ง ์ถ๊ฐ
// true๋ฅผ ๋ฐํํ๋ฉด ๋ธ๋ผ์ฐ์ ์ ๊ธฐ๋ณธ ์๋ฌ ์ฒ๋ฆฌ(์ฝ์ ์ถ๋ ฅ, ์๋ฌ ์ฌ๋ณผ ํ์ ๋ฑ)๋ฅผ ๋ง์ต๋๋ค.
return true;
};
// 2. ์ ์ญ Unhandled Promise Rejection ํธ๋ค๋ฌ (๋น๋๊ธฐ ์๋ฌ ์ฒ๋ฆฌ)
// 'event' ๊ฐ์ฒด๋ 'reason' ์์ฑ์ ํตํด ๊ฑฐ๋ถ๋ Promise์ ๊ฐ์ ๊ฐ์ง๋๋ค.
window.addEventListener('unhandledrejection', function(event) {
console.error('--- ์ ์ญ Promise ๊ฑฐ๋ถ ๊ฐ์ง (unhandledrejection) ---');
console.error('๊ฑฐ๋ถ ์ฌ์ :', event.reason); // ์ฃผ๋ก Error ๊ฐ์ฒด์ด๊ฑฐ๋ Promise.reject()๋ก ์ ๋ฌ๋ ๊ฐ
// ์ฌ๊ธฐ์ ์ค์ ์๋ฌ ๋ชจ๋ํฐ๋ง ์๋น์ค(์: Sentry.captureException(event.reason))๋ก ํ๋ก๋ฏธ์ค ๊ฑฐ๋ถ๋ฅผ ์ ์กํ๋ ๋ก์ง ์ถ๊ฐ
// event.preventDefault()๋ฅผ ํธ์ถํ์ฌ ๋ธ๋ผ์ฐ์ ์ ๊ธฐ๋ณธ ๋์(์ฝ์ ๊ฒฝ๊ณ )์ ๋ฐฉ์งํฉ๋๋ค.
event.preventDefault();
});
function fetchData() {
const config = null;
console.log(config.apiUrl); // ์ด ์๋ฌ๋ window.onerror์ ์ํด ๊ฐ์ง๋ฉ๋๋ค.
}
function performAsyncTask() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์คํจ!")); // ์ด ๊ฑฐ๋ถ๋ unhandledrejection์ ์ํด ๊ฐ์ง๋ฉ๋๋ค.
}, 500);
});
}
setTimeout(fetchData, 1000);
performAsyncTask();
console.log("์ ํ๋ฆฌ์ผ์ด์
์ด ์คํ ์ค์
๋๋ค. ์ด์ ๋ฐ์ํ ์๋ฌ๋ฅผ ์ ์ญ์ ์ผ๋ก ๊ฐ์งํ ์ ์์ต๋๋ค.");
/*
React ์ ํ๋ฆฌ์ผ์ด์
์ ๊ฒฝ์ฐ, ์ปดํฌ๋ํธ ๋ ๋๋ง ๋จ๊ณ์์ ๋ฐ์ํ๋ ์๋ฌ๋ฅผ ์ก๊ธฐ ์ํด
'Error Boundary' ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ฌ ํน์ UI ๋ถ๋ถ์ ์๋ฌ๋ฅผ ๊ฒฉ๋ฆฌํ๊ณ ํด๋ฐฑ UI๋ฅผ ์ ๊ณตํ ์ ์์ต๋๋ค.
์์:
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error("React Error Boundary Caught:", error, errorInfo);
// ์ฌ๊ธฐ์ Sentry ๋ฑ ์๋ฌ ๋ชจ๋ํฐ๋ง ์๋น์ค๋ก ์๋ฌ๋ฅผ ์ ์กํ๋ ๋ก์ง ์ถ๊ฐ
this.setState({ error, errorInfo });
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary>
<YourPotentiallyBuggyComponent />
</ErrorBoundary>
);
}
*/