GraphQL(Graph Query Language)는 데이터를 효율적으로 가져오고 조작하기 위한 쿼리 언어이자 API와 상호작용 시 사용하는 옵션 및 접근 방식입니다.
REST API와 달리 클라이언트가 필요한 데이터를 명시적으로 요청할 수 있어, 과도한 데이터 전송을 방지하고 여러 데이터 소스에서 데이터를 한 번에 가져올 수 있습니다.
REST API와 구체적으로 어떤 차이가 있는지에 대해서는 아래 포스팅 링크를 참고 바랍니다.
데이터를 읽기(Read) 위해 사용하는 연산입니다. 클라이언트는 필요한 데이터의 구조를 정의하여 서버에게 요청합니다. (REST API로 따지면 GET Method와 같은 역할을 합니다. 다만, Query와 Mutation은 모두 항상 POST 요청 방식을 통해 서버로 전송합니다.)
아래는 간단한 블로그 데이터를 다루는 GraphQL 예시 코드입니다.
// postId라는 변수를 받아 특정 id를 가진 포스트의 정보를 검색하는 쿼리
const getPostQuery = `
query GetPost($postId: ID!) {
getPost(id: $postId) {
id
title
content
}
}
`;
// 모든 포스트에 대한 정보를 검색하는 쿼리
const getAllPostsQuery = `
query {
getAllPosts {
id
title
content
}
}
`;
데이터를 변경하기 위해 사용하는 연산입니다. 데이터의 생성(Create), 수정(Update), 삭제(Delete) 등의 작업을 처리합니다.
뮤테이션을 사용하려면 서버 측에서 이에 해당하는 스키마와 리졸버가 구현되어 있어야 합니다. 만약 스키마나 리졸버가 구현되지 않았다면, 클라이언트는 이러한 뮤테이션을 사용할 수 없습니다.
// title과 content를 받아 새로운 포스트를 생성
// return: 생성된 포스트의 id, title, content 반환
const createPostMutation = `
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
}
}
`;
// 특정 postId를 가진 포스트를 찾아 업데이트
// return: 업데이트된 id, title, content 반환
const updatePostMutation = `
mutation UpdatePost($postId: ID!, $title: String, $content: String) {
updatePost(id: $postId, title: $title, content: $content) {
id
title
content
}
}
`;
// 특정 postId를 가진 포스트를 찾아 삭제
// return: 삭제 결과 Boolean 값으로 반환
const deletePostMutation = `
mutation DeletePost($postId: ID!) {
deletePost(id: $postId)
}
`;
데이터의 타입과 관계를 정의한 문서로, 서버와 클라이언트 간의 계약 역할을 합니다. (GraphQL의 타입에 대한 내용은 아래에 조금 더 다룰 것입니다.)
// 스키마 정의
const schema = buildSchema(`
type Post {
id: ID!
title: String!
content: String!
}
type Query {
getPost(id: ID!): Post
getAllPosts: [Post]
}
type Mutation {
createPost(title: String!, content: String!): Post
updatePost(id: ID!, title: String, content: String): Post
deletePost(id: ID!): Boolean
}
`);
서버에서 클라이언트의 쿼리에 대한 응답을 생성하는 함수로, 스키마에 정의된 타입과 필드에 대한 로직을 구현합니다.
// 리졸버 구현
const root = {
getPost: ({ id }) => posts.find(post => post.id === id),
getAllPosts: () => posts,
createPost: ({ title, content }) => {
const newPost = { id: String(posts.length + 1), title, content };
posts.push(newPost);
return newPost;
},
updatePost: ({ id, title, content }) => {
const postIndex = posts.findIndex(post => post.id === id);
if (postIndex !== -1) {
posts[postIndex] = { ...posts[postIndex], title, content };
return posts[postIndex];
}
return null;
},
deletePost: ({ id }) => {
const postIndex = posts.findIndex(post => post.id === id);
if (postIndex !== -1) {
posts.splice(postIndex, 1);
return true;
}
return false;
},
};
Node.js와 Express를 사용하여 구현되었다고 가정합니다.
// server.js
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
// 스키마 정의
const schema = buildSchema(`
type Post {
id: ID!
title: String!
content: String!
}
type Query {
getPost(id: ID!): Post
getAllPosts: [Post]
}
type Mutation {
createPost(title: String!, content: String!): Post
updatePost(id: ID!, title: String, content: String): Post
deletePost(id: ID!): Boolean
}
`);
// 더미 데이터
const posts = [
{ id: '1', title: 'GraphQL Basics', content: 'Introduction to GraphQL' },
{ id: '2', title: 'GraphQL Resolvers', content: 'How to use resolvers' },
];
// 리졸버 구현
const root = {
getPost: ({ id }) => posts.find(post => post.id === id),
getAllPosts: () => posts,
createPost: ({ title, content }) => {
const newPost = { id: String(posts.length + 1), title, content };
posts.push(newPost);
return newPost;
},
updatePost: ({ id, title, content }) => {
const postIndex = posts.findIndex(post => post.id === id);
if (postIndex !== -1) {
posts[postIndex] = { ...posts[postIndex], title, content };
return posts[postIndex];
}
return null;
},
deletePost: ({ id }) => {
const postIndex = posts.findIndex(post => post.id === id);
if (postIndex !== -1) {
posts.splice(postIndex, 1);
return true;
}
return false;
},
};
// Express 앱 설정
const app = express();
app.use('/graphql', graphqlHTTP({ schema, rootValue: root, graphiql: true }));
// 서버 시작
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}/graphql`);
});
아래는 Node.js에서의 GraphQL 클라이언트 예시 코드입니다. fetch를 사용하여 GraphQL 쿼리 및 뮤테이션을 서버에 전송합니다. (위에서 언급 했듯이, fetch 시 Query와 Mutation은 모두 항상 POST Method로 보내는 것을 확인할 수 있습니다.)
const fetch = require('node-fetch');
// GraphQL 엔드포인트
const graphqlEndpoint = 'http://localhost:3000/graphql';
// GraphQL 쿼리 예시
const getPostQuery = `
query GetPost($postId: ID!) {
getPost(id: $postId) {
id
title
content
}
}
`;
const getAllPostsQuery = `
query {
getAllPosts {
id
title
content
}
}
`;
// GraphQL 뮤테이션 예시
const createPostMutation = `
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
}
}
`;
const updatePostMutation = `
mutation UpdatePost($postId: ID!, $title: String, $content: String) {
updatePost(id: $postId, title: $title, content: $content) {
id
title
content
}
}
`;
const deletePostMutation = `
mutation DeletePost($postId: ID!) {
deletePost(id: $postId)
}
`;
// GraphQL 쿼리 및 뮤테이션 실행 함수
async function executeGraphQL(query, variables = {}) {
const response = await fetch(graphqlEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
const result = await response.json();
return result.data;
}
// 예시: 특정 포스트 가져오기
const postIdToRetrieve = '1';
executeGraphQL(getPostQuery, { postId: postIdToRetrieve })
.then(data => console.log('Get Post:', data.getPost));
// 예시: 모든 포스트 가져오기
executeGraphQL(getAllPostsQuery)
.then(data => console.log('Get All Posts:', data.getAllPosts));
// 예시: 포스트 생성
const newPostData = {
title: 'New Post',
content: 'This is a new post created with GraphQL mutation.',
};
executeGraphQL(createPostMutation, newPostData)
.then(data => console.log('Created Post:', data.createPost));
// 예시: 포스트 업데이트
const updatedPostData = {
postId: '1',
title: 'Updated Post Title',
};
executeGraphQL(updatePostMutation, updatedPostData)
.then(data => console.log('Updated Post:', data.updatePost));
// 예시: 포스트 삭제
const postIdToDelete = '2';
executeGraphQL(deletePostMutation, { postId: postIdToDelete })
.then(data => console.log('Delete Post Result:', data.deletePost));
타입은 어떤 종류의 데이터를 나타내는지를 지정하며, GraphQL 스키마에서 데이터 모델을 정의할 때 사용됩니다.
scalar
키워드를 통해 스칼라 타입을 정의하여 사용할 수 있습니다.
scalar Date # 스칼라 타입 정의
type Character {
id: ID!
name: String!
createdAt: Date # 정의한 Date 스칼라 타입 사용
}
타입 뒤에 느낌표 !
를 추가하면 Non-null 속성이 지정되어, 서버는 항상 이 필드에 대해 null이 아닌 값을 반환할 것을 기대하며(해당 값이 null이 아닌 것을 보장), null값이 발생되면 GraphQL 실행 오류가 발생하고, 클라이언트에게 무언가 잘못되었음을 알립니다.
type Character {
id: ID!
name: String!
}
리스트 타입과 Non-null 연산자를 함께 사용할 때, 아래와 같이 연산자가 어디에 위치해 있는지에 따라 의미가 달라질 수 있습니다.
[String]
: 배열 안에 담긴 문자열의 값은 null 가능.[String!]
: 배열 안에 담긴 문자열의 값은 null 불가능.[String]!
: 배열 안에 담긴 문자열의 값은 null이 가능하나, 배열은 null 불가능[String!]!
: 배열 안에 담긴 문자열의 값과 배열 둘 다 null 불가능열거 타입은 특정한 값의 집합을 나타냅니다. 허용된 값 중 하나임을 검증하며, enum 키워드를 이용하여 타입을 생성할 수 있습니다.
# 열거 타입 생성
enum Status {
ACTIVE
INACTIVE
PENDING
}
type Task {
# 반환되는 status의 타입은 ACTIVE, INACTIVE, PENDING 중 하나
status: Status
}
GraphQL에서도 인터페이스를 지원합니다. interface
키워드를 사용하여 정의할 수 있습니다.
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
따라서 인터페이스 타입은 객체 타입의 공통 필드를 정의하는 데 사용됩니다. 여러 객체가 동일한 인터페이스를 구현할 수 있습니다. (인터페이스로 정의한 타입을 다른 타입에 implements할 수 있습니다.)
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
유니언 타입은 여러 타입 중 하나일 수 있는 값을 나타냅니다. 즉, 서로 다른 타입의 값들을 하나의 필드로 표현할 수 있습니다.
유니언 타입은 union
키워드를 사용하여 정의하며, 타입 사이에 |
연산자를 사용하여 표현합니다. 단, 인터페이스나 유니온 타입에서 다른 유니온 타입을 사용할 수 없습니다.
union SearchResult = Book | Author | Magazine
type Book {
title: String
author: String
}
type Author {
name: String
books: [String]
}
type Magazine {
title: String
publisher: String
}
위의 예제에서 SearchResult는 Book, Author, 또는 Magazine 중 하나일 수 있는 유니언 타입입니다. 클라이언트는 이 유니언 타입을 사용하여 쿼리 결과로부터 책, 작가, 또는 잡지 정보를 동시에 다룰 수 있습니다.
인풋 타입은 쿼리나 뮤테이션에 대한 인자(arguments)로 객체를 전달할 수 있는데, input
키워드를 사용하여 이 인자로 전달할 객체에 대한 유형을 정의할 수 있습니다. 즉, 서버로 전달되는 데이터의 구조를 정의합니다.
아래는 input type없이 mutation의 인자들에 대해 직접 타입을 지정하는 방식입니다.
# createUser의 인자들의 타입을 직접 정의하는 방식
type Mutation {
createUser(username: String, email: String, password: String): User
}
type User {
id: ID
username: String
email: String
}
아래는 input type을 선언하여 mutation의 인자들의 타입으로 지정하는 방식입니다.
# input 타입 선언
input CreateUserInput {
username: String
email: String
password: String
}
# input 타입 지정
type Mutation {
createUser(input: CreateUserInput): User
}
type User {
id: ID
username: String
email: String
}