REST API의 한계인 오버페칭과 언더페칭 문제를 분석하고, GraphQL의 타입 시스템, 스키마, 리졸버 등 핵심 개념을 소개합니다. 2026년 GraphQL 생태계 현황과 도입 판단 기준을 다룹니다.
REST는 지난 20년간 웹 API의 사실상 표준으로 자리 잡았습니다. 리소스 중심의 URL 설계와 HTTP 메서드를 활용한 직관적인 인터페이스는 많은 서비스에서 성공적으로 사용되어 왔습니다. 하지만 프런트엔드 애플리케이션이 복잡해지면서 REST의 구조적 한계가 뚜렷하게 드러나기 시작했습니다.
사용자 프로필 카드에 이름과 아바타만 필요한 상황을 생각해 보겠습니다.
// GET /api/users/123
const response = await fetch('/api/users/123');
const user = await response.json();
// 실제 필요한 데이터: name, avatar
// 실제 받는 데이터: id, name, email, avatar, address, phone,
// createdAt, updatedAt, preferences, ...REST 엔드포인트는 리소스 전체를 반환하므로, 클라이언트가 필요하지 않은 데이터까지 전송됩니다. 단일 요청에서는 미미해 보이지만, 모바일 환경에서 수십 개의 API 호출이 발생하면 불필요한 대역폭 소비가 누적됩니다.
블로그 포스트 목록 페이지에서 각 포스트의 작성자 이름과 댓글 수를 함께 보여주어야 하는 상황입니다.
// 1단계: 포스트 목록 조회
const posts = await fetch('/api/posts').then(r => r.json());
// 2단계: 각 포스트의 작성자 정보 조회 (N번 요청)
const authors = await Promise.all(
posts.map(post =>
fetch(`/api/users/${post.authorId}`).then(r => r.json())
)
);
// 3단계: 각 포스트의 댓글 수 조회 (N번 요청)
const commentCounts = await Promise.all(
posts.map(post =>
fetch(`/api/posts/${post.id}/comments/count`).then(r => r.json())
)
);하나의 화면을 구성하기 위해 1 + N + N번의 네트워크 요청이 필요합니다. 이것이 바로 REST의 언더페칭(Under-fetching) 문제이며, 흔히 N+1 요청 문제라고도 부릅니다.
클라이언트 요구사항이 다양해지면 REST API는 엔드포인트가 기하급수적으로 늘어납니다.
GET /api/posts
GET /api/posts?include=author
GET /api/posts?include=author,comments
GET /api/posts?fields=id,title,summary
GET /api/posts/feed # 모바일용 경량 버전
GET /api/posts/admin # 관리자용 확장 버전이러한 문제들을 해결하기 위해 2015년 Facebook(현 Meta)이 공개한 것이 바로 GraphQL입니다.
GraphQL은 API를 위한 쿼리 언어(Query Language) 이자 해당 쿼리를 실행하기 위한 런타임(Runtime) 입니다. 핵심은 세 가지 개념으로 구성됩니다.
GraphQL의 가장 강력한 특징은 강력한 타입 시스템입니다. 모든 데이터의 형태를 스키마(Schema) 로 명시적으로 정의합니다.
type User {
id: ID!
name: String!
email: String!
avatar: String
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
createdAt: DateTime!
}
type Comment {
id: ID!
body: String!
author: User!
}!는 Non-null을 의미하며, [Post!]!는 "Post 타입의 non-null 요소로 이루어진 non-null 배열"을 뜻합니다. 이 타입 시스템 덕분에 클라이언트는 API 문서 없이도 어떤 데이터를 요청할 수 있는지 정확히 알 수 있습니다.
GraphQL 스키마는 세 가지 루트 타입(Root Type) 을 가집니다.
type Query {
user(id: ID!): User
posts(first: Int, after: String): PostConnection!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
}스키마가 "무엇을 요청할 수 있는가"를 정의한다면, 리졸버(Resolver) 는 "어떻게 데이터를 가져오는가"를 구현합니다.
const resolvers = {
Query: {
user: async (_parent: unknown, args: { id: string }, context: Context) => {
return context.dataSources.users.findById(args.id);
},
posts: async (_parent: unknown, args: PaginationArgs, context: Context) => {
return context.dataSources.posts.findAll(args);
},
},
User: {
posts: async (parent: User, _args: unknown, context: Context) => {
return context.dataSources.posts.findByAuthorId(parent.id);
},
},
};리졸버는 필드 단위로 동작합니다. User 타입의 posts 필드를 요청하면, 해당 리졸버가 호출되어 관련 데이터를 반환합니다. 이 구조가 GraphQL의 유연성을 만들어냅니다.
REST와 GraphQL의 가장 큰 차이는 클라이언트가 필요한 데이터를 직접 선언한다는 점입니다.
query BlogPostList {
posts(first: 10) {
edges {
node {
id
title
summary
author {
name
avatar
}
commentCount
createdAt
}
}
}
}이 단일 쿼리로 REST에서 1 + N + N번 필요했던 요청을 한 번에 해결합니다. 서버는 클라이언트가 요청한 필드만 정확히 반환하므로 오버페칭도 발생하지 않습니다.
{
"data": {
"posts": {
"edges": [
{
"node": {
"id": "post-1",
"title": "GraphQL 시작하기",
"summary": "GraphQL의 기본 개념을...",
"author": {
"name": "Kreath",
"avatar": "/avatars/kreath.jpg"
},
"commentCount": 12,
"createdAt": "2026-03-15T09:00:00Z"
}
}
]
}
}
}응답의 구조가 쿼리의 구조와 정확히 일치합니다. 이 예측 가능성이 프런트엔드 개발 경험을 크게 향상시킵니다.
GraphQL이 모든 상황에서 REST보다 우월한 것은 아닙니다. 도입 판단 시 다음 기준을 고려해야 합니다.
GraphQL 도입은 기술적 결정인 동시에 조직적 결정입니다. 스키마 설계와 거버넌스에 대한 팀 합의가 선행되어야 합니다. "트렌디해서"가 아닌 "문제를 해결하기 위해" 도입해야 합니다.
2026년 현재 GraphQL 생태계는 성숙기에 접어들었습니다. 주요 도구와 트렌드를 살펴보겠습니다.
| 도구 | 특징 |
|---|---|
| Apollo Server | 가장 넓은 생태계, Federation 지원, Apollo Studio 통합 |
| GraphQL Yoga | 경량, 표준 준수, Envelop 플러그인 시스템 |
| Pothos | TypeScript 코드 퍼스트 스키마 빌더, 뛰어난 타입 추론 |
| Nexus | 코드 퍼스트 접근, Prisma 통합 |
| 도구 | 특징 |
|---|---|
| Apollo Client | 정규화 캐시, 풍부한 상태 관리 |
| urql | 경량, 교환(Exchange) 기반 확장 구조 |
| Relay | Facebook 제작, Fragment Colocation, 컴파일러 기반 최적화 |
| TanStack Query + graphql-request | 가볍게 시작하기 좋은 조합 |
Federation 2.0의 보편화: 대규모 조직에서 마이크로서비스를 하나의 슈퍼그래프(Supergraph) 로 통합하는 패턴이 표준으로 자리 잡았습니다. Apollo Router가 게이트웨이 역할을 수행하며, 각 팀이 독립적으로 서브그래프(Subgraph) 를 개발하고 배포합니다.
Persisted Queries의 일반화: 보안과 성능을 위해 사전 등록된 쿼리만 허용하는 방식이 프로덕션 환경의 기본 관행이 되었습니다.
코드 생성(Code Generation)의 필수화: GraphQL Code Generator를 통한 TypeScript 타입, React 훅, SDK 자동 생성이 개발 워크플로의 핵심 단계가 되었습니다.
이번 장에서는 REST API의 구조적 한계인 오버페칭과 언더페칭 문제를 살펴보고, 이를 해결하기 위해 등장한 GraphQL의 핵심 개념을 학습했습니다. 타입 시스템, 스키마, 리졸버라는 세 기둥이 GraphQL의 유연성과 예측 가능성을 만들어냅니다.
GraphQL은 만능 도구가 아니며, 프로젝트의 복잡도와 팀 역량에 따라 신중하게 도입을 결정해야 합니다. 2026년 현재 생태계는 충분히 성숙했으며, Federation, Code Generation, Persisted Queries 등의 실전 패턴이 안정화되었습니다.
2장에서는 GraphQL API의 근간이 되는 스키마 설계 패턴을 다룹니다. 스키마 퍼스트와 코드 퍼스트 접근법의 차이, Node 인터페이스와 글로벌 ID를 활용한 타입 설계, 그리고 Relay 스타일 커서 페이지네이션 패턴을 상세히 살펴보겠습니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
GraphQL 스키마 설계의 두 가지 접근법과 실전 타입 설계 패턴을 다룹니다. Node 인터페이스, Relay 커서 페이지네이션, 유니온/인터페이스 등 프로덕션 수준의 스키마 설계 원칙을 학습합니다.
GraphQL 리졸버 체인의 실행 흐름과 컨텍스트 설계를 이해합니다. 데이터 소스 추상화, 부모-자식 리졸버 관계, Apollo Server와 GraphQL Yoga에서의 실전 구현 패턴을 다룹니다.
GraphQL에서 가장 흔한 성능 문제인 N+1 문제의 원인을 분석하고, DataLoader를 통한 배칭과 캐싱 최적화를 구현합니다. 쿼리 복잡도 분석과 깊이 제한 전략도 다룹니다.