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의 numberInt인지 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 코드 자체는 정상이며, 패키지 버전 이슈다.