
NestJS 기반 프로젝트를 개발 중에 곧 배포를 앞두고 있어서 로깅 시스템이 필요했다.
처음에는 Winston을 그대로 사용하려 했지만, 생각보다 부족한 기능이 많았다.
1. Winston 기본 설정이 부족 → NestJS와 TypeORM에서 바로 사용하기 어려움
2. SQL 쿼리 로깅이 비효율적 → 중요한 정보를 가독성 좋게 보기 어려움
3. 모듈 기반 로깅 미지원 → 특정 모듈에서 발생한 로그를 명확하게 구분하기 어려움
4. 파일 로그 관리 부족 → 로그 파일이 커지면 관리가 어려워지고, 파일 회전 기능이 필요함
이러한 문제를 해결하기 위해 일일히 Winston을 커스텀하기 시작했다.
특히 NestJS 및 TypeORM 환경에 최적화하여 HTTP 요청 로깅, SQL 쿼리 하이라이팅, 에러 스택 추적 등
다양한 기능을 사용할 수 있도록 개발하였다.
만들다 보니 생각보다 NestJS와 TypeORM에 최적화된 기능을 추가하게 된 것 같아서
처음으로 npm에 배포를 해보기로 했다 !
Blanc-Logger
그래서 Winston 기반 로깅 라이브러리 "Blanc-Logger" 를 만들게 되었다.
먼저 전반적인 파일 구성에 대해 살펴보자.
├─ src
│ ├─ helper
│ │ ├─ sql-formatter.ts // SQL 구문 하이라이팅, 포맷팅, 및 쿼리 분석 기능을 제공
│ │ └─ uuid.ts // UUIDv5를 사용하여 고유한 로그 식별자를 생성하는 기능을 제공
│ ├─ index.ts // 라이브러리의 주요 모듈들을 한 곳에서 통합하여 내보내는 엔트리 포인트 역할
│ ├─ logger
│ │ ├─ blanc-logger.ts // Winston 기반 로거의 메인 설정 및 콘솔/파일 로그 전송을 구성
│ │ ├─ custom-blanc.logger.ts // NestJS의 LoggerService를 구현해서 기본 로거 대체 제공
│ │ ├─ logger.config.ts // 사용자 커스터마이징 로깅 설정을 로드하고 관리하는 구성 파일
│ │ └─ typeorm-blanc-logger.ts // TypeORM 전용 로거로, SQL 쿼리와 데이터베이스 관련 로그를 상세하게 기록
│ └─ middleware
│ └─ blanc-logger.middleware.ts // HTTP 요청에서 모듈명을 추출하여 로그 컨텍스트에 추가하는 미들웨어를 제공
상세 코드는 포스팅 하단의 GitHub 링크에서 확인할 수 있다.

우선 로그 출력 콘솔부터 살펴보자.
디버깅할 때 추적이 편해지게, 각 로그에 UUIDv5로 생성된 고유 LogID를 부여하고,
{timestamp, level, message} 구조의 JSON 포맷으로 출력되어 외부 로그 분석 도구와 연동하기 용이하도록 하였다.
커스텀 콘솔 포맷에서는 로그 레벨에 따라 이모지와 색상을 적용하여 가독성을 높였다.
이어서 SQL 쿼리 내의 주요 키워드를 식별하고, 해당 토큰에 서식을 적용하여 쿼리의 가독성을 높였다.
쿼리 하이라이팅은 키워드, 값, 테이블명을 색상 강조 및 들여 쓰기로 구분하며, 파라미터 값도 별도로 강조했다.
이렇게 콘솔에 출력되는 로그와 SQL 쿼리의 하이라이팅을 통해 사용성과 가독성을 개선했다.

다음으로 에러 발생 시 다중 스택 레이어와 추가 메타데이터를 포함하여 상세한 에러 로그를 기록했다.
그리고 BlancLoggerMiddleware를 활용하여 HTTP 요청의 URL에서 모듈명을 자동 추출해서 메시지에 활용했다.
미들웨어 적용으로 모듈별 컨텍스트 추적이 가능해져,
[UserService → AuthModule → Subsystem] 과 같이 계층 구조로 로깅할 수 있게 되어,
각 로그 메시지의 출처를 명확하게 파악할 수 있었다.
╔═ SQL Query ═════════════════════════════════
SELECT
"user"."id" AS "userId",
"user"."email" AS "userEmail"
FROM "user" "user"
WHERE "user"."age" > $1
╠═ Parameters ═══════════════════════════════
[18]
╠═ Analysis ═════════════════════════════════
⚠️ Avoid SELECT * - specify columns explicitly
╚═════════════════════════════════════════════
다음과 같이 SQL 쿼리 출력 시 간단한 성능 모니터링과 실행 계획(Explain Plan) 리포트를 구축했다.
또한, SELECT * 및 JOIN 조건 누락과 같은 SQL 성능 저하 패턴을 자동 감지하고,
Slow Query (예: 100ms 이상)를 자동 감지를 해 경고 로그로 기록하도록 구성했다.
이렇게 쿼리 최적화가 필요한 부분을 사전에 식별 가능하게 되었다.
combined-YYYY-MM-DD.log // 모든 레벨의 로그 메시지가 기록되는 파일
error-YYYY-MM-DD.log // 에러 레벨의 로그 메시지만 기록되는 파일
exceptions-YYYY-MM-DD.log // 처리되지 않은 예외(Unhandled Exception) 로그가 기록되는 파일
rejections-YYYY-MM-DD.log // 미처리된 Promise 거부(Unhandled Rejection) 로그가 기록되는 파일
// error-2025-03-05.log
{"level":"error","message":"HTTP Exception: DUPLICATION","stack":[{"moduleName":"user","path":"/api/user/profile","stack":"ConflictException: DUPLICATION\n at UserService.getProfile (/path/to/src/user/user.service.ts:60:13)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)"}],"timestamp":"2025-03-05 02:29:51"}
이제 중요한 로그 파일에 대해 알아보자.
아까 살펴본 콘솔 출력과 파일 저장을 동시에 지원하게끔 했다.
그리고 로그 파일이 무작정 쌓이지 않도록 일정 크기(예: 20MB) 또는 일정 주기(예: 15일)에 따라
자동으로 파일 회전(Rotation) 되고 날짜별로 파일이 분리되어 생성되도록 구현했다.
또한 필요한 정보만 선별 기록하여 로그의 가독성과 관리 효율성을 증가시키기 위해
개발 및 운영 환경에 따라 [debug, verbose, info, warn, error] 로그 레벨을 실시간으로 조절할 수 있도록 했다.
예를 들어 FILE_LOG_LEVEL 변수를 info로 세팅 시 해당 값보다 높은 로그 레벨인,
[info, warn, error] 세 가지의 로그 레벨이 로그 파일에 적재되고,
FILE_LOG_LEVEL 변수를 error로 세팅 시 가장 높은 레벨이므로 error 로그만 파일에 적재된다.
이렇게 실시간으로 레벨을 조절하여 필요한 정보만 효과적으로 기록 가능하게 했다.
LOG_DIR: logs # 로그 파일 저장 경로 (기본: 프로젝트 루트/logs)
CONSOLE_LOG_LEVEL: info # 콘솔 출력 로그 레벨 (debug, verbose, info, warn, error)
FILE_LOG_LEVEL: error # 파일 출력 로그 레벨 (debug, verbose, info, warn, error)
ROTATION_DAYS: 30d # 로그 파일 보관 기간 (예: 30일)
MAX_FILE_SIZE: 20m # 단일 파일 최대 크기 (예: 20MB)
방금 얘기한 변숫값들을 기본 설정을 제공하지만, 필요에 따라 사용자 환경에 맞게 커스터마이징 할 수 있게 했다.
설정을 변경하려면 프로젝트 루트에 logger-config.yaml 파일을 생성하고 원하는 값을 세팅하고,
실행 시 자동으로 Override 되어서 사용자가 원하는 환경설정을 할 수 있게끔 구성했다.
설정 방법
blanc-logger는 NestJS 프로젝트에 아주 간단하게 통합할 수 있도록 설계되었다.
npm install blanc-logger
우선 install을 하자.
1. NestJS 전역 Logger 적용 (main.ts)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { blancLogger, BlancLoggerMiddleware } from 'blanc-logger';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: blancLogger, // 전역 Logger 적용
});
// 미들웨어를 적용하여 요청 시 모듈명 등 컨텍스트 정보를 추가
app.use(new BlancLoggerMiddleware().use);
await app.listen(3000);
}
bootstrap();
Blanc Logger를 전역 로거로 적용하면, 애플리케이션 전체에서 동일한 로깅 설정을 사용할 수 있다.
이 미들웨어는 AppModule에서 DI(Dependency Injection) 방식으로 등록해도 무방하다.
2. TypeORM Logger 적용 (AppModule)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmBlancLogger } from 'blanc-logger';
@Module({
imports: [
TypeOrmModule.forRoot({
// ... DB 설정
logging: true,
logger: new TypeOrmBlancLogger(), // TypeORM용 로거 사용
}),
// 다른 모듈들...
],
})
export class AppModule {}
TypeORM 설정에서 Blanc Logger의 전용 로거를 사용하여,
SQL 쿼리 및 데이터베이스 관련 로그를 효과적으로 기록할 수 있다.
이러면 사용을 위한 설정은 모두 끝났다 !
기존 Logger 대체 방법
이제 사용법을 살펴보자.
사용법도 정말 간단하다.
Blanc Logger를 사용하여 기존의 NestJS 내장 로거를 대체할 수 있다.
1. 기존 코드
// 기존 NestJS 내장 로거 사용:
this.logger.error('Error message', error.stack);
this.logger.log('Log message');
2. Blanc Logger 사용
// 에러 발생 시, 스택 정보와 함께 기록
blancLogger.error('Error message', { moduleName: 'ModuleName', stack: error.stack });
blancLogger.log('Log message', { moduleName: 'ModuleName' });
blancLogger.warn('Warn message', { moduleName: 'ModuleName' });
blancLogger.verbose('Verbose message', { moduleName: 'ModuleName' });
blancLogger.debug('Debug message', { moduleName: 'ModuleName' });
이제 기존 Logger를 대체하여 사용할 수 있다.
사용 예시
이렇게 적용한 Logger에 적용 예시를 살펴보자.
1. Logging Interceptor 구현
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { blancLogger } from 'blanc-logger';
import * as chalk from 'chalk';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const startTime = Date.now();
const req = context.switchToHttp().getRequest();
const decodedUrl = decodeURIComponent(req.url);
const moduleName = (req as any).moduleName || 'UnknownModule';
return next.handle().pipe(
tap(() => {
const delay = Date.now() - startTime;
const delayStr =
delay > 100 ? chalk.bold.red(`${delay}ms 🚨`) : chalk.magenta(`${delay}ms`);
const message = `Request processed: ${chalk.yellow(req.method)} ${chalk.green(
decodedUrl,
)} ${delayStr}`;
blancLogger.log(message, moduleName);
}),
);
}
}
전역 인터셉터로 적용하려면 AppModule에 아래와 같이 등록하면 된다.
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './commons/interceptors/logging.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor, // 전역 인터셉터로 등록
},
],
})
export class AppModule {}
HTTP 요청 처리 시간을 측정하여 로그로 기록하는 인터셉터 예시다.
아까 적용한 BlancLoggerMiddleware를 통해 설정된 모듈명 정보가 자동으로 포함된다.

이런 식으로 로그가 출력된다.
(응답 시간 초과 시 (예: 100ms)강조)
2. Global Exception Filter 구현
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
InternalServerErrorException,
} from '@nestjs/common';
import { blancLogger } from 'blanc-logger';
import { Request, Response } from 'express';
interface ExceptionResponse {
status: number;
message: string;
stack?: string;
}
/** 예외 객체를 처리하여 상태, 메시지, 스택을 반환하는 함수 */
const handleException = (exception: unknown, _request: Request): ExceptionResponse => {
if (exception instanceof HttpException) {
const status = exception.getStatus();
const res = exception.getResponse();
const message =
typeof res === 'object' && res !== null
? (res as any).message ?? exception.message
: exception.message;
return {
status,
message: `HTTP Exception: ${message}`,
stack: exception instanceof Error ? exception.stack : '',
};
}
if (exception instanceof Error) {
const status = new InternalServerErrorException().getStatus();
return {
status,
message: `Unhandled exception: ${exception.message}`,
stack: exception.stack,
};
}
return { status: 500, message: 'Unknown error' };
};
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const moduleName = (request as any)?.moduleName ?? 'Global';
const { status, message, stack } = handleException(exception, request);
blancLogger.error(message, {
moduleName,
path: request.url,
stack,
});
response.status(status).json({
statusCode: status,
status: message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
전역 필터로 적용하려면 AppModule에 아래와 같이 등록하면 된다.
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { GlobalExceptionFilter } from './commons/filters/global-exception.filter';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter, // 전역 필터로 등록
},
],
})
export class AppModule {}
이렇게 다 적용을 했으면 예외를 던져보자.
throw new ConflictException('DUPLICATION');

// error-2025-03-05.log
{"level":"error","message":"HTTP Exception: DUPLICATION","stack":[{"moduleName":"user","path":"/api/user/profile","stack":"ConflictException: DUPLICATION\n at UserService.getProfile (/path/to/src/user/user.service.ts:60:13)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)"}],"timestamp":"2025-03-05 02:29:51"}
이런 식으로 에러 로그 출력과 함께 로그 파일에 정상적으로 저장된다.
npm 배포
처음엔 npm배포 후 GitHub 패키지까지 둘 다 배포를 하였으나
이후 package.json 파일에 아래 설정을 적용하여 동시에 배포하려 하였다.
"publishConfig": {
"registry": "https://npm.pkg.github.com/"
}
이를 적용하였을 때, 두 레지스트리 간의 설정 충돌과 배포 관련 이슈가 발생하였으며,
동시에 관리하는 것이 비효율적인 것으로 확인되었다.
GitHub 패키지는 주로 프라이빗 패키지 관리 용도로 활용하고
CI/CD 환경에서 npm 패키지가 더 쉽게 배포 및 설치 가능하다.
따라서 최종적으로 불필요한 복잡성을 줄이기 위해 npm 배포에 집중하기로 결정하였다.
이를 위해 package.json에 다음과 같은 설정이 필요하였다.
1. 빌드 결과물만 포함하도록 하였다.
"files": ["dist"]
2. ES 모듈, CommonJS, 타입스크립트 타입 파일을 각각 지원하도록 구성하였다.
"exports": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
3. 검색 최적화를 위한 키워드 추가
"keywords": [
"winston",
"nestjs",
"logger",
"typeorm",
],
그리고 tsconfig.json 파일을 통해 엄격한 컴파일 환경 (예: CommonJS 모듈, ES6 타겟, 데코레이터 지원 등)을 설정하였다.
npm에 배포된 blanc-logger는 아래 링크에서 확인할 수 있다.
NestJS를 사용하시는 분들이라면 한번 사용해 보시길 !
https://www.npmjs.com/package/blanc-logger
blanc-logger
Advanced Winston logger for NestJS & TypeORM with structured logging (npm package).. Latest version: 1.0.9, last published: a day ago. Start using blanc-logger in your project by running `npm i blanc-logger`. There are no other projects in the npm registry
www.npmjs.com
https://github.com/yooseungmo/blanc-logger
GitHub - yooseungmo/blanc-logger: Advanced Winston logger for NestJS & TypeORM with structured logging (npm package).
Advanced Winston logger for NestJS & TypeORM with structured logging (npm package). - yooseungmo/blanc-logger
github.com
'Web > NestJS' 카테고리의 다른 글
| [NestJS] 비동기의 핵심, 이벤트 루프 + libuv 파헤치기 (2편) (3) | 2025.05.26 |
|---|---|
| [NestJS] @automock/jest로 번거로운 Unit Test Mocking 자동화하기 (0) | 2025.02.02 |
| [Nest.JS] 구글, 네이버, 카카오 소셜로그인 구현 - 2 (1) | 2023.11.04 |
| [Nest.JS] 구글, 네이버, 카카오 소셜로그인 구현 - 1 (1) | 2023.10.16 |
| [Nest.JS] Nest.JS에서 cache-manager를 활용한 캐싱 방법 (0) | 2023.09.17 |