table 구상

user-info

uid : unsigned int, auto_increment, primary_key, comment: user id

company : unsigned int, not null, comment: 법인

department : varchar(100), not null , comment: 부서

groupware-id : varchar(100), not null , comment: 그룹웨어 id

join-date : timestamp, comment: 가입

birthday : timestamp, comment: 생일

grade: unsigned int, not null, default : 1, comment : 등급

state: unsigned int, not null, default : 1, comment : 활동 여부 (활동 중, 휴면)

register_time : timestamp

update_time : timestamp

* index : idx_uid (uid), idx_birthday (birthday)

company

idx : unsigned int, auto_increment, primary_key, comment: 법인

name : varchar(20), not null , comment: 법인 이름

user-point

uid : unsigned int, auto_increment, primary_key, comment: user id

point : unsigned int, default 0, comment: 포인트

register_time : timestamp

update_time : timestamp

 

SQL문

-- ===========================
-- 1) company 테이블
-- ===========================
CREATE TABLE daracbang.company (
    idx            INT UNSIGNED    NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '법인',
    name           VARCHAR(20)     NOT NULL                COMMENT '법인 이름'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- ===========================
-- 2) user_info 테이블
-- ===========================
CREATE TABLE daracbang.user_info (
    uid            INT UNSIGNED    NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'user id',
    company        INT UNSIGNED    NOT NULL                COMMENT '법인 idx (참조는 애플리케이션 레벨에서)',
    department     VARCHAR(100)    NOT NULL                COMMENT '부서',
    groupware_id   VARCHAR(100)    NOT NULL                COMMENT '그룹웨어 id',
    join_date      TIMESTAMP       NULL                    COMMENT '가입일',
    birthday       TIMESTAMP       NULL                    COMMENT '생일',
    grade          INT UNSIGNED    NOT NULL DEFAULT 1      COMMENT '등급',
    state          INT UNSIGNED    NOT NULL DEFAULT 1      COMMENT '활동 여부 (활동 중=1, 휴면=0 등)',
    register_time  TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각',
    update_time    TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각',
    INDEX idx_birthday (birthday),
    INDEX idx_company  (company)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- ===========================
-- 3) user_point 테이블
-- ===========================
CREATE TABLE daracbang.user_point (
    uid            INT UNSIGNED    NOT NULL                COMMENT 'user_info.uid (참조는 애플리케이션 레벨에서)',
    point          INT UNSIGNED    NOT NULL DEFAULT 0      COMMENT '포인트',
    register_time  TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각',
    update_time    TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각',
    PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

 

Entity

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('company')
export class CompanyEntity {
    @PrimaryGeneratedColumn({ type: 'int', unsigned: true, name: 'idx' })
    idx: number;

    @Column({ type: 'varchar', length: 20, nullable: false })
    name: string;
}
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';

@Entity('user_info')
@Index('idx_birthday', ['birthday'])
@Index('idx_company', ['company'])
export class UserInfoEntity {
    @PrimaryGeneratedColumn({ type: 'int', unsigned: true, name: 'uid' })
    uid: number;

    @Column({ type: 'int', unsigned: true, name: 'company' })
    company: number;  // 법인 idx (참조는 서비스 로직에서 처리)

    @Column({ type: 'varchar', length: 100, nullable: false })
    department: string;

    @Column({ type: 'varchar', length: 100, name: 'groupware_id', nullable: false })
    groupwareId: string;

    @Column({ type: 'timestamp', name: 'join_date', nullable: true })
    joinDate: Date;

    @Column({ type: 'timestamp', name: 'birthday', nullable: true })
    birthday: Date;

    @Column({ type: 'int', unsigned: true, default: 1 })
    grade: number;

    @Column({ type: 'int', unsigned: true, default: 1 })
    state: number;

    @Column({
        type: 'timestamp',
        name: 'register_time',
        default: () => 'CURRENT_TIMESTAMP',
    })
    registerTime: Date;

    @Column({
        type: 'timestamp',
        name: 'update_time',
        default: () => 'CURRENT_TIMESTAMP',
        onUpdate: 'CURRENT_TIMESTAMP',
    })
    updateTime: Date;
}
import { Entity, PrimaryColumn, Column } from 'typeorm';

@Entity('user_point')
export class UserPointEntity {
    @PrimaryColumn({ type: 'int', unsigned: true, name: 'uid' })
    uid: number;  // user_info.uid 값을 수동으로 넣어야 함

    @Column({ type: 'int', unsigned: true, default: 0 })
    point: number;

    @Column({
        type: 'timestamp',
        name: 'register_time',
        default: () => 'CURRENT_TIMESTAMP',
    })
    registerTime: Date;

    @Column({
        type: 'timestamp',
        name: 'update_time',
        default: () => 'CURRENT_TIMESTAMP',
        onUpdate: 'CURRENT_TIMESTAMP',
    })
    updateTime: Date;
}

** erd는 귀찮아서 생략해본다.... 시간이 되면 그려야

errorcode 정의하기

  • core / common 에 ErrorCode 정의
  • enum 으로 정의함

response 처리하기

현 상황 파악

  • return 값이 걍 달랑 저렇게 옴
  • response의 형태가 있었으면 좋겠다고 생각함

response 형태 만들기

  • post일 때와 get 일 때 다르게 하기 - 가 필요한가 고민해보자
  • status : ErrorCode // 통신 결과 상태 코드 data : any // 통신 결과 data message : string // 에러 시 메세지
    import { IsNumber, IsOptional } from 'class-validator';
    import { ErrorCode } from '@/core';
    
    export class ResponseApi {
      constructor(data?: any) {
        this.code = ErrorCode.SUCCESS;
        this.message = '';
        this.data = data;
      }
      @IsNumber()
      code: number;
      @IsOptional()
      message: string;
      @IsOptional()
      data: any;
    }
  • 형태 고민
    • data 넣기 → key 형태가 camel로 되게 하기
    • code 넣기 - 통신 성공 시 ErrorCode.SUCCESS 가게 함
    • message 넣기 - 실패 시 메세지 넣고 성공 시 메세지 빼기

interceptor 만들기

  • response를 주기 전에 interceptor로 가로채서 data 형태 만들어주기
  • NestInterceptor - https://docs.nestjs.com/interceptors
    • next.handle()는  RxJS Observable 을 반환함.
    • 대표적으로 사용되는 연산자는 map, tap, catchError, finalize
    • map() : Observable에서 방출되는 값을 변환(transform)해서 새로운 값을 내보냄.
    • tap() : 스트림의 값을 변경하지 않고, 사이드 이펙트(side effect)만 수행
    • catchError() : Observable 내부에서 발생한 에러를 잡아서 처리할 수 있게 해줌
    • finalize() : 스트림이 완료(complete) 되거나 에러(error) 로 종료될 때 무조건 한 번 호출되는 콜백을 지정함
    • switchMap / mergeMap / concatMap : 새로운 내부 Observable을 생성하고, 기존 스트림을 그 안으로 전환(switchMap) 하거나 병합(mergeMap), 또는 순차 처리(concatMap) 할 때 사용
import {
  CallHandler,
  ExecutionContext,
  HttpStatus,
  Injectable,
  Logger,
  NestInterceptor,
} from '@nestjs/common';
import { Response } from 'express';
import { map, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { ResponseApi } from '@/core/response/api/response.api';

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  constructor(private readonly logger: Logger) {}
  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {
    const request: Request = context.switchToHttp().getRequest();
    const response: Response = context.switchToHttp().getResponse();

    const { method } = request;

    return next.handle().pipe(
      map((data) => {
        if (method === 'POST') {
          // POST 기본은 201 이지만 200 으로 변경.
          response.status(HttpStatus.OK);
          return new ResponseApi(data).response();
        } else {
          return data;
        }
      }),
      tap((data) => {}),
    );
  }
}
  • 즉 스트림에 데이터를 포함시키고, 스트림이 종료된 후 할 일을 지정하면 된다.!
    • 스트림에 데이터를 포함할 때 data 형태를 ResponseApi 형태로 넣어주기
    • 종료된 후 뭘 할지 고민해보기.

interceptor 등록하기

  • Appmodule 의 provider에 다름과 같은 형태로 등록해준다.
providers: [
    {
      provide: APP_INTERCEPTOR,
      scope: Scope.REQUEST, // 요청마다 생성되고 요청이 종료되면 삭제됨.
      useClass: ResponseInterceptor,
    },
    AppService,
  ],

post 쏴보기

request 보기

  • request 가 들어오는 것을 확인하고 싶음.
  • request도 interceptor로 만들어서 log를 찍어볼 생각.

access-log-interceptor 만들기

  • request 가 들어오면 들어온 시간 보여주기
  • request 에 들어온 parameter, payload 보여주기
  • response 보여주기
  • request가 느리다 싶으면 log 쌓기(는 고민해봄)
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  Logger,
  NestInterceptor,
} from '@nestjs/common';
import { Request } from 'express';
import { catchError, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { inspect } from 'util';
import moment from 'moment/moment';
import { ResponseApi } from '@/core/response';

@Injectable()
export class AccessLogInterceptor implements NestInterceptor {
  constructor(private readonly logger: Logger) {}
  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {
    const request: Request = context.switchToHttp().getRequest();

    const now = moment().format('YYYY-MM-DD HH:mm:ss');
    const { ip, method, originalUrl } = request;
    const userAgent = request.get('user-agent') || '';
    const accessLogData = {
      date: now,
      method: method,
      url: originalUrl,
      ip: ip,
      agent: userAgent,
      req: request.body,
      res: {},
    } as AccessLogApi;

    return next.handle().pipe(
      tap(
        (response) => {
          accessLogData.res = new ResponseApi(response).response();
          const accessLog = `[accessLog:Success] ${inspect(accessLogData)}`;
          this.logger.debug(accessLog);
        },
        (error) => {
          accessLogData.res = error;
          const accessLog = `[accessLog:Error] ${inspect(accessLogData)}`;
          this.logger.debug(accessLog);
        },
      ),
    );
  }
}

interceptor 등록하기

  • Appmodule 의 provider에 다름과 같은 형태로 등록해준다.
providers: [
...
    {
      provide: APP_INTERCEPTOR,
      scope: Scope.REQUEST, // 요청마다 생성되고 요청이 종료되면 삭제됨.
      useClass: AccessLogInterceptor,
    },
...
  ],

request 쏴보기

exception 정의

하기

exception 종류 및 필터가 하는 일 정해보기

BasicException 정의하기

  • 기본 예외
  • 예외들의 형태를 정의해본다.
import { HttpException, HttpStatus } from '@nestjs/common';

export class BasicException extends HttpException {
    name: string;
    code: number;
    isLog: boolean;

    constructor(code: number, message: string, isNotice = true, isLog = true) {
        super(message, HttpStatus.INTERNAL_SERVER_ERROR);
        this.code = code;
        this.name = 'Basic';
        this.isLog = isLog;
    }
}

CoreException 정의하기

  • core module 에서 일어나는 예외에 작동
  • 필터는 에러코드와 에러메세지 출력해서 결과 뿌려줌

ApiException 정의하기

  • api들에서 일어나는 예외에 작동
  • 필터는 에러코드와 에러메세지 출력해서 결과 뿌려줌
  • post와 get으로 나누어 get일때는 에러페이지로 redirect

exception 일으키기

고민할 것

access-log-interceptor

  • request slow 로그 쌓기
  • request 실패했을 때 로그

response-interceprtor

  • 종료된 후 뭘 할지 고민해보기
  • POST 이더라도 ResponseApi 형태로 보여주고 싶지 않은 경우
  • Get 이더라도 ResponseApi 형태로 보여주고 싶은 경우

exception

  • post, get 말고 다른 것으로 response나 exception filter 결과를 분기하고 싶다.

validation 설치

  • 설치 후 main.ts에 전역pip인 validator 사용한다고 server에 등록해줌
iimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CONFIG } from '@/core/config';
import { winstonConfig } from '../winstonconfig';
import { WinstonModule } from 'nest-winston';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: WinstonModule.createLogger(winstonConfig),
  });

  // validator pipe register
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(CONFIG.APP.PORT);
}
bootstrap();

nest start!

  • 그간 삽질을 좀 했는데…. 그 이유가 빌드 위치나 nest를 띄웠는데 next의 페이지가 빌드가 안되는 등의 문제가 많았다. ㅜㅜㅜㅜㅜㅜㅜ tsconfig와 next.config.js, tsconfig.server.json의 적절한.. 설정이 중요하다.
  • 아래 것을 package.json 의 scripts 부분에 붙여보자
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "yarn build:next && yarn build:nest",
    "build:next": "next build --no-lint",
    "build:nest": "cross-env NODE_ENV=production nest build nest build --path ./tsconfig.server.json",
    "start": "node ./dist/server/main.js",
    "start:next": "next dev",
    "start:dev": "nest start cross-env NODE_ENV=development --path ./tsconfig.server.json --watch",
    "start:debug": "nest start --path ./tsconfig.server.json --debug --watch",
    "start:prod": "node dist/main"
  },
> npm run start:dev

헤보면

잘 뜬다

MySQL 과 typeOrm 설정하기

typeorm 설정하기

  • TypeOrmModuleOptions형태에 맞게 DB 정보를 꾸리자
    DB: {
        name: 'default',
        type: 'mysql',
    		host: 'localhost', // 112.175.87.252 ㅊㅇㄷㅋ4ㄱ5ㅇㄹ
        port: 33317,
        username: 'root',
        password: '****',
        database: '********',
        entities: [join(__dirname, '/**/*.entity{.ts,.js}')],
        synchronize: false,
        logging: ['query', 'log', 'info', 'warn', 'error'], // LogLevel 형태로
          // logger 옵션: 콘솔에 찍히는 방식 선택 ('advanced-console', 'simple-console', 'file', 또는 커스텀 인스턴스)
        logger: 'advanced-console',
      }
  • forRoot로 넣게 되면 static 하게 동기식으로 구현된다.
  • 만약 비동기로 구현하고 싶다면
    • logger이 커스텀 일 경우 forRootAsync로 등록해야 하는데, 등록 시 useFactory로 TypeOrmModuleOptions 를 만들어 넣어줘야 한다.
    const dbOption = {
      name: 'default',
      type: 'mysql',
    	host: 'localhost', // 112.175.87.252 ㅊㅇㄷㅋ4ㄱ5ㅇㄹ
      port: 33317,
      username: 'root',
      password: '****',
      database: '********',
      entities: [join(__dirname, '/**/*.entity{.ts,.js}')],
      synchronize: false,
      logging: ['query', 'log', 'info', 'warn', 'error'], // LogLevel 형태로
      maxQueryExecutionTime: 10, // 0 하면 OFF 이고 최소값이 1이라 1로 고정한다.
    } as TypeOrmModuleOptions;
    
    @Module({
      imports: [
        TypeOrmModule.forRootAsync({
          useFactory: async (
          ): Promise<TypeOrmModuleOptions> => ({
            ...dbOption,
          }),
        }),
      ],
      // providers: [],
      exports: [TypeOrmModule],
    })
    export class DbModule {}
    
  • TypeOrm에 entities들을 등록해야 사용할 수 있는데
    TypeOrmModule.forFeature(EntitiesList);

    • 이렇게 넣어줄 수 있다.
    • entity가 선언되는 모듈마다 넣어줄 수도 있고, 나처럼 한 방에 관리할 수도 있다.
     imports: [
        TypeOrmModule.forRootAsync({
         ...
        }),
        TypeOrmModule.forFeature(CONFIG.DB_ENTITIES),
      ],
    

custom logger 모듈 넣기

  • custom Logger를 사용하기 위해 forRootAsync를 사용하는 것이므로 custom logger를 만들어 보자.
  • 위에서 만든 winston LoggerModule도 사용한다.
  • DB에서 사용할 커스텀 로거라서 service가 필요한데, DB모듈이 뜨기 전에 서비스가 injection 되어야 한다. 따라서 이 서비스는 DB모듈과 따로 관리 되어야 한다.

DbLoggerModule을 만든다.

import { Module } from '@nestjs/common';
import { DbLogger } from '@/core';
import { LoggerModule } from '@/core/logger/logger.module';

@Module({
  imports: [LoggerModule], // appModule에서 등록했어도 여기서도 써줘야 함
  controllers: [],
  providers: [DbLogger],
  exports: [DbLogger],
})
export class DbLoggerModule {}

사용할 DbLogger 서비스를 만들어보자.

import { Injectable, Logger } from '@nestjs/common';
import { Logger as TypeOrmLoggerInterface, QueryRunner } from 'typeorm';
import * as util from 'util';

@Injectable()
export class DbLogger implements TypeOrmLoggerInterface {
  constructor(private readonly logger: Logger) {}
  // 기타 일반 로그 (warn, info 등)
  log(
    level: 'log' | 'info' | 'warn' | 'error',
    message: any,
    queryRunner?: QueryRunner,
  ) {
    switch (level) {
      case 'log': {
        this.logger.log(`[DbLog] ${message}`);
        break;
      }
      case 'info': {
        this.logger.log(`[DbLog] ${message}`);
        break;
      }
      case 'warn': {
        this.logger.warn(`[DbLog] ${message}`);
        break;
      }
      case 'error': {
        this.logger.error(`[DbLog] ${message}`);
        break;
      }
    }
  }

  // 쿼리가 실행될 때마다 로그
  logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) {
    this.logger.log(
      `[logQuery] query:${query} / parameters:${util.inspect(parameters)}`,
    );
  }

  // slowQuery: 지정된 시간이 넘어간 쿼리
  logQuerySlow(
    time: number,
    query: string,
    parameters?: any[],
    queryRunner?: QueryRunner,
  ) {
    this.logger.warn(
      `[logQuerySlow] time:${time} query:${query} / parameters:${util.inspect(
        parameters,
      )}`,
    );
  }

  // 스키마 build 관련 로그
  logSchemaBuild(message: string, queryRunner?: QueryRunner) {
    this.logger.error(`[logSchemaBuild] ${message}`);
  }

  // 에러 발생 시 로그
  logMigration(message: string, queryRunner?: QueryRunner) {
    this.logger.error(`[logMigration] ${message}`);
  }

  // 쿼리 실행 중 에러 발생 시
  logQueryError(
    error: string | Error,
    query: string,
    parameters?: any[],
    queryRunner?: QueryRunner,
  ) {
    const errMsg =
      error instanceof Error ? error.stack || error.message : error;
    this.logger.error(
      `Query Failed: ${query} -- Parameters: ${JSON.stringify(parameters)} -- Error: ${errMsg}`,
    );
  }
  //
  // // 스키마 동기화 중 에러 발생 시
  // logSyncError(error: Error, queryRunner?: QueryRunner) {
  //   this.logger.error(`[logSyncError] ${error.stack || error.message}`);
  // }
}

  • 각 로그 레벨에서 저 메서드들이 각자 동작한다. interface형태이므로 Di 되어 사용될 것으로 보인다.
  • TypeOrmLoggerInterface ← 요 형태를 선언하면 자동으로 interface에 정의된 메서드들을 implement 해줘야 하므로 직접 커스터마이징 해보자.

DbLoggerModule 삽입하기

  • DbModule에 삽입하여 커스텀한 logger 를 써보자.
import { Module } from '@nestjs/common';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { CONFIG } from '@/core/config';
import { DbLogger } from '@/core/db/db.logger';
import { DbLoggerModule } from '@/core/db/db-logger.module';
import { LoggerModule } from '@/core/logger/logger.module';

const dbOption = {
  ...CONFIG.DB,
  entities: CONFIG.DB_ENTITIES,
  maxQueryExecutionTime: 10, // 0 하면 OFF 이고 최소값이 1이라 1로 고정한다.
} as TypeOrmModuleOptions;

@Module({
  imports: [
    DbLoggerModule,
    LoggerModule,
    TypeOrmModule.forRootAsync({
      imports: [DbLoggerModule],
      inject: [DbLogger],
      useFactory: async (
        dbLogger: DbLogger,
      ): Promise<TypeOrmModuleOptions> => ({
        ...dbOption,
        logger: dbLogger,
      }),
    }),
    TypeOrmModule.forFeature(CONFIG.DB_ENTITIES),
  ],
  providers: [DbLogger],
  exports: [TypeOrmModule],
})
export class DbModule {}
  • 주의할 점 - 지키지 않으면 주입 문제로 에러가 발생하니 주의하자
    • imports 안에 DBLoggerModule과 LoggerModule이 전부 import 되어 있다는 점
    • TypeOrmModule 등록할 때 imports에 DbLoggerModule을 삽입 후 inject에 DbLogger 서비스를 주입한다는 것.
    • useFactory로 TypeOrmModuleOptions 을 만들 때 매개변수로 dbLogger 서비스를 넣어주고 밑에 logger에 선언하는 것

appModule에 주입하기

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import Next from 'next';
import { RenderModule } from 'nest-next';
import { DbModule } from '@/core/db/db-module';
import { DbLoggerModule } from '@/core/db/db-logger.module';
import { LoggerModule } from '@/core/logger/logger.module';

@Module({
  imports: [
    RenderModule.forRootAsync(
      Next({
        dev: process.env.NODE_ENV !== 'production',
      }),
      { viewsDir: null },
    ),
    LoggerModule,
    DbLoggerModule,
    DbModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • 만든 모든 모듈을 넣어준다!

winston 설정하기

winston을 사용하는 이유

  • 다양한 전송(transport)을 지원
    • Console, File, HTTP, Stream 등 여러 형태의 transport를 설정해둔 뒤, 각기 다른 로그 레벨에 맞춰 로그를 분리해서 출력할 수 있습니다.
  • 로그 레벨(Level) 관리
    • error, warn, info, http, verbose, debug, silly 등 여러 레벨을 지원하며, 개발·운영 환경에 따라 출력할 레벨을 조절할 수 있습니다.
  • 포맷(format) 커스터마이징
    • Timestamp 추가, JSON 형식, 컬러링, printf 등을 이용해 로그 메시지 형식을 자유롭게 정의할 수 있습니다.
  • 플러그인·확장성
    • 커스텀 transport를 직접 구현하거나, 커뮤니티에서 제공하는 여러 가지 확장 모듈(nested-logger, winston-daily-rotate-file 등)을 연동할 수 있습니다.

winstonconfig.ts 설정하기

const winstonConfig = {
  format: logFormat, // FormatWrap 
  level: LOG_CONFIG.DEFAULT_LEVEL, // LogLevel
  transports: transportList, // transport 하는 것들
} as WinstonModuleOptions;
  • 요거에 맞게 지정해주면 된다.
    • ConsoleTransport: 개발 중에 터미널 창에서 바로 로그를 확인할 때 유용합니다. colorize()를 쓰면 info, warn, error 등 레벨별 색상 표시가 가능합니다.
    • FileTransport (error.log): 에러 레벨 로그만 별도로 logs/error.log에 기록하게 해 두면, 운영 환경에서 에러만 빠르게 확인할 수 있습니다.
    • DailyRotateFile: logs/application-2025-06-04.log처럼 날짜별로 로그 파일을 구분해서 보관합니다. 오래된 파일 자동 삭제, 압축(zippedArchive) 등 설정이 가능합니다.
import {
  utilities as nestWinstonModuleUtilities,
  WinstonModuleOptions,
} from 'nest-winston';
import { CONFIG } from '@/core';
import { format, transports } from 'winston';
import * as Transport from 'winston-transport';
import DailyRotateFile from 'winston-daily-rotate-file';


let LOG_CONFIG = CONFIG.LOG;

const logFormat = format.combine(
  format.timestamp({
    format: 'YYYY-MM-DD HH:mm:ss',
  }),
  format.ms(),
  format.errors({ stack: true }),
  nestWinstonModuleUtilities.format.nestLike(LOG_CONFIG.APP_NAME, {
    prettyPrint: true,
  }),
);

const transportList: Transport[] = [
  new transports.Console({
    level: LOG_CONFIG.CONSOLE.LEVEL,
    format: logFormat,
  }),
];

if (LOG_CONFIG.FILE.ACTIVE) {
  transportList.push(
    new DailyRotateFile({
      level: 'error',
      datePattern: 'YYYYMMDD',
      dirname: LOG_CONFIG.FILE.DIR,
      filename: 'error.%DATE%.log',
      maxSize: `10m`, //파일의 최대 크기
      maxFiles: `30d`, //보관일수
      format: logFormat,
    }),
    new DailyRotateFile({
      level: LOG_CONFIG.FILE.LEVEL,
      datePattern: 'YYYYMMDD',
      dirname: LOG_CONFIG.FILE.DIR,
      filename: 'app.%DATE%.log',
      maxSize: `10m`, //파일의 최대 크기
      maxFiles: `30d`, //보관일수
      format: logFormat,
    }),
  );
}

const winstonConfig = {
  format: logFormat,
  level: LOG_CONFIG.DEFAULT_LEVEL,
  transports: transportList,
} as WinstonModuleOptions;

export { winstonConfig };

앱 모듈에 등록

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import Next from 'next';
import { RenderModule } from 'nest-next';
import { LoggerModule } from '@/core/logger/logger.module';

@Module({
  imports: [
    RenderModule.forRootAsync(
      Next({
        dev: process.env.NODE_ENV !== 'production',
      }),
      { viewsDir: null },
    ), // 먼저 등록한 렌더 모듈
    LoggerModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

main.ts 에 선언하기

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CONFIG } from '@/core/config';
import {winstonConfig} from "../winstonconfig";
import {WinstonModule} from "nest-winston";

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: WinstonModule.createLogger(winstonConfig), // 앱을 띄울 때 이거 쓴다!
  });
  await app.listen(CONFIG.APP.PORT);
}
bootstrap();

모듈화 기준

  • 각 기능들을 독립적으로 수정할 수 있도록 각 기능 별 모듈을 만들어 appModule에 등록하는 방식으로 작성
  • 서비스나 컨트롤러는 따로 관리
  • 코어한 기능의 서비스들만 독립모듈로 관리할 예정

renderModule 만들기

  • SSR을 구현하고 싶으므로 사용!
  • nestjs에서 next앱을 띄운다.

RenderModule

  • next.js를 nest.js 애플리케이션에 통합하는 nestjs 모듈을 제공하며, nestjs 컨트롤러를 통해 next.js 페이지를 렌더링하고 페이지에 초기 속성을 제공
  • @Render() 데코레이나 response.render() 을 호출 할 때 작동

RenderModule과 Appmodule

  • nestjs에서 next 앱을 실행하기 위해 RenderModule을 등록해야 함
  • main.ts에서 사용하기 전에 각 모듈을 사용할 수 있게 앱에 등록을 해야 함
  • app.module.ts에 등록하면 main.ts에서 nest app을 만들 때 해당 모듈을 가지고 만듦
async function bootstrap() {

...

 const server = await NestFactory.create<NestExpressApplication>(AppModule);

...
}

RenderModule 등록하기

  • RenderModule은 모듈을 등록해두면 되는 것이라 따로 커스텀 할 것이 없음 → AppModule에 직접적으로 등록하여 사용할 것
  • AppModule
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import Next from 'next';
import { RenderModule } from 'nest-next';

@Module({
  imports: [
    RenderModule.forRootAsync(
      Next({
        dev: process.env.NODE_ENV !== 'production',
      }),
      { viewsDir: null },
    ),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • 이렇게 등록하면 basPath 없다고 에러가 뜨는데.. 확인해 보니 next와 nest-next 모듈 버전이 문제였다
    • next 13 버전 까지가 nest-next를 안정적으로 지원한다고..
    • 패키지 의존성 때문에 아래 것들을 다 바꿔야 한다….. 화난다.
    • 이거 문제가 많네;;;
    "nest-next": "^9.6.0",
    "next": "12.1.5",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    

main.ts에 설정하기

  • main.ts가 nest 앱을 만들고, 앱을 구동하므로 여기서 nextjs를 연결해 줘야 함
  • Next 앱을 띄우고 SSR 준비
  • RenderService에서 error를 handling 할 수 있으므로 여기서 등록
async function bootstrap() {

  const server = await NestFactory.create<NestExpressApplication>(AppModule);

  await server.listen(3000);
    
...

}

cross env와 dotenv, 그리고 config 파일

  • 위 모듈들은 잘 안 쓰는 것 같아서…

DeepPartial 을 통한 config 파일 merge

+ Recent posts