GraphQL 입문
RESTful API는 다음과 같은 몇 가지 한계점을 가질 수 있습니다.
- 오버페칭 (Over-fetching): 클라이언트가 필요로 하는 데이터보다 더 많은 데이터를 서버에서 가져오는 경우. (예: 사용자 이름만 필요한데 모든 사용자 정보가 포함된 응답을 받는 경우)
- 언더페칭 (Under-fetching): 클라이언트가 여러 종류의 데이터를 얻기 위해 여러 번의 API 요청을 보내야 하는 경우. (예: 사용자 정보와 해당 사용자의 게시물 목록을 가져오기 위해 두 번의 요청을 보내는 경우)
- 잦은 엔드포인트 변경: 프론트엔드 요구사항이 바뀔 때마다 백엔드 엔드포포인트를 수정하거나 새로 만들어야 하는 경우.
이러한 문제들을 해결하고 클라이언트가 필요한 데이터를 정확하고 효율적으로 가져올 수 있도록 페이스북(현 Meta)에서 개발한 쿼리 언어(Query Language)이자 런타임이 바로 GraphQL입니다. GraphQL은 API 개발의 새로운 패러다임을 제시하며, 모던 웹 및 모바일 애플리케이션 개발에서 빠르게 채택되고 있습니다.
이번 장에서는 GraphQL의 기본 개념과 RESTful API와의 차이점, 그리고 GraphQL이 제공하는 핵심 기능들을 학습할 것입니다. 이를 통해 여러분은 데이터 통신의 효율성을 극대화하고 프론트엔드와 백엔드의 유기적인 협업을 강화하는 GraphQL의 매력을 이해할 수 있을 것입니다.
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): 가장 널리 사용되는 GraphQL 서버 라이브러리입니다.
graphql.js
(JavaScript 공식 구현체)graphql-yoga
(간단한 Express 기반 서버)- 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로, 쿼리 작성, 스키마 탐색, 실행 등을 할 수 있습니다. 대부분의 GraphQL 서버가 기본으로 제공합니다.
- GraphQL Playground: GraphiQL과 유사한 기능을 제공하는 인기 있는 IDE.
GraphQL 시작하기
간단한 GraphQL 서버를 구축하는 흐름을 살펴보겠습니다.
1. 프로젝트 초기화 및 패키지 설치
mkdir my-graphql-server
cd my-graphql-server
npm init -y
npm install apollo-server graphql
npm install --save-dev nodemon # 개발용 서버 자동 재시작
2. index.js
파일 생성 (GraphQL 서버 구현)
// index.js
const { ApolloServer } = require('apollo-server');
const { gql } = require('apollo-server');
// 1. 스키마 정의 (Schema Definition Language - SDL)
const typeDefs = gql`
# 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 });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
console.log(`Explore your API at ${url}`);
});
3. package.json
스크립트 추가
"scripts": {
"start": "nodemon index.js"
},
4. 서버 실행
npm start
서버가 시작되면 콘솔에 출력된 URL(예: http://localhost:4000/
)로 접속하여 GraphQL Playground (또는 GraphiQL)를 통해 직접 쿼리를 테스트해볼 수 있습니다.
# 예시 쿼리: 모든 사용자 정보와 그들이 작성한 게시물의 제목을 가져옵니다.
query GetUsersWithPosts {
users {
id
name
email
posts {
id
title
}
}
}
# 예시 뮤테이션: 새로운 사용자를 생성합니다.
mutation CreateNewUser {
createUser(name: "Charlie", email: "charlie@example.com") {
id
name
email
}
}
마무리하며
이번 장에서는 클라이언트가 원하는 데이터를 정확하게 가져오는 효율적인 API 패러다임인 GraphQL에 대해 학습했습니다.
여러분은 RESTful API의 한계점(오버페칭, 언더페칭, 잦은 엔드포인트 변경)을 이해하고, GraphQL이 이러한 문제들을 어떻게 해결하는지 파악했습니다. 스키마, 타입, 쿼리, 뮤테이션, 서브스크립션, 리졸버와 같은 GraphQL의 핵심 개념들을 배웠고, 이러한 요소들이 어떻게 상호작용하여 유연하고 강력한 데이터 통신을 가능하게 하는지 알아보았습니다. 또한, Apollo Server를 사용한 간단한 GraphQL 서버 구축 예제를 통해 실제 동작 방식을 경험했습니다.
GraphQL은 프론트엔드와 백엔드 간의 데이터 계약을 명확히 하고, 클라이언트 중심의 데이터 요청을 통해 개발 효율성과 애플리케이션 성능을 크게 향상시킬 수 있는 강력한 기술입니다. 모든 프로젝트에 GraphQL이 필요한 것은 아니지만, 복잡한 데이터 요구사항을 가진 애플리케이션이나 여러 클라이언트(웹, 모바일)를 지원해야 하는 경우 매우 유용한 선택지가 될 수 있습니다.
이 장에서 배운 지식들을 바탕으로 여러분의 웹 개발 역량을 한 단계 더 높이고, 변화하는 웹 기술 동향을 주도적으로 이해하고 적용할 수 있기를 바랍니다. 이로써 '나 혼자 웹 개발' 교재의 모든 내용을 마치며, 웹 개발의 중요한 여정을 마무리합니다. 앞으로도 끊임없이 학습하고 도전하여 훌륭한 웹 개발자로 성장하시길 응원합니다.