반응형
Waht Subscription?

 

GraphQL 구독은 서버의 실시간 메시지를 듣기로 선택한 클라이언트들에게 서버의 데이터를 푸시하는 방법입니다.

 

WebSocket이라고 이해하면 된다 !

 

SubScription 설정방법

 

노마드 우버이츠 클론코딩을보며 공부하였으나,

버전이 올라감에따라 Subscription설정방법이 바뀌어서

 

기록용으로 서술합니다 !

 

// app.module.ts


imports:[
...

    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      installSubscriptionHandlers: true, //구독 활성화
      subscriptions: { //Nest10버전부터는 이와같이 설정해주어야 구독이 활성화된다 !
        'subscriptions-transport-ws': {
          onConnect: (connectionParams, WebSocket, context) => {
            // header정보 는 ConnectionParams로 들어옴
            // console.log('context', context); //Context정보
            // console.log('WebSocket', WebSocket); //Websockect정보
            // console.log('connectionParams', connectionParams);
            return { message:"Hello Connection!!!!" };
          },
        },
      },
      autoSchemaFile: true, //스키마를 메모리에 저장,
      context: ({ req, connection }) => {
        if (req) {
          return req;
        }
        if (connection) {
         //구버전에서는 구독 활성화 시 Connection으로 Websocket정보가 전달되었으나,
         //Nest10버전 기준으로 undefined된 듯 함.
        }
      },
      formatError: formmatError, //에러 포맷 설정
    }),
    

...
반응형
반응형
Repository 패턴이란,

 

먼저 CustomRepository를 사용하기에앞서,

Repository패턴이 무엇인지 알아보죠.

소프트웨어 디자인 패턴 중 하나로, 데이터베이스와의 상호 작용울 추상화하여
데이터 엑세스 코드를 분리하는데 사용됩니다

 

Repository 패턴의 핵심 아이디어는 아래와 같습니다.

 

1. 데이터 엑세스의 추상화 : Repository 패턴은 데이터 엑세스를 위한 인터페이스를 제공하여, 데이터베이스와의 통신을 추상화 합니다.

이로써 데이터 엑세스 코드가 실제데이터베이스와 직접  결합되자않고 인터페이스를 통해 접근할 수 있습니다.

 

2. 도메인로직과 데이터 엑세스의 분리 : Repository패턴을 사용하면 도메인 로직과 데이터 엑세스 코드를 분리할 수 있습니다.
도메인 로직은 데이터에 대한 비즈니스 규칙과 관련이 있고, Repository는 데이터 엑세스에 대한 구체적인 구현을 담당합니다.

 

3. 유지보수성 향상 : 데이터베이스 구현의 변경이나 데이터 엑세스 로직의 수정이 상대적으로 쉬어집니다.

실제 데이터베이스와의 상호 작용을 추상화하기 때문에 데이터베이스 변경이나 데이터 엑세스 코드 수정에 대한 영향을 최소화 할 수 있습니다.

 

4. 테스트 용이성 : 데이터베이스에 의존하지 않고도 도메인 로직을 테스트하기 쉬워집니다. (모크데이터를 만들어 단위테스트가 용이해짐)

 

 

CustomRepository의 사용이유

 

자주 호출되며 복잡한 요청이 필요할때 CustomRepository를 이용해

TypeORM의 Repository를 확장하여 사용자 정의 메서드를 추가해 사용합니다.

 

한가지 예를 들어 아래 코드는 등록하고자하는 카테고리가 없는 경우 생성해주는 작업입니다.

등록, 수정시에 사용되는 작업이라면 12줄의 코드가 총 2번 사용될텐데,

CustomRepository를 사용해 확장 메소드를 등록해놓으면 간소화할 수 있겠죠

    const categoryName = name.trim().toLowerCase();
    const categorySlug = categoryName.replace(/ /g, '-');
    let category = await this.categories.findOne({
      where: { slug: categorySlug },
    });
    if (!category) {
      category = await this.categories.save(
        this.categories.create({ slug: categorySlug, name: categoryName }),
      );
    }
    return category;

 

 

TypeOrm 버전Up

 

TypeOrm 버전이 0.2.x에서 0.3.x로 올라오면서 많은 변화들이 생겼다.

 

그중 하나가 @EntityRepositody를 사용해서 커스텀 레포지토리를 만들어서 사용할 수 있었다면,

0.3.x버전으로 올라오면서 deprecated되어 더이상 사용할 수 없다.

 

 

0.2 버전의 Custom Repository

 

// cateogory.service.ts
@Injectable()
export class CategoryService {
	constructor(
		@InjectRepository(Category)
		private categoryRepository: CategoryRepository) {}

	createCategory(): Promise<CreateCategoryOutput> {
		return this.categoryRepository.getOrCreateCategory();
	}
}

 

// category.repository.ts
@EntityRepository(Category)
export class CategoryRepository extends Repository<Category> {
	async getOrCreateCategory() {
		const categoryName = name.trim().toLowerCase();
    		const categorySlug = categoryName.replace(/ /g, '-');
    		let category = await this.categories.findOne({
    		  where: { slug: categorySlug },
    		});
    		if (!category) {
    		  category = await this.categories.save(
   		     this.categories.create({ slug: categorySlug, name: categoryName }),
   		   );
  		  }
  		  return category;
	}
}

 

0.2버전의 방식은 @EntityRepository 데코레이터를 Repository에 작성해준 뒤 Service 레이어에서 Repository 의존성을 주입 한 후 사용했습니다.

 

하지만 TypeOrm 버전이 0.3으로 올라감에 따라 @EntityRepository 데코레이터를 사용할 수 없게 변경되어 이에따라 Custom Repository를 사용하기 위해선 직접 데코레이터를 사용한 후 Service 레이어로 의존성을 주입해야 하도록 변경되었습니다.

 

0.3버전의 Custom Repository
/* src/typeorm/custom.repository.decorator.ts */

import { SetMetadata } from '@nestjs/common';

export const TPYEORM_CUSTOM_REPOSITORY = 'TPYEORM_CUSTOM_REPOSITORY';

// eslint-disable-next-line
export function CustomRepository(entity: Function): ClassDecorator {
  return SetMetadata(TPYEORM_CUSTOM_REPOSITORY, entity);
}

 

우선 CustomRepository의 데코레이터를 정의해줍니다. SetMetaData는 메타데이터를 설정해주는 역할을 합니다.
(TYPEORM_CUSTOM_REPOSITORY라는 키의 메타데이터를 설정함)

 

 

/* src/typeorm/typeorm.module.ts */

import { DynamicModule, Provider } from '@nestjs/common';
import { getDataSourceToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { TPYEORM_CUSTOM_REPOSITORY } from './custom.repository.decorator';
/**
 *  @CustomRepository 데코레이터를 사용할경우
 *  CustomRepository에 담겨있는 메타데이터들을 가져온다. 그 후 DataSource 정보를 주입받고
 *  providers 에 추가한다.
 *  해당 모듈을 통해 우리가 만든 데코레이터로 CustomRepository 기능을 사용할 수 있게 된다.
 */
export class TypeOrmCustomModule {
  public static forCustomRepository<T extends new (...args: any[]) => any>(
    repositories: T[],
  ): DynamicModule {
    const providers: Provider[] = [];

    console.log('repositories', repositories);
    for (const repository of repositories) {
      const entity = Reflect.getMetadata(TPYEORM_CUSTOM_REPOSITORY, repository);

      if (!entity) {
        continue;
      }

      providers.push({
        inject: [getDataSourceToken()],
        provide: repository,
        useFactory: (dataSource: DataSource): typeof repository => {
          const baseRepository = dataSource.getRepository<any>(entity);
          return new repository(
            baseRepository.target,
            baseRepository.manager,
            baseRepository.queryRunner,
          );
        },
      });
    }

    return {
      exports: providers,
      module: TypeOrmCustomModule,
      providers,
    };
  }
}

 

그 후 CustomRepository를 사용하기 위해 모듈을 생성줍니다

@CustomRepository 데코레이터를 사용할 경우 CustomRepository에 담겨있는 메타데이터들을 가져옵니다.

그 후 DataSource 정보를 주입받고 providers에 추가합니다.

해당 모듈을 통해 우리가 만든 데코레이터로 CustomRepository기능을 사용할 수 있게 됩니다.

 

/* /src/categories/category.module.ts */

@Module({
  imports: [
    TypeOrmModule.forFeature([Restaurant, Category]),
    TypeOrmCustomModule.forCustomRepository([CategoryRepository]),
  ],
  providers: [CategoryResolver, CategoryService],
})
export class CategoryModule {}

 

CustomRepository를 사용하려면 기존 TypeORM모듈에서 생성한 CustomModule로 바꿔주어야 합니다.

 

이후 Serivide에 의존성 주입후 사용하면 됩니다 !

 

EntityRepository 데코레이터 대신, CustomRepository를 사용하는 부분을 유념하세요 !

// category.repository.ts
@CustomRepository(Category)
export class CategoryRepository extends Repository<Category> {
	async getOrCreateCategory() {
		const categoryName = name.trim().toLowerCase();
    		const categorySlug = categoryName.replace(/ /g, '-');
    		let category = await this.categories.findOne({
    		  where: { slug: categorySlug },
    		});
    		if (!category) {
    		  category = await this.categories.save(
   		     this.categories.create({ slug: categorySlug, name: categoryName }),
   		   );
  		  }
  		  return category;
	}
}

 

 

 

반응형
반응형
에러 핸들링(Formmat)을 하게된 배경.

 

스터디에서 NestJS를 공부하고 있다.

 

에러 포맷팅을 한 케이스

{
  "errors": [
    {
      "message": "[10000] - 로그인정보가 잘못되었습니다."
    }
  ],
  "data": null
}

 

 

에러 포맷팅을 하지 않은 케이스

{
  "errors": [
    {
      "message": "Custom Error",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "me"
      ],
      "extensions": {
        "code": "BAD_REQUEST",
        "stacktrace": [
          "CustomError: Custom Error",
          "    at AuthGuard.canActivate (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/src/auth/auth.guard.ts:20:15)",
          "    at GuardsConsumer.tryActivate (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/@nestjs/core/guards/guards-consumer.js:15:34)",
          "    at canActivateFn (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/@nestjs/core/helpers/external-context-creator.js:155:59)",
          "    at target (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/@nestjs/core/helpers/external-context-creator.js:73:37)",
          "    at Object.me (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/@nestjs/core/helpers/external-proxy.js:9:30)",
          "    at field.resolve (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/@apollo/server/src/utils/schemaInstrumentation.ts:82:22)",
          "    at executeField (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/graphql/execution/execute.js:492:20)",
          "    at executeFields (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/graphql/execution/execute.js:414:22)",
          "    at executeOperation (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/graphql/execution/execute.js:344:14)",
          "    at execute (/Users/maverick/Desktop/source/nuber-eats/maverick/nuber-eats-backend/node_modules/graphql/execution/execute.js:136:20)"
        ],
        "originalError": {
          "ok": false,
          "statusCode": 10000
        }
      }
    }
  ],
  "data": null
}

 

 

에러포맷팅을 하지 않는 경우, GraphQL에서 Exception이 발생한 경우, 클라이언트는 확인할 필요없는 StackTrace같은 정보도 나와서,

보기도 어렵고 클라이언트는 매번 에러작업을 해야하는데 불편함을 겪을 수 있어,

클라이언트도 작업을 편하게 할 수 있도록 포맷팅을 하였다

 

소스작업



/* src/ExceptiomnFormat.ts */

import { GraphQLError } from 'graphql';
import { ErrorDto } from './common/dtos/exception,dto';
import { ErrorCode, ErrorMessageV1 } from './common/types/exception.types';

export const formmatError = (error: GraphQLError): ErrorDto => {
  const originalError: any = error.extensions.originalError;
  const newError = new ErrorDto(originalError.statusCode);

  return newError;
};



/* src/HttpExceptionFilter.ts */
/* 아래 클래스 추가 */

export class CustomError extends BadRequestException {
  errorCode: number;
  
  constructor(errorCode: number) {
    super({ ok: false, statusCode: errorCode });
    this.errorCode = errorCode;
  }
}

 

/* src/common/types/exception.types.ts */

export enum ErrorCode {
  UNAUTHORIZED = 10000,
  FORBIDDEN = 10001,
  EXPIRED_TOKEN = 10002,
  QUEUE_EMPTY = 30000,
  QUEUE_ALREADY_ON_RUNNING = 30001,
  INTERNAL_SERVER_ERROR = 50000,
  AUTH_PASSWORD_SHORT = 20000,
  AUTH_INVALID_PASSWORD = 20001,
  AUTHORIZATION_NOT_EXIST = 20002,
  GPM_CATALOG_REJECT_LIST_EMPTY = 21000,
  GPM_CATALOG_REJECT_LIST_BE_ONE_OR_MORE = 21001,
}
export const ErrorMessageV1 = {
  [ErrorCode.UNAUTHORIZED]: {
    code: 10000,
    message: '로그인정보가 잘못되었습니다.',
  },
  [ErrorCode.FORBIDDEN]: { code: 10001, message: 'Access forbidden.' },
  [ErrorCode.EXPIRED_TOKEN]: { code: 10002, message: 'Token has expired.' },
  [ErrorCode.QUEUE_EMPTY]: { code: 30000, message: 'Queue is empty.' },
  [ErrorCode.QUEUE_ALREADY_ON_RUNNING]: {
    code: 30001,
    message: 'Queue is already on running.',
  },
  [ErrorCode.INTERNAL_SERVER_ERROR]: {
    code: 50000,
    message: 'Internal server error.',
  },
  [ErrorCode.AUTH_PASSWORD_SHORT]: {
    code: 20000,
    message: 'Password is too short.',
  },
  [ErrorCode.AUTH_INVALID_PASSWORD]: {
    code: 20001,
    message: 'Invalid password.',
  },
  [ErrorCode.AUTHORIZATION_NOT_EXIST]: {
    code: 20002,
    message: 'Authorization does not exist.',
  },
  [ErrorCode.GPM_CATALOG_REJECT_LIST_EMPTY]: {
    code: 21000,
    message: 'Catalog reject list is empty.',
  },
  [ErrorCode.GPM_CATALOG_REJECT_LIST_BE_ONE_OR_MORE]: {
    code: 21001,
    message: 'Catalog reject list should be one or more.',
  },
};

 

 

/* src/common/dtos/exception.dto.ts */

import { ErrorCode, ErrorMessageV1 } from '../types/exception.types';

export class ErrorDto {
  message: string;

  constructor(errorCode: ErrorCode) {
    this.message = `[${errorCode}] - ${
      ErrorMessageV1[ErrorCode[ErrorCode[errorCode]]].message
    }`;
  }
}

 

 

위와같이 코드 작업 후 실제 Exception을 발생시킬때,

throw new CustomError(ErrorCode.UNAUTHORIZED);

 

위와 같이 발생시키면 되며,

클라이언트에서는 Response 된 에러에서 Message에서 코드와 메시지를 사용하여 에러메시지를 공통적으로 처리할 수 있다.


반응형
반응형

1. 설치

 

npm install --save @nestjs/typeorm typeorm mysql

 

2. 사용

루트모듈에서

 

import {TypeOrmModule} from "@nestjs/typeorm"


//imports에 추가
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: process.env.DB_HOST,
      port: +process.env.DB_PORT,
      username: process.env.DB_USERNAME,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
      synchronize: true,
      logging: false,
    }),
반응형
반응형

1. 설치

npm i --save @nestjs/config //설치
npm i cross-env //가상 변수를 설정할 수 있게해줌

 

3. Package.json 수정

    "start:dev": "cross-env NODE_ENV=dev nest start --watch", //시작시 환경변수추가

 

2. 사용

루트모듈 > Import에 입력

    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: process.env.NODE_ENV === 'dev' ? '.env.dev' : '.env.test',
    }),

 

 

3. 테스트

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    console.log(process.env.DB_HOST);
    return this.appService.getHello();
  }
}
반응형
반응형

* 본 포스팅은 리액트와 타입스크립트가 설치되었다는 가정하게 작성하도록 하겠습니다.

 

일반적으로 타입스크립트로 코드를 작성할 경우, 실행 시 2-step이 필요합니다.

 

Step1. TypeScript => JavaScript 컴파일

tsc practice.ts

Step2. 컴파일된 JavaScript 실행

node practice.js

 

매번 Live compile을 세팅하면 매번 컴파일을 해야하는 번거로움을 덜 수 있습니다.

 

ts-node 설치하기
npm install ts-node --save-dev
nodemon 설치하기
npm install nodemon --save-dev

 

package.json script작성
  "scripts": {
    "start": "npm run build:live",
    "build": "tsc -p .",
    "build:live": "nodemon  --exec 'ts-node' src/main.ts -e ts,tsx,js,json", //ts,tsx,js,json 확장자 확인
  },

 

 

 

이후 npm run start로 실행하면 정상적으로 실행되는부분을 확인 할 수 있다.

반응형

'BackEnd > Node' 카테고리의 다른 글

dotenv  (0) 2023.01.15

+ Recent posts