GrapghQL(gql)은 페이스북에서 만든 API를 위한 쿼리 언어이다.
gql의 문장은 주로 클라이언트 시스템에서 작성하고 호출한다. 서버사이드 gql 어플리케이션은 gql로 작성된 쿼리를 입력으로 받아 쿼리를 처리한 결과를 다시 클라이언트로 돌려준다. HTTP API 자체가 특정 데이터페이스나 플랫폼에 종속적이지 않은것 처럼 gql 역시 특정 데이터베이스나 플랫폼에 종속적이지 않다.
REST API는 URL, METHOD 등을 조합하기 때문에 다양한 Endpoint가 존재한다. 반면, gql은 Endpoint가 단 하나이다. 또한, gql API에서는 불러오는 데이터의 종류를 쿼리 조합을 통해 결정한다. 예를 들어, REST API에서는 각 Endpoint마다 데이터베이스 sql 쿼리가 달라지는 반면, gql API는 gql 스키마의 타입마다 sql 쿼리가 달라진다.
위 그림처럼, gql API를 이용하면 여러번의 네트워크 호출 없이, 한번의 네트워크 호출로 처리가 가능하다.
또한, Response 데이터도 차이가 있다. REST API의 경우 return 되는 Response 데이터가 정해져 있다. 그렇기 때문에 이전 API Response Spec이 변경되면 그에 따라 해당 파일들을 수정해줘야 하고, API 별로 Response가 다르면 그에 따른 DTO를 각각 만들어줘야 했다.
하지만, gql은 쿼리로 요청하기에 클라이언트가 원하는 데이터만을 커스터마이징해서 사용할 수 있다.
{
hero {
name
# 쿼리에 주석을 쓸 수도 있습니다!
friends {
name
}
}
}
REST API Method에 GET, POST, PUT, DELETE, PATCH가 있는 것처럼, gql에는 query, mutation, subscription 방식이 있다.
{
hero {
name
# 쿼리에 주석을 쓸 수도 있습니다!
friends {
name
}
}
}
=>
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
gql은 객체에 대한 특정 필드를 요청하는 것이 매우 간단하다. 쿼리와 결과가 정확히 동일한 형태이다. gql은 연관된 객체와 필드를 탐색 할 수 있으므로 클라이언트는 기존 REST 구조처럼 여러번 요청을 수행하는 대신 한번의 요청으로 많은 데이터를 가져올 수 있다.
{
human(id: "1000") {
name
height
}
}
=>
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 1.72
}
}
}
REST와 같은 시스템에서는 쿼리 파라미터와 URL 세그먼트 같은 단일 인자들만 전달할 수 있었지만, gql은 모든 객체가 인자를 가질 수 있다. 인자는 다양한 타입이 될 수 있는데, ENUM열거형도 가능하다.
{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
=>
{
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}
별칭을 사용해서 필드의 결과를 원하는 이름으로 바꿀 수 있다.
프래그먼트는 gql의 재사용 단위이다. 프래그먼트를 사용하면 필드셋을 구성한 다음 필요한 쿼리에 포함시킬 수 있다.
{
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
지금까지는 query 키워드와 query name을 생략 한 단축 문법을 사용했지만, 실제 코드에서는 명시하는 것이 좋다.
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
인자 값을 동적으로 할당할 수 있다. 변수는 달러 표시 '$'로 선언한다. Kotlin처럼 default 값을 줄 수도 있다.
query HeroNameAndFriends($episode: Episode = "DefaultEpisode") {
hero(episode: $episode) {
name
friends {
name
}
}
}
# variables
{
"episode": "JEDI"
}
인자에 변수를 사용하여 동적 쿼리를 구현할 수 있지만, 지시어를 통해 동적 쿼리를 좀 더 향상시킬 수 있다.
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
# variables
{
"episode": "JEDI",
"withFriends": false
}
지시어는 필드나 프래그먼트 안에 삽인되어 서버가 원하는 방식으로 쿼리 실행에 영향을 줄 수 있다.
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
# variables
{
"ep": "JEDI"
}
# response
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
hero 필드는 Character를 반환하는데, episod 인자에 따라서 Human이나 Droid 중 하나일 수 있다. 필드를 직접 선택할 때는 name과 같이 명시하고, 특정 타입의 필드를 요청하려면 타입 조건과 함께 인라인 프래그먼트를 사용해야 한다.
반환된 Character가 Human이면 명시한 name과 height 필드를 응답하고, Droid인 경우 name과 primaryFunction을 응답한다.
클라이언트가 gql에서 리턴될 타입을 모르는 경우 메타 필드 __typename를 사용할 수 있다.
{
search(text: "an") {
__typename
... on Human {
name
}
... on Droid {
name
}
... on Starship {
name
}
}
}
query는 읽기(R) 전용으로 데이터를 가져오는 메서드이다.
query HeroNameAndFriends($episode: Episode = "JEDI") {
hero(episode: $episode) {
name
friends {
name
}
}
}
mutation은 데이터를 변경(CUD)하는 메서드이다.
query 대신 mutaion 키워드를 사용한다. mutation은 query와 마찬가지로 여러 필드를 포함할 수 있다. 하지만 중요한 차이점이 있다. query 필드는 병렬로 실행되지만 mutation 필드는 하나씩 차례대로 실행된다. 즉, 하나의 요청에서 두 개의 incremetCredit mutation을 보내면 첫 번째는 두 번쨰 요청 전에 완료되는 것이 보장된다.
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
# variables
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
subscription은 실시간으로 변경된 데이터를 가져오는 메서드이다. 웹소켓을 통해 소켓 통신을 열어두고 데이터 업데이트 시 알리는 방식으로 이루어진다. 데이터가 자주 업데이트 되어야 하는 채팅과 같은 화면에서 매번 상태가 변경될 때마다 HTTP 요청을 보내는 것은 자원 낭비기에 소켓 통신으로 처리하는 것이 좋은데 이럴 때 사용한다.
참고 자료 출처