GraphQL에 대해 배우게 되었다.
GraphQL은 세상에 나온지 얼마 안 돼서 API 개발의 선두로 올라온 언어라고 한다.
나한테는 처음 보는 개념이라 솔직히 이게 뭔지 잘 몰랐지만, 배우고나니 꽤 편리한 기능을 가지고 있다고 생각을 하게 되었다.
이번 유닛에서는 지난 섹션에서 배웠던 개념 중 REST API와 GraphQL의 차이 및 GraphQL을 다루는 법에 대해 학습하였고, GraphQL을 잘 배워두면 API 개발을 하는 데에 있어서 더 폭 넓은 선택을 할 수 있도록 도와줄 것 같다!
GraphQL란?
GraphQL은 Graph + Query Language의 줄임말로 페이스북에서 만든 쿼리 언어이고, 2016년 처음으로 등장해 현재까지 인지도 및 만족 부분에서 높은 비율을 차지하고 있다고 한다.
쉽게 말해 API를 위한 쿼리 언어라고 할 수 있다.
왜 Graph를 사용할까?
GraphQL의 아이디어는 그래프로 생각하기에서부터 출발하는데, 그래프라는 자료구조는 인간의 뇌 구조 및 언어적인 설명과 비슷하기 때문에 실제 현실 세계의 많은 현상들을 모델링할 수 있는 강력한 도구이다. 따라서 그래프 자료구조를 살펴보면 우리가 특정 개념을 학습하고 이를 다른 개념과 연관시킬 때 자연스럽게 사용하는 마인드맵과 유사한 데이터 구조를 가진다는 것을 알 수 있다.
GraphQL에서는 모든 데이터가 그래프 형태로 연결되어 있다고 전제하는데, 일대일로 연결된 관계도, 여러 계층으로 이루어진 관계도 모두 그래프이다. 트리나 그래프나 노드와 노드를 연결하는 간선으로 구성된 자료구조이기 때문이다. 단지 그 그래프를 누구의 입장에서 정렬하느냐에 따라 트리 구조를 이룰 수 있다.
이를 통해 GraphQL은 클라이언트 요청에 따라 유연하게 트리 구조의 JSON 데이터를 응답으로 전송할 수 있는데, 다시 말해 GraphQL은 REST API 방식의 고정된 자원이 아닌 클라이언트 요청에 따라 유연하게 자원을 가져올 수 있다는 점이 아주 큰 이점을 갖는다.
GraphQL의 특징
REST API의 한계
예를 들어 유저의 이름만 필요한 상황에서 REST API를 사용한다면, 응답 데이터에는 유저의 주소, 생일 등과 같이 실제로는 클라이언트에게 필요없는 정보가 포함되어 있을 수도 있다.
REST API에서는 자원의 크기와 형태를 서버에서 결정하기 때문에 클라이언트가 직접 데이터의 형태를 결정할 수 없다고 한다. 예를 들어 클라이언트에서 필요한 데이터의 내용이 변할 경우 다른 endpoint를 통해 변경된 데이터를 가져오거나 수정을 해야 한다.
REST API와 GraphQL의 다른점
- REST API는 Resource에 대한 형태 정의와 데이터 요청 방법이 연결되어 있지만, GraphQL에서는 Resource에 대한 형태 정의와 데이터 요청이 완전히 분리되어 있다.
- REST API는 Resource의 크기와 형태를 서버에서 결정하지만, GraphQL에서는 Resource에 대한 정보만 정의하고, 필요한 크기와 형태는 클라이언트 단에서 요청 시 결정한다.
- REST API는 URI가 Resource를 나타내고 Method가 작업의 유형을 나타내지만, GraphQL에서는 GraphQL Schema가 Resource를 나타내고 Query, Mutation 타입이 작업의 유형을 나타낸다.
- REST API는 여러 Resource에 접근하고자 할 때 여러 번의 요청이 필요하지만, GraphQL에서는 한번의 요청에서 여러 Resource에 접근할 수 있다.
- REST API에서 각 요청은 해당 엔드포인트에 정의된 핸들링 함수를 호출하여 작업을 처리하지만, GraphQL에서는 요청 받은 각 필드에 대한 resolver를 호출하여 작업을 처리한다.
장점
- 하나의 endpoint 요청
/graphql이라는 하나의 endpoint 로 요청을 받고 그 요청에 따라 query , mutation을 resolver 함수로 전달해서 요청에 응답한다. 모든 클라이언트 요청은 POST 메소드를 사용한다.
- No! under & overfetching
여러 개의 endpoint 요청을 할 필요없이 하나의 endpoint에서 쿼리를 이용해 원하는 데이터를 정확하게 API에 요청하고 응답으로 받을 수 있다.
- 강력한 playground
graphql 서버를 실행하면 playground라는 GUI를 이용해 resolver 와 schema 를 한 눈에 보고 테스트 해 볼 수 있다.
- 클라이언트 구조 변경에도 지장이 없음
클라이언트 구조가 바뀌어도 필요한 데이터를 결정하고 받는 주체가 클라이언트이기 때문에 서버에 지장이 없다.
단점
- REST API에 친숙한 개발자의 경우 GraphQL를 학습하는 데 시간이 필요하다.
- 캐싱이 REST보다 훨씬 복잡하다.
- 고정된 요청과 응답만 필요할 경우에는 Query 로 인해 요청의 크기가 RESTful API 의 경우보다 더 커진다.
이제 GraphQL 구조에 대해 자세히 알아보자.
{
hero {
name
}
}
{
"data": {
"hero": {
"name": "king"
}
}
}
hero의 name을 쿼리를 실핼 했을 때이다.
필드의 name은 String 타입을 반환하고, 쿼리와 결과가 정확하게 같은 모양을 하고 있음을 확인할 수 있다.
{
hero {
name
# 이런 식으로 GraphQL 내에서 주석도 작성 가능.
friends {
name
}
}
}
{
"data": {
"hero": {
"name": "king",
"friends": [
{
"name": "queen"
},
{
"name": "baby"
}
]
}
}
}
히어로의 이름과 히어로의 친구의 이름이 조회가능하다. 이런 식으로 원하는 필드를 중첩하여 쿼리하는 것도 가능하다는 것이다.
필드에 인수를 전달하는 부분을 추가하게 되면 쿼리의 필드 및 중첩된 객체들에 전달하여 원하는 데이터만 받아올 수 있다.
{
human(id: "77") {
name
height
}
}
{
"data": {
"human": {
"name": "zozo",
"height": 1.77
}
}
}
id가 1000인 human의 name과 height를 쿼리를 하였다.
이런 식으로 id가 77 human의 이름과 키를 쿼리해 올 수 있었다.
필드 이름을 중복해서 사용할 수 없으므로, 필드 이름을 중복으로 사용해서 쿼리를 해야 할 때는 별명을 붙여서 쿼리해야 한다.
{
hero(episode: king) {
name
}
hero(episode: queen) {
name
}
}
이런식으로 불가능하다.
{
kingHero: hero(episode: king) {
name
}
queenHero: hero(episode: queen) {
name
}
}
앞에 알아볼 수 있는 별명을 붙여주면 쿼리할 수 있다.
{
"data": {
"kingHero": {
"name": "zozo"
},
"queenHero": {
"name": "mimi"
}
}
}
위와 같이 다른 이름으로 별명을 지정하면 한 번의 요청으로 두 개의 결과를 모두 얻어낼 수 있다.
실제 앱에서는 코드를 모호하지 않게 작성하는 것이 중요하다고 한다.
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
{
"data": {
"hero": {
"name": "king",
"friends": [
{
"name": "queen"
},
{
"name": "baby"
}
]
}
}
}
앞의 query는 오퍼레이션 타입으로, 오퍼레이션 타입에는 query 뿐만 아니라 mutation, subscription, describes 등이 있다.
쿼리를 약식으로 작성하지 않는 한 이런 오퍼레이션 타입은 반드시 필요하다.
여태껏 고정된 인수를 받아 쿼리했지만, 실제 앱을 사용할 때는 고정된 인수를 받는 것보다는 동적으로 인수를 받아 쿼리하는 경우가 대다수이다.
query HeroNameAndFriends($episode: king) {
hero(episode: $king) {
name
friends {
name
}
}
}
오퍼레이션 네임 옆에 변수를 $변수 이름: 타입 형태 로 정의하면 되고, 뒤에 !가 붙는다면 episode는 반드시 Episode여야 한다는 뜻이다.
GraphQL은 대개 데이터를 가져오는 데에 중점을 두고 있지만 서버측 데이터를 수정하기도 한다.
mutation CreateReviewForEpisode($ep: king!, $review: ReviewInput!) {
createReview(king: $ep, review: $review) {
stars
commentary
}
}
GraphQL 스키마의 가장 기본적인 구성 요소는 서비스에서 가져올 수 있는 객체의 종류, 그리고 포함하는 필드를 나타내는 객체 유형이다.
type Character {
name: String!
appearsIn: [king!]!
}
배열에도 !가 붙을 수 있다. 여기서는 ! 이 뒤에 붙어 있어 null 값을 허용하지 않으므로 항상 0개 이상의 요소를 포함한 배열을 기대할 수 있게 된다.
요청에 대한 응답을 결정해주는 함수로써 GraphQL의 여러 가지 타입 중 Query, Mutation, Subscription과 같은 타입의 실제 일하는 방식 즉 로직을 작성한다.
위와 같이 스키마를 정의하면 그 스키마 필드에 사용되는 함수의 실제 행동을 Resolver에서 정의하고, 또한 이러한 함수들이 모여 있기 때문에 보통 Resolvers라고 부른다.
const db = require("./../db")
const resolvers = {
Query: { // **Query :** 저장된 데이터 가져오기
getUser: async (_, { email, pw }) => {
db.findOne({
where: { email, pw }
}) ... // 실제 디비에서 데이터를 가져오는 로직을 작성
...
}
},
Mutation: { // **Mutation :** 저장된 데이터 수정
createUser: async (_, { email, pw, name }) => {
...
}
}
Subscription: { // **Subscription :** 실시간 업데이트
newUser: async () => {
...
}
}
};