NestJS에서 GraphQL 설정
6장에서는 REST API의 한계를 극복하고 클라이언트-서버 간의 데이터 통신을 더욱 효율적으로 만들어주는 강력한 대안, GraphQL에 대해 알아보겠습니다.
GraphQL은 Facebook이 개발한 쿼리 언어이자 런타임으로, 클라이언트가 필요한 데이터를 정확히 요청할 수 있도록 하여 오버페칭(Over-fetching)과 언더페칭(Under-fetching) 문제를 해결합니다. NestJS는 GraphQL 서버를 구축하는 데 있어 매우 강력하고 유연한 통합을 제공합니다. 이번 절에서는 NestJS 프로젝트에 GraphQL 환경을 설정하는 기본적인 방법을 살펴보겠습니다.
GraphQL이란 무엇인가?
GraphQL은 API를 위한 쿼리 언어이자 기존 데이터로 쿼리를 실행하기 위한 런타임입니다. GraphQL은 클라이언트가 요청하는 데이터의 구조를 정확하게 정의하고, 서버는 그 정의에 따라 데이터를 제공합니다.
GraphQL의 주요 특징
- 클라이언트가 원하는 데이터만 요청: 클라이언트가 필요한 필드만 정확히 지정하여 요청할 수 있습니다. 이는 REST API에서 여러 엔드포인트를 호출하거나 불필요한 데이터를 받는 오버페칭 문제를 해결합니다.
- 단일 엔드포인트: 일반적으로 모든 GraphQL 요청은
/graphql
과 같은 단일 엔드포인트로 전송됩니다. 이는 REST의 여러 엔드포인트와 대조됩니다. - 강력한 타입 시스템: GraphQL은 API에 대한 강력한 타입 시스템을 가지고 있어, API가 어떤 데이터를 제공하는지 명확하게 정의할 수 있습니다. 이는 API의 자체 문서화 역할도 합니다.
- 실시간 데이터(Subscriptions): 클라이언트가 서버에서 발생하는 특정 이벤트에 대해 실시간으로 업데이트를 받을 수 있는 메커니즘을 제공합니다.
- 쉬운 버전 관리: 클라이언트가 필요한 필드만 요청하므로, API 변경 시 하위 호환성을 유지하기 더 쉽습니다. (새 필드 추가는 기존 클라이언트에 영향을 주지 않음)
GraphQL vs REST API (간략 비교)
특징 | REST API | GraphQL |
---|---|---|
데이터 요청 | 고정된 데이터 구조를 갖는 여러 엔드포인트 | 클라이언트가 원하는 데이터 구조를 쿼리로 요청 |
엔드포인트 | 리소스별 다수의 엔드포인트 | 일반적으로 단일 엔드포인트 /graphql |
문제점 | 오버페칭(Over-fetching), 언더페칭(Under-fetching) | 비교적 적음 |
버전 관리 | URI 버전 관리 등 필요 (하위 호환성 유지 어려움) | 필드 단위 추가/제거로 하위 호환성 유지 용이 |
NestJS에서 GraphQL 통합하기
NestJS는 두 가지 방식으로 GraphQL을 지원합니다.
- Code-first: TypeScript 데코레이터를 사용하여 GraphQL 스키마를 자동으로 생성하는 방식 (권장).
- Schema-first: SDL(Schema Definition Language)로 GraphQL 스키마를 먼저 정의하고, 이를 기반으로 코드를 작성하는 방식.
이 절에서는 NestJS에서 권장하는 Code-first 접근 방식을 중심으로 설정 방법을 알아보겠습니다.
단계 1: 필요한 패키지 설치
NestJS에 GraphQL을 통합하기 위해 다음 패키지들을 설치해야 합니다.
npm install @nestjs/graphql @apollo/server graphql
npm install --save-dev @nestjs/apollo @types/graphql
@nestjs/graphql
: NestJS GraphQL 통합의 핵심 패키지입니다.@apollo/server
: GraphQL 서버를 구축하기 위한 Apollo Server 라이브러리입니다. (이전apollo-server-express
대체)graphql
: GraphQL 코어 라이브러리입니다 (GraphQL 쿼리 언어를 구문 분석하고 유효성을 검사하는 데 사용).@nestjs/apollo
: NestJS와 Apollo Server의 통합을 위한 어댑터입니다.@types/graphql
: GraphQL 타입 정의입니다.
단계 2: AppModule
에 GraphQLModule
설정
GraphQLModule
을 NestJS의 루트 모듈인 AppModule
에 임포트하고 설정합니다.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql'; // GraphQLModule 임포트
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; // ApolloDriver 임포트
import { join } from 'path'; // 경로 조작을 위해 join 임포트
// import { UsersModule } from './users/users.module'; // 다음 절에서 사용할 예정
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver, // ApolloDriver 사용 명시
// Code-first 접근 방식 설정
autoSchemaFile: join(process.cwd(), 'src/schema.gql'), // 스키마 파일을 자동으로 생성할 경로
sortSchema: true, // 생성된 스키마 필드를 알파벳 순으로 정렬
playground: true, // 개발 환경에서 GraphQL Playground 활성화 (기본값: production 환경에서는 비활성화)
}),
// UsersModule, // 다음 절에서 GraphQL과 연동할 사용자 모듈
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
GraphQLModule.forRoot<ApolloDriverConfig>()
옵션 설명:
driver: ApolloDriver
: NestJS가 GraphQL 서버로 Apollo Server를 사용하도록 지정합니다.autoSchemaFile: join(process.cwd(), 'src/schema.gql')
: 이 옵션은 NestJS가 TypeScript 코드(데코레이터 사용)를 기반으로schema.gql
이라는 GraphQL 스키마 정의 파일을 자동으로 생성하도록 지시합니다.process.cwd()
는 현재 작업 디렉토리를 나타냅니다.sortSchema: true
: 생성된schema.gql
파일 내부의 타입, 필드 등을 알파벳 순으로 정렬하여 가독성을 높입니다.playground: true
: 개발 환경에서 GraphQL Playground라는 대화형 웹 IDE를 활성화합니다. 이를 통해 API를 쉽게 탐색하고 쿼리를 테스트할 수 있습니다. (기본적으로NODE_ENV
가production
이 아닐 때 활성화됩니다.)
단계 3: GraphQL 모듈에 Resolver 및 Type 정의 추가
GraphQL은 기본적으로 **타입(Type)**과 **리졸버(Resolver)**로 구성됩니다.
- 타입(Type): 데이터의 구조를 정의합니다. (예:
User
타입은id
,name
,email
필드를 가짐). - 리졸버(Resolver): 클라이언트의 쿼리나 뮤테이션 요청이 들어왔을 때, 해당 타입의 필드에 대한 실제 데이터를 어떻게 가져올지(또는 변경할지) 정의하는 함수입니다.
간단한 Post
타입과 이를 조회하는 리졸버를 생성해 보겠습니다.
nest g mo posts
nest g re posts
위 명령어는 posts.module.ts
와 posts.resolver.ts
파일을 생성합니다.
// src/posts/models/post.model.ts (새로 생성)
import { Field, Int, ObjectType } from '@nestjs/graphql'; // GraphQL 타입 데코레이터 임포트
@ObjectType() // 이 클래스가 GraphQL Object Type임을 선언
export class Post {
@Field(() => Int) // id 필드의 GraphQL 타입을 Int로 지정
id: number;
@Field() // title 필드의 GraphQL 타입을 String으로 지정 (기본값)
title: string;
@Field({ nullable: true }) // content 필드는 Nullable String으로 지정
content?: string;
}
@ObjectType()
: 이 데코레이터는 해당 TypeScript 클래스가 GraphQL 스키마의 객체 타입으로 매핑됨을 나타냅니다.@Field()
: 클래스 속성을 GraphQL 필드로 정의합니다. 괄호 안에 GraphQL 타입 (() => Int
)을 지정할 수 있으며,nullable: true
로 선택적 필드를 정의할 수 있습니다.
// src/posts/posts.resolver.ts
import { Resolver, Query, Args, Int } from '@nestjs/graphql'; // GraphQL 데코레이터 임포트
import { Post } from './models/post.model'; // 정의한 Post 모델 임포트
@Resolver(() => Post) // 이 리졸버가 Post 타입에 대한 리졸버임을 NestJS에 알림
export class PostsResolver {
private readonly posts: Post[] = [ // 임시 데이터
{ id: 1, title: '첫 번째 게시물', content: 'GraphQL 학습 중입니다.' },
{ id: 2, title: '두 번째 게시물', content: 'NestJS와 GraphQL 통합!' },
];
@Query(() => [Post], { name: 'allPosts' }) // 모든 게시물을 반환하는 쿼리 정의
// GraphQL의 쿼리 이름은 'allPosts'이며, 반환 타입은 Post 배열
getAllPosts(): Post[] {
return this.posts;
}
@Query(() => Post, { name: 'post' }) // 단일 게시물을 ID로 조회하는 쿼리 정의
// GraphQL의 쿼리 이름은 'post'이며, 반환 타입은 Post (null 가능)
getPost(@Args('id', { type: () => Int }) id: number): Post {
// @Args('id')는 쿼리 인자 'id'를 메서드 인자로 매핑
return this.posts.find(post => post.id === id);
}
}
@Resolver(() => Post)
: 이 리졸버가Post
GraphQL 타입에 대한 필드를 처리함을 나타냅니다.@Query(() => [Post], { name: 'allPosts' })
:allPosts
라는 GraphQL 쿼리를 정의합니다. 반환 타입은Post
객체의 배열입니다.@Query(() => Post, { name: 'post' })
:post
라는 GraphQL 쿼리를 정의합니다. 반환 타입은 단일Post
객체입니다.@Args('id', { type: () => Int })
: 클라이언트에서 전송되는 쿼리 인자id
를 메서드 인자id
에 매핑합니다.@nestjs/graphql
은 자동으로 인자 타입 유효성 검사 및 변환을 처리합니다.
단계 4: PostsModule
설정
생성한 리졸버를 PostsModule
에 등록하고, 이 모듈을 AppModule
에 임포트합니다.
// src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { PostsResolver } from './posts.resolver'; // PostsResolver 임포트
@Module({
providers: [PostsResolver], // 리졸버 등록
})
export class PostsModule {}
// src/app.module.ts (업데이트)
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { PostsModule } from './posts/posts.module'; // PostsModule 임포트
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
sortSchema: true,
playground: true,
}),
PostsModule, // PostsModule 추가
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
GraphQL 서버 테스트하기
-
애플리케이션을 실행합니다:
npm run start:dev
-
src/schema.gql
파일이 자동으로 생성되었는지 확인합니다. 내용은 다음과 유사할 것입니다.# src/schema.gql # ------------------------------------------------------ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # ------------------------------------------------------ type Post { id: Int! title: String! content: String } type Query { allPosts: [Post!]! post(id: Int!): Post }
이 스키마는 우리가 TypeScript 데코레이터로 정의한 내용이 GraphQL SDL로 정확히 변환되었음을 보여줍니다.
-
웹 브라우저를 열고
http://localhost:3000/graphql
로 접속합니다.- GraphQL Playground가 나타날 것입니다.
-
쿼리 테스트: 좌측 쿼리 편집기에 다음 쿼리를 입력하고 실행 버튼(▶)을 클릭합니다.
-
모든 게시물 조회
query { allPosts { id title content } }
응답은 다음과 유사할 것입니다.
{ "data": { "allPosts": [ { "id": 1, "title": "첫 번째 게시물", "content": "GraphQL 학습 중입니다." }, { "id": 2, "title": "두 번째 게시물", "content": "NestJS와 GraphQL 통합!" } ] } }
-
특정 게시물 조회
query { post(id: 1) { id title } }
응답은 다음과 유사할 것입니다. (
content
필드를 요청하지 않았으므로 포함되지 않습니다.){ "data": { "post": { "id": 1, "title": "첫 번째 게시물" } } }
-
존재하지 않는 게시물 조회
query { post(id: 999) { id title content } }
응답은 다음과 유사할 것입니다.
{ "data": { "post": null } }
-
GraphQL Playground의 오른쪽 'DOCS' 탭을 클릭하면 자동으로 생성된 API 문서를 확인할 수 있습니다. Query
타입, Post
타입 등의 상세 필드 정보를 볼 수 있습니다.
이것으로 NestJS에서 GraphQL 서버를 설정하고 기본적인 쿼리 기능을 구현하는 방법을 알아보았습니다. Code-first 방식을 통해 TypeScript 데코레이터만으로 GraphQL 스키마를 정의하고 리졸버를 구현하는 것이 얼마나 편리한지 확인할 수 있었습니다.
다음 절에서는 GraphQL의 핵심 기능인 뮤테이션(Mutation) 을 사용하여 데이터를 변경하고, 서브스크립션(Subscription) 을 통해 실시간 통신을 구현하는 방법에 대해 알아보겠습니다. 계속해서 함께 성장해 나가시죠!