728x90
왜 GraphQL인가?
REST API의 두 가지 고질적인 문제를 해결하기 위해 등장했다.
- Over-fetching: 필요하지 않은 데이터까지 응답에 포함된다.
- Under-fetching: 필요한 데이터를 얻으려면 여러 번 요청해야 한다.
GraphQL은 클라이언트가 원하는 데이터의 구조와 필드를 직접 명시해서 요청한다. 단 한 번의 요청으로 원하는 형태 그대로 응답을 받을 수 있다.
Code-First vs Schema-First
| 방식 | 설명 |
|---|---|
| Code-First | TypeScript 클래스 + 데코레이터로 작성 → SDL 자동 생성 |
| Schema-First | .graphql 파일에 SDL로 먼저 작성 → 타입 자동 생성 |
NestJS에서는 Code-First가 더 자연스럽다. TypeScript와 데코레이터 스타일이 NestJS 철학과 어울리기 때문이다.
핵심 개념
ObjectType
GraphQL 스키마의 타입을 선언한다. @ObjectType()으로 클래스를 선언하고, @Field()로 노출할 프로퍼티를 지정한다.
@ObjectType()
export class Post {
@Field(() => Int)
id: number;
@Field()
title: string;
}
@Field(() => Int): TypeScript의number는Int인지Float인지 알 수 없어 명시적으로 지정한다.@Field()가 없는 프로퍼티는 GraphQL 응답에 포함되지 않는다.
Query
데이터를 조회할 때 사용한다. REST의 GET과 같다.
@Query(() => [Post])
getPosts(): Post[] {
return this.posts;
}
@Query(() => Post, { nullable: true })
getPost(@Args('id', { type: () => Int }) id: number): Post | undefined {
return this.posts.find(post => post.id === id);
}
{ nullable: true }: 결과가 없을 때null을 반환할 수 있도록 허용한다.@Args(): GraphQL 쿼리의 인자를 받는다.
Mutation
데이터를 변경할 때 사용한다. REST의 POST, PUT, DELETE와 같다.
@Mutation(() => Post)
createPost(
@Args('title') title: string,
@Args('content') content: string,
): Post {
const newPost = { id: this.posts.length + 1, title, content };
this.posts.push(newPost);
return newPost;
}
Subscription
서버에서 변화가 생겼을 때 클라이언트에 자동으로 데이터를 푸시한다. 내부적으로 WebSocket을 사용한다.
const pubSub = new PubSub();
// Mutation에서 이벤트 발행
await pubSub.publish('postCreated', { postCreated: newPost });
// Subscription에서 구독
@Subscription(() => Post)
postCreated() {
return pubSub.asyncIterableIterator('postCreated');
}
PubSub: 발행/구독 패턴의 구현체.publish()로 이벤트를 발행하고asyncIterableIterator()로 구독한다.- 이벤트 이름(
postCreated)이 Subscription 메서드 이름과 일치해야 한다.
흐름:
클라이언트 A ──subscribe──▶ 서버 대기 (WebSocket 연결 유지)
클라이언트 B ──mutation──▶ createPost 실행
pubSub.publish('postCreated', data)
↓
클라이언트 A ◀──push───── 자동으로 새 게시글 수신DataLoader (N+1 문제 해결)
N+1 문제
게시글 목록과 각 게시글의 작성자를 함께 조회하면:
게시글 목록 조회 → 1번
게시글 1 작성자 → 1번
게시글 2 작성자 → 1번
게시글 3 작성자 → 1번
...
게시글 N개 → N+1번 쿼리 발생DataLoader의 배칭(Batching)
이벤트 루프의 한 틱 안에 들어온 요청을 모아서 한 번에 처리한다.
게시글 목록 조회 → 1번
작성자 ID 수집 → [1, 2, 3, ...]
한 번에 조회 → WHERE id IN (1,2,3,...) → 1번
총 2번만 조회const userLoader = new DataLoader(async (ids: number[]) => {
const users = await userService.findByIds(ids);
return ids.map(id => users.find(u => u.id === id));
});
코드 상으로는 개별 요청처럼 보여도 DataLoader가 내부적으로 배칭 처리한다. 같은 id를 두 번 요청하면 두 번째는 캐시에서 반환한다.
REST vs GraphQL 비교
| 항목 | REST | GraphQL |
|---|---|---|
| 엔드포인트 | 리소스마다 다름 | /graphql 단일 엔드포인트 |
| 데이터 선택 | 서버가 결정 | 클라이언트가 결정 |
| 여러 리소스 조회 | 여러 번 요청 | 한 번에 요청 |
| 실시간 | 별도 WebSocket 구현 | Subscription 내장 |
| 캐싱 | URL 기반으로 단순 | 복잡 (별도 처리 필요) |
| 파일 업로드 | 간단 | 복잡 (실무에서 REST로 분리) |
트러블슈팅
@as-integrations/express5 패키지 누락
NestJS v10+이 Express v5를 사용하면서 Apollo Server v4가 해당 패키지를 요구한다.
npm install @as-integrations/express5
Subscription WebSocket 연결 불가
@as-integrations/express5가 WebSocket upgrade 처리를 지원하지 않는 호환성 문제. Subscription 코드 자체는 정상이며, 패키지 버전 이슈다.
'프로그래밍 > NestJS' 카테고리의 다른 글
| Rate Limiting — @nestjs/throttler로 API 요청 제한하기 (0) | 2026.04.28 |
|---|---|
| NestJS CSRF Protection (1) | 2026.04.27 |
| NestJS Helmet — HTTP 보안 헤더 (0) | 2026.04.26 |
| NestJS 암호화 & 해싱 (0) | 2026.04.25 |
| NestJS Authorization (인가) (0) | 2026.04.24 |



