GraphQL 입문
RESTful API는 다음과 같은 몇 가지 한계점을 가질 수 있습니다.
- 오버페칭 (Over-fetching): 클라이언트가 필요로 하는 데이터보다 더 많은 데이터를 서버에서 가져오는 경우. (예: 사용자 이름만 필요한데 모든 사용자 정보가 포함된 응답을 받는 경우)
- 언더페칭 (Under-fetching): 클라이언트가 여러 종류의 데이터를 얻기 위해 여러 번의 API 요청을 보내야 하는 경우. (예: 사용자 정보와 해당 사용자의 게시물 목록을 가져오기 위해 두 번의 요청을 보내는 경우)
- 잦은 엔드포인트 변경: 프론트엔드 요구사항이 바뀔 때마다 백엔드 엔드포포인트를 수정하거나 새로 만들어야 하는 경우.
이 문제를 해결하고 클라이언트가 필요한 데이터를 정확하고 효율적으로 가져오도록 설계된 기술이 GraphQL입니다. GraphQL은 페이스북(현 Meta)에서 개발한 쿼리 언어이자 런타임으로, 모던 웹/모바일 개발에서 빠르게 채택되고 있습니다.
이번 장에서는 GraphQL 기본 개념, RESTful API와의 차이, 핵심 기능을 정리합니다. 목표는 데이터 통신 효율을 높이고 프론트엔드-백엔드 협업 구조를 개선하는 포인트를 이해하는 것입니다.
GraphQL 이란?
GraphQL은 API를 위한 쿼리 언어(Query Language)이자, 이 쿼리를 사용하여 서버에서 데이터를 가져오는 런타임(Runtime)입니다. 클라이언트가 API로부터 정확히 필요한 데이터만을 요청할 수 있도록 함으로써, 오버페칭과 언더페칭 문제를 해결하고 효율적인 데이터 통신을 가능하게 합니다.
GraphQL의 핵심 아이디어
- 하나의 엔드포인트: GraphQL API는 일반적으로
/graphql과 같이 단 하나의 HTTP 엔드포인트를 가집니다. 클라이언트는 모든 종류의 데이터를 이 단일 엔드포인트를 통해 요청합니다. - 클라이언트가 요청하는 데이터의 형태를 결정: 클라이언트는 서버에게 이러이러한 형태의 데이터를 달라고 명시적인 쿼리를 보냅니다. 서버는 클라이언트가 요청한 형태와 동일한 JSON 응답을 반환합니다.
- 타입 시스템: GraphQL은 강력한 타입 시스템을 가지고 있습니다. API 스키마를 통해 서버가 제공하는 데이터의 형태를 명확하게 정의하고, 클라이언트는 이 스키마를 기반으로 쿼리를 작성하여 자동 완성 및 유효성 검사의 이점을 누릴 수 있습니다.
GraphQL vs. RESTful API
| 특징 | GraphQL | RESTful API |
|---|---|---|
| 엔드포인트 | 단일 엔드포인트 (/graphql) | 리소스별로 여러 엔드포인트 (/users, /posts/123) |
| 데이터 요청 | 클라이언트가 쿼리로 필요한 필드 명시 | 서버가 정의한 고정된 데이터 구조 반환 |
| 데이터 양 | 필요한 데이터만 가져옴 (No Over/Under-fetching) | 오버페칭/언더페칭 발생 가능 |
| 버전 관리 | 스키마 확장성으로 버전 관리 용이 | 새로운 버전 API 엔드포인트 생성 (/v1/users, /v2/users) |
| API 탐색 | 인트로스펙션(Introspection)으로 스키마 탐색 용이 | OpenAPI/Swagger 문서 등 별도 문서 필요 |
| 캐싱 | 클라이언트 캐싱 전략 직접 구현 필요 | HTTP 캐싱 메커니즘 활용 가능 (GET 요청) |
| 복잡도 | 초기 학습 곡선 존재, 스키마 설계 중요 | 단순한 CRUD에 직관적, 익숙한 개발자가 많음 |
GraphQL의 핵심 구성 요소
GraphQL을 이해하고 사용하기 위해 알아야 할 주요 개념들입니다.
스키마 (Schema)와 타입 (Types)
- 스키마: GraphQL API에서 사용할 수 있는 모든 데이터의 구조와 기능을 정의하는 핵심입니다. 서버는 이 스키마에 따라 데이터를 제공하고, 클라이언트는 이 스키마를 기반으로 쿼리를 작성합니다.
- 타입: 스키마는 객체 타입(Object Type), 필드(Field), 스칼라 타입(Scalar Type), 열거 타입(Enum Type) 등으로 구성됩니다. 각 필드는 특정 타입을 가집니다.
# 객체 타입 정의 type User { id: ID! # ID!는 필수 값 (Non-Nullable) name: String! email: String posts: [Post!]! # Post 타입의 배열 (필수) } type Post { id: ID! title: String! content: String author: User! # User 타입 참조 }
쿼리 (Query)
- 데이터를 읽어오는(Fetch) 작업입니다. REST의 GET 요청과 유사합니다.
- 클라이언트는 쿼리를 작성하여 서버에 요청하고, 서버는 요청된 필드에 해당하는 데이터만 JSON 형태로 응답합니다.
# 모든 사용자 정보 중 id와 name만 요청 query GetUsers { users { id name } } # 특정 사용자 정보와 해당 사용자의 게시물 제목만 요청 (중첩 쿼리) query GetUserAndPosts($userId: ID!) { # $userId는 변수 user(id: $userId) { id name email posts { id title } } }
뮤테이션 (Mutation)
- 데이터를 수정, 생성, 삭제하는(Write) 작업입니다. REST의 POST, PUT, DELETE 요청과 유사합니다.
- 뮤테이션 또한 쿼리와 유사한 구조를 가지며, 작업 후 변경된 데이터를 다시 요청할 수 있습니다.
# 새로운 게시물 생성 mutation CreatePost($title: String!, $content: String, $authorId: ID!) { createPost(title: $title, content: $content, authorId: $authorId) { id title author { name } } }
서브스크립션 (Subscription)
- 실시간 데이터 업데이트를 구독하는 기능입니다. 웹소켓(WebSocket)을 통해 구현되는 경우가 많습니다.
- 클라이언트는 특정 이벤트가 발생할 때마다 서버로부터 업데이트된 데이터를 자동으로 푸시 받을 수 있습니다.
# 새로운 게시물이 생성될 때마다 알림 받기 subscription OnNewPost { newPost { id title author { name } } }
리졸버 (Resolver)
- 서버 측에서 스키마의 각 필드에 대한 데이터를 실제로 어떻게 가져올지(Resolve) 정의하는 함수입니다.
- 데이터베이스에서 데이터를 가져오거나, 다른 REST API를 호출하거나, 파일을 읽는 등 실제 데이터 처리 로직을 담당합니다.
- GraphQL 서버를 구축할 때 개발자가 직접 구현해야 하는 부분입니다.
GraphQL 개발 생태계 (간략)
GraphQL은 활발한 개발 생태계를 가지고 있습니다.
-
GraphQL 서버 구현
@apollo/server(Node.js): Apollo Server v4+ 공식 패키지로, 최신 유지보수 기준입니다.graphql.js(JavaScript 공식 구현체)graphql-yoga(경량 대안 서버)- Python (
Graphene), Ruby (GraphQL-Ruby), Java (graphql-java) 등 다양한 언어에서 GraphQL 서버를 구현할 수 있습니다.
-
GraphQL 클라이언트 라이브러리
- Apollo Client: React, Vue, Angular 등 다양한 프론트엔드 프레임워크와 통합하기 쉬운 강력한 캐싱 기능을 가진 클라이언트 라이브러리입니다.
- Relay: Facebook에서 개발한 클라이언트 라이브러리로, React와 긴밀하게 통합되어 있습니다.
urql: 가볍고 유연한 GraphQL 클라이언트.graphql-request: 단순한 GraphQL 클라이언트.
-
개발 도구
- GraphiQL: 브라우저 기반의 GraphQL IDE로, 쿼리 작성, 스키마 탐색, 실행 등을 할 수 있습니다.
- Apollo Sandbox: Apollo Server 환경에서 기본으로 연동하기 쉬운 최신 테스트 도구입니다.
GraphQL 시작하기
간단한 GraphQL 서버를 구축하는 흐름을 살펴보겠습니다.
mkdir my-graphql-server
cd my-graphql-server
npm init -y
npm install @apollo/server graphql express cors body-parser @as-integrations/express4
npm install --save-dev nodemon # 개발용 서버 자동 재시작apollo-server 패키지는 구버전 예시에서 자주 보이지만, 현재는 @apollo/server와 프레임워크 통합 패키지를 함께 사용하는 구성이 권장됩니다.
index.mjs 파일 생성 (GraphQL 서버 구현)// index.mjs
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@as-integrations/express4';
import express from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
// 1. 스키마 정의 (Schema Definition Language - SDL)
const typeDefs = `
# User 타입 정의
type User {
id: ID!
name: String!
email: String
posts: [Post!]! # 사용자가 작성한 게시물 목록
}
# Post 타입 정의
type Post {
id: ID!
title: String!
content: String
author: User! # 게시물 작성자
}
# 쿼리 타입 (데이터를 읽어오는 작업)
type Query {
users: [User!]! # 모든 사용자 목록
user(id: ID!): User # 특정 ID를 가진 사용자
posts: [Post!]! # 모든 게시물 목록
post(id: ID!): Post # 특정 ID를 가진 게시물
}
# 뮤테이션 타입 (데이터를 생성, 수정, 삭제하는 작업)
type Mutation {
createUser(name: String!, email: String): User!
createPost(title: String!, content: String, authorId: ID!): Post!
}
`;
// 임시 데이터 (실제 프로젝트에서는 데이터베이스에서 가져옴)
let users = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
];
let posts = [
{ id: '101', title: 'GraphQL Introduction', content: '...', authorId: '1' },
{ id: '102', title: 'Learn Apollo Server', content: '...', authorId: '2' },
];
// 2. 리졸버 정의 (데이터를 가져오는 로직)
const resolvers = {
Query: {
users: () => users,
user: (parent, args) => users.find(user => user.id === args.id),
posts: () => posts,
post: (parent, args) => posts.find(post => post.id === args.id),
},
User: { // User 타입의 posts 필드를 리졸브 (중첩 쿼리 처리)
posts: (parent) => posts.filter(post => post.authorId === parent.id),
},
Post: { // Post 타입의 author 필드를 리졸브
author: (parent) => users.find(user => user.id === parent.authorId),
},
Mutation: {
createUser: (parent, args) => {
const newUser = {
id: String(users.length + 1),
name: args.name,
email: args.email,
};
users.push(newUser);
return newUser;
},
createPost: (parent, args) => {
const newPost = {
id: String(posts.length + 101),
title: args.title,
content: args.content,
authorId: args.authorId,
};
posts.push(newPost);
return newPost;
},
},
};
// 3. ApolloServer 인스턴스 생성 및 시작
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
const app = express();
app.use('/graphql', cors(), bodyParser.json(), expressMiddleware(server));
app.listen(4100, () => {
console.log('🚀 Server ready at http://localhost:4100/graphql');
console.log('Explore your API in Apollo Sandbox');
});package.json 스크립트 추가 "scripts": {
"start": "nodemon index.mjs"
},npm start서버가 시작되면 http://localhost:4100/graphql로 접속하여 Apollo Sandbox 또는 GraphiQL에서 직접 쿼리를 테스트할 수 있습니다.
# 예시 쿼리: 모든 사용자 정보와 그들이 작성한 게시물의 제목을 가져옵니다.
query GetUsersWithPosts {
users {
id
name
email
posts {
id
title
}
}
}
# 예시 뮤테이션: 새로운 사용자를 생성합니다.
mutation CreateNewUser {
createUser(name: "Charlie", email: "charlie@example.com") {
id
name
email
}
}GraphQL 핵심 정리
이 절에서는 스키마 중심 설계가 API 변경 비용을 어떻게 줄이는지 실무 관점에서 다시 점검합니다. 이번 장에서는 클라이언트가 원하는 데이터를 정확하게 가져오는 효율적인 API 패러다임인 GraphQL에 대해 학습했습니다.
여러분은 RESTful API의 한계점(오버페칭, 언더페칭, 잦은 엔드포인트 변경)을 이해하고, GraphQL이 이러한 문제들을 어떻게 해결하는지 파악했습니다. 스키마, 타입, 쿼리, 뮤테이션, 서브스크립션, 리졸버와 같은 GraphQL의 핵심 개념들을 배웠고, 이러한 요소들이 어떻게 상호작용하여 유연하고 강력한 데이터 통신을 가능하게 하는지 알아보았습니다. 또한, Apollo Server를 사용한 간단한 GraphQL 서버 구축 예제를 통해 실제 동작 방식을 경험했습니다.
GraphQL은 프론트엔드와 백엔드 간의 데이터 계약을 명확히 하고, 클라이언트 중심의 데이터 요청을 통해 개발 효율성과 애플리케이션 성능을 크게 향상시킬 수 있는 강력한 기술입니다. 모든 프로젝트에 GraphQL이 필요한 것은 아니지만, 복잡한 데이터 요구사항을 가진 애플리케이션이나 여러 클라이언트(웹, 모바일)를 지원해야 하는 경우 매우 유용한 선택지가 될 수 있습니다.
이 장에서 정리한 기준을 바탕으로, 프로젝트 요구사항에 맞는 API 패러다임(REST/GraphQL)을 선택하고 스키마 중심 설계를 운영 관점에서 검증할 수 있습니다. 이로써 나 혼자 웹 개발 교재의 모든 내용을 마치며, 이후에는 서비스 특성에 맞춰 성능·보안·배포 기준을 연결해 확장해 나가면 됩니다.