NestJS/GraphQL 기본 개념과 구조

캡틴 노드랭크·2021년 9월 21일
0

NestJS

목록 보기
4/5

SDL(Schema Definision Language)란?

우선 SQL은 들어봤어도 SDL은 또 뭔지 처음 들어보고 헷갈렸었다.. GraphQL을 접하기 전까진 익숙치 않은 개념이었다.

가장 먼저 스키마Schema 는 필드를 포함하는 객체 유형의 모음이다.(데이터 타입의 집합) 각 필드에는 고유한 유형은 GraphQL에서 스칼라 유형이거나 다른 객체 유형일 수 있다.

type Student {
  _id: String,
  FirstName: String,
  LastName: String,
  Standard: Int,
}
  
type Query {
  getAllStudents:[Student],
  getStudentById(id:String):Student
}

필드 이름은(camelCase), 콜론, 필드 유형(스칼라 또는 객체)로 선언된다. 필드가 null이 아니어야 하는 경우 해당 유형 뒤에 항상 !를 추가한다.

이렇게 스키마를 정의하면, 스키마의 정의는 API 문서같은 역할을 하며, 백엔드 개발자는 어떤 데이터를 전달해야하는지, 프론트엔드 개발자는 인터페이스 작업 시 필요한 데이터를 정의할 수 있다.

스키마 우선과 코드 우선 접근방식

이 두개념은 디자인 방법론 중의 하나이다. Schema First Developmeent는 개발 시 스키마를 우선으로 개발한다.

Schema First

스키마 우선 접근 방식에서의 진실된 소스는 GraqhQL SDL(Schema Definision Language) 파일이다.

모든 프로그래밍 언어와 독립적이며, 통합되는 언어이고, NestJS에서는 GraphQL 스키마를 TypeScript의 클래스 및 인터페이스 형식으로 구현한다.

type Student {
  _id: String,
  FirstName: String,
  LastName: String,
  Standard: Int,
}
  
type Query {
  getAllStudents:[Student],
  getStudentById(id:String):Student
}

특히, 스키마 우선 작업방식은 .gql 혹은 .graphql파일을 생성하여 그곳에서 작성해준다.

Code First

TypeScript로만 작성하고, 문법 간 Context Switch를 피하는 경우에 사용한다. GraphQL의 스키마를 자동으로 생성하는 Decorater를 사용한다.

import { Field, ID, Int, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Student {
  @Field(() => ID)
  _id: string,
  
  @Field()
  FirstName: String,
  
  @Field()  
  LastName: String,
    
  @Field(() => Int)  
  Standard: Int,
}
 

student.resolver || controller.ts


@Resolver(() => Student)
export class StudentResolver {
  constructor(private readonly studentService: StudentService) {}

  @Query(() => Student, { name: 'student', nullable: true })
  getStudent(@Args() getStudent: GetStudent): Student {
    return this.usersService.getStudent(getStudent);
  }

NestJS 기본 구조

NestJS 프로젝트를 시작했을때, 가장 기본적으로 생성되는 파일/폴더의 구조이다.

A. main.ts

프로젝트의 시작점이며, 서버를 실행하는 코드를 가진다.

핵심중 하나이며, NestFactory를 사용하여 Nest 애플리케이션 인스턴스를 생성하는 엔트리 파일이다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

bootstrap() 라는 기본 비동기 함수는 await인 AppModule을 호출하며, 이 어플리케이션에 포트 3000을 리스닝 한다.

함수 이름은 임의로 지정할 수 있다.

  • NestFactory:

B. App.Moudle.ts

app.module.ts는 애플리케이션의 루트 모듈이다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

AppModule은Class 형태라는 것을 파악할 수있다.

여기서 @Module()라는 데코레이터를 사용해주었다. 이는 Nest 애플리케이션의 구조를 구성하는데 사용하는 메타데이터를 제공하며, 반드시 필요한 데코레이터이다.

Graphql의 URL이 /graphql로 하나의 URL로 묶여서 동작하는 것처럼, NestJS의 각각의 모듈들은 최종적으로 하나로 취합한다.


@Module({
  imports: [
    //환경변수 설정
    ConfigModule.forRoot({
      isGlobal: true,
      ignoreEnvFile: true,
      validationSchema: Joi.object({
        NODE_ENV: Joi.string(),
      }),
    }),

    //typeOrm, 데이터베이스 모듈
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: process.env.MYSQL_HOST,
      port: +process.env.MYSQL_PORT,
      username: process.env.MYSQL_USER,
      password: process.env.MYSQL_PASS,
      database: process.env.MYSQL_DB,
      synchronize: true,
      logging: false,
      entities: [USER_TB],
    }),
    GraphQLModule.forRoot({
      playground: true,
      autoSchemaFile: true,
    }),
    AuthModule,
    UserModule,
    CommonModule,
    PostModule,
  ],
  controllers: [],
  providers: [CommonService, PostService, PostResolver],
})
export class AppModule {}

결국 모든 모듈을 활용하기 위해선 루트 모듈인 App.module에 통합하며, main.ts에서 NestFactory를 통해 생성된 app이 실행된다.

Provider이란?

Provider은 Nest의 기본 개념이고, 대부분의 기본 Nest Class인services, repositories, factories, helpers등이 provider로 취급될 수 있다.

Provider 구분하는 방법은 쉬운데 단순히 @Injectable() 데코레이터가 달린 클래스를 Provider라고한다.

AppService라는 클래스는 Provider이다.

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

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

@Module() 데코레이터의 구조

  • Imports: 이 모듈에 필요한 프로바이더를
  • Provider: @Injectable() 데코레이터가 달린 클래스는 모두 Provider이라고 하는데, Nest injector에 의해 인스턴스화되고, 현재 모듈에서 공유될 Provider의 집합이다.
  • imports: 현재 모듈에서 필요한 Provider들을 내보내는 import로 가져온 모듈 목록이다.
  • exports: 이 모듈에서 제공하며, 이 모듈을 import 하는 다른 모듈에서 사용할 수 있게 해주는 Provider의 하위집합이다.
  • controller: 인스턴스화 해야하는, 현재 모듈에 정의된 Controller의 집합

모듈은 기본적으로 프로바이더를 캡슐화하기때문에, providers에 등록하지 않았거나import로 가져온 모듈에서 export하지 않는 ProviderInject하는 행위는 불가능하다.

C. App.Controller.ts

D. App.Resolver.ts

해당 파일은 GraphQL이 각 요청을 수행하는 코드를 작성하는 파일이다.

GraphQL에서 CRUD를 담당하는 @Query()@Mutation()데코레이터를 작성하여 해당 요청에 대한 응답을 처리한다.

@Resolver((of) => User)
export class UserResolver {
  constructor(private readonly userService: UserService) {}
  
  @Query(() => InputDTO)
   async action1(@Args('data') id:number){
  	return await this.userService.getID(id)
  }

  @Mutation(() => OutPutDTO2)
   async action1(@Args('data') createData:CreaeDataInputDTO){
  	return await this.userService.action2(createData)
  }
}

실제 비즈니스 로직이 작성된 Service파일에 접근하기 위해서는 constructor 함수의 매개변수로 넣어주어야 한다.

이후 클래스 내부에 작성된 각 함수들에선 this 키워들르 통해 service 에 접근이 가능하다.

  • @Query()

Query는 CRUD의 READ를 수행하며, 우리가 입력한 어떠한 객체를 반환할 것을 명시한다. 아래의 코드의 경우 InputDTO에 작성된 객체들을 반환한다.

 @Query(() => InputDTO)
  async action1(@Args('data') id:number){
  	return await this.userService.action1(id)
  }

플레이그라운드를 통해 확인할 수 있다.

query {
  action1(id:1) {
    id
    username
    email
  }
}
  • @Mutation()

Mutation은 CRUD의 CUD를 수행하며, 어떠한 정보를 입력, 수정, 삭제를 요청하며, 그에 따른 결과를 응답받을 수 있다.

 @Mutation(() => OutPutDTO2)
  async action1(@Args('data') createData:CreaeDataInputDTO){
  	return await this.userService.action2(createData)
  }

플레이그라운드를 통해 확인할 수 있다.

mutation {
  action1(data:{   //입력할 내용 작성
    title: 'sdfasdfasdf'
    content: '23213123213'    
    }) {
    isWrite //입력에 따른 응답 작성(OutPutDTO2에 작성되었다면) 
  }
}

E. App.Service.ts

이곳이 실제로 데이터 저장 및 검색을 담당하는 파일이며, 주로 이런 데이터처리를 수행하는 것을 비즈니스 로직이라고 부른다.

DB의 각 테이블에 접근할수있도록 여기에 가져와 사용한다.

@Injectable()
export class UserServie {
  constructor(
    @InjectRepository(USER_TB)
    private readonly uesrRepository: Repository<USER_TB>,
  ) {}

  async createUser(user: USER_TB) {
    try {
      return await this.uesrRepository.save(this.uesrRepository.create(user));
    } catch (error) {
      console.log('에러:', error);
    }
  }
}

@Injectable()는 위에서 설명한 것과같이 Moudule파일에서 Providers로 활용하기 위해서 이 데코레이터로 정의해준다.

@InjectRepository()TypeORM.moduleforFeature() 메서드를 통해 Import받으면, Module에 종속된다.

@Module({
  imports: [TypeOrmModule.forFeature([USER_TB])],
  providers: [UserServie],
  exports: [],
})
export class UserModule {}

이제 Service에서 @InjectRepository를 통해 저장소를 정의하고 사용할 수 있는 저장소 패턴을 지원한다. service에서는 당연히 비즈니스 로직, 데이터의 저장 수정 삭제 등등을 처리해야하므로 당연히 DB에 접근해야하기 때문에 필요한 부분만 가져와 사용하면 될 것 같다.

resolver와 controller의 차이점

Module의 종류

profile
다시 처음부터 천천히... 급할필요가 없다.

0개의 댓글