
GraphQL은 페이스북에서 개발한 데이터 쿼리 언어이자 런타임으로, 기존 REST API가 가진 구조적 한계를 보완하기 위해 등장했다. 전통적인 REST API에서는 각 엔드포인트마다 반환되는 데이터 구조가 고정되어 있고, 동일한 리소스에 대해 서로 다른 작업을 수행하기 위해 HTTP method를 조합하는 방식이 일반적이었다.
반면 GraphQL은 단 하나의 엔드포인트를 사용하고, 클라이언트가 필요한 데이터의 형태를 직접 명시해 요청한다. 서버는 그 요청에 정확히 맞는 데이터만 응답한다. 이 구조 덕분에 불필요한 데이터 전송이 줄어들고, 한 번의 요청으로 여러 자원에 대한 데이터를 동시에 가져올 수 있어 네트워크 사용 측면에서도 효율적이다.
표준적인 GraphQL 요청은 POST method만을 사용하며, application/json 형식의 바디에 쿼리를 담아 전송한다.
GraphQLModule
Nest.js에서 GraphQL을 사용하려면 @nestjs/graphql과 @nestjs/apollo 패키지를 설치하고, 이를 통해 동적 모듈인 GraphQLModule을 AppModule에 등록해야 한다.
Apollo는 GraphQL 서버와 클라이언트를 구현하기 위한 도구 모음으로, Nest.js에서는 Apollo Server를 기반으로 한 ApolloDriver가 이미 구현되어 있다. 덕분에 복잡한 설정 없이도 GraphQL 서버를 빠르게 구성할 수 있다.
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
playground: true,
path: '/api/graphql',
}),
],
})autoSchemaFile 옵션은 코드 기반(code-first) GraphQL 스키마 생성을 가능하게 한다. @ObjectType, @InputType, @Resolver 등에 정의된 메타데이터를 수집해, GraphQL 스키마 파일을 자동으로 생성한다. 이 파일은 절대 경로 기준으로 생성되며, import된 모든 타입을 기반으로 스키마가 구성된다.
GraphQL Playground는 REST API의 Swagger에 해당하는 도구로, 쿼리 작성과 응답 확인을 동시에 할 수 있다. app.setGlobalPrefix('api')를 설정하더라도 GraphQL은 기본적으로 /graphql 경로를 사용하지만, 나는 REST API와 경로 규칙을 일부 통일하기 위해 path 옵션을 사용했다.
Resolver
GraphQL에서 리졸버(Resolver) 는 특정 필드의 데이터를 조회하거나 변경하는 함수다. Nest.js의 REST 컨트롤러와 유사한 역할을 하지만, 리소스 중심이 아니라 응답 스키마 중심으로 구성된다는 점에서 차이가 있다.
REST 컨트롤러가 보통 “유저”, “게시글” 같은 서버 자원 단위로 나뉜다면, GraphQL 리졸버는 “어떤 타입을 어떻게 응답할 것인가”를 기준으로 구성되는 경우가 많다. 물론 이 역시 강제되는 규칙은 아니며, 여러 응답을 하나의 리졸버에서 처리한다고 해서 문제가 되는 것은 아니다.
@Resolver(() => Book)
export class BookResolver {
constructor(private bookService: BookService) {}
@Query(() => [Book])
async getBooks(
@Args({ name: 'isbn13s', type: () => [String] })
isbn13s: string[],
) {
return this.bookService.getBooksByIsbnList(isbn13s);
}
@Mutation(() => Book)
async patchBook(
@Args({ name: 'input', type: () => BookInput })
bookInput: BookInput,
) {
return this.bookService.patchBook(bookInput);
}
}@Resolver 데코레이터의 타입 지정은 가독성을 높이기 위한 용도에 가깝지만, @Query와 @Mutation 데코레이터의 반환 타입은 GraphQL 스키마 자체에 영향을 미치므로 반드시 정확해야 한다.
InputType과 ObjectType
위 예제에서 getBooks는 isbn13s라는 Args를 받는다. 타입은 () => [String], 즉 Array<string>이다. 여기서 주의할 점은 GraphQL에서는 배열의 요소 타입을 혼합할 수 없다는 것이다.
// ❌ 잘못된 정의
@Args({ type: () => [String, Number] })
isbn13s: Array<string | number>;GraphQL 스키마에서는 이런 정의가 허용되지 않기 때문에, 필요한 경우 타입을 분리해야 한다.
// ✅ 올바른 정의
@Args({ name: 'isbn13Strings', type: () => [String], nullable: true })
isbn13Strings?: string[];
@Args({ name: 'isbn13Numbers', type: () => [Number], nullable: true })
isbn13Numbers?: number[];Query나 Mutation에서 여러 값을 하나의 객체로 받고 싶다면 InputType을 사용해야 한다. InputType은 클라이언트 → 서버 방향의 데이터 구조를 정의하며, 서버 → 클라이언트 방향의 응답 구조는 ObjectType으로 정의한다.
@ObjectType()
export class Book {
@Field()
isbn13: string;
@Field()
title: string;
@Field()
author: string;
@Field({ nullable: true })
description?: string;
@Field({ nullable: true })
cover?: string;
}@InputType()
export class BookInput {
@Field()
isbn13: string;
@Field({ nullable: true })
title?: string;
}이 구조는 REST에서 DTO를 정의하는 방식과 크게 다르지 않으며, @nestjs/graphql 역시 @nestjs/swagger와 마찬가지로 mapped type 유틸리티를 제공해 타입 재사용을 돕는다.
Custom AuthGuard
기존 REST API에서는 커스텀 가드에서 HTTP request 객체를 직접 가져와 쿠키에 담긴 access token과 refresh token을 검증했다. 하지만 GraphQL 요청에서는 request가 일반적인 HTTP 요청과 다른 경로로 전달된다.
GraphQL 요청의 경우 context 안에 request가 포함되며, 이를 꺼내오는 방식이 HTTP와 다르다. 따라서 나는 getType()을 사용해 요청 타입을 구분하고, 그에 따라 request를 추출하도록 로직을 수정했다.
canActivate(context: ExecutionContext) {
const type = context.getType();
let request;
switch (type) {
case 'http':
request = context.switchToHttp().getRequest();
break;
case 'graphql':
const gqlContext = GqlExecutionContext.create(context);
request = gqlContext.getContext().req;
break;
}
return this.validateRequest(request);
}이렇게 GraphQL 요청이 AuthGuard를 통과하면, 이후 리졸버에서는 @Context 데코레이터를 통해 user 정보를 바로 사용할 수 있다.
@Mutation(() => Book)
@UseGuards(AuthGuard)
async patchBook(
@Args('input') input: BookInput,
@Context('req') { user }: { user: User },
) {
return this.prisma.book.update({
where: { isbn13: input.isbn13 },
data: { title: input.title },
});
}마무리
여기까지 살펴보면 GraphQL이 REST API와 구조적으로 크게 다르지 않다고 느껴질 수도 있다. 컨트롤러가 리졸버로 바뀌고, DTO가 ObjectType과 InputType으로 분리된 정도로 보일 수 있기 때문이다. 실제로 서버 구현 관점에서 보면 패러다임의 전환이라기보다는 표현 방식의 차이에 가깝게 느껴질 수도 있다.
하지만 GraphQL, 보다 정확히는 Apollo 생태계의 핵심 강점은 클라이언트와 서버가 하나의 타입 시스템으로 강하게 결합된다는 점에 있다. 서버가 정의한 스키마를 기준으로 클라이언트는 요청과 응답을 자동으로 타입화할 수 있고, 그 결과 런타임 에러의 상당 부분을 컴파일 타임에 제거할 수 있다. 이 장점은 Apollo Client와 코드 생성 도구를 함께 사용할 때 비로소 체감되며, 실제 사용 흐름과 개발 경험에 대해서는 Apollo Client 포스트에서 이어서 살펴본다.
더 읽어보기
2025.05.21
14. Redis를 사용한 세션 관리 및 캐싱
새로운 프로젝트를 준비하면서 인증 방식부터 다시 고민하게 되었다. 이전 프로젝트에서는 JWT를 사용해 유저 인증과 상태 관리를 처리했지만, 이번에는 Redis를 활용한 세션 방식으로 전환하기로 결정했다. 이 방식은 유저 상태를 서버에서 직접 관리할 수 있어 보안 측면에서도 유리하고, 필…
2025.01.30
비즈니스 로직은 어디에 있어야 할까
Nest.js에서 컨트롤러와 각각의 프로바이더는 분명한 책임을 갖는다. 하지만 막상 서버를 개발하다 보면, 이 책임들을 깊이 고민하지 않은 채 코드를 작성하게 되고, 그 결과 클래스들이 서로의 영역을 침범하는 상황이 반복된다. 나 역시 컨트롤러에 비즈니스 로직이 섞이거나, 서비스가 지나…
2024.12.07
13. Jest 테스트 구성
이 포스트를 시작하기에 앞서 한 가지 분명히 밝혀두고 싶은 점이 있다. 나는 컨트롤러(혹은 GraphQL 기준으로는 리졸버) 와 레포지토리 클래스에 대한 테스트를 거의 작성하지 않는다.이유는 단순하다. 애플리케이션에서 가장 많은 비즈니스 규칙이 응집되어 있는 곳은 서비스 계층이며, 테스…
2024.11.11
11. webSocketGateway를 사용한 알림 기능
웹소켓은 클라이언트와 서버 간에 지속적인 연결을 유지함으로써 실시간 양방향 통신을 가능하게 하는 프로토콜이다. 일반적인 HTTP 요청이 요청–응답 단위로 연결을 맺고 끊는 방식이라면, 웹소켓은 한 번 연결을 맺은 뒤 그 연결을 계속 유지하며 데이터를 주고받는다. 이 덕분에 알림, 채팅과…
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
파일을 다운로드할 때 가끔 몇 퍼센트 진행되었는지 혹은 진행 막대(progress bar)가 조금씩 채워지는 모습을 볼 수 있다. 그런데 모든 다운로드가 이런 식으로 진행률을 보여 주는 것은 아니다. 어떤 다운로드는 퍼센트가 표시되지만, 어떤 경우에는 진행 막대 없이 로딩 스피너만 계속…
2026.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감을 느끼게 된다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 살펴보고 있는데, 어딘가 낯설지 않다. Java를 써 본 사람이라면 InputStream, OutputStre…
댓글
댓글을 불러오는 중...