Node.js 오류 처리 시스템을 구축하는 방법
게시 됨: 2022-03-11어떤 사람들은 오류를 처리하기 위해 고군분투하고 어떤 사람들은 심지어 그것을 완전히 놓치고 있다는 것을 보는 것은 어렵지 않습니다. 오류를 적절하게 처리한다는 것은 버그와 오류를 쉽게 찾아 개발 시간을 단축할 뿐만 아니라 대규모 애플리케이션을 위한 강력한 코드베이스를 개발하는 것을 의미합니다.
특히 Node.js 개발자는 때때로 다양한 종류의 오류를 처리하면서 깨끗하지 않은 코드로 작업하는 자신을 발견하고 처리하기 위해 동일한 논리를 모든 곳에 잘못 적용합니다. 그들은 계속 스스로에게 "Node.js가 오류를 처리하는 데 좋지 않습니까?"라고 묻습니다. 또는 그렇지 않은 경우 어떻게 처리합니까?” 그에 대한 내 대답은 "아니요, Node.js는 전혀 나쁘지 않습니다. 그것은 우리 개발자들에게 달려 있습니다.”
여기에 내가 가장 좋아하는 솔루션 중 하나가 있습니다.
Node.js의 오류 유형
먼저 Node.js의 오류에 대한 명확한 이해가 필요합니다. 일반적으로 Node.js 오류는 작동 오류 와 프로그래머 오류 의 두 가지 범주로 나뉩니다.
- 작동 오류 는 결과가 예상되고 적절한 방식으로 처리되어야 하는 런타임 문제를 나타냅니다. 작동 오류가 애플리케이션 자체에 버그가 있다는 의미는 아니지만 개발자는 이를 신중하게 처리해야 합니다. 작동 오류의 예로는 "메모리 부족", "API 끝점에 대한 잘못된 입력" 등이 있습니다.
- 프로그래머 오류 는 잘못 작성된 코드의 예기치 않은 버그를 나타냅니다. 이는 코드 자체에 해결해야 할 몇 가지 문제가 있고 잘못 코딩되었음을 의미합니다. 좋은 예는 "undefined" 속성을 읽으려고 시도하는 것입니다. 문제를 해결하려면 코드를 변경해야 합니다. 그것은 운영상의 오류가 아니라 개발자가 만든 버그입니다.
이를 염두에 두고 오류의 두 가지 범주를 구별하는 데 문제가 없어야 합니다. 작동 오류는 응용 프로그램의 자연스러운 부분이고 프로그래머 오류는 개발자에 의해 발생하는 버그입니다. 논리적 질문은 "두 범주로 나누어 처리하는 것이 왜 유용한가?"입니다.
오류에 대한 명확한 이해가 없으면 오류가 발생할 때마다 애플리케이션을 다시 시작하고 싶은 생각이 들 수 있습니다. 수천 명의 사용자가 응용 프로그램을 즐기고 있을 때 "파일을 찾을 수 없음" 오류로 인해 응용 프로그램을 다시 시작하는 것이 합리적입니까? 절대적으로하지.
그러나 프로그래머 오류는 어떻습니까? 응용 프로그램에 예기치 않은 눈덩이 효과를 일으킬 수 있는 알 수 없는 버그가 나타날 때 응용 프로그램을 계속 실행하는 것이 합리적입니까? 다시 말하지만 절대 아닙니다!
오류를 올바르게 처리해야 할 때입니다.
비동기 JavaScript 및 Node.js에 대한 경험이 있다고 가정하면 오류 처리를 위해 콜백을 사용할 때 단점이 발생할 수 있습니다. 그것들은 당신이 오류를 중첩된 오류까지 확인하도록 강요하여 코드 흐름을 따르기 어렵게 만드는 악명 높은 "콜백 지옥" 문제를 일으킵니다.
약속이나 async/await를 사용하는 것은 콜백을 대체하는 좋은 방법입니다. async/await의 일반적인 코드 흐름은 다음과 같습니다.
const doAsyncJobs = async () => { try { const result1 = await job1(); const result2 = await job2(result1); const result3 = await job3(result2); return await job4(result3); } catch (error) { console.error(error); } finally { await anywayDoThisJob(); } }
Node.js 내장 Error 객체를 사용하는 것은 대부분의 개발자가 오류의 원인을 추적하기 위해 의존하는 StackTrace와 같은 오류에 대한 직관적이고 명확한 정보를 포함하고 있기 때문에 좋은 방법입니다. 그리고 Error 클래스를 확장하여 HTTP 상태 코드 및 설명과 같은 의미 있는 추가 속성은 더 많은 정보를 제공합니다.
class BaseError extends Error { public readonly name: string; public readonly httpCode: HttpStatusCode; public readonly isOperational: boolean; constructor(name: string, httpCode: HttpStatusCode, description: string, isOperational: boolean) { super(description); Object.setPrototypeOf(this, new.target.prototype); this.name = name; this.httpCode = httpCode; this.isOperational = isOperational; Error.captureStackTrace(this); } } //free to extend the BaseError class APIError extends BaseError { constructor(name, httpCode = HttpStatusCode.INTERNAL_SERVER, isOperational = true, description = 'internal server error') { super(name, httpCode, isOperational, description); } }
단순함을 위해 일부 HTTP 상태 코드만 구현했지만 나중에 추가할 수 있습니다.
export enum HttpStatusCode { OK = 200, BAD_REQUEST = 400, NOT_FOUND = 404, INTERNAL_SERVER = 500, }
BaseError 또는 APIError를 확장할 필요는 없지만 필요와 개인 취향에 따라 일반적인 오류에 대해 확장해도 됩니다.
class HTTP400Error extends BaseError { constructor(description = 'bad request') { super('NOT FOUND', HttpStatusCode.BAD_REQUEST, true, description); } }
그래서 어떻게 사용합니까? 다음을 입력하세요.
... const user = await User.getUserById(1); if (user === null) throw new APIError( 'NOT FOUND', HttpStatusCode.NOT_FOUND, true, 'detailed explanation' );
중앙 집중식 Node.js 오류 처리
이제 Node.js 오류 처리 시스템의 주요 구성 요소인 중앙 집중식 오류 처리 구성 요소를 빌드할 준비가 되었습니다.
일반적으로 오류를 처리할 때 가능한 코드 중복을 피하기 위해 중앙 집중식 오류 처리 구성 요소를 구축하는 것이 좋습니다. 오류 처리 구성 요소는 예를 들어 시스템 관리자에게 알림을 보내고(필요한 경우) Sentry.io와 같은 모니터링 서비스로 이벤트를 전송하고 이를 로깅하여 포착된 오류를 이해할 수 있도록 만드는 역할을 합니다.
다음은 오류를 처리하기 위한 기본 워크플로입니다.
코드의 일부에서 오류가 포착되어 오류 처리 미들웨어로 전송됩니다.
... try { userService.addNewUser(req.body).then((newUser: User) => { res.status(200).json(newUser); }).catch((error: Error) => { next(error) }); } catch (error) { next(error); } ...
오류 처리 미들웨어는 오류 유형을 구별하고 중앙 집중식 오류 처리 구성 요소로 보내는 좋은 장소입니다. Express.js 미들웨어의 오류 처리에 대한 기본 사항을 아는 것은 확실히 도움이 될 것입니다.

app.use(async (err: Error, req: Request, res: Response, next: NextFunction) => { if (!errorHandler.isTrustedError(err)) { next(err); } await errorHandler.handleError(err); });
이제 일부 기능을 이미 사용했기 때문에 중앙 집중식 구성 요소가 어떻게 생겼는지 상상할 수 있습니다. 구현 방법은 전적으로 귀하에게 달려 있지만 다음과 같이 보일 수 있습니다.
class ErrorHandler { public async handleError(err: Error): Promise<void> { await logger.error( 'Error message from the centralized error-handling component', err, ); await sendMailToAdminIfCritical(); await sendEventsToSentry(); } public isTrustedError(error: Error) { if (error instanceof BaseError) { return error.isOperational; } return false; } } export const errorHandler = new ErrorHandler();
때로는 기본 "console.log"의 출력으로 인해 오류를 추적하기 어렵습니다. 오히려 개발자가 문제를 빠르게 이해하고 수정되었는지 확인할 수 있도록 형식이 지정된 방식으로 오류를 인쇄하는 것이 훨씬 더 나을 수 있습니다.
전반적으로 이렇게 하면 개발자의 시간을 절약할 수 있어 오류를 쉽게 추적하고 가시성을 높여 오류를 처리할 수 있습니다. winston 또는 morgan과 같은 사용자 지정 가능한 로거를 사용하는 것이 좋습니다.
다음은 맞춤형 winston 로거입니다.
const customLevels = { levels: { trace: 5, debug: 4, info: 3, warn: 2, error: 1, fatal: 0, }, colors: { trace: 'white', debug: 'green', info: 'green', warn: 'yellow', error: 'red', fatal: 'red', }, }; const formatter = winston.format.combine( winston.format.colorize(), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.splat(), winston.format.printf((info) => { const { timestamp, level, message, ...meta } = info; return `${timestamp} [${level}]: ${message} ${ Object.keys(meta).length ? JSON.stringify(meta, null, 2) : '' }`; }), ); class Logger { private logger: winston.Logger; constructor() { const prodTransport = new winston.transports.File({ filename: 'logs/error.log', level: 'error', }); const transport = new winston.transports.Console({ format: formatter, }); this.logger = winston.createLogger({ level: isDevEnvironment() ? 'trace' : 'error', levels: customLevels.levels, transports: [isDevEnvironment() ? transport : prodTransport], }); winston.addColors(customLevels.colors); } trace(msg: any, meta?: any) { this.logger.log('trace', msg, meta); } debug(msg: any, meta?: any) { this.logger.debug(msg, meta); } info(msg: any, meta?: any) { this.logger.info(msg, meta); } warn(msg: any, meta?: any) { this.logger.warn(msg, meta); } error(msg: any, meta?: any) { this.logger.error(msg, meta); } fatal(msg: any, meta?: any) { this.logger.log('fatal', msg, meta); } } export const logger = new Logger();
기본적으로 제공하는 것은 형식이 다른 여러 수준에서 선명한 색상으로 로깅하고 런타임 환경에 따라 다른 출력 미디어에 로깅하는 것입니다. 이것의 좋은 점은 winston의 내장 API를 사용하여 로그를 보고 쿼리할 수 있다는 것입니다. 또한 로그 분석 도구를 사용하여 형식이 지정된 로그 파일을 분석하여 응용 프로그램에 대한 보다 유용한 정보를 얻을 수 있습니다. 굉장하지, 그렇지?
지금까지 우리는 주로 운영 오류 처리에 대해 논의했습니다. 프로그래머 오류는 어떻습니까? 이러한 오류를 처리하는 가장 좋은 방법은 즉시 충돌이 발생하고 PM2와 같은 자동 재시작기로 정상적으로 재시작하는 것입니다. 뜻밖의 방법으로.
process.on('uncaughtException', (error: Error) => { errorHandler.handleError(error); if (!errorHandler.isTrustedError(error)) { process.exit(1); } });
마지막으로 처리되지 않은 약속 거부 및 예외 처리에 대해 언급하겠습니다.
Node.js/Express 애플리케이션에서 작업할 때 약속을 처리하는 데 많은 시간을 할애하는 자신을 발견할 수 있습니다. 거절 처리를 잊어버렸을 때 처리되지 않은 promise 거절에 대한 경고 메시지를 보는 것은 어렵지 않습니다.
경고 메시지는 로깅 외에는 별로 하지 않지만 적절한 대체를 사용하고 process.on('unhandledRejection', callback)
을 구독하는 것이 좋습니다.
일반적인 오류 처리 흐름은 다음과 같습니다.
// somewhere in the code ... User.getUserById(1).then((firstUser) => { if (firstUser.isSleeping === false) throw new Error('He is not sleeping!'); }); ... // get the unhandled rejection and throw it to another fallback handler we already have. process.on('unhandledRejection', (reason: Error, promise: Promise<any>) => { throw reason; }); process.on('uncaughtException', (error: Error) => { errorHandler.handleError(error); if (!errorHandler.isTrustedError(error)) { process.exit(1); } });
마무리
모든 것이 끝나면 오류 처리는 선택 사항이 아니라 개발 단계와 프로덕션 모두에서 응용 프로그램의 필수 부분이라는 것을 깨달아야 합니다.
Node.js의 단일 구성 요소에서 오류를 처리하는 전략은 개발자가 귀중한 시간을 절약하고 코드 중복 및 누락된 오류 컨텍스트를 방지하여 깨끗하고 유지 관리 가능한 코드를 작성할 수 있도록 합니다.
이 기사를 재미있게 읽고 논의된 오류 처리 워크플로와 구현이 Node.js에서 강력한 코드베이스를 구축하는 데 도움이 되었기를 바랍니다.
Toptal 엔지니어링 블로그에 대한 추가 정보:
- Promise 기반 오류 처리를 위해 Express.js 경로 사용