처음부터 차근차근

[NestJS] GraphQL Resolvers 본문

FrameWork/NestJS

[NestJS] GraphQL Resolvers

HangJu_95 2023. 11. 29. 19:53
728x90

저번 시간에는 NestJS에 GraphQL 초기 설정을 진행했습니다.

안 보고 오신분들이라면 한번 쯤 보고 오시는 것을 추천드립니다.

 

[GraphQL] NestJS에서 GraphQL 초기 설정

GraphQL이란? GraphQL에 대한 상세한 설명이 적혀 있습니다. [GraphQL] GraphQL이란? GraphQL이란? GraphQL은 API를 위한 쿼리 언어이며 이미 존재하는 데이터로 쿼리를 수행하기 위한 런타임 입니다. GraphQL은 AP

hangju95.tistory.com

 

이번 시간에는 NestJS에 Resolver를 만들어보겠습니다.

Object type

GraphQL은 Query를 통해 Data를 받아오는데, 이때 Schema가 사용됩니다.

따라서, Resolver를 만들기 전 우리는 GraphQL의 Schema부터 제작해야 하며, Code를 통해 직접 Schema를 만들어보겠습니다.

간단한 예시로 User를 불러오는 Schema를 만들어보겠습니다.

// post.schema.ts
import { Field, InputType, Int, ObjectType } from '@nestjs/graphql';

// graphQL의 Schema를 생성하기 위해 Type 만들기 진행
@ObjectType()
export class Post {
  // @Field : 클래스의 프로퍼티를 GraphQL 필드로 정의할 떄 사용.
  // 해당 데코레이터를 사용하므로써 해당 프로퍼티가 GraphQL 스키마에 포함되고, 해당 필드를 쿼리 할 수 있게 된다.
  // 프로퍼티의 타입과 다른 메타데이터(설명, 기본값 등)을 GraphQL에 제공한다.
  // (type) => Int의 의미는 Typescript에서 사용되는 함수
  // 해당 필드가 GraphQL 스키마에서 어떤 타입을 갖는지 명시하는 데이터
  // Int : GraphQL의 내장 스칼라 타입 중 하나, 정수를 나타낸다.
  // 정수만 받고, 클라이언트에게 정수만 내보낸다.
  @Field((type) => Int)
  id: number;

  // description : Swagger와 동일하며, 필드의 속성을 설명해줍니다.
  @Field({ nullable: true, description: 'Post를 생성한 UserId' })
  userId?: string;

  @Field({ description: '제목' })
  title: string;

  @Field({ nullable: true })
  content?: string;
}
// user.schema.ts
import { Field, InputType, ObjectType } from '@nestjs/graphql';

// GraphQL Schema를 작성하기 위해서는 @ObjectType() 혹은 @InputType() 데코레이터를 넣어줘야 합니다.
@ObjectType()
export class User {
  // @Field 데코레이터를 통해 각 필드의 GraphQL 유형 및 선택 사항에 대한
  // 메타데이터를 줄 수 있다.
  @Field()
  id: string;

  // 간단한 예시로, nullable 옵션을 줘서 GraphQL Schema 유형에 선택사항을 제공할 수 있다.
  @Field({ nullable: true })
  email?: string;

  // Array로 받고 싶은 경우, type을 해당 클래스로 입력한다.
  // 배열의 nullable : 해당 필드 또는 필드의 항목들이 null을 가질 수 있는지
  // item : 배열 내의 개별 항목들이 null일 수 있다.
  // true : 필드 자체가 null일 수 있다.
  // itemsAndList : 필드 자체와 배열 내의 항목이 모두 null이 될 수 있다.
  @Field((type) => [Post], { nullable: true })
  posts?: Post[];
}

두 가지의 간단한 Schema를 Code를 통해 작성했습니다.

GraphQL은 query를 통해 객체의 어떠한 Field 데이터를 가지고 올 건지 정의할 수 있습니다.

이를 @Field를 통해 Schema를 정의할 수 있습니다.

 

예를 들어 "내가 Post 객체에서 id와 title만 필요해!" 라고 가정한다면

Query를 다음과 같이 작성할 수 있습니다.

자동으로 생성되는 Schema는 다음과 같이 출력됩니다.

type Post {
  id: Int!

  """Post를 생성한 UserId"""
  userId: String

  """제목"""
  title: String!
  content: String
}

type User {
  id: String!
  email: String
  posts: [Post!]
}

 

@Field 데코레이터의 옵션에는 다음이 들어갈 수 있습니다.

  • nullable: 필드가 null을 허용하는지 여부를 지정합니다(SDL에서 각 필드는 기본적으로 null을 허용하지 않습니다).boolean
  • description: 필드 설명을 설정합니다.string
  • deprecationReason: 필드를 더 이상 사용되지 않는 것으로 표시합니다.string

필드가 배열인 경우 다음과 같이 코드를 수정할 수 있습니다.

// Post 객체가 아닌, 원시타입이여도 가능합니다.
@Field(type => [Post])
posts: Post[];

Resolver

이제 클라이언트에서 데이터를 가지고 올 수 있도록 Resolver를 만들어보겠습니다.

REST API와 비교해보자면, Resolver는 Controller라고 할 수 있습니다.

간단한 예시로, User Resolver Controller를 보겠습니다.

// User.Resolve.ts

// Resolver 데코레이터를 호출
// Resolver 내에 있는 인수는 선택 사항이지만, 그래프가 중요해지면 작동
// 객체 그래프를 통해 아래로 탐색할 때 필드 확인 기능이 사용하는 상위 객체를 제공하는데 사용됩니다.
// 즉, 우리는 Post라는 하위 객체를 User라는 상위 객체를 통해 찾아야 되기 때문에 입력합니다.
@Resolver((of) => User)
export class UserResolver {
  constructor(private userService: UserService, private postService: PostService) {}
  
  // 한 명 조회하기
  // Query를 통해 데이터를 받을 수 있습니다.
  @Query((returns) => User)
  // User의 @Args를 통해 id 값을 받습니다.
  // 이때 @Args('input') 은 input값으로 받을 값을 의미합니다.
  async User(@Args('input') id: string) {
    return this.userService.findUser(id);
  }
  
  // ResolverField
  // User의 하위 집합인 Post의 데이터를 받을 때 사용합니다.
  // @Parant 메서드는 상위 객체에서 어떠한 데이터를 받아올 때 사용합니다.
  @ResolverField()
  async posts(@Parent() user: User) {
    const { id } = user;
    return this.postService.findPostByUserId(id);
  }
}

간단한 Resolver Class를 만들어봤습니다.

@Query() 데코레이터는 데이터를 받을 때(Get) 할 때 사용됩니다. @Query 데코레이터를 통해 Query를 만들면 Schema에 자동으로 등록되어집니다. 이를 통해 Query를 아래와 같이 동작 시킬 수 있게 됩니다.

만들어진 Query

해당 그림과 같이 User라는 Query가 만들어졌습니다. 이를 한번 사용해보겠습니다.

또한 User에는 Post라는 Field가 있습니다. 이 Field는 Post를 Array 형태로 반환합니다.

@ResolverField 데코레이터를 이용하면 Post 데이터를 가지고 올 수 있습니다.

 

@ResolverField를 사용하기 위해서는 어떤 객체가 상위 객체인지 명시해줘야 합니다. 이를 위해서 우리는 @Resolver 데코레이터의 인자를 통해 User라고 알려주었습니다.

 

추가적으로 RESTAPI와 동일하게 메서드는 getUser이지만 Query name은 User로 두고 싶은 경우가 있습니다.

이러한 경우에는 @Query에 존재하는 속성을 사용하면 됩니다.

// NestJS 공식 문서 코드

@Resolver(of => Author)
export class AuthorsResolver {
  constructor(
    private authorsService: AuthorsService,
    private postsService: PostsService,
  ) {}

  // Author라고 name을 붙여 Query name을 author로 등록
  @Query(returns => Author, { name: 'author' })
  async getAuthor(@Args('id', { type: () => Int }) id: number) {
    return this.authorsService.findOneById(id);
  }

  // ResolveField도 마찬가지로 등록할 수 있다.
  @ResolveField('posts', returns => [Post])
  async getPosts(@Parent() author: Author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }
}

Query 데코레이터 옵션

  • name: 쿼리 이름; string
  • description: GraphQL 스키마 문서를 생성하는 데 사용되는 설명(예: GraphQL 플레이그라운드) string
  • deprecationReason: 쿼리가 더 이상 사용되지 않는 것으로 표시되도록 쿼리 메타데이터를 설정합니다(예: GraphQL 플레이그라운드에서). string
  • nullable: 쿼리가 null 데이터 응답을 반환할 수 있는지 여부. boolean 또는 'items' 또는 'itemsAndList'

Args 데코레이터 옵션

핸들러에 사용할 요청에서 Argument를 추출해야 한다면, @Args() 데코레이터를 통해 추출할 수 있습니다.

GraphQL은 조금 똑똑하지 않아, 만약 Int나 Float로 Argument를 받고 싶다면 Int나 Float를 명시적으로 전달해야 합니다.

@Query(returns => Author, { name: 'author' })
async getAuthor(@Args('id', { type: () => Int }) id: number) {
  return this.authorsService.findOneById(id);
}

옵션 객체에는 다음과 같은 속성이 있습니다.

  • type: GraphQL 유형을 반환하는 함수
  • defaultValue: 기본값 : any
  • description: 설명 메타데이터 : string
  • deprecationReason: 필드를 더 이상 사용하지 않고 이유를 설명하는 메타 데이터를 제공합니다.string
  • nullable: 필드가 null을 허용하는지 여부

간단한 예시를 보겠습니다.

getAuthor(
  @Args('firstName', { nullable: true }) firstName?: string,
  @Args('lastName', { defaultValue: '' }) lastName?: string,
) {}

전용 arguments class

@Args데코레이터를 많이 사용하다보면 코드가 커지는 불편한 문제가 발생합니다.

이럴때는 DTO를 작성하는 것과 같이 @ArgsType을 작성할 수 있습니다.

// NestJS 공식 문서

import { MinLength } from 'class-validator';
import { Field, ArgsType } from '@nestjs/graphql';

@ArgsType()
class GetAuthorArgs {
  @Field({ nullable: true })
  firstName?: string;

  @Field({ defaultValue: '' })
  @MinLength(3)
  lastName: string;
}

이를 통해 Controller에서 DTO를 통해 데이터를 받아오는 것 처럼 GraphQL도 가능합니다.

또한 여기에는 class-validator를 통해서 입력 데이터를 검증할 수 있습니다.

참고

https://docs.nestjs.com/graphql/resolvers