2026년 NestJS와 GraphQL 완벽 가이드: 스키마 설계, 리졸버 구현, 면접 대비
NestJS 11에서 GraphQL 통합을 다룹니다. 코드 퍼스트와 스키마 퍼스트 설계 방식, 리졸버 패턴, DataLoader를 활용한 N+1 문제 해결, 인증 및 인가 구현을 학습합니다.

NestJS의 GraphQL 통합은 TypeScript 데코레이터와 GraphQL 쿼리 언어를 활용하여 타입 안전한 API를 구축하는 체계적인 접근 방식을 제공합니다. 이 튜토리얼에서는 스키마 퍼스트와 코드 퍼스트 두 가지 접근 방식, 리졸버 패턴, 그리고 2026년 기술 면접에서 채용 담당자가 묻는 질문들을 다룹니다.
NestJS 11에서는 코드 퍼스트 GraphQL이 기본값으로 설정되어 있으며, TypeScript 클래스에서 스키마를 자동 생성합니다. 기존 .graphql 파일이나 SDL 기반 워크플로우를 사용하는 팀을 위해 스키마 퍼스트도 계속 지원됩니다.
Apollo Server를 활용한 NestJS GraphQL 설정
NestJS는 @nestjs/graphql 패키지를 통해 Apollo Server와 통합됩니다. 설정 방식은 선택한 접근 방식에 따라 다릅니다. 코드 퍼스트는 데코레이터에서 SDL을 생성하고, 스키마 퍼스트는 .graphql 파일을 직접 파싱합니다.
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'), // Code-first: generates schema
sortSchema: true, // Alphabetical ordering for readability
playground: process.env.NODE_ENV !== 'production', // Disable in prod
introspection: process.env.NODE_ENV !== 'production',
}),
],
})
export class AppModule {}autoSchemaFile 옵션은 코드 퍼스트 모드를 활성화합니다. true로 설정하면 디스크에 기록하지 않고 메모리에서 스키마를 생성합니다. 이는 파일 시스템 접근이 제한될 수 있는 서버리스 환경에서 유용합니다.
코드 퍼스트 데코레이터를 사용한 GraphQL 타입 정의
코드 퍼스트에서는 @ObjectType()으로 데코레이트된 TypeScript 클래스를 사용하여 GraphQL 타입을 정의합니다. 각 필드는 @Field()를 사용하여 GraphQL 타입과 null 허용 여부를 지정합니다.
import { ObjectType, Field, ID, Int } from '@nestjs/graphql';
@ObjectType({ description: 'Application user' }) // Description appears in schema docs
export class User {
@Field(() => ID) // Maps to GraphQL ID scalar
id: string;
@Field()
email: string;
@Field({ nullable: true }) // Optional field in GraphQL
displayName?: string;
@Field(() => Int, { defaultValue: 0 })
postCount: number;
@Field(() => [Post], { nullable: 'itemsAndList' }) // Both list and items can be null
posts?: Post[];
// Fields without @Field() are excluded from GraphQL schema
passwordHash: string;
}nullable 옵션은 세 가지 값을 허용합니다: true(필드가 선택적), 'items'(리스트 항목이 null 가능), 'itemsAndList'(리스트와 항목 모두 null 가능). 이러한 세분화된 설정은 GraphQL의 null 허용 의미론과 정확히 일치합니다.
쿼리와 뮤테이션을 위한 리졸버 구축
리졸버는 수신된 GraphQL 작업을 처리합니다. NestJS에서는 @Resolver()를 사용하여 클래스를 리졸버로 표시하고, 메서드 데코레이터로 작업 유형을 지정합니다.
import { Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';
import { CreateUserInput } from './dto/create-user.input';
@Resolver(() => User) // Binds resolver to User type for field resolution
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
@Query(() => [User], { name: 'users' }) // Explicit query name
findAll(): Promise<User[]> {
return this.usersService.findAll();
}
@Query(() => User, { nullable: true })
user(@Args('id', { type: () => ID }) id: string): Promise<User | null> {
return this.usersService.findOne(id);
}
@Mutation(() => User)
createUser(@Args('input') input: CreateUserInput): Promise<User> {
return this.usersService.create(input);
}
}@Resolver(() => User) 데코레이터는 @ResolveField() 데코레이터의 컨텍스트를 설정하여 계산된 데이터나 연관 데이터의 필드 수준 해결을 가능하게 합니다.
class-validator를 사용한 입력 타입과 유효성 검사
GraphQL 입력 타입은 뮤테이션 페이로드를 정의합니다. @InputType()과 class-validator 데코레이터를 결합하면 스키마 수준과 런타임 모두에서 유효성 검사가 가능합니다.
import { InputType, Field } from '@nestjs/graphql';
import { IsEmail, MinLength, IsOptional, Matches } from 'class-validator';
@InputType()
export class CreateUserInput {
@Field()
@IsEmail({}, { message: 'Invalid email format' })
email: string;
@Field()
@MinLength(8, { message: 'Password must be at least 8 characters' })
@Matches(/[A-Z]/, { message: 'Password must contain uppercase letter' })
password: string;
@Field({ nullable: true })
@IsOptional()
@MinLength(2)
displayName?: string;
}main.ts에서 ValidationPipe를 전역으로 활성화하여 유효성 검사를 실행할 수 있습니다. GraphQL 오류는 extensions 필드에 유효성 검사 메시지를 포함하여 클라이언트 호환성을 유지합니다.
Node.js / NestJS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
@ResolveField와 DataLoader를 사용한 연관 데이터 해결
필드 리졸버는 타입 간의 관계를 처리합니다. 최적화 없이 사용자 목록과 게시글을 가져오면 N+1 쿼리가 발생합니다. 사용자 조회에 1개의 쿼리, 각 사용자의 게시글 조회에 N개의 쿼리가 필요합니다.
import { Resolver, ResolveField, Parent } from '@nestjs/graphql';
import { User } from './user.entity';
import { Post } from '../posts/post.entity';
import { PostsLoader } from '../posts/posts.loader';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly postsLoader: PostsLoader) {}
@ResolveField(() => [Post])
async posts(@Parent() user: User): Promise<Post[]> {
// DataLoader batches requests: one query for all user IDs
return this.postsLoader.batchByUserId.load(user.id);
}
@ResolveField(() => Int)
async postCount(@Parent() user: User): Promise<number> {
const posts = await this.postsLoader.batchByUserId.load(user.id);
return posts.length;
}
}DataLoader는 단일 GraphQL 작업 내에서 요청을 배치 처리하고 캐시합니다. NestJS에서는 @Injectable({ scope: Scope.REQUEST })를 사용하여 DataLoader를 요청 스코프로 설정합니다.
import { Injectable, Scope } from '@nestjs/common';
import * as DataLoader from 'dataloader';
import { PostsService } from './posts.service';
import { Post } from './post.entity';
@Injectable({ scope: Scope.REQUEST }) // New instance per request
export class PostsLoader {
constructor(private readonly postsService: PostsService) {}
public readonly batchByUserId = new DataLoader<string, Post[]>(
async (userIds: readonly string[]) => {
// Single query: SELECT * FROM posts WHERE user_id IN (...)
const posts = await this.postsService.findByUserIds([...userIds]);
// Map results back to input order
const postsMap = new Map<string, Post[]>();
posts.forEach(post => {
const existing = postsMap.get(post.userId) || [];
postsMap.set(post.userId, [...existing, post]);
});
return userIds.map(id => postsMap.get(id) || []);
}
);
}DataLoader는 N+1 쿼리를 단일 배치 쿼리로 줄입니다. 이는 클라이언트가 쿼리 깊이를 제어하는 GraphQL API에서 매우 중요합니다. NestJS 아키텍처 패턴에 대한 자세한 내용은 NestJS 모듈 및 의존성 주입 면접 질문을 참고하시기 바랍니다.
실시간 데이터를 위한 서브스크립션
GraphQL 서브스크립션은 WebSocket 연결을 통해 클라이언트에 데이터를 푸시합니다. NestJS는 GraphQL over WebSocket 프로토콜을 구현하는 graphql-ws 라이브러리를 사용합니다.
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
subscriptions: {
'graphql-ws': true, // Modern protocol
'subscriptions-transport-ws': false, // Deprecated legacy protocol
},
}),import { Resolver, Subscription } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { Post } from './post.entity';
const pubSub = new PubSub(); // Use Redis PubSub for multi-instance deployments
@Resolver(() => Post)
export class PostsResolver {
@Subscription(() => Post, {
filter: (payload, variables) =>
payload.postCreated.userId === variables.userId, // Client-side filtering
})
postCreated() {
return pubSub.asyncIterableIterator('postCreated');
}
@Mutation(() => Post)
async createPost(@Args('input') input: CreatePostInput): Promise<Post> {
const post = await this.postsService.create(input);
pubSub.publish('postCreated', { postCreated: post }); // Trigger subscription
return post;
}
}다중 인스턴스 프로덕션 배포에서는 클러스터 전체에 이벤트를 브로드캐스트하기 위해 인메모리 PubSub을 graphql-redis-subscriptions로 교체합니다.
GraphQL에서의 인증과 인가
NestJS의 Guards는 GraphQL 리졸버와 원활하게 작동합니다. 실행 컨텍스트가 REST와 다르므로 GqlExecutionContext를 사용하여 요청을 추출합니다.
import { Injectable, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req; // Extract request from GraphQL context
}
}Guards는 리졸버 수준이나 메서드 수준에서 적용할 수 있습니다. 필드 수준 인가는 사용자 역할에 기반한 조건부 로직과 @ResolveField()를 사용합니다.
@UseGuards(GqlAuthGuard)
@Resolver(() => User)
export class UsersResolver {
@Query(() => User)
me(@CurrentUser() user: User): User {
return user; // Return authenticated user
}
@ResolveField(() => String, { nullable: true })
email(@Parent() user: User, @CurrentUser() currentUser: User): string | null {
// Only return email if viewing own profile or admin
if (user.id === currentUser.id || currentUser.role === 'ADMIN') {
return user.email;
}
return null;
}
}복잡한 인가의 경우 GraphQL Shield 또는 NestJS의 내장 CASL 통합을 고려하시기 바랍니다. NestJS Guards와 Interceptors 글에서 이러한 패턴을 자세히 다룹니다.
자주 출제되는 NestJS GraphQL 면접 질문
NestJS 포지션의 기술 면접에서는 GraphQL 관련 질문이 자주 출제됩니다. 다음은 채용 담당자가 평가하는 패턴입니다.
Q: NestJS는 GraphQL에서 N+1 문제를 어떻게 처리합니까?
DataLoader는 단일 요청 내에서 필드 리졸버 호출을 배치 처리합니다. 여러 부모 객체가 같은 필드를 요청하면 DataLoader는 모든 키를 수집하고 하나의 배치 쿼리를 실행한 다음 결과를 분배합니다. 로더는 요청 스코프여야 하며, 이를 통해 요청 간 캐시 문제를 방지합니다.
Q: NestJS GraphQL에서 코드 퍼스트와 스키마 퍼스트의 차이점은 무엇입니까?
코드 퍼스트는 런타임에 TypeScript 데코레이터에서 GraphQL 스키마를 생성하며, 타입과 스키마를 자동으로 동기화합니다. 스키마 퍼스트는 .graphql SDL 파일을 파싱하며, 수동 타입 정의가 필요합니다. 코드 퍼스트는 TypeScript 네이티브 팀에 적합하고, 스키마 퍼스트는 스키마가 프론트엔드와 백엔드 팀 간의 계약인 경우에 적합합니다.
Q: 필드 수준 권한을 어떻게 구현합니까?
세 가지 접근 방식이 있습니다: (1) 사용자 컨텍스트에 기반한 조건부 반환이 있는 @ResolveField(), (2) 필드 해결 전에 권한을 확인하는 커스텀 데코레이터, (3) 디렉티브 트랜스포머로 처리되는 @auth(requires: ADMIN) 같은 스키마 디렉티브. 첫 번째 접근 방식이 가장 유연하며, 디렉티브는 가장 깔끔한 스키마 문서화를 제공합니다.
Q: NestJS에서 GraphQL 컨텍스트를 설명하십시오.
컨텍스트 객체는 요청 내 모든 리졸버를 통과합니다. NestJS는 기본적으로 HTTP 요청을 포함합니다. 커스텀 컨텍스트는 GraphQLModule.forRoot()의 context 옵션에서 구성합니다. DataLoader 인스턴스, 인증된 사용자 또는 데이터베이스 연결을 추가하는 데 유용합니다.
Q: 서브스크립션은 여러 서버 인스턴스 간에 어떻게 확장됩니까?
인메모리 PubSub은 단일 인스턴스 배포에서만 작동합니다. 다중 인스턴스 아키텍처에서는 외부 브로커가 필요하며, Redis PubSub이 표준입니다. 각 서버 인스턴스는 Redis 채널을 구독하고, 어느 인스턴스에서든 이벤트를 발행하면 모든 인스턴스가 이를 수신하여 연결된 WebSocket 클라이언트에 푸시합니다.
NestJS 면접 준비에 대한 추가 내용은 요청 라이프사이클 패턴을 다루는 미들웨어와 Interceptors 모듈을 참고하시기 바랍니다.
결론
- NestJS GraphQL은 코드 퍼스트와 스키마 퍼스트 두 가지 접근 방식을 모두 지원하며, 코드 퍼스트는 TypeScript 프로젝트를 단순화하고 스키마 퍼스트는 SDL 기반 워크플로우에 적합합니다
- DataLoader는 단일 작업 내에서 필드 리졸버 요청을 배치 처리하여 N+1 쿼리를 제거합니다
- 요청 스코프의 DataLoader 인스턴스는 동시 요청 간 캐시 오염을 방지합니다
GqlExecutionContext는 NestJS Guards와 GraphQL의 리졸버 컨텍스트를 연결합니다- 프로덕션 서브스크립션은 다중 인스턴스 이벤트 배포를 위해 Redis PubSub이 필요합니다
- 필드 수준 인가는
@ResolveField()와 사용자 컨텍스트 확인을 결합합니다
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
공유
관련 기사

NestJS와 TypeORM 2026: 마이그레이션, 관계 설정, 기술 면접 핵심 정리
NestJS와 TypeORM을 활용한 백엔드 개발 실전 가이드. 마이그레이션 관리, 관계 모델링, 트랜잭션 처리 방법과 기술 면접에서 자주 출제되는 질문을 다룹니다.

Node.js 24 핵심 기능 완전 분석: URLPattern, 퍼미션 모델, 면접 대비 가이드 (2026년판)
Node.js 24 LTS(Krypton)의 주요 신기능인 URLPattern, 퍼미션 모델, 명시적 리소스 관리를 실전 코드 예제와 면접 대비 질문으로 상세하게 분석한다.

2026년 NestJS 마이크로서비스: 아키텍처, gRPC, 면접 질문 완벽 가이드
NestJS 마이크로서비스 아키텍처의 핵심 개념, gRPC 트랜스포트 구성, 스트리밍 패턴, 안정성 패턴, 면접 빈출 질문을 실무 중심으로 다룹니다.