[GraphQL] GraphQL의 기본 형식, 데이터 통신 방법 및 Types

Janet·2023년 12월 28일
0

GraphQL

목록 보기
2/3

GraphQL의 기본 형식, 데이터 통신 방법 및 GraphQL Types


GraphQL이란?

GraphQL(Graph Query Language)는 데이터를 효율적으로 가져오고 조작하기 위한 쿼리 언어이자 API와 상호작용 시 사용하는 옵션 및 접근 방식입니다.

REST API와 달리 클라이언트가 필요한 데이터를 명시적으로 요청할 수 있어, 과도한 데이터 전송을 방지하고 여러 데이터 소스에서 데이터를 한 번에 가져올 수 있습니다.

REST API와 구체적으로 어떤 차이가 있는지에 대해서는 아래 포스팅 링크를 참고 바랍니다.


GraphQL 기본 형식 및 데이터 통신 방법


1. Query(쿼리)

데이터를 읽기(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
    }
  }
`;

2. Mutation(뮤테이션)

데이터를 변경하기 위해 사용하는 연산입니다. 데이터의 생성(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)
  }
`;

3. Schema(스키마)

데이터의 타입과 관계를 정의한 문서로, 서버와 클라이언트 간의 계약 역할을 합니다. (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
  }
`);

4. Resolver(리졸버)

서버에서 클라이언트의 쿼리에 대한 응답을 생성하는 함수로, 스키마에 정의된 타입과 필드에 대한 로직을 구현합니다.

// 리졸버 구현
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;
  },
};

전체 예시 코드1 - Schema, Resolver

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`);
});

전체 예시 코드2 - Query, Mutation

아래는 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 Types


타입은 어떤 종류의 데이터를 나타내는지를 지정하며, GraphQL 스키마에서 데이터 모델을 정의할 때 사용됩니다.

1. Default Scalar Types

  • Int: 32비트 부호 있는 정수
  • Float: 부동 소수점 숫자
  • String: UTF‐8 문자 시퀀스
  • Boolean: true/false
  • ID: 고유 식별자. 객체를 refetch하거나 캐시의 키로 사용되는 고유값.

2. Custom Scalar Types

scalar 키워드를 통해 스칼라 타입을 정의하여 사용할 수 있습니다.

scalar Date # 스칼라 타입 정의

type Character {
	id: ID!
    name: String!
    createdAt: Date # 정의한 Date 스칼라 타입 사용
}

3. Non-null 연산자와 List Type

타입 뒤에 느낌표 ! 를 추가하면 Non-null 속성이 지정되어, 서버는 항상 이 필드에 대해 null이 아닌 값을 반환할 것을 기대하며(해당 값이 null이 아닌 것을 보장), null값이 발생되면 GraphQL 실행 오류가 발생하고, 클라이언트에게 무언가 잘못되었음을 알립니다.

type Character {
	id: ID!
    name: String!
}

리스트 타입과 Non-null 연산자를 함께 사용할 때, 아래와 같이 연산자가 어디에 위치해 있는지에 따라 의미가 달라질 수 있습니다.

  • [String] : 배열 안에 담긴 문자열의 값은 null 가능.
  • [String!] : 배열 안에 담긴 문자열의 값은 null 불가능.
  • [String]! : 배열 안에 담긴 문자열의 값은 null이 가능하나, 배열은 null 불가능
  • [String!]! : 배열 안에 담긴 문자열의 값과 배열 둘 다 null 불가능

4. Enumeration Types

열거 타입은 특정한 값의 집합을 나타냅니다. 허용된 값 중 하나임을 검증하며, enum 키워드를 이용하여 타입을 생성할 수 있습니다.

# 열거 타입 생성
enum Status {
  ACTIVE
  INACTIVE
  PENDING
}

type Task {
  # 반환되는 status의 타입은 ACTIVE, INACTIVE, PENDING 중 하나
  status: Status
}

5. Interfaces

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
}

6. Union Types

유니언 타입은 여러 타입 중 하나일 수 있는 값을 나타냅니다. 즉, 서로 다른 타입의 값들을 하나의 필드로 표현할 수 있습니다.
유니언 타입은 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 중 하나일 수 있는 유니언 타입입니다. 클라이언트는 이 유니언 타입을 사용하여 쿼리 결과로부터 책, 작가, 또는 잡지 정보를 동시에 다룰 수 있습니다.

7. Input Types

인풋 타입은 쿼리나 뮤테이션에 대한 인자(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
}
profile
😸

0개의 댓글