GraphQL


프로젝트를 작업하면서 GraphQL 를 사용한 적이 있는데 기능을 구현하는 정도로만 사용했기 때문에 명확한 개념을 파악하지 못해 기본적으로 제공하는 기능들을 활용하지 않거나 이슈가 발생했을 때 빠르게 파악할 수 없었던 적이 생기는 아쉬운 부분들이 되게 많았다.
그래서 GraphQL 공식문서를 통해서 기본적인 특징을 정리해보려고 한다.

정의

GraphQL 은 Facebook 에서 만든 API 를 위한 쿼리 언어이다.

Query

데이터를 읽는 경우(R) 사용

필드 (Fields)

Field 요청 형식과 응답 형식을 보면 구조가 비슷한 것을 확인할 수 있다. 이로 인해서 직관적인 데이터 확인이 가능하다.

인자 (Arguments)

개발을 할 때 특정 조건에 맞는 데이터만 가져와야 되는 경우가 발생하는데 Fields 에 인자값을 적용하여 해당 조건에 해당하는 데이터만 가져올 수 있다. (ex. id 가 ‘1000’ 인 데이터의 name 과 height 를 가져옴)

별칭 (Aliase)

하나의 EndPoint 에서 Arguments 를 통해 다른 결과값을 보여주려고 하는 경우에 구분을 쉽게 하기 위해 Alias(별칭)을 사용하여 구분해줄 수 있다.
ex) hero 라는 공통된 Fields 를 사용하여 episode Field 의 값이 다른 2개의 결과값을 가져오는 경우

프래그먼트 (Fragments)

상단에서 Alias 를 통해 하나의 EndPoint 를 2가지의 다른 응답값으로 가져오도록 설정했는데 2가지 Alias 모두 name 이라는 중복된 Field 를 가지고 있는 것을 볼 수 있다. (실제 프로젝트에는 Field 가 엄청 많아진다.) 이렇게 적용하면 추후에 Field 가 추가되는 경우 Alias 마다 Field 를 하나하나 추가해줘야 되는 번거로움이 생기는데, 이를 Fragments 를 통해 해결할 수 있다.

프레그먼트에 변수 적용

Operation name

상단의 예제들을 보면 중괄호에서 바로 작성하는 방식으로 진행했는데, 실제 프로젝트에서 해당 방식으로 진행하게 되면 Fields 마다 구분이 힘들어져 유지보수 등의 어려움이 생길 수 있기 때문에 query 루트 타입과 Operation name 을 통해 각각 Fields 마다 이름을 지어줌으로써 구분을 쉽게 할 수 있도록 설정할 수 있다. 또한 Variables 를 적용시키는 경우에 사용된다.

변수 (Variables)

상단의 Arguments 를 통해서 특정 값에 해당하는 데이터만 가져오도록 설정하였는데 Arguments 같은 경우 Fields 안에 특정 값이 정적으로 적용이 되어있어 재사용성이 떨어지는 이슈가 발생한다. 이를 Variables 를 통해 조건을 동적으로 설정하여 재사용성을 높일 수 있다.

지시문 (Directives)

Variables 를 통해 특정 Fields 의 데이터를 가져오거나 생략할 수 있게 해준다.
  • @include(if: Boolean): if 값이 true 인 경우 Field 를 포함
  • @skip(if: Boolean): if 값이 true 인 경우 Field 를 생략

Mutation

데이터를 변경하는 경우(CUD) 사용

Schema & Type

Schema: 데이터 타입의 집합, API 문서 역할도 가능하다.
GraphQL 의 SDL (Schema Definition Language) 지원을 통해 다른 언어에 의존하지 않는 방식으로 스키마를 정의한다.

객체 타입 (Object types)

  • Character: 객체 타입
  • name, appearsIn: Character 객체 타입의 Field
  • String: 내장 스칼라 타입
  • String!: name Field 가 Non-Nullable 인것을 의미 (Null 이 아닌 String 값)
  • [Episode]!: Episode 객체의 리스트(배열) 타입

인자 타입 (Arguments)

예를 들어 하나의 학교(School) 에서 선생님의 고유 아이디(id) 를 통해 정보(teacher) 를 가져오는 경우 해당 코드로 표현이 될 것이다.
teacher 에 적용된 인자 타입을 아래와 같이 표현할 수 있다.
인자값 id 의 타입을 스칼라 타입인 ID 로 적용하고 1을 기본값으로 적용하였다. 그리고 name 과 height Field 를 반환하기 위해 Teacher 객체 타입을 적용하였다.

쿼리 타입 & 뮤테이션 타입

만약 블로그 포스팅의 정보를 가져오는 기능과 포스팅을 작성하는 기능이 있는 경우, 가져오는 Fields 의 타입은Query 타입을 사용하고 작성하는 Fields 의 타입은 Mutation 타입을 사용하여 진입점을 정의한다.
포스팅의 정보를 가져오는 경우 query 루트 타입을 통해 상단에서 작성한 type Query 가 적용되었다.

스칼라 타입 (Scalar types)

대부분의 타입이 객체 타입이지만 구체적인 타입을 지정해야 되는 경우가 생기는데, 그런경우 GraphQL 에 내장된 스칼라 타입을 사용하여 타입을 적용시킬 수 있다. 또한 스칼라 타입은 더 이상 하위 Field 가 존재하지 않는 것을 의미한다. (스칼라 타입은 하위 개념이 없기 때문)
  • Int: 부호가 있는 32비트 정수
  • Float: 부호가 있는 부동소수점 값
  • String: UTF-8 문자열
  • Booleantrue 또는 false
  • ID: 객체를 다시 요청하거나 캐시의 키로 주로 사용됨
 
위의 리스트에 없는 경우 커스텀 스칼라를 통해 타입을 추가할 수 있다.

열거형 타입 (Enumeration types)

Typescript 에서도 사용되는 Enum 타입의 개념이다.
예제 코드를 보면 Episode 라는 열거형 타입을 적용하면 해당 필드에는 NEWHOPE, EMPIRE, JEDI 값만 허용이 된다.

리스트 & Non-Null

  • 리스트(배열) 타입은 [] 를 통해 타입을 정의한다.
  • ! 를 통해 Null 이 아닌 값을 정의한다.
Character 객체 타입에 name, appearsIn 필드를 적용시킨 상태에서 name 필드에 스칼라 타입인 String 타입에 느낌표를 적용시켜 Null 값이 아닌 String 타입을 정의하였고, appearsIn 필드는 Episode 객체 타입을 대괄호로 감싸 배열 형태로 정의한 뒤 느낌표를 적용하여 Null 값이 아닌 값을 정의하였다.
 
만약 느낌표를 통해 Null 값이 아닌 타입을 정의한 상태에서 Null 값이 발생하면 GraphQL 에서 오류를 반환한다.
상단의 예제에서도 몇 번 나왔지만 리스트와 Non-Null 를 결합하여 사용할 수도 있다.
예제1
예제1 의 경우 리스트 자체에 Non-Null 이 적용되어있지 않기 때문에 myField 의 값은 Null 이 가능하지만 리스트 안의 데이터는 String 과 Non-Null 이 적용되어있기 때문에 Null 이 적용되면 에러가 발생한다.
예제2
예제2 의 경우 리스트 자체에 Non-Null 이 적용되어있고 리스트 내부의 데이터는 Non-Null 이 적용되어있지않은 String 타입이 적용되어있기 때문에 myField 의 값이 Null 인 경우 에러가 발생한다.

인터페이스 (Interfaces)

FPS 게임의 경우 하나의 캐릭터가 아닌 다양한 캐릭터를 통해 여러 가지 상황을 만들어 게임을 더 재밌게 즐길 수 있다. 이 경우 모든 캐릭터가 공통으로 가지고 있는 데이터가 존재할 것이다.
위의 코드처럼 모든 캐릭터에 필수적으로 들어가는 Character 라는 인터페이스를 생성하였다.
이제 생성한 Character 인터페이스를 각각의 캐릭터에 적용해보자.
두 캐릭터 모두 Character 인터페이스의 타입들을 포함한 상태에서 Tracer 는 원거리 공격을 하는 캐릭터기 때문에 ammoCount 라는 필드를, Reinhardt 는 근거리 캐릭터기 때문에 ammoCount 대신 attackSpeed 라는 필드를 가지고 있다.
인터페이스를 사용하면 필수적이고 중복되는 타입들을 하나의 인터페이스로 생성하여 implements 를 통해 각각의 타입에 적용시킬 수 있어 명확하고 유지 보수가 좋은 타입을 지정할 수 있게 해준다.

인라인 인터페이스

이제 위에서 생성한 타입을 사용해서 쿼리를 생성해보자.
두 캐릭터가 각각 가지고 있는 ammoCount, attackSpeed 필드를 적용하여 캐릭터의 정보를 가져오는 쿼리를 생성한 뒤, Tracer 캐릭터의 정보를 가져오도록 변수에 값을 적용하였다.
 
하지만 해당 코드는 에러를 반환한다.
 
왜냐하면 character 는 Character 인터페이스에 존재하는 타입만 적용되었기 때문에 Tracer, Reinhardt 이 각각 가지고 있는 타입은 적용되지 않았기 때문이다.
이런 경우 인라인 프레그먼트를 통해서 name 값에 따른 타입들을 쿼리 안에 따로 지정해줘야 한다.

유니온 타입 (Union types)

유니온 타입은 인터페이스와 비슷하지만 인터페이스와 다르게 공통된 타입을 지정해주지 않아도 된다는 차이점이 있다.
게임 캐릭터를 검색한 결과를 리스트로 반환하는 기능이 있는 경우, 검색 결과는 여러 가지 캐릭터 정보를 리스트 형식으로 반환하기 때문에 캐릭터 각각에 적용되어있는 타입을 적용시켜줘야한다.
search 쿼리에 … on [type] 을 통해 각각의 캐릭터의 타입에 맞는 필드를 구분해서 적용시켜줌으로써 검색 결과가 해당 타입인 경우 적용된 필드의 값을 반환하도록 설정할 수 있다.

입력 타입

뮤테이션을 통해서 변수에 등록할 새로운 값을 적용시켜 값을 변경시킬 수 있는데, 이 경우 새로운값에 대한 타입을 지정할 수 있다.
캐릭터의 스킬이나 노하우 같은 정보들을 작성할 수 있는 리뷰 기능이 있는 경우를 예시로 들어보려고 한다.
review 라는 인자를 추가한 뒤 CharacterReviewInput 타입을 적용시켰기 때문에 CharacterReviewInput 타입에 맞게 새로운 데이터를 입력해야한다.
 

GraphQL 의 장단점

장점

  • 데이터를 가져오는 경우 필요한 데이터만 선택해서 가져오는 것을 통해 OverFetching 문제 해결
  • 하나의 EndPoint를 통해 여러 데이터를 가져오는 것을 통해 UnderFetching 문제 해결
  • HTTP 의 사이즈를 줄일 수 있다.
 
기존의 REST API 에서는 크게 2가지의 단점이 존재했다.
  1. API 를 통해 데이터를 가져오는 경우 해당 페이지에 필요하지 않는 데이터도 가져오기 때문에 HTTP 사이즈도 증가하고 유지보수도 힘들어진다. (ex. 마이페이지 - ‘age’ 항목이 필요 없지만 데이터에 포함되어 가져와짐)
  1. 하나의 페이지에 필요한 데이터를 가져오는 경우 각각의 영역에 필요한 데이터를 따로 가져와야 됨 (ex. 마이페이지 - 프로필 정보 API, 팔로우 정보 API, 좋아요 정보 API)
1번 문제점을 OverFetching 이라 하고 2번 문제점을 UnderFetching 이라고 하는데 GraphQL 에서는 이 문제점을 해결해준다.

단점

  • 캐싱 처리가 REST API 보다 복잡하다.
  • 약간의 러닝커브가 필요하다. (개인적인 생각)
 
apollo-client + Next.js 환경의 회사 프로젝트를 진행한 적이 있었는데 리스트와 상세 데이터의 구분이 조금 어려웠었고, 데이터의 Depth 가 깊어질수록 복잡해지는 상황이 몇 번 발생했었다.
apollo 의 공식 문서에 있는 Apollo Sandbox예제를 보면 locations, location 라는 Fields 가 있는데 locations 가 리스트를 가져오는 Fields 고 location 는 하나의 특정 데이터만 가져오는 Fields 이다. 예제에서는 특정 항목 (location) 이 하나밖에 없어서 구분이 쉬운데 실제 개발을 진행하면 수많은 Fields 가 추가되기 때문에 구분이 힘들어지는 경우가 생길 수 있다.
 

 
공식문서를 참고하여 예제를 만들어보면서 간단한 특징에 대해서 정리해보았다.
추후에 apollo-client 나 relay 같은 라이브러리도 예제 프로젝트를 통해 정리할 계획이다.
 
출처