
GraphQL과 REST API의 차이점을 상세히 비교하고, 각 상황에 맞는 최적의 선택 방법을 알아봅니다.
API 설계는 현대 웹 애플리케이션 개발에서 가장 중요한 결정 중 하나입니다. REST API는 오랫동안 업계 표준으로 자리잡았지만, Facebook이 2015년 GraphQL을 오픈소스로 공개한 이후 많은 개발자들이 새로운 선택지를 고민하고 있습니다. 이 글에서는 두 접근 방식의 핵심 차이점을 깊이 있게 살펴보고, 프로젝트에 맞는 최적의 선택을 할 수 있도록 의사결정 프레임워크를 제공합니다.
REST(Representational State Transfer)는 Roy Fielding이 2000년 박사 논문에서 제안한 아키텍처 스타일입니다. HTTP 프로토콜을 기반으로 하며, 리소스 중심의 설계 철학을 따릅니다.
블로그 시스템의 REST API를 예로 들어보겠습니다:
# 모든 게시글 조회
GET /api/posts
Response: [
{ "id": 1, "title": "첫 번째 글", "authorId": 100 },
{ "id": 2, "title": "두 번째 글", "authorId": 101 }
]
# 특정 게시글 조회
GET /api/posts/1
Response: {
"id": 1,
"title": "첫 번째 글",
"content": "내용...",
"authorId": 100,
"createdAt": "2025-09-30T10:00:00Z"
}
# 저자 정보 조회 (별도 요청 필요)
GET /api/users/100
Response: {
"id": 100,
"name": "홍길동",
"email": "hong@example.com"
}
# 댓글 조회 (또 다른 요청)
GET /api/posts/1/comments
Response: [
{ "id": 1, "postId": 1, "content": "좋은 글이네요", "authorId": 102 }
]위 예제에서 볼 수 있듯이, 게시글과 저자 정보, 댓글을 모두 가져오려면 최소 3번의 HTTP 요청이 필요합니다. 이를 N+1 문제라고 부르며, REST의 주요 단점 중 하나입니다.
GraphQL은 API를 위한 쿼리 언어이자 런타임입니다. Facebook이 모바일 앱의 복잡한 데이터 요구사항을 해결하기 위해 개발했으며, 클라이언트가 필요한 데이터를 정확히 명시할 수 있습니다.
/graphql 하나만 사용동일한 블로그 시스템을 GraphQL로 구현하면:
# 스키마 정의
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
createdAt: DateTime!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
}
type Query {
posts: [Post!]!
post(id: ID!): Post
user(id: ID!): User
}# 단일 쿼리로 모든 데이터 가져오기
query GetPostWithDetails {
post(id: "1") {
id
title
content
createdAt
author {
id
name
}
comments {
id
content
author {
name
}
}
}
}
# 응답 (요청한 필드만 정확히 반환)
{
"data": {
"post": {
"id": "1",
"title": "첫 번째 글",
"content": "내용...",
"createdAt": "2025-09-30T10:00:00Z",
"author": {
"id": "100",
"name": "홍길동"
},
"comments": [
{
"id": "1",
"content": "좋은 글이네요",
"author": {
"name": "김철수"
}
}
]
}
}
}단 한 번의 요청으로 게시글, 저자, 댓글 정보를 모두 가져올 수 있습니다. 또한 email 필드가 필요 없다면 요청하지 않아 불필요한 데이터 전송을 줄일 수 있습니다.
REST의 문제점:
/api/v1/posts, /api/v2/posts 등 버전별 엔드포인트 관리 필요GraphQL의 장점:
예시 비교:
// REST: 사용자 프로필 페이지 로딩
async function loadUserProfile(userId) {
const user = await fetch(`/api/users/${userId}`);
const posts = await fetch(`/api/users/${userId}/posts`);
const followers = await fetch(`/api/users/${userId}/followers`);
const following = await fetch(`/api/users/${userId}/following`);
// 4번의 HTTP 요청, 많은 불필요한 데이터 포함
return { user, posts, followers, following };
}
// GraphQL: 동일한 작업
async function loadUserProfile(userId) {
const result = await fetch('/graphql', {
method: 'POST',
body: JSON.stringify({
query: `
query UserProfile($userId: ID!) {
user(id: $userId) {
name
avatar
bio
posts(limit: 10) {
title
createdAt
}
followerCount
followingCount
}
}
`,
variables: { userId }
})
});
// 1번의 요청, 필요한 데이터만 정확히 반환
return result.data.user;
}REST:
GraphQL:
// GraphQL Code Generator 사용 예
// schema.graphql에서 자동 생성된 타입
import { GetPostQuery, GetPostQueryVariables } from './generated/graphql';
const { data } = useQuery<GetPostQuery, GetPostQueryVariables>(GET_POST_QUERY, {
variables: { id: '1' }
});
// data.post는 완전한 타입 안정성 보장
console.log(data.post.title); // ✅ 타입 체크됨
console.log(data.post.nonExistent); // ❌ 컴파일 오류REST의 장점:
GraphQL의 도전과제:
// Apollo Client 캐싱 설정
import { InMemoryCache } from '@apollo/client';
const cache = new InMemoryCache({
typePolicies: {
Post: {
fields: {
comments: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
}
}
});
// 동일한 데이터를 참조하는 여러 쿼리가 자동으로 동기화됨REST:
GraphQL:
// DataLoader로 N+1 문제 해결
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds) => {
// 여러 userId를 한 번에 조회
const users = await db.users.findMany({
where: { id: { in: userIds } }
});
// userIds 순서에 맞게 정렬하여 반환
return userIds.map(id => users.find(u => u.id === id));
});
// Resolver에서 사용
const resolvers = {
Post: {
author: (post) => userLoader.load(post.authorId)
// 100개 게시글이 있어도 author 쿼리는 1번만 실행됨
}
};REST:
GraphQL:
# GraphQL Subscription 예제
subscription OnCommentAdded($postId: ID!) {
commentAdded(postId: $postId) {
id
content
author {
name
}
}
}// 클라이언트에서 구독
const subscription = client.subscribe({
query: ON_COMMENT_ADDED,
variables: { postId: '1' }
}).subscribe({
next: ({ data }) => {
console.log('새 댓글:', data.commentAdded);
// UI 자동 업데이트
}
});장점:
단점:
장점:
단점:
단순한 CRUD API
높은 캐싱 요구사항
팀의 경험과 학습 곡선
파일 처리가 주요 기능
복잡한 관계형 데이터
모바일 최적화
빠른 프론트엔드 반복
실시간 기능
마이크로서비스 통합
두 방식을 혼합하는 것도 가능합니다:
# 사용 사례별 분리
- GraphQL: 프론트엔드 애플리케이션용 (복잡한 쿼리)
- REST: 외부 API (간단한 공개 엔드포인트)
- REST: 파일 업로드/다운로드 전용
# 점진적 마이그레이션
- 기존 REST API 유지
- 새로운 복잡한 기능은 GraphQL로 구현
- Apollo의 RESTDataSource로 REST → GraphQL 래핑# 상품 목록
GET /api/products?page=1&limit=20
# 상품 상세 (5번의 요청 필요)
GET /api/products/123
GET /api/products/123/reviews
GET /api/products/123/related
GET /api/sellers/456 # 판매자 정보
GET /api/categories/789 # 카테고리 정보
# 장바구니
POST /api/cart/items
GET /api/cart
DELETE /api/cart/items/1# 상품 상세 (1번의 요청)
query ProductDetail($id: ID!) {
product(id: $id) {
id
name
price
images
seller {
name
rating
}
category {
name
breadcrumb
}
reviews(first: 10) {
rating
comment
author {
name
}
}
relatedProducts(limit: 5) {
id
name
price
thumbnail
}
}
}
# Mutation으로 장바구니 관리
mutation AddToCart($productId: ID!, $quantity: Int!) {
addToCart(productId: $productId, quantity: $quantity) {
cart {
items {
product {
name
price
}
quantity
}
totalPrice
}
}
}위 예제에서 GraphQL은 상품 상세 페이지 로딩을 5배 빠르게 만들 수 있습니다 (5번 요청 → 1번 요청).
(드문 경우지만)
GraphQL과 REST는 각각의 강점이 있으며, "어느 것이 더 좋다"는 질문에 정답은 없습니다. 핵심은 프로젝트의 요구사항, 팀의 역량, 유지보수 비용을 종합적으로 고려하는 것입니다.
간단한 가이드라인:
가장 중요한 것은 성급한 최적화를 피하고, 현재 문제를 해결하는 데 집중하는 것입니다. REST로 시작해서 필요할 때 GraphQL로 마이그레이션하거나, 두 가지를 혼합해서 사용하는 것도 완전히 유효한 전략입니다.