[๋„คํŠธ์›Œํฌ] ๐ŸŒธGraphQL ์‚ฌ์šฉ๋ฒ•

TATAยท2023๋…„ 3์›” 28์ผ
0

๋„คํŠธ์›Œํฌ

๋ชฉ๋ก ๋ณด๊ธฐ
6/8

โ–ท GraphQL

Graph + Query Language

Server API๋ฅผ ํ†ตํ•ด ์ •๋ณด๋ฅผ ์ฃผ๊ณ ๋ฐ›๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ์ฟผ๋ฆฌ ์–ธ์–ด์ด๋‹ค.

GraphQL์€ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ๊ทธ๋ž˜ํ”„ ํ˜•ํƒœ๋กœ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋‹ค๊ณ  ๋ณธ๋‹ค.
(๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ์„œ๋กœ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์–ด์„œ ์ƒํ˜ธ์ž‘์šฉํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธ)

GraphQL์—์„œ๋Š” ์ด๋Ÿฌํ•œ ๊ทธ๋ž˜ํ”„ ๊ตฌ์กฐ๋ฅผ ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›๊ธฐ ์œ„ํ•ด ํƒ์ƒ‰ํ•œ๋‹ค. ์ฆ‰, ํด๋ผ์ด์–ธํŠธ์—์„œ ์š”์ฒญํ•œ ์ฟผ๋ฆฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์„œ๋ฒ„๋Š” ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์•„์„œ ํŠธ๋ฆฌ ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์ด๋ ‡๊ฒŒ ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ๋ฐ˜ํ™˜๋˜๋Š” ๊ฒฐ๊ณผ๋Š” ํด๋ผ์ด์–ธํŠธ์—์„œ ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ์„ ํƒ์ ์œผ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜์ง€ ์•Š์•„๋„ ๋˜๊ณ , ๋ฐ์ดํ„ฐ ์ „์†ก ์‹œ๊ฐ„๊ณผ ๋Œ€์—ญํญ์„ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ๋‹ค.

โ—๏ธGraphQL์˜ ํŠน์ง•
โˆ’ HTTP๋ฅผ ํ†ตํ•ด API ์„œ๋ฒ„๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์‘๋‹ต์„ ๋ฐ›๋Š”๋‹ค.
โˆ’ ์‘๋‹ต์„ ๋ฐ›์„ ์‹œ, ๋ฐ์ดํ„ฐ ๊ฒฐ๊ณผ๋ฅผ JSON ํ˜•์‹์œผ๋กœ ๋ฐ›๋Š”๋‹ค.
โˆ’ ์„œ๋ฒ„ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž‘์„ฑํ•œ ๊ฐ ํ•„๋“œ์— ๋Œ€์‘ํ•˜๋Š” resolver ํ•จ์ˆ˜๋กœ ๊ฐ ํ•„๋“œ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค.
โˆ’ GraphQL ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์กฐํšŒ ๋Œ€์ƒ schema๊ฐ€ ์œ ํšจํ•œ์ง€ ๊ฒ€์‚ฌํ•œ๋‹ค.


โ–ท GraphQL ์‚ฌ์šฉ๋ฒ•

Query : ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
Mutation : ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ ์ˆ˜์ •ํ•˜๊ธฐ
ย  ย  ย  โ†’ Create: ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
ย  ย  ย  โ†’ Update: ๊ธฐ์กด์˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ •
ย  ย  ย  โ†’ Delete: ๊ธฐ์กด์˜ ๋ฐ์ดํ„ฐ ์‚ญ์ œ
Subscription : ํŠน์ • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒ ์‹œ ์„œ๋ฒ„๊ฐ€ ๋Œ€์‘ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†ก

๐ŸŒธ ์ฟผ๋ฆฌ - query, ๋ฐ์ดํ„ฐ ์กฐํšŒ

ํ•„๋“œ(field)

// ์š”์ฒญ
{
  hero {
    name
  }
}

-------
// ์–ป์€ ๊ฒฐ๊ณผ
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

์ค‘์ฒฉ ํ•„๋“œ

# ์š”์ฒญ
{
  hero {
    name
    # ์ด๋Ÿฐ ์‹์œผ๋กœ GraphQL ๋‚ด์—์„œ ์ฃผ์„๋„ ์ž‘์„ฑ ๊ฐ€๋Šฅ
    friends {
      name
    }
  }
}

-------
# ์–ป์€ ๊ฒฐ๊ณผ
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

์ „๋‹ฌ์ธ์ž(Arguments)

ํ•„๋“œ์— ์ธ์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋งŒ ์„ ํƒ์ ์œผ๋กœ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

// ์š”์ฒญ
// id๊ฐ€ 1000์ธ human์˜ name๊ณผ height๋ฅผ ์ฟผ๋ฆฌ
{
  human(id: "1000") {
    name
    height
  }
}

-------
// ์–ป์€ ๊ฒฐ๊ณผ
// id๊ฐ€ 1000์ธ human์˜ ์ด๋ฆ„๊ณผ ํ‚ค๋ฅผ ์ฟผ๋ฆฌํ•ด ์˜ด
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 1.72
    }
  }
}

๋ณ„๋ช…(Aliases)

ํ•„๋“œ ์ด๋ฆ„ ์ค‘๋ณต ์‹œ, ๋ณ„๋ช…์„ ์‚ฌ์šฉํ•ด ์ฟผ๋ฆฌํ•œ๋‹ค.

// ์š”์ฒญ
{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}

-------
// ์–ป์€ ๊ฒฐ๊ณผ
// hero๊ฐ€ ์•„๋‹Œ empireHero์™€ jediHero๋กœ ๋ฐ”๋€œ
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

์˜คํผ๋ ˆ์ด์…˜ ๋„ค์ž„(Operation name)

์‹ค์ œ๋กœ๋Š” ์ง€๊ธˆ๊นŒ์ง€์˜ ์œ„์˜ ์ฝ”๋“œ์™€๋Š” ๋‹ค๋ฅด๊ฒŒ
์•„๋ž˜์ฒ˜๋Ÿผ query keyword์™€ query name์„ ์ ์–ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

// ์š”์ฒญ
// query๋Š” ์˜คํผ๋ ˆ์ด์…˜ ํƒ€์ž…์ด๋‹ค.
// ์˜คํผ๋ ˆ์ด์…˜ ํƒ€์ž… ์ข…๋ฅ˜: query, mutation, subscription, describes ๋“ฑ..
query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}

-------
// ์–ป์€ ๊ฒฐ๊ณผ
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

๋ณ€์ˆ˜(Variables)

์‹ค์ œ ์•ฑ์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๊ณ ์ •๋œ ์ธ์ˆ˜๋ฅผ ๋ฐ›๋Š” ๊ฒƒ๋ณด๋‹ค๋Š”
๋™์ ์œผ๋กœ ์ธ์ˆ˜๋ฅผ ๋ฐ›์•„ ์ฟผ๋ฆฌ ํ•œ๋‹ค.

// ๋ณ€์ˆ˜๋ฅผ ์จ์„œ ์ž‘์„ฑ๋œ ์ฟผ๋ฆฌ
// $episode: Episode ๋’ค์— !๊ฐ€ ๋ถ™๋Š”๋‹ค๋ฉด
// episode๋Š” ๋ฐ˜๋“œ์‹œ Episode์—ฌ์•ผ ํ•œ๋‹ค๋Š” ๋œป์ด๋‹ค.
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

๐ŸŒธ ๋ฎคํ…Œ์ด์…˜ - mutation, ๋ฐ์ดํ„ฐ ์ˆ˜์ •

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

๐ŸŒธ ์Šคํ‚ค๋งˆ / ํƒ€์ž… - Schema / Type

์Šคํ‚ค๋งˆ๋Š” ์„œ๋น„์Šค์—์„œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด์˜ ์ข…๋ฅ˜์™€
ํ•ด๋‹น ๊ฐ์ฒด๊ฐ€ ๊ฐ€์ง€๋Š” ํ•„๋“œ๋“ค์„ ์ •์˜ํ•œ๋‹ค.

// ๊ฐ ๊ฐ์ฒด ์œ ํ˜•๊ณผ ํ•„๋“œ๋“ค์˜ ๋ฐ์ดํ„ฐ ํƒ€์ž…, ์ธ์ž, ๋ฐ˜ํ™˜๊ฐ’ ๋“ฑ์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค.
// !๋ฅผ ๋ถ™์ด๋ฉด, ๋ฐ˜๋“œ์‹œ ๊ฐ’์„ ๋ฐ›๋Š”๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.
// []๋ฐฐ์—ด์— !๊ฐ€ ๋ถ™์œผ๋ฉด null๊ฐ’์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๋œป!
type Character {
  name: String!
  appearsIn: [Episode!]!
}

๐ŸŒธ ๋ฆฌ์กธ๋ฒ„ - Resolver

์Šคํ‚ค๋งˆ์—์„œ ์ •์˜๋œ ํƒ€์ž…(Query, Mutation, Subscription)์— ๋Œ€ํ•œ
๋™์ž‘ ๋ฐฉ์‹์„ ์ •์˜ํ•˜๊ณ , ๋ฐ์ดํ„ฐ ์†Œ์Šค์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ ์ ์ ˆํ•œ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

const db = require("./../db")
const resolvers = {
  Query: { // **Query :** ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (REST ์— GET ๊ณผ ๋น„์Šทํ•ฉ๋‹ˆ๋‹ค.)
		getUser: async (_, { email, pw }) => {
			db.findOne({
				where: { email, pw }
			}) ... // ์‹ค์ œ ๋””๋น„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. 
			...
		}
  },
  Mutation: { // **Mutation :** ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ ์ˆ˜์ •ํ•˜๊ธฐ ( Create , Update , Delete )
		createUser: async (_, { email, pw, name }) => {
			...
		}
  }
  Subscription: { // **Subscription :** ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
    newUser: async () => {
      ...
		}
  }
};

โ–ท GitHub GraphQL API

GitHub GraphQL API์—์„œ ์ œ๊ณตํ•˜๋Š” explorer๋Š”
์ผ์ข…์˜ playground๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

GitHub ์•„์ด๋””๋กœ ๋กœ๊ทธ์ธํ•˜๋ฉด ์ž์‹ ์ด ๋งŒ๋“  ๋ ˆํฌ์ง€ํ† ๋ฆฌ์™€ ์ด๋ ฅ,
๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ๋งŒ๋“  ๋ ˆํฌ์ง€ํ† ๋ฆฌ์—๋„ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.

๐ŸŒธ playground ์‚ฌ์šฉ๋ฒ•

โ’ˆ explorer์— ์ ‘์† โ†’ https://docs.github.com/en/graphql/overview/explorer
โ’‰ ์ž์‹ ์˜ GitHub id๋กœ ๋กœ๊ทธ์ธ
โ’Š GraphQL ์ฟผ๋ฆฌ๋ฅผ ์ด์šฉํ•˜์—ฌ ์ž์‹ ์ด ๋งŒ๋“ค์—ˆ๋˜ repository๋“ค์— ์ ‘๊ทผํ•ด ์กฐํšŒ
โ’‹ ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด Reference๋ฅผ ๋ณด๊ณ  ์ž‘์„ฑ โ†’ https://docs.github.com/en/graphql/reference/queries
โ’Œ ๋˜๋Š” Explorer์˜ Explorer ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์ง์ ‘ ๊ด€๋ จ๋œ field๋ฅผ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ๋‹ค.


๐ŸŒธ GitHub GraphQL API ์‚ฌ์šฉ๋ฒ•

// ์„ค์น˜
// graphQL์˜ ์ฟผ๋ฆฌ๋ฅผ ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ ์‰ฝ๊ฒŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž„
npm install @octokit/graphql

@octokit/graphql์˜ ์‚ฌ์šฉ๋ฒ•๊ณผ
Graphql Queries-GitHub Docs๋ฅผ ๋ณด๊ณ  ๊นƒํ—™์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

// graphql ๊ฐ€์ ธ์˜ค๊ธฐ
import { graphql } from "@octokit/graphql";
import React, { useState, useEffect } from "react";

async function myData() {
  const CLIENT_TOKEN = process.env.REACT_APP_GHP_TOKEN;
  const { repository, viewer } = await graphql(
    `
      {
        repository(name: "์ด๋ฆ„", owner: "์†Œ์œ ์ž") {
          discussions(last: 10) {
            edges {
              node {
                id
                title
                url
                author {
                  resourcePath
                }
              }
            }
          }
          issues(last: 10) {
            edges {
              node {
                id
                title
              }
            }
          }
          pullRequests(last: 10) {
            edges {
              node {
                id
                title
              }
            }
          }
        }
        viewer {
          login
        }
      }
    `,
    {
      headers: {
        authorization: `token ${CLIENT_TOKEN}`,
      },
    }
  );
  return { repository, viewer };
}

const GetInfo = () => {
  const [data, setData] = useState([]);
  const [issues, setIssues] = useState([]);
  const [pull, setPull] = useState([]);

  useEffect(() => {
    myData().then((res) => {
      setData(res.repository.discussions.edges);
      setIssues(res.repository.issues.edges);
      setPull(res.repository.pullRequests.edges);
    });
  }, []);

  return (
    <>
      <div>
        {data.map((el) => {
          return <div key={el.node.id}>{el.node.title}</div>;
        })}
      </div>
      <br />

      <span>ISSUE</span>
      <div>
        {issues.map((el) => {
          return <div key={el.node.id}>{el.node.title}</div>;
        })}
      </div>
      <br />

      <span>PULL REQUEST</span>
      <div>
        {pull.map((el) => {
          return <div key={el.node.id}>{el.node.title}</div>;
        })}
      </div>
    </>
  );
};

export default GetInfo;

โ–ท GraphQL ์ •๋ฆฌ

GraphQL์˜ ์žฅ๋‹จ์ 

์žฅ์ ๋‹จ์ 
ํ•˜๋‚˜์˜ endpoint๋กœ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์š”์ฒญ ๊ฐ€๋ŠฅREST API์— ์ต์ˆ™ํ•˜์ง€ ์•Š์€ ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ๋Š” ํ•™์Šต ๊ณก์„ ์ด ์žˆ์Œ
๋ฐ์ดํ„ฐ ์˜ค๋ฒ„ํŽ˜์นญ๊ณผ ์–ธ๋”ํŽ˜์นญ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•จ์บ์‹ฑ์ด REST API๋ณด๋‹ค ๋ณต์žกํ•จ
Playground๋ฅผ ์ œ๊ณตํ•ด schema์™€ resolver๋ฅผ ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธ์ฟผ๋ฆฌ ํฌ๊ธฐ๊ฐ€ ๊ณ ์ •๋œ ์š”์ฒญ์˜ ๊ฒฝ์šฐ RESTful API๋ณด๋‹ค ๋” ํฌ๊ฒŒ ๋  ์ˆ˜ ์žˆ์Œ
ํด๋ผ์ด์–ธํŠธ ๊ตฌ์กฐ ๋ณ€๊ฒฝ์—๋„ ์„œ๋ฒ„์— ์ง€์žฅ์ด ์—†์Œ

๐ŸŒธGraphQL vs ๐ŸŒREST API

REST APIGraphQL
Resource ํ˜•ํƒœ ์ •์˜URI์— ์˜ํ•ด ์ •์˜GraphQL Schema์— ์˜ํ•ด ์ •์˜
๋ฐ์ดํ„ฐ ์š”์ฒญ ๋ฐฉ๋ฒ•HTTP Method์— ์˜ํ•ด ์ •์˜Query, Mutation ํƒ€์ž…์— ์˜ํ•ด ์ •์˜
๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ์ •์˜๋จํด๋ผ์ด์–ธํŠธ์—์„œ ์š”์ฒญ ์‹œ ๊ฒฐ์ •๋จ
์—ฌ๋Ÿฌ Resource ์š”์ฒญ์—ฌ๋Ÿฌ ๋ฒˆ์˜ ์š”์ฒญ ํ•„์š”ํ•œ ๋ฒˆ์˜ ์š”์ฒญ์—์„œ ์—ฌ๋Ÿฌ Resource ์ ‘๊ทผ ๊ฐ€๋Šฅ
๋ฐ์ดํ„ฐ ํ•ธ๋“ค๋ง์—”๋“œํฌ์ธํŠธ ํ•ธ๋“ค๋ง ํ•จ์ˆ˜์— ์˜ํ•ด ์ฒ˜๋ฆฌํ•„๋“œ๋ณ„ Resolver ํ•จ์ˆ˜์— ์˜ํ•ด ์ฒ˜๋ฆฌ
ํด๋ผ์ด์–ธํŠธ ์š”๊ตฌ์‚ฌํ•ญ์„œ๋ฒ„์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์™€ ์ผ์น˜ํ•ด์•ผ ํ•จํด๋ผ์ด์–ธํŠธ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๊ฒŒ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ



๐Ÿ‘‰ GraphQL์˜ Learn ๋ณด๋Ÿฌ๊ฐ€๊ธฐ
๐Ÿ‘‰ @octokit/graphql์˜ ์‚ฌ์šฉ๋ฒ•
๐Ÿ‘‰ ์œ ํŠœ๋ธŒ ์˜์ƒ - graphQL๋กœ ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ๋ฐฉ๋ฒ•

profile
๐Ÿพ

0๊ฐœ์˜ ๋Œ“๊ธ€