(친절한 GraphQL은 한국어도 지원한다!!)
SEQUEL
GraphQL에 대해 본격적으로 알아보기 전에 SEQUEL부터 살펴보자. IBM에서 관계형 데이터베이스를 만들면서 사용하는 언어가 SEQUEL (Structured English Query Language)이다. 낯설게 느껴질지 모르는 단어지만 이게 나중에는 우리가 아는 SQL로 불리게 된다.
SQL은 도메인에 종속된 언어로 DB 안의 데이터에 접근, 관리, 조작하는 데에 사용된다. SQL은 매우 유용한 언어지만, SQL로 실행할 수 있는 명령어는 굉장히 한정적이다. (SELECT, INSERT, UPDATE, DELETE - 데이터 작업은 이게 끝이다^^;;). 그래도 쿼리문 한 줄로 DB 안의 여러 데이터 테이블에서 필요한 데이터를 한 번에 추출할 수 있다. 갑자기 SQL이 나온 이유는 이 개념들이 REST에 영향을 많이 줬기 때문이다. 데이터는 오로지 읽고, 쓰고, 갱신하고, 삭제만 할 수 있다는 점을 REST에서는 GET, POST, PUT, DELETE
의 각기 다른 HTTP 메서드를 사용한다. 하지만, REST에서 데이터를 읽거나 변경하려면 엔드포인트 URL을 사용할 수 밖에 없고, 실제 쿼리 언어는 사용할 수 없다.
GraphQL은 쿼리 데이터베이스용으로 만들어진 개념을 가져다가 인터넷에 적용해 만들어진 개념이다(GraphQL에 QL이 쿼리 언어(query language)이다). 쿼리 하나로 여기저기 흩어져 있는 데이터를 한데 모아 받는 것이다. 그렇다면 SQL과 다른 점은 무엇일까?
일단 사용 환경이 다르다. SQL 쿼리는 DB로 보내지만, GraphQL은 API로 보낸다. SQL 데이터는 데이터 테이블 안에 저장되어 있으나 GraphQL 데이터는 저장 환경을 가리지 않는다.
두번째로 구문이 다르다. GraphQL에서는 SELECT 대신 query
를 사용해 데이터 요청을 보내며 모든 작업의 중심은 쿼리이다. 또한 INSERT, UPDATE, DELETE
대신 GraphQL은 Mutation
이라는 데이터 타입을 가지고 데이터를 조작하고 인터넷에서 사용될 용도기 때문에 Subscription
타입도 존재한다(나중에 자세히..).
세번째로 GraphQL은 명세에 따라 표준화되어 있다. 프로그래밍 언어에 종속적이지 않다는 말이다. GraphQL은 GraphQL 쿼리일 뿐이다. 프로젝트에 사용하는 언어(자바이건 하스겔이건 등등) 상관이 없다.
GraphQL 쿼리
# 다중 데이터 요청
{
hero {
name
# 쿼리에 주석을 쓸 수도 있습니다!
friends {
name
}
}
}
위와 같이 요청을 보내면 아래와 같은 데이터가 반환된다. 이제 이런 쿼리를 조금 더 알아보자.
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
쿼리 작업으로 API에 데이터를 요청할 수 있다. 이제 더 이상의 고민은 필요없다. 프론트 개발자는 쿼리 안에는 GraphQL 서버에서 받고 싶은 데이터만 쓰면 된다. 다만 쿼리를 보낼때는 요청 데이터를 필드로 적어 넣으면 된다. 그럼 위에 예제에서 볼 수 있듯이 서버에서 가져온 필드는 JSON 응답 데이터의 필드와 일치한다. (맨 앞에 data
라는 키가 붙는 것은 정상적인 쿼리를 보냈을 때 data키가 오는 것이다. 만약 비정상적인 쿼리였다면 응답키로 error 키가 들어있는 JSON 데이터가 날아왔을 것이다.)
쿼리 문서에는 여러 개의 쿼리를 추가할 수도 있다. 그러나 한번에 한 쿼리에 대해서만 요청이 이루어지기 때문에 여러개의 쿼리를 사용할거라면, 아래와 같이 쿼리를 나눠서 적어야 한다.
{
hero {
name
friends {
name
}
}
}
{
heroSkills {
name
effect {
name
power
}
}
}
이렇게 말이다. 물론 지금 사용한건 단축 문법
으로 실제로 사용할때는 꼭 아래와 같이 작업 타입에 대한 키워드를 넣어줘야 한다. 쿼리(query)
, 뮤테이션(mutatin)
, 구독(subscription)
등이다. (아래 예제부터는 가급적 단축문법을 사용하지 않겠다. - 이유는 실제 사용시 쿼리 이름을 넣어줘야 디버깅이 쉬워진다.)
query heroes {
hero {
name
friends {
name
}
}
}
query skills {
heroSkills {
name
effect {
name
power
}
}
}
그렇다면 위 데이터를 한 번에 받고 싶다면 어떻게 해야될까?
query heroesAndSkills {
heroes {
hero {
name
friends {
name
}
}
}
skills {
heroSkills {
name
effect {
name
power
}
}
}
}
여기서 나오는게 GraphQL의 장점이라고 할 수 있는 부분이다. 그냥 같은 쿼리 안에 전부 써주면 된다. 쿼리 한 번에 여러 종류의 데이터를 받을 수 있는 것이다. 슈퍼쿨(Super Cool)!
인자
이미 GraphQL은 객체와 필드를 탐색하는 것만으로도 매우 유용하다. 하지만 필드에 인자를 전달하는 기능까지 가능하다. 우리가 일반적으로 함수에 인자를 전달하는 것과 같은 형태이다. 예를 들어 사람을 조회하면서 id = 812
인 사람을 조회하고 싶다. 그럼 아래와 같이 인자값을 전달하면 된다.
query human {
human(id: 812) {
name
height
}
}
REST 방식에서 우리가 단일 인자를 전달하는 것과는 다른 모습이다. 그냥 원하는 필드와 그것들을 중첩하고 필드에 인자값을 넘겨주면 여러번의 API fetch를 완벽하게 대체할 수 있다. 이 뿐만이 아니라 인자는 다양한 타입
을 가질 수 있고 커스텀 타입 또한 가질 수 있다.
query human {
human(id: 812) {
name
height(unit: FOOT)
}
}
길이 다위인 FOOT
타입을 사용했다. GraphQL에서 기본 제공하는 타입들이 있으며 커스텀 타입 선언도 할 수 있다.
타입은 https://graphql-kr.github.io/learn/schema/ 여기 주소를 참고하면 된다. 간단하게만 언급하자면 GrahpQL 쿼리어는 스칼라(scalar) 타입과 객체(object) 타입 둘 중 하나에 속하게 된다. 먼저 스칼라 타입은 다른 언어의 원시 타입과 유사하며, 정수(Int)
, 실수(Float)
, 문자열(String)
, 부울(Boolean)
, 고유 실별자(ID)
가 있다. 여기서 특이한 아이는 ID
인데 GraphQL은 반드시 유일한 문자열을 반환하도록 되어 있다. 그리고 객체 타입
은 스키마에 정의한 필드를 그룹으로 묶어둔 것이고, 그 타입은 중첩도 가능하다.
# Response JSON data
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 5.6430448
}
}
}
Query 타입
Query
는 GraphQL의 타입이며 Root Type
이라고도 한다. GraphQL API에서 query에 사용할 수 있는 필드는 API 스키마에 정의하면 된다(이따가 좀 더 자세히!).
Selection set
쿼리를 작성할 때는 필요한 필드를 중괄호로 감싼다. 이 중괄호로 묶인 블록을 셀렉션 세트라고 한다. 그 안에 들어 있는 필드는 모두 Query 타입 안에 정의되어 있다. 그리고 위에서 봤듯이 셀렉션 세트는 서로 중첩시킬 수 있다. 중첩 시에는 똑같이 중괄호를 사용하며, 새로운 셀랙션 세트를 만들기만 하면 된다. 이렇게 작성하고 GraphQL 서버로 요청을 보내 정상적으로 데이터를 받았다면, 데이터는 JSON 포맷
으로 되어 있고, 쿼리의 형태와 똑같은 모양을 하고 있다. 그리고 각각의 JSON 필드명은 쿼리의 필드명과 동일하다.
Alias (별칭)
별칭은 두 가지 경우에 많이 사용됩니다. 먼저 위에서 언급했듯이
쿼리의 형태와 똑같은 모양을 하고 있다. 그리고 각각의 JSON 필드명은 쿼리의 필드명과 동일하다.
라고 했다. 하지만 응답 객체의 필드명을 다르게 받고 싶을 수 있다. 이때는 아래와 같이 필드명에 별칭을 부여하면 된다.
query human {
user: human {
name
height(unit: FOOT)
}
userGender: humanType {
gender
}
}
이렇게 별칭을 부여해서 데이터를 요청하면 아래와 같은 응답을 받을 수 있다.
{
"data": {
"user": {
"name": "Luke Skywalker",
"height": 5.6430448
},
"userGender": {
"gender": "men"
}
}
}
또 한가지 별칭을 자주 사용하는 경우는 같은 필드를 다른 인자를 사용하여 쿼리하고 싶을 때다. 예를 들어 사람1(id=1)
과 사람2(id=2)
의 이름을 가지고 오고 싶다고 하자. 먼저 잘못된 경우를 보면 아래와 같다.
query human {
human(id: 1) {
name
}
human(id: 2) { # human 필드가 중복되어 충돌이 난다!
name
}
}
이런 경우에 위와 동일하게 타입 별칭을 지정해 준다.
query human {
user1: human(id: 1) {
name
}
user2: human(id: 2) {
name
}
}
아래와 아래와 같이 데이터를 받을 수 있다.
{
"data": {
"user1": {
"name": "Master jung",
},
"user2": {
"name": "Jacob",
}
}
}
Fragment (프래그먼트)
GraphQL 쿼리 안에는 각종 작업에 대한 정의와 프래그먼트에 대한 정의가 들어갈 수 있다. 간단히 생각하면 셀렉트 세트
를 변수에 담아 재활용한다는 느낌으로 받아드리면 편하겠다. 예제를 보면 이 말이 더 이해가 될 것이다.
query {
Lift(id: "jazz-cat") {
name
status
capacity
night
elevationGain
trailAccess {
name
difficulty
}
}
Trail(id: "river-run") {
name
difficulty
accessedByLifts {
name
status
capacity
night
elevationGain
}
}
}
나는 jazz-cat
과 river-run
에 대한 정보를 요청하고 싶다. 물론 저렇게 보낸다고 틀린 쿼리는 아니지만, 개발자가 제일 싫어하는 중복이 존재한다.
이때 사용하는게 프래그먼트이고 fragment
키워드를 이용하여 만들 수 있다. 위 예제를 프래그먼트를 사용해 수정해 보자.
query {
Lift(id: "jazz-cat") {
...liftInfo
trailAccess {
name
difficulty
}
}
Trail(id: "river-run") {
name
difficulty
accessedByLifts {
...liftInfo
}
}
}
fragment liftInfo on Lift { # Lift 타입에 대한 셀렉션 세트이다라는 의미
name
status
capacity
night
elevationGain
}
자바스크립트의 스프레드 연산자
와 비슷해 보이는 구문이다. 실제 역할도 비슷해서 객체에서 다른 객체로 할당하고자 할 때 사용할 수 있다. 그리고 프레그먼트는 얼마든지 생성하고 사용할 수 있다. 위 예제에서 프레그먼트를 추가해서 아래와 같이 사용해도 문제 없다.
query {
Lift(id: "jazz-cat") {
...liftInfo
trailAccess {
name
difficulty
}
}
Trail(id: "river-run") {
...trailInfo
}
}
fragment trailInfo on Trail {
name
difficulty
accessdByLifts {
...liftInfo # 프레그먼트 안에서 다른 프레그 먼트도 얼마든지 사용
}
}
fragment liftInfo on Lift { # Lift 타입에 대한 셀렉션 세트이다라는 의미
name
status
capacity
night
elevationGain
}
인라인 프래그먼트(inline fragment)
쿼리 안에서 특정 타입을 바로 셀렉션 세트에 넣는 방식이며, 특정한 타입의 필드를 요청하려면 타입 조건과 함께 인라인 프래그먼트를 사용하면 된다. 그리고 인터페이스나 유니언 타입을 반환하는 필드를 쿼리하는 경우, 인라인 프래그먼트
를 사용해야 한다.
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
{
"ep": "JEDI"
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
meta fields 메타 필드
인라인 프래그먼트나 GraphQL의 서비스를 이용하다보면, 리턴될 타입을 모르는 상황이 발생한다. 예를 들어 여러 타입에서 같은 필드명을 조회하는 경우다. 예를 보자.
{
search(text: "an") {
... on Human {
name
}
... on Droid {
name
}
... on Starship {
name
}
}
}
이런 요청을 보낸다고 생객해보자. 데이터가 모두 정상적으로 쿼리되었다는 가정하에 과연 어떤 모습을 예상할 수 있을까?
{
"data": {
"search": [
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
},
{
"name": "TIE Advanced x1"
}
]
}
}
자, 이제 프론트엔드 개발자는 고민해야 한다. 저 데이터의 타입을 내가 요청한 타입의 순서와 동일하다는 가정을 하고 백엔드 개발자에게 무한한 신뢰를 보내거나, 불신하고 데이터베이스의 권한을 어떻게든 얻어내서 검증을 하는 것이다. 아, 하나하나 다시 보내서 확인하는 방법도 물론 있을 수도 있겠지만, 똑똑한 GraphQL은 이 부분에 대해서 해결책을 제시한다. 바로 GraphQL 쿼리 어느 지점에서나 사용가능한 메타필드인 __tpyename
을 제공한다. 자 그러면 요청을 바꿔보자.
{
search(text: "an") {
__typename
... on Human {
name
}
... on Droid {
name
}
... on Starship {
name
}
}
}
크게 달라진건 없다. 그냥 나의 요청에 메타 필드(__typename
)를 추가했다.
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo"
},
{
"__typename": "Human",
"name": "Leia Organa"
},
{
"__typename": "Starship",
"name": "TIE Advanced x1"
}
]
}
}
그랬더니 모든 필드의 타입이 나왔다. 배려심이 좋은 GraphQL이다!
변수(variable)
지금까지는 모든 인자를 쿼리 문자열 안에 작성했지만, GraphQL에서 제공하는 변수를 이용하면 동적으로 처리가 가능하다. GraphQL에서 언제나 변수명 앞에 $(달러)
표시를 넣어주면 된다.
# 쿼리문
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
{
"episode": "JEDI"
}
# Response Data
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
변수 정의
변수 정의는 위 예제에서는 $episode: Episode
이 부분에 해당한다. 변수명 앞에는 $
표시를 적어줬으며, 그 뒤에는 타입
을 적어준다. (typescript
와 비슷해서 익숙하다.) 선언된 모든 변수는 스칼라
, 열거형
, input object type
이어야 한다. 그리고 필수값과 옵션값도 설정할 수 있는데 변수명 옆에 필수값일 경우 !
붙여주면 되고, !
값이 없다면 옵션값이다.
변수 기본값
아래와 같이 타입 선언 다음에 기본값을 명시해 주면되며, 모든 변수에 기본값이 있을 경우, 변수를 전달하지 않아도 쿼리를 호출할 수 있다.
query HeroNameAndFriends($episode: Episode = "JEDI") {
hero(episode: $episode) {
name
friends {
name
}
}
}
Directives (지시자)
위에서 변수를 동적으로 사용하는 방법을 알아보았다. 하지만, 저 멋진 기능이 있다. 바로 지시자
기능이다. 지시자
기능은 간단하게 말하면 조건문
기능을 쿼리문에서 사용할 수 있도록 도와주는 것들이다. 현재 두 가지 지시자(@include
, @skip
)가 있다.
@include(if: Boolean)
: 인자가 true
인 경우에만 이 필드를 결과에 포함한다. @skip(if: Boolean)
: 인자가 true
이면 이 필드를 건너 뛴다.
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
# 변수에 전달하는 데이터
{
"episode": "JEDI",
"withFriends": false
}
# Response 데이터
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
아주 멋진 기능이다. 다만 코어 GraphQL 사양에는 포함되어 있으나, 실제 GraphQL 서버에서 이를 지원해야 한다.
참고자료
- https://graphql.org/
- https://graphql-kr.github.io/
- Learing GraphQL by Eve Porello and Alex Banks(O'Reilly). Copyright 2018 Moon Highway, LLC, 978-1-492-03071
'GraphQL' 카테고리의 다른 글
[GraphQL] 5. GraphQL 실행 환경 구성하기(with Apollo) (1) | 2019.12.09 |
---|---|
[GraphQL] 4. GraphQL 스키마 정의 (0) | 2019.12.09 |
[GraphQL] 3. GraphQL 쿼리어2 (0) | 2019.12.09 |
[GraphQL] 1. 기초 개념 및 배경지식 (0) | 2019.12.09 |