Node.js - NestJS & GraphQL

Temporary·2024년 8월 2일
0

Nods.js

목록 보기
21/39

NestJS started with GraphQL & TypeScript

이전에는 NestJS 와 Rest API & TypeScript를 사용했지만 이번에는 GraphQL & TypeScript를 사용하는 방법에 대해서 알아보자

Nest는 GraphQL 애플리케이션을 빌드 하는데 스키마 우선(schema first) 및 코드 우선(code first) 방법을 제공한다.

스키마 우선과 코드 우선은 무엇을 의미하는 건지 아래에서 자세히 알아보겠다.



Schema First(스키마 우선)

Schema first 는 graphql의 schema를 먼저 정의하고, 그 schema 정의에 맞게 코드를 작성하는 방법이다. schema를 작성하기 위해서는 graphql의 data model을 나타내기 위해 만들어진 SDL(Schema Definition Language) 를 사용한다.

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

따라서, GraphQL 스키마를 기반으로 TypeScript 정의 (클래스 또는 인터페이스 사용)를 자동으로 생성하여 중복된 상용구 코드를 작성할 필요성을 줄여주며 전체 관계를 직관적으로 나타냄으로 팀 커뮤니케이션 장애가 적은 장점이 있다.

하지만 실제 코드에 해당하는 Resolver는 포함되지 않으므로 스키마 정의와 함께 Resolver가 계속적으로 동기화 되어야한다.

그렇기때문에 Resolver 코드와 SDL의 정의가 정확하게 일치해야하는 단점이 존재한다.

GraphQLModule.forRoot({
  typePaths: ['./**/*.graphql'],
}),

스키마 우선 접근 방식을 사용하려면 먼저 옵션 개체에 typePaths 속성을 추가한다. typePaths 속성은 GraphQLModule이 작성할 GraphQL SDL 스키마 정의 파일을 찾아야하는 위치를 나타낸다.

아래처럼 .graphql스키마를 직접 작성해줘야 한다.

// cat.graphql

type Query {
  cats: [Cat]
  cat(id: ID!): Cat
}

type Mutation {
  createCat(createCatInput: CreateCatInput): Cat
}

type Subscription {
  catCreated: Cat
}

type Owner {
  id: Int!
  name: String!
  age: Int
  cats: [Cat!]
}

type Cat {
  id: Int
  name: String
  age: Int
  owner: Owner
}

input CreateCatInput {
  name: String
  age: Int
}

Code First(코드 우선)

코드 우선 접근 방식에서는 데코레이터와 TypeScript 클래스를 사용하여 먼저 작성한 Resolver 을 기반으로 해당 GraphQL 스키마를 자동 생성한다.

이 방법은 TypeScript 로만 작업하고 언어 구문 간의 컨텍스트 전환을 피하려는 경우 유용하다.

Schema와 Resolver간의 type safety를 보장하며 코드 중복이 최소화되는 장점이 있다.

하지만, SDL과 비교해서 직관적이지 못한 단점이 존재한다.

코드 우선 접근 방식을 사용하려면 먼저 옵션 객체에 autoSchemaFile 속성을 추가해서

GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
			autoSchemaFile: 'src/common/graphql/schema.gql',
})

autoSchemaFile 속성 값은 자동으로 생성된 스키마가 생성될 경로를 뜻한다.

즉,
schema first 는 graphql schema를 먼저 작성한 후, typescript 클래스나 인터페이스를 생성해 주고
code first 는 typescript로 클래스를 만들면 해당 클래스에 해당하는 graphql schema를 자동으로 만들어 주는 것에 차이가 있다.

GraphQL 폴더 구조 셋팅(Code First)

typescript에 좀 더 익숙해지기 위해서 code first 방식으로 애플리케이션을 빌드 해보자

section08 폴더로 이동해서 08-01-nestjs 폴더를 복사하여 붙여주고

폴더 이름을 08-02-nestjs-with-graphql 폴더로 변경한 후, src 폴더 안에 boards 폴더를 만들어준다.

board 폴더 안에는 게시물 관련 API 가 들어가게 된다.

따라서, board 폴더 안에 boards.module.ts , boards.resolver.ts, boards.service.ts 파일을 만들어 준다.

  • .module.ts : .resolver.ts + .service.ts 합쳐주는 파일
  • .resolver.ts : 실질적 API 로직
    • .controller.ts 와 같은 파일로 이름만 다른 파일이다. 앞으로 controller 대신에 resolver 를 사용한다.
  • .service.ts : 핵심 비즈니스 로직

boards.service.ts 파일 만들기

08-02-nestjs-with-graphqlsrcboardsboards.service.ts 에 문자열 “Hello World!”를 반환하는 qqq 라는 비즈니스 로직을 아래와 같이 만든다.

// boards.service.ts

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

@Injectable({ scope: Scope.DEFAULT })
export class BoardsService {
  qqq(): string {
    return 'Hello World!';
  }
}
  • 🚨 import 가 자동으로 되지 않으면 직접 작성해야 한다.
  • @Injectable : 의존성주입 관련 데코레이터 DEFAULT는 싱글톤 패턴을 의미한다.
  • qqq(): string : 함수 내 리턴 타입을 string으로 명시해 준다.
    • 따라서, return 111 로 숫자로 리턴을 하게 되면 에러가 발생하게 되므로, 안전한 코드 작성을 할 수 있다.

boards.resolver.ts 파일 만들기

클라이언트는 아직 서버와 상호 작용할 수 있는 방법이 없다. 이를 해결하려면 Resolver 클래스를 만들어야 한다. 코드 우선 방식에서 리졸버 클래스는 Resolver 함수를 정의하고 쿼리 유형을 생성한다.

Routing 개념을 적용하여 어떤 Resolver가 어떤 Request를 수신하는지 제어한다. 각 Resolver는 최소 1개의 Route를 가지며 각 Route는 다른 action으로 동작한다.

constructor에 BoardsService를 주입하고 getHello라는 Resolver 함수를 만들어 BoardsService의 qqq 비즈니스 로직을 실행시켜 보겠다.

08-02-nestjs-with-graphqlsrcboardsboards.resolver.ts 를 아래와 같이 수정한다.

// boards.resolver.ts

import { Controller, Get } from '@nestjs/common';
import { BoardsService } from './boards.service';

// @Controller()
export class BoardsResolver {
  constructor( // 의존성을 받아주는 nest의 생성자 코드
    private readonly boardsService: BoardsService, //
  ) {}

  // @Get('/products/buy)
  getHello(): string {
    return this.boardsService.qqq();
  }
}
  • fetchBoards(): string : 함수 내 리턴 타입을 string으로 명시한다.
    • this.boardsService.qqq() 의 결과값은 'Hello World!’ 이므로 리턴 타입을 string으로 명시해 주지 않는다면 에러가 발생하게 된다. → 안전한 코드

boards.module.ts 파일 만들기

08-02-nestjs-with-graphqlsrcboardsboards.module.ts 를 아래와 같이 수정한다

// boards.module.ts

import { Module } from '@nestjs/common';
import { BoardsResolver } from './boards.resolver';
import { BoardsService } from './boards.service';

@Module({
  imports: [],
  providers: [
    BoardsResolver, //
    BoardsService,
  ],
})
export class BoardsModule {}

Board 외에도 Product, User 등 많은 파일들이 만들어지면서 각각의 모듈들이 생겨날 것이기 때문에 각각의 모듈을 최종적으로 app.module.ts 파일로 조립해야 완전히 완료가 된다.

이제, GraphQL 설치 를 해주자

GraphQL 설치 를 위해 NestJS 공식문서에 들어가, Documentation을 클릭해 확인해본다.

https://nestjs.com/

옆의 목차에서 GRAPHQL → Quick start 클릭하면 Installation에 대한 안내가 나와있다.

yarn으로 설치하기 위해 아래의 명령어를 복사해 해당 폴더 터미널에 붙여넣어 설치해준다.

$ yarn add @nestjs/graphql @nestjs/apollo graphql apollo-server-express

이제 Query를 사용할 수 있다.

VSCode에서 boards.resolver.ts 파일을 수정한다.

// boards.resolver.ts

import { Query, Resolver } from '@nestjs/graphql';
import { BoardsService } from './boards.service';

@Resolver()
export class BoardsResolver {
  constructor( // 의존성을 받아주는 nest의 생성자 코드
    private readonly boardsService: BoardsService, //
  ) {}

  @Query()
  getHello(): string {
    return this.boardsService.qqq();
  }
}
  • Query 를 import 할 때 '@nestjs/graphql' 로 import 가 되는지 꼭 확인해주자

app.module.ts 파일 만들기

08-02-nestjs-with-graphqlsrcapp.odule.ts 파일로 가서 아래와 같이 수정한다.

//app.module.ts

import { Module } from '@nestjs/common';
import { BoardsModule } from './boards/boards.module';

@Module({
  imports: [
    BoardsModule,
    // ProductsModule,
    // UsersModule,
  ],
})
export class AppModule {}
  • 최종적으로 조립된 AppModule 이 main.ts에서 실행되게 되는 것 을 알아두자

다음으로 GraphQL 셋팅 이다.

NestJS 공식 문서로 다시 돌아가 스크롤을 조금 내리시면 Getting started with GraphQL & TypeScript 에 대한 안내가 있다.

VSCode 로 돌아가셔서 08-02-nestjs-with-graphql → src 폴더로 들어가셔서 app.module.ts 파일을 아래와 같이 수정한다.

// app.module.ts

import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { BoardsModule } from './boards/boards.module';


@Module({
  imports: [
    BoardsModule,
    // ProductsModule,
    // UsersModule,
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
			autoSchemaFile: 'src/commons/graphql/schema.gql',
    }),
  ],

})
export class AppModule {}
  • autoSchemaFile: 'src/commons/graphql/schema.gql' 위치에 파일을 만들기 위해 src 폴더 안에 commons 폴더를 만들고, 그 안에 graphql 폴더를 만들었다.
    • 해당 파일 경로에 schema.gq라는 SchemaFile이 자동으로 만들어지고 실행된다.
    • GraphQL playground에 들어오는 Type들을 Schema First 를 사용하면 직접 만들어 줘야했다. 하지만 Code-First를 사용하면 자동으로 생성 가능하다.

api에 관련된 코드들을 따로 모아두기 위해,
src 폴더에 apis 폴더를 만들어 boards와 같은 폴더들을 안에 넣어준다.

app.moduel.ts 파일에서 boards.module을 import 하는 경로만 수정해주고

NestJS 기반 기본 Setting
GraphQL setting 뿐 아니라, Rest API setting을 하거나 채팅 관련된 setting을 하고 싶을 때 등에 필요한 기본 setting(보일러 플레이트)들을 확인하여 초기 뼈대 구조를 잡을 수 있다.
이는 NestJS 공식문서에 들어가서 Source code → sample 폴더에서 확인 가능 하다.

borads.resolver.ts 파일에서 Query의 리턴 타입을 명시해준다.

import { Query, Resolver } from '@nestjs/graphql';
import { BoardsService } from './boards.service';

@Resolver()
export class BoardsResolver {
  constructor(
    private readonly boardsService: BoardsService, //
  ) {}

  @Query(() => String) // fetchBoards 함수의 return 타입을 정의해줌
  fetchBoards(): string {
    return this.boardsService.qqq();
  }
}
  • @Query() 안에 fetchBoards 함수에 대한 Return 타입을 정의할 수 있다. 화살표 함수를 사용하여 정의했다.
    • @Query(() => String) 👉🏻  GraphQL에서 API Docs 를 만들기 위한 type으로 대문자로 시작 <- 이는 매우 중요
    • fetchBoards(): string 👉🏻  typescript type 지정으로 해당 타입 지정은 없어도 되며, 소문자로 시작

마지막으로 만든 API 를 실행시켜서 확인해보자

터미널을 08-02-nestjs-with-graphql 로 이동하여 yarn install 을 통해 node_module 을 만들어주고

yarn start:dev 를 입력해 서버를 실행한다.

http://localhost:3000/graphql 의 DOCS 를 확인해보면 아래처럼 만들어준 API 가 존재한다.

API를 query를 통해 요청해보게 되면 “Hello World!” 가 return 된다면 성공적으로 만든것이다.

VSCode를 보시면 다음과 같이 commongraphqlschema.gql의 폴더와 파일이 자동으로 생성된 것을 확인할 수 있다.

  • ! 가 붙은 필수 입력 상태로 만들어 진다. 필수 입력 상태가 Default 다.

만일, 필수 입력을 하고 싶지 않을 때는 boards.resolver.ts 파일을 아래와 같이 수정한다.

// boards.resolver.ts

import { Query, Resolver } from '@nestjs/graphql';
import { BoardsService } from './boards.service';

@Resolver()
export class BoardsResolver {
  constructor(private readonly boardsService: BoardsService) {}

  @Query(() => String, { nullable: true }) // 수정된 부분 - nullable: true 추가
  fetchBoards(): string {
    return this.boardsService.aaa();
  }
}

profile
Temporary Acoount

0개의 댓글