GraphQL 기초

추석 전 마지막 스프린트 진행이 생각보다 빨라서 enhancement로 API 서버에 GraphQL을 구성했다.

GraphQL은 API 쿼리 랭귀지다. RESTful한 API는 endpoint마다 출력결과 형태가 고정되어서, API client는 필요한 데이터를 위해 API를 여러번 호출하거나, 필요없는 데이터까지 받아올 수 밖에 없는 경우가 많다. GraphQL은 API client가 원하는 데이터를 원하는 모양으로 출력할 수 있게 한다.

API client 입장에서 GraphQL을 이용하여 어떻게 요청하는지부터 알아보자. 서버가 어떻게 만들어지는지, 데이터를 어떻게 불러오는지는 지금 몰라도 좋다. 왼쪽과 같은 GraphQL 쿼리를 POST body로 서버에 요청하면, 오른쪽 데이터가 그 응답으로 반환된다. 쿼리와 결과의 모양을 보자.

query {
  hero {
    name
  }
}
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

GraphQL API를 사용하는 입장에서는 원하는 모양 그대로 요청하면 되니 아주 직관적이다. 데이터를 읽는 요청을 query라 하고, 데이터를 변경하는 요청을 mutation이라 한다.

Query

query는 데이터를 불러오는 구문이다. RESTful API에서의 GET 메소드에 해당한다.

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

처음 예제와 다르게 시작 부분에 query가 생략되었다. query문의 경우는 생략이 가능하고, mutation문일 경우는 명시 해주어야 한다. human, name, height 등 필요한 리소스의 field를 특정하여 응답 데이터의 형태를 구성할 수 있다. field에는 마치 함수처럼 argument를 전달할 수도 있다.

field는 리소스 그 자체가 될 수도 있고, 리소스의 property가 될 수도 있다. 어떤 field들이 있는지, 어떤 field에 어떤 argument를 쓸 수 있는지는 서버 사이드에서 정의한다.

Mutation

mutation은 데이터를 변경하는 구문이다. 리소스를 생성하는 구문과 수정하는 요청 모두 mutation를 사용 할 수 있다. RESTful API에서의 POSTPUT 혹은 FETCH에 해당한다. REST에서 http 요청 메소드와 CRUD의 관계는 걍제성 없는 약속과 같듯, GraphQL에서도 query로 데이터를 변경할 수는 있다. 하지만 GraphQL이 POST 메소드만 사용하기 때문에 해당 요청이 데이터 변경임을 명시적으로 하기 위해 mutation을 사용하자.

mutation {
  updateHuman(id: "1000", input: {
    name: "Vichyssoise"
  }) {
    name
    height(unit: FOOT)
  }
}
{
  "data": {
    "updateHuman": {
      "name": "Vichyssoise",
      "height": 5.6430448
    }
  }
}

해당 요청이 mutation임을 명시적으로 나타냈다. updateHuman은 2개의 argument를 받는다. 이 mutation은 id로 human을 찾아서 input의 내용대로 변경한다. mutation이 어떤 argument를 받을지와 어떻게 작동할지 역시 서버에서 정의한다. 그 다음 query와 마찬가지로 어떤 형태로 데이터를 반환할지 명시한다.


지금까지는 GraphQL API를 어떻게 이용하는지 봤다. 이번엔 GraphQL API가 어떻게 만들어지는지 보자.

GraphQL 서버는 schema와 resolver로 이루어진다. schema는 query와 mutation을 정의하고, 어떤 field에 어떤 하위 field가 있는지를 정의한다. resolver는 field의 데이터를 불러오는 방법을 정의한다. resolver는 GraphQL 그 자체의 영역은 아니다. GraphQL을 사용할 수 있게 해주는 여러 구현체(라이브러리)들이 있어서, 여기에 맞춰 코드를 작성해야 한다.

Schema

schema 정의는 root object부터 시작한다. root object는 query와 mutation을 가지며 mutation은 필수가 아니다.

schema {
  query: Query
  mutation: Mutation
}

queryQuery 타입이고, mutationMutation 타입이다. 이 두 타입은 직접 정의해야 하며 타입 명이 꼭 QueryMutation일 필요는 없다. Query는 어떻게 정의할 수 있을까?

type Query {
  player(id: ID!): Player
}

위의 정의는 다음과 같다. Query 타입은 player field가 있다. playerPlayer타입이고 id를 argument로 받는다. 그리고 idID타입이고 null이 아니다.

모든 field와 argument에는 타입이 있다. GraphQL에서 타입의 종류는 2가지로 Scalar 타입과 Object 타입이 있다. Scalar 타입은 이미 정의된 최소 단위의 데이터 타입을 말한다. Int, Float, String, Boolean, ID가 Scalar 타입이며 ID 타입은 object의 캐싱을 위해 사용된다. Object 타입은 field들을 가진 데이터 타입으로 이 field들 또한 Scalar 타입이거나 Object 타입이다. Object 타입은 직접 정의해주어야 하며, 여기선 QueryPlayer가 Object 타입이다. 필요한 Object들을 마저 정의하자.

type Player {
  id: ID!
  name: String!
  team: Team
}

type Team {
  id: ID!
  name: String!
  players: [Player]!
}

타입 뒤에 !는 해당 데이터가 null이 아님을 나타낸다. [Player]처럼 Array 타입을 정의할 수도 있다.

위에도 언급했듯 query와 mutation은 데이터 변경을 명시화해준다는 것 외에는 차이가 없으니 Mutation 타입 정의는 생략하겠다. (작동 방식에 차이가 좀 있지만 몰라도 상관없음)

위와 같이 구성된 GraphQL 서버를 다음과 같은 구문으로 사용할 수 있다.

{
  player(id: 100) {
    name
  }
}
{
  "data": {
    "player": {
      "name": "Vichyssoise"
    }
  }
}
{
  player(id: 100) {
    name
    team {
      id
    }
  }
}
{
  "data": {
    "player": {
      "name": "Vichyssoise",
      "team": {
        "id": 99
      }
    }
  }
}
{
  player(id: 100) {
    name
    team {
      players {
        name
      }
    }
  }
}
{
  "data": {
    "player": {
      "name": "Vichyssoise",
      "team": {
        "players": [
          {
            "name": "Vichyssoise"
          }
        ]
      }
    }
  }
}

심지어 이런 짓도 가능하다.

{
  player(id: 100) {
    team {
      players {
        team {
          players {
            team {
              id
            }
          }
        } 
      }
    }
  }
}
{
  "data": {
    "player": {
      "team": {
        "players": [
          {
            "team": {
              "players": [
                {
                  "team": {
                    "id": 99
                  }
                }
              ]
            }
          }
        ]
      }
    }
  }
}

요청 구문에서 제일 하위 field들은 모두 Scalar 타입임에 주목하자.

Withcamp 해커톤

hhkb_pro2

첫 야외 해커톤에, 첫 하드웨어 해커톤이다.

우리팀은 얼굴 각도를 인식해서 자동으로 뿅망치 맛을 보여주는 참참참 기계를 만들기로 했다. 톱질, 망치질, 사포질을 위해 몸을 쓰는 해커톤이라니! 신선하지 않을 수가 없다.

우리팀 팀원은 기계공학과 대학생과 대학원생들이었다. 설계하는데 다들 알파벳으로 이야기하니까 뭔가 있어보였다. 대충 설계를 끝내고 주최측에서 제공하지 않지만 필요한 물품들을 다이소에서 구매했다. 행사 당일 길거리에서 누가 버린 작은 책장과 목재를 가져다가 프레임을 만들고, 다이소에서 샀던 악력기를 뜯고 부셔서 뿅망치의 스프링으로 사용했다.

이 친구들은 코딩도 어느정도 할 줄 알아서 난 기기간 시리얼 통신만 열어주고, 코드가 작동하지 않으면 검수해서 고쳐주는 정도만 했다. 사실 코딩보다 하드웨어 만지는게 더 재미있어 보여서 대부분의 코드는 C#할 줄 아는 다른 팀원이 짰다. 뜨거운 물에 넣으면 녹아서 모양을 만들 수 있는 물라스틱, 사람 얼굴을 인식해서 데이터를 뽑아주는 인텔 리얼센스, 20만원짜리 모터(이름을 까먹었다)와 시리얼 통신으로 작동하는 하드웨어 모듈까지 조금도 쉴 새 없이 계속 새로웠다.

설계의 착오로 뿅망치 장전을 위해 낚시줄을 사용해 감아서 발표 때 1회 시연하고 망가져 버렸지만, 제대로 작동하는 걸 확인했다. 사회자님 눈은 괜찮으실라나.

모듈과 부품들 잔뜩 사놓고 취미삼아 만져봐도 재밌을 것 같다. 시간만 있다면…

9xd 해커톤

9xd에서 첫번째 해커톤을 한다길래 얼른 신청했다. 9xd 모임 신청이 수강신청만큼이나 경쟁률이 높아서 이번 해커톤도 아마 신청자 수가 만만치 않았으리라.

예전에 몇몇 해커톤에 불평하는 글을 쓰고 페이스북에 링크한 적이 있는데 9xd를 만든 진유림님이 그걸 공유하신 덕에 내 블로그 일일 방문자가 400을 찍었었다. 나와 비슷한 생각을 하고 계신 것 같아서 직접 주최하는 해커톤에 대한 기대가 컸다.

기대한 만큼 만족도 컸다.

팀 구성을 미리 하지 않았다. 참가자를 받으면서 팀을 미리 구성했지만 행사 당일에 팀을 발표했다. 참가 신청을 받을 때 참가자의 기술스택도 파악해서 아웃풋이 없는 팀이 나오지 않도록 적절하게 팀을 구성한 것도 박수.

주제가 어느정도 좁혀져 있었다. 주최측에서 정하는게 아니라 참가자끼리 주제를 여러개 만들고 랜덤으로 뽑아서 선정했다. 다수의 해커톤 경험을 미루어 볼 때, ‘무엇을 만들까’로 이야기가 길어지는 팀은 대부분 실패한다. 이 때문에 모든 팀이 빠르게 구현에 집중할 수 있지 않았나 싶다.

축제 분위기를 위한 많은 장치가 있었다. BM은 신경쓰지 않고 정말 만들고 싶었던 재미있는 장난감을 만들 수 있게 유도하고, 모든 팀이 상을 받고(우리 팀이 받은 한 뼘 크기 쿠키런 피규어가 젤 맘에 든다), 맥락없는 춤 타임도 있었다.

맥락없는 춤 타임은 왜 있는지 모르겠지만 ㅋㅋ

우리팀은 ‘월급 루팡도 측정기’를 만들었다. Rescue Time과 비슷한데, 업무와 연관된 사이트 접속 비율을 측정해서 화면에 을 뿌려주는 크롬 확장 프로그램이다. 크롬 확장 프로그램은 처음 만들어보았는데, 웹 프론트엔드를 조금만 할 수 있다면 누구나 쉽게 만들 수 있다. API 문서도 상당히 깔끔하게 되어 있다. 발표할 때는 네이버를 비업무사이트의 예로 두고 시연했는데, 참가자 중에 네이버 개발자 분이 계셨다. ㅋㅋㅋㅋㅋㅋ

프로그램도 알찼고, 축제 같은 느낌, 무엇보다 내가 좋아하는 ‘어설픈 행사 진행과 사회’가 있어서 만족스러웠다.(공감 못하면 그냥 넘어가자…ㅋㅋ)