현인

GraphQL 학습 - 2장 쿼리(Query) & 뮤테이션(Mutation) 본문

기술 학습/GraphQL

GraphQL 학습 - 2장 쿼리(Query) & 뮤테이션(Mutation)

현인(Hyeon In) 2024. 11. 14. 17:25

이번 장에서는 GraphQL 서버에 어떻게 질의가 이루어지는지 상세하게 배워보겠다.

Fields

GraphQL은 단순하게 말하면 객체의 특정 필드를 요청하는 것이다. 매우 간단한 쿼리와 이를 실행했을 때 얻는 결과를 살펴보자.

//Operation
hero {
    name
  }
}
//Response
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }

쿼리 결과와 정확히 같은 모양이라는 것을 바로 알 수 있다. 이것은 GraphQL에게 필수적이다. 항상 기대하는 것을 반환받고, 서버는 클라이언트가 어떤 필드를 요청하는지 정확히 알고 있기 때문이다.

name 필드는 String 타입을 반환한다.

위 예시에서는 우리는 영웅의 이름만 요청했고, 그 결과 문자열이 반환되었지만, 필드는 객체를 참조할 수도 있다. 다음 예제의 경우 해당 객체에 대한 필드의 하위 선택(sub-selection)을 할 수 있다. GraphQL 쿼리는 관련 객체와 해당 필드를 탐색하여 클라이언트가 클래식 REST 아키텍처에서 필요했던 여러 번의 왕복 대신 한 번의 요청으로 많은 관련 데이터를 가져올 수 있도록 한다.

// Operation
hero {
    name
    # Queries can have comments!
    friends {
      name
    }
  }
}
//Response
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

이 예에서 friends필드는 아이템 배열을 반환합니다. GraphQL 쿼리는 단일 아이템아나 아이템 리스트에 대해 동일하게 보이지만 스키마에 표시된 내용에 따라 어떤 것을 기대해야 할지 알 수 있습니다.

Arguments

필드에 인수를 전달하여 특정 데이터를 조회할 수 있다. 아래는 id값이 1000인 사람을 찾는 쿼리

{
  human(id: "1000") {
    name
    height
  }
}
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 1.72
    }
  }
}

REST는 리퀘스트의 쿼리 매개변수와 URL 세그먼트인 단일 인수 집합만 전달 가능했다. 하지만 GraphQL에서는 모든 필드와 중첩된 객체가 자체 인수 집합을 얻을 수 있으므로 GraphQL은 여러 API fetch를 생성하는 것에 대한 완벽한 대체제가 된다.

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 5.6430448
    }
  }
}

arguments의 타입은 여러 가지 다른 타입일 수 있다. 기본 타입 집합이 제공되지만, 전송 형식으로 직렬화 할 수 있는 한 고유한 사용자 정의 타입을 선언할 수도 있다

Aliases

response 객체 필드가 쿼리의 필드 이름과 일치하지만 인수를 포함하지 않기 때문에 다른 인수로 동일한 필드를 직접 쿼리할 수 없다. 이 때 별칭 기능을 활용해 필드의 결과를 원하는대로 바꿀 수 있다.

{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

위의 예에서 두 hero필드는 충돌하지만, 두 필드에 다른 이름을 지정할 수 있으므로 한 번의 요청으로 두 결과를 모두 얻을 수 있다.

Fragments

GraphQL에서 제공하는 재사용 가능한 유닛. 필드 세트를 미리 구성한 다음 동일한 필드 셋을 가지는 쿼리에 포함하여 동일한 필드 셋을 중복으로 작성하는 것을 막을 수 있다.

{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}
{
  "data": {
    "leftComparison": {
      "name": "Luke Skywalker",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "friends": [
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        },
        {
          "name": "C-3PO"
        },
        {
          "name": "R2-D2"
        }
      ]
    },
    "rightComparison": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

Fragments 개념은 복잡한 애플리케이션 데이터 요구 사항을 더 작은 청크로 분할하는 데 자주 사용되며, 특히 여러 조각이 있는 많은 UI 구성 요소를 하나의 초기 데이터 페치로 결합해야 할 때 더욱 그렇습니다.

Fragment 내부 변수 사용

Fragment가 쿼리나 뮤테이션에서 선언된 변수에 접근할 수 있다.

query HeroComparison($first: Int = 2) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}
{
  "data": {
    "leftComparison": {
      "name": "Luke Skywalker",
      "friendsConnection": {
        "totalCount": 4,
        "edges": [
          {
            "node": {
              "name": "Han Solo"
            }
          },
          {
            "node": {
              "name": "Leia Organa"
            }
          }
        ]
      }
    },
    "rightComparison": {
      "name": "R2-D2",
      "friendsConnection": {
        "totalCount": 3,
        "edges": [
          {
            "node": {
              "name": "Luke Skywalker"
            }
          },
          {
            "node": {
              "name": "Han Solo"
            }
          }
        ]
      }
    }
  }
}

Operation name

위의 예시들은 키워드와 쿼리 이름을 모두 생략하는 단축 구문을 사용했지만, 프로덕션 앱에서는 모호성을 줄이기 위해 사용하는 것이 좋다.

작업 타입으로 query를 가지고 작업 이름으로 HeroNameAndFriends를 가지는 예시를 보자

query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

작업 타입은 query, mutation, subscription 셋 중에 하나고 이들 중 어떤 작업인지 표현하기 위해 사용한다.

작업 이름은 작업에 대한 의미 있고 명확한 이름이다. 다중 작업 문서에서만 필요하지만 디버깅 및 서버 측 로깅에 매우 유용하므로 사용이 권장된다. 문제가 발생하면 내용을 해독하려고 하는 대신 작업 이름으로 코드베이스에서 쿼리를 식별하는 것이 더 쉽다.

변수

필드에 대한 인수는 대부분의 어플리케이션에서 동적일 것이다. 이러한 동적 인수를 쿼리 문자열에 직접 전달하는 것은 좋은 생각이 아니다. 클라이언트 측 코드가 런타임에 쿼리 문자열을 동적으로 조작하고 GraphQL 특정 형식으로 직렬화 해야 하기 때문이다. 대신 GraphQL은 쿼리에서 동적 값을 인수분해하고 별도의 사전으로 전달하는 일류 방식을 제공한다. 이러한 값을 변수라고 한다.

변수 작업을 시작 할 때 세 가지 작업을 해야한다.

  1. 쿼리의 정적 값을 $variableName 과 같은 형식으로 바꾼다.
  2. $variableName 을 쿼리에서 허용되는 변수 중 하나로 선언한다.
  3. 별도의 전송 관련 변수 딕셔너리(variableName: value)를 전달한다.
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

//변수
{
	"episode": "JEDI"
}
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

변수 정의

변수 정의는 위의 쿼리에서 보이는 대로, 타입 언어의 함수에 대한 인수 정의와 똑같이 작동한다. 모든 변수를 나열하고, $ 접두사로 시작하고, 그 뒤에 타입이 나오는데, 위 경우에서는 Episode가 된다.

선언된 모든 변수는 스칼라, 열거형 또는 입력 객체 타입이어야 한다. 따라서 복잡한 객체를 필드에 전달하려면 서버에서 일치하는 타입을 알아야 한다.

변수 정의는 선택 사항 또는 필수 사항일 수 있다. 위의 경우, 타입 옆에 ! 가 없으므로, 선택 사항이다. 하지만 변수를 전달하는 필드에 null이 아닌 인수가 필요한 경우 변수도 필수 사항이어야 한다.

기본 변수

기본 값을 타입 뒤에 추가하여 쿼리의 변수에 기본 값을 할당할 수도 있다.

query HeroNameAndFriends($episode: Episode = JEDI) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

모든 변수에 대한 기본값이 제공되면 변수를 전달하지 않고도 쿼리를 호출할 수 있다. 변수 사전의 일부로 변수가 전달되면 기본값을 재정의한다.

Directives

변수를 사용하여 쿼리의 구조와 모양을 동적으로 변경할 방법도 필요하다. GraphQL에서 지시어라는 기능을 통해 동적으로 쿼리를 변경할 수 있다. 지시어는 서버가 원하는 방식으로 쿼리 실행에 영향을 미칠 수 있다. 핵심 GraphQL 사양에는 두 가지 지시어가 포함되어 있다.

  • @include(if: Boolean) 인수가 true인 경우에만 이 필드를 결과에 포함한다.
  • @skip(if: Boolean) 인수가 true인 경우에 이 필드를 건너 뛴다.
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}

//var
{
  "episode": "JEDI",
  "withFriends": false
}
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

Mutations

기술적으로 쿼리를 통해 데이터 쓰기를 일으키는 것은 가능하나, 쓰기를 일으키는 모든 작업은 뮤테이션을 통해 명시적으로 전송 해야 한다는 관례를 수립하는 것이 좋다.

쿼리에서와 마찬가지로, 뮤테이션 필드가 객체 타입을 반환하는 경우 중첩된 필드를 요청할 수 있다. 이는 업데이트 후 객체의 새 상태를 가져오는데 유용하다.

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

//var
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}
{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}

Mutation의 다중 필드

뮤테이션은 쿼리처럼 여러 필드를 포함할 수 있다. 쿼리와 뮤테이션 사이에는 이름 외에도 중요한 차이가 있다. 쿼리 필드는 병렬로 실행되는 반면, 뮤테이션 필드는 차례로 연속적으로 실행된다. 즉, 순서가 보장된다

인라인 Fragment

인터페이스나 유니온 타입을 반환하는 필드를 쿼리하는 경우 구체적 타입에 액세스하려면 인라인 Fragment를 사용해야 한다.

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}

{
  "ep": "JEDI"
}
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}

이 쿼리에서, hero 필드는 Character 타입을 반환하고, 이 타입은 episode에 따라 Human 혹은 Droid 타입일 수 있다.

따라서 Droid인지 Human인지에 따라 달라지려면 인라인 Fragment를 사용하여 정의해야 한다.

… on Droid 처럼 특정 타입에 대해 받고 싶은 값을 정의할 수 있다.

Meta Fields

GraphQL에서는 때때로 요청한 데이터의 타입이나 스키마에 대한 정보를 알아야 할 때가 있다. GraphQL은 이를 위해 메타 필드를 제공한다. 메타 필드는 쿼리의 어느 지점에서나 요청할 수 있으며, 스키마의 내부 정보를 제공한다. 가장 유용한 메타 필드는 __typename으로, 이는 해당 객체의 타입 이름을 반환한다.

{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
    ... on Droid {
      name
    }
    ... on Starship {
      name
    }
  }
}
{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo"
      },
      {
        "__typename": "Human",
        "name": "Leia Organa"
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1"
      }
    ]
  }
}

출처

https://graphql.org/learn/queries/

 

Queries and Mutations | GraphQL

Copyright © 2024 The GraphQL Foundation. All rights reserved. For web site terms of use, trademark policy and general project policies please see https://lfprojects.org

graphql.org

https://graphql-kr.github.io/learn/queries/

 

GraphQL: API를 위한 쿼리 언어

GraphQL은 API에 있는 데이터에 대한 완벽하고 이해하기 쉬운 설명을 제공하고 클라이언트에게 필요한 것을 정확하게 요청할 수 있는 기능을 제공하며 시간이 지남에 따라 API를 쉽게 진화시키고

graphql-kr.github.io

반응형