티스토리 뷰

GraphQL이 한글패치해서 제공하는 공식 튜토리얼이 있다.
잘 정리해서 알려주고 있어 매우 도움이 되니 참고하자.

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

 

GraphQL: API를 위한 쿼리 언어

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

graphql-kr.github.io

 

회사에 들어가니 REST API가 아닌 GraphQL을 사용한다고해서 급하게 공부하게 됐다. 학부 4년 내내 공부 및 플젝을 하면서 서버-클라이언트 통신 구조에서 REST를 사용하지 않는 경우는 배워본적도 들어본적도 없었지만 이는 매우 우물한 개구리에 불과했던 나의 시야로 인한것이였다.

일단 GraphQL은 페이스북(현 메타)에서 개발한 API 쿼리로 REST의 한계점을 보완하기 위해 등장한 기술이다. 그러면 REST API의 한계점을 알아보지 않을 수 없다. 한번 알아보자.

REST API의 한계점

1. Overfetching : 요청에 비해 너무 많은 데이터(정보)가 오는 경우

스타워즈 등장인물에 대해 조회하는 요청을 보내보자. https://swapi.dev/ 로 가서 다들 한번씩 request를 눌러보면 이해가 빠를 것이다.

이러한 요청을 보내면

{
	"name": "Luke Skywalker",
	"height": "172",
	"mass": "77",
	"hair_color": "blond",
	"skin_color": "fair",
	"eye_color": "blue",
	"birth_year": "19BBY",
	"gender": "male",
	"homeworld": "https://swapi.dev/api/planets/1/",
	"films": [
		"https://swapi.dev/api/films/1/",
		"https://swapi.dev/api/films/2/",
		"https://swapi.dev/api/films/3/",
		"https://swapi.dev/api/films/6/"
	],
	"species": [],
	"vehicles": [
		"https://swapi.dev/api/vehicles/14/",
		"https://swapi.dev/api/vehicles/30/"
	],
	"starships": [
		"https://swapi.dev/api/starships/12/",
		"https://swapi.dev/api/starships/22/"
	],
	"created": "2014-12-09T13:50:51.644000Z",
	"edited": "2014-12-20T21:17:56.891000Z",
	"url": "https://swapi.dev/api/people/1/"
}

이러한 대용량의 데이터가 반환된다. 만약 우리가 필요한건 해당 등장인물의 이름과 키만 필요하다면 필요없는 정보까지 over해서 받는 것인 것이다. 이런것을 Overfetcing 이라고 한다.

 

반면 GraphQL로 요청을 보내면 아래와 같이 원하는 데이터만 지정해서 요청하는 것이 가능해진다. 이것은 온전히 요청 쿼리로만 이뤄지는 것이다.

이렇게 불필요한 데이터를 가져오지 않으므로 응답속도가 빨라질 여지가 있다.

 

2. Underfetching : 요청에 비해 정보가 부족한 경우

위와 반대되는 경우로 필요한 데이터를 가져오기 위해 여러번의 API 호출이 이뤄져야 하는 경우를 말한다.

REST에서 등장인물 "Luke Skywalker"를 조회했을 때 films 필드안에 String이 배열로 존재하는 것을 볼 수 있다. 만약 "Luke Skywalker"가 출연한 모든 영화의 제목만을 원한다면 어떻게 해야 할까?

먼저 Luke Skywalker를 조회하고 -> 각 film을 조회해서 title을 뽑아와야 한다. 이렇게 여러번의 조회가 발생하게 되는 문제가 있다.

{
	"title": "A New Hope",
	"episode_id": 4,
	"opening_crawl": "It is a period of civil war.\r\nRebel spaceships, striking\r\nfrom a hidden base, have won\r\ntheir first victory against\r\nthe evil Galactic Empire.\r\n\r\nDuring the battle, Rebel\r\nspies managed to steal secret\r\nplans to the Empire's\r\nultimate weapon, the DEATH\r\nSTAR, an armored space\r\nstation with enough power\r\nto destroy an entire planet.\r\n\r\nPursued by the Empire's\r\nsinister agents, Princess\r\nLeia races home aboard her\r\nstarship, custodian of the\r\nstolen plans that can save her\r\npeople and restore\r\nfreedom to the galaxy....",
	"director": "George Lucas",
	"producer": "Gary Kurtz, Rick McCallum",
	"release_date": "1977-05-25",
	"characters": [
		"https://swapi.dev/api/people/1/",
		...
		"https://swapi.dev/api/people/81/"
	],
	...
	"species": [
		"https://swapi.dev/api/species/1/",
		...
		"https://swapi.dev/api/species/5/"
	],
	"created": "2014-12-10T14:23:31.880000Z",
	"edited": "2014-12-20T19:49:45.256000Z",
	"url": "https://swapi.dev/api/films/1/"
}

이런식으로 조회되는 영화 정보에서 title만 취한 후 반환하게 될 것이다.

 

위와 같은 구조는[회원 조회] -> [회원이 가지고 있는 영화에 대해 각각 조회] & [타이틀 가져오기] 이렇게 이뤄지게 된다. 이렇듯 한번의 요청에 원하는 데이터를 가져오지 못하고 여러번의 API 호출이 필요한 경우를 Underfetching 이라고 한다.

 

GraphQL은 하나의 쿼리안에서 여러개의 객체를 조회할 수 있기때문에 이를 손쉽게 해결할 수 있다. 서브쿼리를 통해 해당 등장인물이 출연한 영화를 바로 취하고 그중에서도 title만 가져올 수 있다.

 

 

이러한 Over/Under fetching 문제에 대한 이해를 바탕으로 GraphQL를 봐보자.


GraphQL

graphQL은 쿼리를 통해 요청하므로 POST로만 요청을 보낼 수 있다. URL을 통한 정보를 표현하는 방식은 REST 방식이므로 사용되지 않는다. 따라서 "IP주소/graphql"로 쿼리를 작성해서 요청을 보내면 된다.

POST http://localhost:4000/graphql
--body--
{
	//graphQL..
}

 

그러면 등록, 수정, 삭제, 조회는 어떻게 구분해서 요청을 해야할까? 이 부분은 body를 통해 구분하게 된다.

  • 조회 : query
  • 등록/수정/삭제 : mutation
  • 구독 : subscription

graphQL에 대해 실습환경을 제공하는 api들이 몇개 있는데 그중에서 두개를 이용해서 문법을 정리하고 테스트해보겠다.


Query

매개변수가 필요하면 함수명 옆 괄호안에 파라미터: 아규먼트(값) 쌍으로 넣어주면 된다. 필드는 Person 객체가 가지는 프로퍼티를 지정해서 넣어주면 된다.

Person이 갖는 Planet 객체

이때 Person이 갖는 다른 객체가 있다면 바로 접근해서 같이 조회하는것이 가능하다.

해당 이름을 바로 가져올 수도 있고 planetName 처럼 별칭을 붙여서 응답하도록 할 수도 있다.

Fragment

조회를 위해서 원하는 필드를 모두 적어줘야 하는데 이는 매우 귀찮을 수 있다. 이러한 필드를 미리 정의해서 재사용할 수 있다. 다만 특정 객체에 국한되어 정의되어야 한다.

이런식으로 Person에서 신체정보에 해당하는 내용만 fragment bodyInfoOfPerson on Person으로 빼서 정의할 수 있다. 여기서 중요한 건 정의 후 사용하지 않으면 오류가 난다.

fragment 타입을 사용하기 위해서는 [...]을 필드위치에 대신 넣어주면 된다.

fragment bodyInfoOfPerson on Person { 
  name
  birthYear
  eyeColor
  sex: gender
  hairColor
  cm: height
  kg: mass
  skinColor
}

하나의 객체에 여러개의 fragment를 정의해서 사용할 수 있다. 다만 중복 필드는 하나만 나온다.

UnionType 

현재 반환하는 것들은 Person이라는 한가지 타입만을 반환하고 있다. 이때 여러가지 타입을 반환하도록 하고 싶다면 Union 타입을 만들면 여러가지 타입을 반환하도록 할 수 있다.

예를들어 캘린더의 일정관리에서 일정 타입을 union 타입으로 AgendaItem을 정의해서 StudyGroup, Workout 두개를 갖게 한 후 두개를 같이 반환하도록 할 수 있다.

type StudyGroup {name: String ...}
type Workout {name: String ...}

union AgendaItem = Workout | StudyGroup

type Query {
  agenda: [AgendaItem!]!
}

이런식으로 agenda 쿼리 함수에 요청을 보내는데 타입별로 다르게 출력하게 할 수 있다.

  1. query search에서 "search"는 해당 쿼리에 대한 별칭이다.
    이 쿼리가 뭘 하는 쿼리인지 명시하는 용도일 뿐 그 이상의 효과는 없다.
  2. __typename은 반환되는 객체의 타입을 명시해준다. 단일 객체가 반환되어도 표시할 수 있긴하다.
  3. ... on Workout(타입명)은 이름없는 프래그먼트로 인라인 프래그먼트(inline fragment)이다. 물론 fragment를 밖에 정의해서 사용해도 된다.

Interface

유니온 타입과 매우 비슷한데 공통 속성을 뺄 수 있다는 차이가 있다. 인터페이스 객체를 만들고 공통적인 부분을 갖게 할 수 있다. 위에 예제를 이어서 보면 name, start, end를 둘다 갖도록 정의할 수 있다.

interface ScheduleItem {
name: String!
start: Int
end: Int
}

type StudyGroup implements ScheduleItem {
  name: String!
  start: Int
  end: Int
  subject: String!
  students: Int!
}

type Workout implements ScheduleItem {
  name: String!
  start: Int
  end: Int
  reps: Int!
}

type Query {
  agenda: [ScheduleItem!]!
}

// !는 필수 값을 의미한다. not nullabe 한 것. (값을 갖고있어야 한다는 것이지 조회 시 포함할 필요는 없다)

이렇게 인터페이스를 활용하면 공통 속성을 좀 더 편리하게 관리할 수 있다.

지시어

조건을 통해 포함, 생략 여부를 지정해줄 수 있다. 필드값에도 가능하고, 객체에도 가능하다.

  • @include(if: T/F) : 포함여부
  • @skip(if: T/F) : 생략여부

gender 필드에 포함여부를 결정하는 @include를 사용하고 있으며, homeworld, vehicleConnection 타입 객체에도 @include, @skip을 사용하는 것을 볼 수 있다.

include는 내부 if가 true이면 포함/ false이면 불포함이고 skip은 반대이다.


지금까진 조회에 대해 알아보았고 이제 데이터를 조작하는 명령에 대해 알아보자.

Mutation

데이터를 수정, 삭제, 등록 할 때 사용하는 명령어이다.

위 2개의 요청을 동일한 결과를 갖는다.

  1. mutation 옆에 오는 이름은 이전에 말했듯이 단순 별칭을 지어주기 위한 것이다.
    다만 동적으로 인자값을 받을 경우 [$변수명: 타입(!은 필수여부)]로 받도록 할 수 있다.
    • 두번째 뮤테이션을 보면 쿼리 변수를 json 형식으로 넣어주는 것을 볼 수 있다.
  2. 모든 변수는 앞에 $를 붙여서 사용한다.
  3. !는 필수로 전달해야 하는 변수를 의미한다.
  4. 필드에 있는 name, status는 해당 mutation이 동작한 후 해당 반환하게 된다.

참고로 스칼라 타입이 아닌 객체 타입을 전달할 수 도 있다. 이를 input object type 이라고 한다. 아래와 같이 만들 수 있고, 기존의 type 대신 input을 사용하여 생성한다.

input ReviewInput {
  stars: Int!
  commentary: String
}

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

이렇게 에피소드에 리뷰를 생성하는 api를 위와같이 만들 수 있다. 쿼리 파라미터를 전달 할 때도 Episode, ReviewInput 타입에 맞춰서 전달해주면 된다.


Subscription

일반적인 REST API에서는 상상도 못했던 기능으로 WebSocket을 이용해 실시간 통신을 하는 것처럼 실시간 데이터 변경을 받을 수 있도록 하는 기능이다.

 

페이스북에서 만든 API 인 만큼 실제 페이스북에서 사용되는 것을 기반으로 만들어졌는데 바로 실시간 좋아요 수 반영이다. 실제 적용된 사례로 모든 클라이언트가 "좋아요" 이벤트를 구독하게 하고 실시간으로 "좋아요"수가 새로고침 없이 없데이트 되는 것을 볼 수 있도록 만들었다.

 

query, mutation 처럼 subscription 이라는 루트 타입을 사용한다.

이렇게 subscription을 걸면 Lift의 상태가 변화할 경우에만 응답을 보내주게된다.

 

왼쪽 : lift 상태 변화에 subscription / 오른쪽 : lift 상태 변경

위 영상을 보면 새로고침 없이 실시간으로 상태변화에 대해 구독하고 변경이 일어날 경우 정의한 필드값을 반환하고 있다.


Introspection

인트로스펙션은 GraphQL에서 제공하는 강력한 기능인데 이를 사용하면 현재 API 스키마의 세부 사항에 관한 쿼리를 작성할 수 있다. 이를 통해 GraphQL 플레이그라운드 인터페이스에서 GraphQL 문서를 보여주는 것이다.

 

REST API에서 Swagger 문서를 통해 확인하던 API 문서를 API 자체에서 기능으로 제공하고 있는 것이다.

 

전체 스키마에 대한 인트로스펙션 조회

query {
  __schema {
    types {
      name
      description
    }
  }
}

스칼라 타입부터 객체 타입에 대해 name, description이 전부 나오는것을 볼 수 있다.

 

Person 객체에 대한 인트로스펙션 조회

query {
  __type(name: "Person") {
    name
    description
    fields {
      name
      description
    }
  }
}

특정 타입만 지정해서 볼 수도 있다.

 

Query, Mutation, Subscription 루트타입을 조회

  • graphQL API 문서를 처음 보는 경우 먼저 쿼리, 뮤테이션, 서브스크립션에 대해 파악해 보자.
query roots {
  __schema {
    queryType {
      ...typeFields
    }
    mutationType {
      ...typeFields
    }
    subscriptionType {
      ...typeFields
    }
  }
}

fragment typeFields on __Type {
  name
  fields {
    name
    description
  }
}
{
  "data": {
    "__schema": {
      "queryType": {
        "name": "Query",
        "fields": [
          {
            "name": "allLifts",
            "description": "A list of all `Lift` objects"
          },
          {
            "name": "allTrails",
            "description": "A list of all `Trail` objects"
          },
          {
            "name": "Lift",
            "description": "Returns a `Lift` by `id` (id: \"panorama\")"
          },
          {
            "name": "Trail",
            "description": "Returns a `Trail` by `id` (id: \"old-witch\")"
          },
          {
            "name": "liftCount",
            "description": "Returns an `Int` of `Lift` objects with optional `LiftStatus` filter"
          },
          {
            "name": "trailCount",
            "description": "Returns an `Int` of `Trail` objects with optional `TrailStatus` filter"
          },
          {
            "name": "search",
            "description": "Returns a list of `SearchResult` objects based on `term` or `status`"
          }
        ]
      },
      "mutationType": {
        "name": "Mutation",
        "fields": [
          {
            "name": "setLiftStatus",
            "description": "Sets a `Lift` status by sending `id` and `status`"
          },
          {
            "name": "setTrailStatus",
            "description": "Sets a `Trail` status by sending `id` and `status`"
          }
        ]
      },
      "subscriptionType": {
        "name": "Subscription",
        "fields": [
          {
            "name": "liftStatusChange",
            "description": "Listens for changes in lift status"
          },
          {
            "name": "trailStatusChange",
            "description": "Listens for changes in trail status"
          }
        ]
      }
    }
  }
}

이렇게 Query, Mutation, Subscription 각각에 존재하는 API 함수명 & 설명을 확인할 수 있다.

반응형

'Web > GraphQL 정리글' 카테고리의 다른 글

GraphQL 2장 _ 스키마 정리  (0) 2022.06.18
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday