NestJS)GraphQL 리졸버

이정훈·2024년 12월 15일

GraphQL

목록 보기
10/13

Resolver

리졸버(Resolver)란 GraphQL 연산자를 데이터로 변환해주는 명령어를 말합니다.
NestJS에서는 함수가 리졸버 역할을 합니다.
즉, GraphQL의 연산자를 서버에서 받고 서버에서는 해당 연산자를 해석하고 이와 관계된 NestJS 서버 내부의 함수를 실행해 원하는 결과를 가져온다고 볼 수 있습니다.

리졸버를 작동시키기 위해 필수적인 스키마와 쿼리에 대해 알아봅시다.

Schema

데이터베이스에서 다뤘던 스키마와 유사합니다.
데이터베이스의 스키마는 테이블이 가질 수 있는 속성들을 정의했다면 GraphQL의 스키마는 클라이언트가 요청할 수 있는 데이터 속성을 정의합니다.
데이터베이스에서 특정 엔티티에 대해 속성을 정의했다면 GraphQL에서는 타입에 대한 속성들을 정의합니다. 여기서 엔티티와 타입이 같다고 봐도 무방합니다.

예를 들어 아래는 Author 타입을 정의하는 것입니다.

type Author {
	id: Int!
    firstName: String
    lastName: String
    posts: [Post!]!
}

객체를 정의하는 것과 매우 유사합니다.
Author이라는 타입에 id, firstName, lastName, posts라는 속성이 존재하며 id는 Int타입, firstName과 lastName은 String타입, posts는 Post타입이라고 정의했습니다.
이때 Int타입 옆에 "!"가 붙어있는데 이는 Null 값이 들어가지 못함을 알리는 역할입니다.

이제 위 Author 타입을 NestJS에서 정의해보겠습니다.

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

@ObjectType()
export class Author {
  @Field(type => Int)
  id: number;

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

  @Field({ nullable: true })
  lastName?: string;

  @Field(type => [Post])
  posts: Post[];
}

데이터베이스의 엔티티를 정의할 때와 매우 유사합니다.

예를 들어 @Entity()데코레이터로 엔티티를 만들때와 유사하게 @ObjectType()데코레이터를 통해 GraphQL의 스키마를 만들고 있습니다.

또한 데이터베이스에서는 각 속성을 @Column()데코레이터를 통해 만들었는데 여기서는 @Field()데코레이터를 통해 스키마에 대한 속성들을 만들어주고 있습니다.

다른 점이 있다면 @Field()데코레이터에 인자를 통해 해당 속성이 GraphQL에서 어떤 속성인지 명시하고 있는 것이 필수적이라는 것입니다.
(단, String과 Boolean타입에 대해서는 명시하지 않아도 됩니다.)

Query

GraphQL에서 쿼리란 데이터를 읽는 작업을 의미합니다.
앞서 만들었던 Author 타입에 대한 쿼리를 만들어 보려고 합니다.

먼저 쿼리를 위해 리졸버가 필요합니다.

리졸버가 있어야 클라이언트를 측으로부터 오는 GraphQL 연산자를 해석해 서버의 함수를 실행시킬 수 있기 때문입니다.

리졸버는 @Resolver()데코레이터를 이용하면 됩니다.

@Resolver(() => Author)
export class AuthorsResolver {}

@Resolver에 들어간 인자는 해당 리졸버가 반환하는 데이터가 무엇인지 알립니다. 위에서는 Auhtor 데이터를 반환함을 알리고 있습니다.

이제 리졸버 안에 쿼리를 만들어 보겠습니다.
쿼리는 @Query()데코레이터를 통해 만들 수 있습니다.

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

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

-----------------------------------------------------------
//참고
// AuthorService
import { Injectable } from '@nestjs/common';

@Injectable()
export class AuthorsService {
  author = [
    { id: 1, firstName: 'I', lastName: 'JH' },
    { id: 2, firstName: 'Y', lastName: 'JM' },
  ];

  async findOneById(id: number) {
    return this.author.find((author) => author.id === id);
  }
}

@Query()데코레이터 인자를 통해 해당 쿼리 결과에 어떤 데이터가 반환되는지 알리고 있습니다.
또한 @Query()를 실행하는 함수의 인자에 @Args()데코레이터는 클라이언트가 쿼리를 하면서 같이 보낸 파라미터들을 받습니다.
@Args()데코레이터의 첫 번째 인자를 통해 id를 파라미터를 받으며 두 번째 인자를 통해 id가 Int타입이라고 정의합니다.
이렇게 기본적인 타입들에 대한 쿼리는 이루어졌습니다.
하지만 완전히 끝난것이 아닙니다.

앞서 Author타입의 posts는 Post타입이였습니다. 그리고 Post타입은 아래와 같은 스키마를 가진다고 가정하겠습니다.

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

@ObjectType()
export class Post {
  @Field((type) => Int)
  id: number;

  @Field()
  title: string;

  @Field((type) => Int, { nullable: true })
  votes?: number;
}

쿼리한 데이터 내부에 또 다른 스키마가 있다면 해당 스키마도 리졸버를 통해 해당 스키마 데이터를 줄 함수를 실행시켜줘야합니다.
즉, 자식 스키마를 처리 해줄 리졸버가 필요하다는 것입니다.
이는 @ResolveField()통해 만들 수 있습니다.

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

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

  @ResolveField('posts', () => [Post])
  async getPosts(@Parent() author: Author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }
}
-----------------------------------------------
//참고
// postsService
import { Injectable } from '@nestjs/common';

@Injectable()
export class PostsService {
  posts = [
    { id: 1, title: 'one', votes: 12, authorId: 1 },
    { id: 2, title: 'two', authorId: 2, votes: 13 },
    { id: 3, title: 'three', authorId: 1 },
  ];

  async findAll(id: number) {
    return this.posts.filter((posts) => posts.authorId === id);
  }

  async upvoteById(postId) {
    let i: number;
    for (i = 0; i < this.posts.length; i++) {
      if (this.posts[i].id === postId) {
        if (!this.posts[i].votes) this.posts[i].votes = 1;
        else this.posts[i].votes += 1;
        break;
      }
    }
    return this.posts[i];
  }
}

@ResolveField()데코레이터의 첫 번째 인자에 리졸버로 처리할 속성을 명시합니다. 두 번째 인자에는 해당 속성이 반환하는 타입을 명시합니다.
@ResolveField()를 호출하는 함수는 @Parent() 데코레이터를 통해 부모가 리졸버를 통해 반환한 데이터에 접근이 가능합니다.

실행

// AuthorsModule
import { Module } from '@nestjs/common';
import { AuthorsResolver } from './authors.resolver';
import { AuthorsService } from './authors.service';
import { PostsModule } from 'src/posts/posts.module';

@Module({
  imports: [PostsModule],
  providers: [AuthorsResolver, AuthorsService],
})
export class AuthorsModule {}

// PostsModule
import { Module } from '@nestjs/common';
import { PostsService } from './posts.service';

@Module({
  providers: [PostsModule, PostsService],
  exports: [PostsService],
})
export class PostsModule {}


// AppModule
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { AppResolver } from './app.resolver';
import { AuthorsModule } from './authors/authors.module';
import { PostsModule } from './posts/posts.module';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    }),
    AuthorsModule,
    PostsModule,
  ],
  providers: [AppResolver],
})
export class AppModule {}

위와 같이 설정하면 실행 한 뒤 테스트 할 수 있게 됩니다.
.../graphQL로 가서 잘 되었는지 확인해 보겠습니다.


참고로 getAuthor은 제가 실행하려는 쿼리에 대해 이름을 붙여준 것입니다.
아무 이름으로 바꿔도 상관없습니다.
또한 getAuthor옆에 ()부분은 받는 인자에 대한 설정을 하는 것입니다.
'$id'를 통해 id인자를 설정했고 해당 id가 Int타입임을 명시합니다.
여기서 $는 클라이언트로 부터 받는 인자임을 알리는 기호입니다.
이후 앞서 만들었던 @Qeury()데코레이터를 받는 author 쿼리를 실행합니다.
author쿼리는 id를 인자로 받는데 id에 $id를 넣고 있습니다.

좌측 하단에 Query Variables가 보이는데 id에 1을 넣어 보내고 있습니다.
즉, $id를 통해 클라이언트가 보낸 id값을 저장하고 해당 $id의 값을 author의 id에 인자로 주고 있습니다.

마무리

여기까지 해서 NestJS로 GraphQL 스키마를 정의하는 방법과 GraphQL의 쿼리에 대응하는 리졸버를 만드는 방법을 알아보았습니다.
다음에는 뮤테이션에 대해 알아보겠습니다.

profile
기록으로 흔적을 남깁니다.

0개의 댓글