๐จ 11. ์๋ฌ ์ฒ๋ฆฌ โ ์์ธ๋ฅผ ์ฐ์ํ๊ฒ ๋ค๋ฃจ๋ ์๋์ด์ ๋ฐฉ์ด ์ ๋ต
๐ ๊ฐ์
try/catch์ ์ฌ๋ฐ๋ฅธ ์ฌ์ฉ, ์ปค์คํ ์๋ฌ ํด๋์ค, ๋น๋๊ธฐ ์๋ฌ ์ ํ, ์ ์ญ ์๋ฌ ํธ๋ค๋ฌ, ์ค๋ฌด ์๋ฌ ์ฒ๋ฆฌ ์ ๋ต์ ์์ ์ ๋ณตํฉ๋๋ค.
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ปค์คํ ์๋ฌ ํด๋์ค๋ก ์๋ฌ๋ฅผ ๊ตฌ์กฐํํ๊ณ ํ์ ๋ณ๋ก ์ฒ๋ฆฌํ๋ค.
- ๋น๋๊ธฐ ํจ์์์ ์๋ฌ๊ฐ ์ ํ๋๋ ๋ฐฉ์์ ์ดํดํ๊ณ ๋น ์ง์์ด ์ฒ๋ฆฌํ๋ค.
- ์ ์ญ ์๋ฌ ํธ๋ค๋ฌ์ ๋ก๊น ์ ๋ต์ผ๋ก ์ค๋ฌด ์๋ฌ๋ฅผ ์ถ์ ํ๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค 1. ์ ์์์ผ ํ๋๊ฐ?
- ๐๏ธ 2. Error ๊ฐ์ฒด์ ์ข ๋ฅ
- ๐ก๏ธ 3. try/catch/finally ํจํด
- ๐จ 4. ์ปค์คํ ์๋ฌ ํด๋์ค
- โก 5. ๋น๋๊ธฐ ์๋ฌ ์ฒ๋ฆฌ
- ๐ 6. ์ ์ญ ์๋ฌ ํธ๋ค๋ฌ
- ๐ผ 7. ์ค๋ฌด ์๋ฌ ์ฒ๋ฆฌ ์ ๋ต
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 18๋ถ(์ ์ฒด) / ํต์ฌ ํํธ๋ง: 11๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
[Error ํ์
] โ [try/catch ํจํด] โ [์ปค์คํ
์๋ฌ] โ [๋น๋๊ธฐ ์๋ฌ] โ [์ ์ญ ํธ๋ค๋ฌ]
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- ์๋ฌ ํ์ ์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌํ๋ ๊ตฌ์กฐํ๋ ์๋ฌ ํธ๋ค๋ง์ ๊ตฌํํ๋ค.
- API ์๋ฌ๋ฅผ ์ปค์คํ ์๋ฌ ํด๋์ค๋ก ๋ํํด ์๋ฏธ ์๋ ์ ๋ณด๋ฅผ ์ ๋ฌํ๋ค.
-
unhandledrejection์ด๋ฒคํธ๋ก Promise ์๋ฌ ๋๋ฝ์ ๋ฐฉ์งํ๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
๐ฃ ์์ฒ : "์ํธ ๋, ์ด์ ๋ฐฐํฌํ๋๋ฐ ์ผ๋ถ ์ ์ ๊ฐ '๊ฒ์๊ธ ์ ์ฅ์ด ์ ๋๋ค'๊ณ ํ๋๋ฐ, ๋ก๊ทธ์๋ ์๋ฌ ๋ฉ์์ง๊ฐ
Error: Failed to fetch๋ฐ์ ์์ด์. ๋ญ๊ฐ ๋ฌธ์ ์ธ์ง ์ ํ ๋ชจ๋ฅด๊ฒ ์ด์. ๋คํธ์ํฌ ์๋ฌ์ธ์ง, ์๋ฒ ์๋ฌ์ธ์ง, ์ธ์ฆ ์๋ฌ์ธ์ง... ์๋ฌ ๋ฉ์์ง๊ฐ ๋๋ฌด ๋น์ฝํด์."
๐ฆ ์ํธ: "์๋ฌ ์ฒ๋ฆฌ๊ฐ ์ ๋ ๊ฒ ์๋๋ผ ์๋ฌ๊ฐ ์ ๋๋ก ๊ตฌ์กฐํ๋์ง ์์ ๊ฑฐ์ผ. ์ปค์คํ ์๋ฌ ํด๋์ค๋ฅผ ๋ง๋ค์ด์ ์๋ฌ ํ์ , HTTP ์ํ ์ฝ๋, ์ฌ์ฉ์ ๋ฉ์์ง, ๊ฐ๋ฐ์ ๋ฉ์์ง๋ฅผ ๋ถ๋ฆฌํด์ผ ํด.
Failed to fetchํ๋๋ก๋ ๋๋ฒ๊น ์ด ๋ถ๊ฐ๋ฅํด. ์๋ฌ๋ ์ค๊ณ๊ฐ ํ์ํด."
๐ค 1. ์ ์์์ผ ํ๋๊ฐ?
์์ ๋์ด "๊ฒ์๊ธ ์ ์ฅ ๋๋ฅด๋ฉด ๊ทธ๋ฅ ํฐ ํ๋ฉด์ด ๋์์"๋ผ๊ณ ์ ๋ณดํ ๋ , ์์ ๋์ด ์๋ฒ ๋ก๊ทธ๋ฅผ ์ด์๋๋ Error: undefined ํ ์ค์ด ์ ๋ถ์๋ค. ์ด๋ ์๋ฒ์ธ์ง, ์ด๋ API์ธ์ง, ์ด๋ค ์ ์ ์ธ์ง โ ์๋ฌด ๋จ์๋ ์์๋ค. ์๋ฌ ์ฒ๋ฆฌ๋ฅผ "try/catch ๊ฐ์ธ๋ฉด ๋"์ผ๋ก ์๋ฉด ๋ฐ๋ก ์ด ์ํฉ์ ๋ฌด๋ ฅํด์ง๋ค:
- ์๋ฌ๊ฐ ๋ฌ๋๋ฐ "Error: undefined"๋ง ๋ก๊ทธ์ ๋จ๋ ๊ฒฝ์ฐ
- Promise chain์์ ์๋ฌ๊ฐ ์กฐ์ฉํ ์ผ์ผ์ง๋ ๊ฒฝ์ฐ
- ์ฌ์ฉ์์๊ฒ๋ ๊ธฐ์ ์ ์๋ฌ ๋ฉ์์ง๊ฐ ๊ทธ๋๋ก ๋ ธ์ถ๋๋ ๊ฒฝ์ฐ
- ์ด๋ API์์ ์๋ฌ๊ฐ ๋ฌ๋์ง ์ถ์ ์ด ์ ๋๋ ๊ฒฝ์ฐ
์ข์ ์๋ฌ ์ฒ๋ฆฌ = ์ฌ์ฉ์ ๊ฒฝํ + ๊ฐ๋ฐ์ ๊ฒฝํ ๋์์ ์งํจ๋ค.
๐๏ธ 2. Error ๊ฐ์ฒด์ ์ข ๋ฅ
์๋ฐ์คํฌ๋ฆฝํธ๊ฐ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๋ Error ๊ฐ์ฒด๋ name, message, stack ์ธ ๊ฐ์ง ํต์ฌ ์์ฑ์ ๊ฐ์ง๋๋ค. ์ํธ ๋ฆฌ๋ ๋์ ์๋ฌ์ ๋ฐ์ ์์ธ(ํ์
)์ ๋จผ์ ์๋ณํด์ผ ์ํฉ์ ๋ง๋ ๋ฐฉ์ด ๋ก์ง์ ์งค ์ ์๋ค๊ณ ์กฐ์ธํฉ๋๋ค.
// ๊ธฐ๋ณธ Error ์์ฑ
try {
null.property; // TypeError ๋ฐ์
} catch (err) {
console.log(err.name); // "TypeError"
console.log(err.message); // "Cannot read properties of null..."
console.log(err.stack); // ์คํ ํธ๋ ์ด์ค (๋๋ฒ๊น
์ ํต์ฌ)
}
// JS ๋ด์ฅ ์๋ฌ ํ์
// TypeError โ ํ์
์ด ๋ง์ง ์์ ๋ (null.foo, undefined())
// ReferenceError โ ์ ์ธ๋์ง ์์ ๋ณ์ ์ฐธ์กฐ (๋ฏธ์ ์ธ ๋ณ์)
// SyntaxError โ ๋ฌธ๋ฒ ์ค๋ฅ (ํ์ฑ ๋จ๊ณ์์ ๋ฐ์, catch๋ก ๋ชป ์ก์)
// RangeError โ ํ์ฉ ๋ฒ์ ์ด๊ณผ (๋ฐฐ์ด ๊ธธ์ด ์์ ๋ฑ)
// URIError โ encodeURI ๋ฑ์์ ์๋ชป๋ URI๐ก๏ธ 3. try/catch/finally ํจํด
์์ฒ ์ด๊ฐ ์ฒ์ ์ธ๋ถ API๋ฅผ ์ฐ๋ํ ๋, ์ฑ๊ณตํ๋ ๊ฒฝ์ฐ๋ง ์๊ฐํ๊ณ ์ฝ๋๋ฅผ ์์ฑํ๋ค๊ฐ ์๋ฒ ์ฅ์ ์ ํ๋ฉด์ด ๋ฉ์ด๋ฒ๋ฆฐ ์ ์ด ์์ต๋๋ค. ์ํธ ๋ฆฌ๋ ๋์ "์คํจ๋ฅผ ์ฐ์ํ๊ฒ ๋ค๋ฃจ๋ ๊ฒ์ด ์๋์ด์ ๊ธฐ๋ณธ๊ธฐ"๋ผ๋ฉฐ try/catch/finally์ ์ ํํ ํ๋ฆ์ ์ ์ํ์ต๋๋ค.
// ๊ธฐ๋ณธ ํจํด
function parseUserData(jsonString) {
try {
return JSON.parse(jsonString);
} catch (err) {
if (err instanceof SyntaxError) {
// ํ์
๋ณ ๋ถ๊ธฐ ์ฒ๋ฆฌ
console.error("JSON ํ์ฑ ์คํจ:", err.message);
return null;
}
throw err; // ์์์น ๋ชปํ ์๋ฌ๋ ๋ค์ ๋์ง๊ธฐ
}
}
// finally โ ๋ฆฌ์์ค ์ ๋ฆฌ์ ์ฌ์ฉ
async function withDbConnection(operation) {
const connection = await getDbConnection();
try {
return await operation(connection);
} catch (err) {
console.error("DB ์์
์คํจ:", err);
throw err;
} finally {
// ์ฑ๊ณต/์คํจ ๋ฌด๊ดํ๊ฒ ํญ์ ์ฐ๊ฒฐ ํด์
await connection.close();
}
}
// โ ํํ ์ค์ โ ์๋ฌ ์ผํค๊ธฐ (Silent Failure)
try {
await riskyOperation();
} catch (err) {
// ์๋ฌด๊ฒ๋ ์ ํจ โ ์๋ฌ๊ฐ ์ฌ๋ผ์ง!
// ์ด๊ฒ๋ณด๋ค๋ ์ต์ํ ๋ก๊ทธ๋ผ๋ ๋จ๊ฒจ์ผ ํ๋ค
}
// โ
์๋ฌ๋ฅผ ์ผ์ผ์ผ ํ๋ค๋ฉด ์๋๋ฅผ ๋ช
์
try {
await optionalOperation(); // ์คํจํด๋ ๊ด์ฐฎ์ ์์
} catch (err) {
// ์๋์ ์ผ๋ก ๋ฌด์: ์ด ์์
์ ์คํจํด๋ ๋ฉ์ธ ํ๋ก์ฐ์ ์ํฅ ์์
console.warn("์ ํ์ ์์
์คํจ (๋ฌด์๋จ):", err.message);
}๐จ 4. ์ปค์คํ ์๋ฌ ํด๋์ค
์คํ๋์์ ์ํธ ๋ฆฌ๋ ๋์ด "์๋ฌ๋ ์ค๊ณ๊ฐ ํ์ํด"๋ผ๊ณ ํ๋ ๊ทธ ์ค๊ณ ๊ฒฐ๊ณผ๋ฌผ์ด๋ค. Failed to fetch ํ๋๋ก๋ ์ธ์ฆ ์๋ฌ์ธ์ง, ๊ถํ ์๋ฌ์ธ์ง, ๋ฆฌ์์ค ์๋ฌ์ธ์ง ๊ตฌ๋ถํ ์ ์๋ค. ์ํธ ๋ฆฌ๋ ๋์ด ํ์ ๋์
ํ ์๋ฌ ๊ณ์ธต ๊ตฌ์กฐ ์ ์ฒด๋ฅผ ๋ณด์.
// ์์๋ค ์ปค๋ฎค๋ํฐ โ API ์๋ฌ ๊ณ์ธต ๊ตฌ์กฐ
// ๊ธฐ๋ฐ ์๋ฌ ํด๋์ค
class AppError extends Error {
constructor(message, options = {}) {
super(message);
this.name = this.constructor.name; // "AppError", "ApiError" ๋ฑ ์๋ ์ค์
this.statusCode = options.statusCode ?? 500;
this.userMessage = options.userMessage ?? "์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.";
this.isOperational = options.isOperational ?? true; // ์์ธก ๊ฐ๋ฅํ ์๋ฌ์ธ๊ฐ?
this.context = options.context ?? {}; // ๋๋ฒ๊น
์ฉ ์ปจํ
์คํธ
// V8์์ ์คํ ํธ๋ ์ด์ค ์ ํํ๊ฒ ์ ์ง
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
// API ์๋ฌ
class ApiError extends AppError {
constructor(message, statusCode, options = {}) {
super(message, { statusCode, ...options });
}
}
// ์ธ์ฆ/์ธ๊ฐ ์๋ฌ
class AuthError extends ApiError {
constructor(message = "์ธ์ฆ์ด ํ์ํฉ๋๋ค.", options = {}) {
super(message, 401, {
userMessage: "๋ก๊ทธ์ธ์ด ํ์ํ ๊ธฐ๋ฅ์
๋๋ค.",
...options,
});
}
}
class ForbiddenError extends ApiError {
constructor(message = "๊ถํ์ด ์์ต๋๋ค.", options = {}) {
super(message, 403, {
userMessage: "์ด ๊ธฐ๋ฅ์ ์ฌ์ฉํ ๊ถํ์ด ์์ต๋๋ค.",
...options,
});
}
}
// ๋ฆฌ์์ค ์์
class NotFoundError extends ApiError {
constructor(resource = "๋ฆฌ์์ค", options = {}) {
super(`${resource}๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.`, 404, {
userMessage: `์์ฒญํ ${resource}๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.`,
...options,
});
}
}
// ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ
class ValidationError extends AppError {
constructor(message, fields = {}) {
super(message, {
statusCode: 400,
userMessage: "์
๋ ฅ๊ฐ์ ํ์ธํด์ฃผ์ธ์.",
context: { fields },
});
this.fields = fields; // ํ๋๋ณ ์๋ฌ ๋ฉ์์ง
}
}// ์ค์ ์ฌ์ฉ ์์
async function updatePost(postId, data, currentUser) {
// ์ ํจ์ฑ ๊ฒ์ฌ
if (!data.title?.trim()) {
throw new ValidationError("์ ๋ชฉ์ ํ์์
๋๋ค.", {
title: "์ ๋ชฉ์ ์
๋ ฅํด์ฃผ์ธ์.",
});
}
const post = await findPost(postId);
if (!post) {
throw new NotFoundError("๊ฒ์๊ธ");
}
if (post.authorId !== currentUser.id && !currentUser.isAdmin) {
throw new ForbiddenError("๊ฒ์๊ธ ์์ ๊ถํ ์์", {
context: { postId, userId: currentUser.id },
});
}
return await savePost(postId, data);
}
// ์๋ฌ ์ฒ๋ฆฌ ์ธก
async function handleUpdatePost(req, res) {
try {
const result = await updatePost(req.params.id, req.body, req.user);
res.json({ success: true, data: result });
} catch (err) {
if (err instanceof ValidationError) {
return res.status(400).json({
error: err.userMessage,
fields: err.fields,
});
}
if (err instanceof ForbiddenError) {
return res.status(403).json({ error: err.userMessage });
}
if (err instanceof NotFoundError) {
return res.status(404).json({ error: err.userMessage });
}
// ์์์น ๋ชปํ ์๋ฌ โ 500 ๋ฐํ + ๋ก๊น
console.error("์์์น ๋ชปํ ์๋ฌ:", err);
res.status(500).json({ error: "์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค." });
}
}โก 5. ๋น๋๊ธฐ ์๋ฌ ์ฒ๋ฆฌ
๋๊ธฐ์ ์ธ ๋ก์ง์์๋ try/catch๊ฐ ์ง๊ด์ ์ด์ง๋ง, ๋น๋๊ธฐ ์ธ๊ณ์์๋ ์๋ฌ๊ฐ ๋ค๋ฅธ ์์ ์ผ๋ก ๋์ด๊ฐ ๋ฒ๋ฆฌ๊ธฐ ๋๋ฌธ์ ์ข
์ข
๋ฐฉ์ด์ ์ ํต๊ณผํ๊ฒ ๋ฉ๋๋ค. ์์ฒ ์ด๊ฐ await๋ .catch๋ฅผ ๋๋ฝํด ์๋ฌ๋ฅผ ํ๊ณต์ ๋ ๋ ค๋ฒ๋ ธ๋ ๊ฒฝํ์ ๋ฐํ์ผ๋ก ์์ ๋ง์ ๊ตฌ์ถํด ๋ด
๋๋ค.
// Promise ์๋ฌ ์ฒ๋ฆฌ โ .catch ๋น ๋จ๋ฆฌ๋ฉด ์ ๋จ
fetchPost(1)
.then((post) => renderPost(post))
.catch((err) => {
// ์ด๋ ๋จ๊ณ์์ ์๋ฌ๊ฐ ๋๋ ์ฌ๊ธฐ์ ์ฒ๋ฆฌ
showError(err.userMessage ?? err.message);
});
// async/await ์๋ฌ ์ฒ๋ฆฌ
async function loadPost(postId) {
try {
const post = await fetchPost(postId);
return post;
} catch (err) {
if (err instanceof NotFoundError) {
return null; // ์๋ ๊ฒ์๊ธ์ null ๋ฐํ
}
throw err; // ๋ค๋ฅธ ์๋ฌ๋ ์๋ก ์ ํ
}
}
// โ ์๋ฌ ์ ํ ๋๋ฝ ํจํด (ํํ ์ค์)
async function badExample() {
const posts = await fetchPosts(); // ์๋ฌ ๋ฐ์ ์...
// try/catch ์์ โ ์๋ฌ๊ฐ ์์๋ก ์ ํ๋จ (์ฌ๊ธฐ์ OK)
return posts;
}
// ๋ฌธ์ : ํธ์ถ ์ธก์์ ์ฒ๋ฆฌํ์ง ์์ผ๋ฉด
badExample(); // Promise rejection์ด ์ฒ๋ฆฌ๋์ง ์์ โ UnhandledPromiseRejection
// โ
ํญ์ ์๋ฌ ์ฒ๋ฆฌ ๋ณด์ฅ
try {
await badExample();
} catch (err) {
handleError(err);
}๐ 6. ์ ์ญ ์๋ฌ ํธ๋ค๋ฌ
๋น์ฆ๋์ค ๋ก์ง ๊ณณ๊ณณ์ try/catch๋ฅผ ๊ผผ๊ผผํ๊ฒ ๋ฐฐ์นํด๋, ์์์น ๋ชปํ ๊ณณ์์ ์๋ฌ๋ ๋ฐ๋์ ํ์ด๋์ค๊ธฐ ๋ง๋ จ์
๋๋ค. ์ํธ ๋ฆฌ๋ ๋์ ์ตํ์ ๋ณด๋ฃจ๋ก ์ ์ญ ์๋ฌ ํธ๋ค๋ฌ๋ฅผ ์ธํ
ํ์ฌ, ๋์๋ ์๋ฌ๋ค์ด ์กฐ์ฉํ ๋ฌปํ์ง ์๊ณ ๊ด์ ์์คํ
์ผ๋ก ๋ชจ์ด๋๋ก ์กฐ์นํ์ต๋๋ค.
// ๋ธ๋ผ์ฐ์ โ ์ ์ญ ์๋ฌ ์บ์น
// ๋๊ธฐ ์๋ฌ (try/catch๋ก ์ก์ง ๋ชปํ ๊ฒ)
window.addEventListener("error", (event) => {
console.error("์ ์ญ ์๋ฌ:", event.error);
sendToErrorTracking({
message: event.error?.message,
stack: event.error?.stack,
filename: event.filename,
lineno: event.lineno,
});
// event.preventDefault()๋ก ๋ธ๋ผ์ฐ์ ๊ธฐ๋ณธ ์๋ฌ ๋ก๊ทธ ์ต์ ๊ฐ๋ฅ
});
// ์ฒ๋ฆฌ๋์ง ์์ Promise rejection
window.addEventListener("unhandledrejection", (event) => {
console.error("์ฒ๋ฆฌ๋์ง ์์ Promise ๊ฑฐ๋ถ:", event.reason);
sendToErrorTracking({
message: event.reason?.message ?? String(event.reason),
stack: event.reason?.stack,
type: "UnhandledPromiseRejection",
});
event.preventDefault(); // ๋ธ๋ผ์ฐ์ ์ฝ์ ๊ฒฝ๊ณ ์ต์
});
// Node.js ์ ์ญ ์๋ฌ ํธ๋ค๋ฌ
process.on("uncaughtException", (err) => {
console.error("์ฒ๋ฆฌ๋์ง ์์ ์์ธ:", err);
// ๋ก๊น
ํ ํ๋ก์ธ์ค ์ข
๋ฃ (uncaughtException ํ ์ํ ๋ถ์์ )
process.exit(1);
});
process.on("unhandledRejection", (reason, promise) => {
console.error("์ฒ๋ฆฌ๋์ง ์์ Promise ๊ฑฐ๋ถ:", reason);
// ์ฌ๊ฐํ ์๋ฌ๋ฉด ์ข
๋ฃ, ๊ทธ๋ ์ง ์์ผ๋ฉด ๊ณ์ ์คํ
});๐ผ 7. ์ค๋ฌด ์๋ฌ ์ฒ๋ฆฌ ์ ๋ต
์ํธ ๋ฆฌ๋ ๋์ด Sentry ์ฐ๋ ์์
๊ณผ ํจ๊ป ํ ์ ์ฒด์ ์ ์ฉํ ์๋ฌ ์ฒ๋ฆฌ ์ํคํ
์ฒ๋ค. ์์ฒ ์ด๊ฐ "์ด๊ฑฐ ๋งค๋ฒ ์ค์ ํด์ผ ํด์?"๋ผ๊ณ ๋ฌผ์์ ๋, ์ํธ ๋์ด "ํ ๋ฒ ErrorService ๋ง๋ค์ด๋๋ฉด ์ด๋์๋ ํ ์ค๋ก ๋๋"๋ผ๊ณ ํ๋ค.
// ์๋ฌ ๋ก๊น
์๋น์ค ์ฐ๋ ํจํด (Sentry ๋ฑ)
class ErrorService {
static report(err, context = {}) {
// 1. ์ฝ์ ๋ก๊ทธ (๊ฐ๋ฐ ํ๊ฒฝ)
if (process.env.NODE_ENV === "development") {
console.error("[ErrorService]", err, context);
}
// 2. ์ด์ ์๋ฌ ์ถ์ ์๋น์ค (Sentry, Datadog ๋ฑ)
if (process.env.NODE_ENV === "production") {
// Sentry.captureException(err, { extra: context });
}
// 3. ์ด์ ๊ฐ๋ฅํ(Operational) ์๋ฌ๋ ์ฌ์ฉ์์๊ฒ ์๋ฆผ
if (err instanceof AppError && err.isOperational) {
return err.userMessage; // UI์ ํ์
}
return "์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."; // ๊ธฐ์ ์ ์๋ฌ๋ ์จ๊น
}
}
// React Error Boundary ํ์ฉ (์ปดํฌ๋ํธ ์๋ฌ)
class PostErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
ErrorService.report(error, { componentStack: errorInfo.componentStack });
}
render() {
if (this.state.hasError) {
return <div>๊ฒ์๊ธ์ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.</div>;
}
return this.props.children;
}
}๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ์ปค์คํ
์๋ฌ ํด๋์ค๋ฅผ ๋ง๋ค ๋ Error.captureStackTrace๋ฅผ ํธ์ถํ๋ ์ด์ ๋?
โ ์ ๋ต: ์คํ ํธ๋ ์ด์ค์์ ์๋ฌ๊ฐ ๋ฐ์ํ ์ค์ ์์น ๋ฅผ ์ ํํ ๊ฐ๋ฆฌํค๋๋ก, ์๋ฌ ํด๋์ค ์์ฑ์ ์์ฒด๊ฐ ์คํ์ ํฌํจ๋๋ ๊ฒ์ ์ ๊ฑฐํ๊ธฐ ์ํจ.
๐ก ์์ธ ํด์ค:
Error.captureStackTrace(this, this.constructor)์์ผ๋ฉด, ์คํ ํธ๋ ์ด์ค์ ์ฒซ ๋ฒ์งธ ์ค์ดAppError์์ฑ์๋ฅผ ๊ฐ๋ฆฌํจ๋ค.- ์ด ํธ์ถ๋ก ์์ฑ์๊ฐ ์คํ์์ ์ ๊ฑฐ๋์ด, ์๋ฌ๊ฐ ์ค์ ๋ก throw๋ ๋น์ฆ๋์ค ๋ก์ง ์์น ๊ฐ ์คํ์ ์ฒซ ๋ฒ์งธ๋ก ์จ๋ค.
- V8 ์์ง(Chrome, Node.js)์์๋ง ์ง์ํ๋ฏ๋ก
if (Error.captureStackTrace)์ฒดํฌ๊ฐ ํ์ํ๋ค. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "์คํ ํธ๋ ์ด์ค๋ ๋๋ฒ๊น ์ ์ง๋๋ค. ์ง๋์ ์์์ ์ด ์๋ฌ ํด๋์ค ์์ฑ์๊ฐ ์๋๋ผ ์ค์ ๋ฌธ์ ๋ฐ์ ์ง์ ์ ๊ฐ๋ฆฌ์ผ์ผ ํ๋ค."
Q2. instanceof๋ก ์๋ฌ ํ์
์ ๊ตฌ๋ถํ ๋ ์ฃผ์ํ ์ ์?
โ
์ ๋ต: ์๋ฌ๋ฅผ ๋ค๋ฅธ realm(iframe, vm ๋ชจ๋ ๋ฑ)์์ ์์ฑํ ๊ฒฝ์ฐ instanceof๊ฐ false๋ฅผ ๋ฐํํ ์ ์๋ค. ๋ํ minification์ผ๋ก ํด๋์ค ์ด๋ฆ์ด ๋ฐ๋๋ฉด err.name ๊ธฐ๋ฐ ์ฒดํฌ๊ฐ ๋ ์์ ์ ์ด๋ค.
๐ก ์์ธ ํด์ค:
// instanceof ์ฒดํฌ (์ผ๋ฐ์ ์ผ๋ก ์์ )
if (err instanceof NotFoundError) { ... }
// name ๊ธฐ๋ฐ ์ฒดํฌ (๋ ๋ฐฉ์ด์ )
if (err.name === "NotFoundError") { ... }
// ํ์
์คํฌ๋ฆฝํธ + type guard ํจํด (๊ถ์ฅ)
function isNotFoundError(err: unknown): err is NotFoundError {
return err instanceof NotFoundError || (err as any)?.name === "NotFoundError";
}- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "์ปค์คํ
์๋ฌ๋
this.name = this.constructor.name์ ์์ฑ์์์ ์ค์ ํ๋ฉด minification ์ดํ์๋err.name์ผ๋ก ์๋ณ ๊ฐ๋ฅํ๋ค."
Q3. ์์ฒ ์ด์ ํ ์คํธ ํ์ โ ๊ธด๊ธ ๋๋ฒ๊น
์์ PM์ด ๋ค๊ธํ๊ฒ ์ธ์ณค์ต๋๋ค: "์์ฒ ๋, ๊ฒ์๊ธ ์ญ์ ๋ฒํผ ๋๋ฅด๋ฉด ์๋ฌด ๋ฐ์์ด ์์ด์! ์ฝ์์๋ ์๋ฌด๊ฒ๋ ์ ๋์์. ์๋ฒ ์๋ฌ์ธ์ง ํด๋ผ์ด์ธํธ ์๋ฌ์ธ์ง์กฐ์ฐจ ๋ชฐ๋ผ์!"
// ๐ฃ ์์ฒ ์ด์ ์ฝ๋
async function deletePost(postId) {
try {
await api.delete(`/posts/${postId}`);
toast.success("์ญ์ ์๋ฃ!");
} catch {
// ์๋ฌด๊ฒ๋ ์ ํจ
}
}โ
์ ๋ต: catch ๋ธ๋ก์ด ์์ ํ ๋น์ด์์ด ์๋ฌ๋ฅผ ๋ฌด์(์ผํค๊ธฐ)ํ๊ณ ์๋ค. ์ฌ์ฉ์์๊ฒ ์๋ฌ ํผ๋๋ฐฑ๋ ์๊ณ , ๋ก๊ทธ๋ ์๋ค.
๐ก ์์ธ ํด์ค:
// โ
๋ฆฌํฉํ ๋ง๋ ์ฝ๋
async function deletePost(postId) {
try {
await api.delete(`/posts/${postId}`);
toast.success("์ญ์ ์๋ฃ!");
} catch (err) {
// 1. ์๋ฌ ํ์
๋ณ ์ฌ์ฉ์ ๋ฉ์์ง
if (err instanceof AuthError) {
toast.error("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.");
redirectToLogin();
return;
}
if (err instanceof ForbiddenError) {
toast.error("์ญ์ ๊ถํ์ด ์์ต๋๋ค.");
return;
}
if (err instanceof NotFoundError) {
toast.error("์ด๋ฏธ ์ญ์ ๋ ๊ฒ์๊ธ์
๋๋ค.");
return;
}
// 2. ์์์น ๋ชปํ ์๋ฌ โ ๋ก๊น
+ ์ผ๋ฐ ๋ฉ์์ง
console.error("๊ฒ์๊ธ ์ญ์ ์คํจ:", err);
ErrorService.report(err, { action: "deletePost", postId });
toast.error("์ญ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.");
}
}- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "๋น catch ๋ธ๋ก์ ๋ฒ๊ทธ์ ์์ธ์ ์ฐพ๊ธฐ ์ด๋ ต๊ฒ ๋ง๋๋ ๋งค์ฐ ์ํํ ํจํด์ ๋๋ค. ์์ธ๋ฅผ ๋ฌด์ํ๊ธฐ๋ณด๋ค๋ ์ต์ํ ์๋ฌ ๋ก๊น ์ ๋จ๊ธฐ๊ณ , ์ด์์ ์ผ๋ก๋ ์ฌ์ฉ์ ํผ๋๋ฐฑ๊ณผ ํจ๊ป ์ฒ๋ฆฌํ๋ ๊ฒ์ด ์์ ํฉ๋๋ค."
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋ ์ฝ๋๋ฆฌ๋ทฐ์์ ๋น catch ๋ธ๋ก์ผ๋ก ํผ๋ฌ๋๋ฐ, ์ด๋ฒ ์ฑํฐ ๋ฐฐ์ฐ๋ฉด์ ๊ทธ๊ฒ ์ผ๋ง๋ ์ํํ ํจํด์ธ์ง ์ ๋๋ก ์ดํดํ๋ค. "์๋ฌ๋ฅผ ์กฐ์ฉํ ์ผํค๋ฉด ๋ฒ๊ทธ๊ฐ ๋ณด์ด์ง ์๋๋ค" โ ์ด ๋ง์ด ๋จธ๋ฆฟ์์์ ๊ณ์ ๋งด๋๋ค.
์ปค์คํ
์๋ฌ ํด๋์ค๋ ์ฒ์์๋ "๊ตณ์ด ํด๋์ค๊น์ง ๋ง๋ค์ด์ผ ํด?" ์ถ์๋๋ฐ, err.userMessage, err.statusCode๋ก ์๋ฌ ํ์
์ ๋ง๋ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค๋ ๊ฑธ ์ง์ ์ฝ๋๋ก ๋ณด๋๊น ๋ฉ๋์ด ๋๋ค. Failed to fetch ํ๋๋ก ์๋ฌด๊ฒ๋ ๋ชป ํ๋ ๊ฒ๊ณผ ๋น๊ต๊ฐ ๋๋ค.
๐ก ์ค๋์ ๊ตํ: "์๋ฌ ์ฒ๋ฆฌ๋ '์์ธ๋ฅผ ๋ง๋ ๊ฒ'์ด ์๋๋ผ '์์ธ๊ฐ ๋ฐ์ํ์ ๋์ ๋ค์ ์๋๋ฆฌ์ค๋ฅผ ์ค๊ณํ๋ ๊ฒ'์ด๋ค. ๋น catch ๋ธ๋ก์ ์ด ์์ธ ์ํฉ์ ๋์ ๊ฐ๋ฆฌ๋ ๊ฒ๊ณผ ๊ฐ๋ค."
์ํธ ๋์ด "์๋ฌ๋ ์ค๊ณ๊ฐ ํ์ํด"๋ผ๊ณ ํ๋๋ฐ, ๊ทธ ๋ง์ด ์ค๋ ํ๋ฃจ ๋ด๋ด ์๊ฐ๋ฌ๋ค. ๋ค์ ์ฃผ์๋ Map/Set ์ฑํฐ์ธ๋ฐ, ์๋ฃ๊ตฌ์กฐ๋ ์ ์จ์ผ๊ฒ ๋ค๋ ์๊ฐ์ด ๋ ๋ค. ํด๊ทผํ๊ณ ์ฐ์ฑ ํ ๋ฐํด ๋๊ณ ์์ผ์ง.