 ## GraphQL 이란 GraphQL은 2015년 Facebook팀에서 발표한 API 쿼리 언어 입니다. GraphQL을 처음 접하게 되는 경우 GraphQL의 정확한 역할과 개념이 무엇인지 감이 안올수도 있습니다. GraphQL을 제대로 이해하려면 해당 단어를 두가지 관점에서 바라보는 것이 좋습니다. Graph 와 QL(Query Language)입니다.  Graph는 많이들 아시는 Graph 자료 구조를 의미하며 QL(Query Language)는 뜻 그대로 쿼리 언어 입니다. 대표적인 쿼리 언어로는 SQL이 있습니다. 그렇다면 GraphQL은 그래프 구조로 질의를 하는 방식을 말할까요? 그렇습니다! GraphQL은 API를 Graph구조로 QL(질의) 하는 언어를 뜻합니다. 그렇다면, 페이스북 팀은 왜 GraphQL을 만들게 된 것일까요? 페이스북 팀에서 GraphQL을 개발을 시작하기로 마음 먹었을 당시에는 RESTful 서버와 FQL(Facebook SQL) 데이터 테이블을 사용하고 있었는데 이는 자주 고장나고 성능이 점점 떨어지고 있다는 것을 깨달았습니다. 이를 해결하기 위해서는 클라이언트와 서버의 데이터 전송 방식을 개선 햐아 한다는 생각을 하게 됩니다. 그리고 만들어낸 방법이 GraphQL입니다. 내부적으로 충분한 테스트와 실제 어플리케이션에 적용을 한 후에 오픈소스로 발표하게 됩니다. 이는 곧 React와 더불어 페이스북에서 개발한 오픈소스 프로젝트 중 가장 유명한 프로젝트가 되었습니다. GraphQL은 네트워크를 새롭게 정의한 것도 아니며 신 기술을 적용하여 기존 개발 방식을 전부 바꿔야 하는 상황을 만들지도 않습니다. 그저 API를 불러오는 "방식"에 변화를 주고 이것을 명세로 정의함으로서 일괄되고 보다 빠르고 편리하게 데이터를 가져오고 전달할 수 있게끔 만들었습니다. 그렇다면 기존에는 어떤 방식이고 이러한 방식에 어떠한 문제점이 존재했기에 새로운 방법을 만들어서 적용시키고 사람들이 GraphQL에 대해 열광하게 만들었을까요? ## REST가 가지고 있던 문제점 REST API는 GraphQL이 등장하기 전까지는 거의 유일한 API 개발 방식이었습니다. HTTP를 이용하여 메서드(GET, PUT, DELETE ...)와 엔드포인트등을 통해 API를 정의하고 개발하여 값을 가져오는 방식입니다. 이는 갈수록 복잡하고 수많은 데이터를 받아오도록 설계해야하는 상황에 적절한 개념이었고 곧 수많은 사람들이 REST API 방식을 이용하여 개발을 하기 시작합니다. 그러나 정말 현재 적용된 REST API 는 **RESTful**할까요? REST API 방식은 위에서 언급한 메서드, 엔드포인트 정의 외에도 다양한 개념이 포함되어 있습니다. (해당 개념은 학위 논문에서 최초 언급되었습니다.) REST API 문서에 정의된 개념들을 모두 지키면서 개발하기엔 쉽지 않습니다. 주위에 REST API를 적용해 개발한 코드들을 봐도 RESTful하지 않다는 것은 쉽게 알 수 있습니다. 그저 HTTP 메서드를 이용하고 엔드포인트로 구분했다고 해서 REST API를 적용한 개발이라고 말할 수 없다는 뜻입니다. (RESTful한 API개발 방식이 무엇인지, 왜 그것들을 모두 지키기 어려운지는 [이 영상](https://tv.naver.com/v/2292653)을 참고하면 좋습니다.) 또 한가지 문제점은 REST API의 장점 중 하나라고 여겨졌던 엔드포인트로 구분해 데이터를 받아오는 방식이 어플리케이션이 복잡해지고 받아와야하는 데이터가 많을 수록 개발하고 유지보수 하기가 어렵다는 점입니다. (이또한 RESTful하게 개발하기 어려운 점 중 하나입니다.) 예를 들어 엔드포인트를 아래와 같이 정의했다고 가정해 봅시다. ``` shell /api/korea/fruit/apple /api/korea/fruit/orange ``` 해당 엔드포인트로 한국에서 구매한 과일의 목록과 정보를 받아온다고 가정해 봅시다. 이렇게 개발 후 잘 사용하다가 갑자기 구매 국가를 한국에서 영국으로 바꿔야 하는 일이 발생했습니다. 그렇게 됐을 경우 해당 엔드포인트를 프론트, 백엔드 영역 모두 수정하고 다시 개발해야합니다. (*물론 RESTful을 포기하고 그냥 이대로 사용할 수도 있습니다*) 이것은 단순히 Copy & Paste를 떠나서 어플리케이션을 복잡하게 만들 수도 있습니다. 우선 기존에 사용하던 엔드포인트는 전부 그대로 두고 새로운 엔드포인트를 만든 후 배포해야합니다. 그렇다면 기존에 코드는 바로 삭제할 수 있느냐, 상황에 따라 다르겠지만 장담할 수 없습니다. 최악의 경우 만약을 대비해 사용하지도 않는 라우트 정보를 남겨둬야 할수도 있습니다. 또다른 REST API 단점으로는 불필요한 정보까지 모두 가져와야 한다는 점입니다. 예를 들어 `student`이라는 데이터를 받아오는 API가 있다고 가정했을때 `student` API를 호출하면 관련된 정보가 모두 나옵니다. 해당 학생의 거주지, 나이, 이름, 몸무게, 성적 등등... 이는 한곳에서 사용하는 것이 아니라 여러곳에서 호출함으로 모든 데이터를 보내줘야 합니다. 그러나 학생 프로필을 보여주는 곳에서는 이름, 나이 정도만 보여주면 된다고 가정한다면 이 외의 정보는 모두 불필요한 정보입니다. 이를 **오버페칭**이라고 하는데 원하는 데이터외에 불필요한 정보가 포함되어 오는 경우입니다. (물론 경우에 따라 조건에 맞춰 엔드포인트를 만들 수도 있습니다. 그러나 이는 어플리케이션을 보다 더 복잡하게 만드는 꼴이 됩니다.) 또는 원하는 데이터를 받은 후 해당 데이터에서 값을 추출해 관련된 데이터를 다시 호출해야하는 경우도 있습니다. 이는 **언더페칭**이라고 하는데 아래와 같은 예를 보겠습니다. ``` json // Student { name: "Tom", age: 28, friends: ["Jenny", "Min", "Herry"] } ``` Tom 학생 정보를 받아온 후, 해당 학생의 친구들의 정보를 같이 받고 싶은 경우, 친구들의 정보를 루프를 돌며 개별로 받아와야 합니다. 만약 기존에는 단순히 해당 학생의 정보만 받기를 원했다가 친구들의 상세 정보까지 받기를 원할 경우 할 수 있는 방법은 두가지 입니다. 위와 같이 말한대로 루프를 돌며 개별로 다시 받아오거나, 새로운 엔드포인트를 만드는 것입니다. 무엇이 되었든 불필요한 작업이 추가되는 경우만 발생하게 됩니다. 그럼 GraphQL은 어떻게 동작할까요? 우선 GraphQL은 엔드포인트가 하나입니다. 그리고 모든 요청은 HTTP PUT을 통해 이루어 지며 Body에 Query를 담아 조건에 맞는 데이터만 가져올 수 있습니다. 아래는 GraphQL Query의 예시입니다. ``` graphql query FindStudent { student { name age friends { name age } } } ``` 하나씩 보자면 우선 query는 값을 가져오겠다는 키워드 입니다. 해당 쿼리의 이름은 FindStudent이며 student 타입에 들어있는 데이터 중 name, age 그리고 freinds 리스트를 받아오는데, 이때 freinds 리스트에는 해당 학생의 이름과 나이도 포함하도록 했습니다. 위와 같이 query를 날리면 아래와 같은 데이터를 받을 수 있습니다. ``` json { "data": { "student": { "name": "Tom", "age": 28, "freinds": [ { "name": "Jenny", "age": 25 }, { "name": "Min", "age": 27 }, { "name": "Herry", "age": 24 } ] } } ``` 처음에 의도한대로 데이터를 받아왔습니다. 단 한번의 요청으로요. 받아온 친구 목록에서 주소를 추가하고 싶다면 백엔드 코드는 수정할 필요 없이 쿼리를 날리는 클라이언트 코드에서 아래와 같이 조건을 하나만 추가해주면 됩니다. ``` graphql query FindStudent { student { name age friends { name age address } } } ``` 끝입니다! 쿼리 조건 외엔 수정할 코드도 없고 받아오는 방식을 변경할 필요도 없습니다. 관심있게 보아야할 것은 받아보고 싶은 데이터의 조건 뿐입니다. ## GraphQL in Front-End GraphQL은 크게 두가지 조건에 따라 정의가 되어있습니다. 첫째는 프론트엔드에서 데이터를 조회, 수정, 삭제, 실시간 데이터 받기 등을 하기 위해 서버로 보내는 방식과 둘째는 백엔드에서 특정 쿼리에 대한 리턴 타입, 업데이트에 필요한 데이터 정의등을 하는 방식입니다. 우선 프론트엔드에서 필요한 GrapQL 정의들을 보도록 하겠습니다. 데이터 조회, 수정, 생성, 삭제 등은 간단히 두가지로 나뉩니다. `query`와 `mutation` 타입입니다. 입니다. query 와 mutation은 GraphQL의 루트 타입으로서 query는 데이터를 조회해올때, mutation은 그 외 행동들을 실행시킬 수 있습니다. 추가로 실시간으로 데이터 변경을 조회하고 싶은 경우 `subscription`을 이용하면 됩니다. query는 조회하고자 하는 데이터 값들을 정의해주면 끝입니다. ```graphql query Fruit { fruit { name price } } ``` 여기서 같은 값들이 반복된다면 `fragment`를 이용해 중복을 제거할 수 있습니다. ```graphql # fruit 정보가 중복된다. query Fruits { fruits { korea { name price } china { name price } } } # fragment 이용해 중복 제거 query Fruits { fruits { korea { ...fruitInfo } china { ...fruitInfo } } } fragment fruitInfo on Fruit { name price } ``` 반복되는 값은 줄일수록 좋습니다. fruitInfo는 Fruit type에 들어있는 값 중 name과 price를 가져오라고 정의하고 있습니다. type과 관련해서는 GraphQL in Back-End에서 자세히 알아보도록 하겠습니다. 위에서 정의한 query들은 전부 Query 타입에 미리 정의되어 있는 값들입니다. 만약 받아오는 데이터 키 값을 변경하고 싶다면 쿼리 안에서 별칭을 부여할 수 있습니다. ```graphql query Fruit { fruit { fruitName: name } } ``` name으로 정의되어 있는 값을 fruitName으로 변경하였습니다. 조건에 따라 값을 조회해오고 싶다면 쿼리 인자를 넘길 수도 있습니다. 아래는 원산지가 한국인 과일 정보들만 받아오는 query입니다. ```graphql query GetFruits { fruits(country: KOREA) { name price } } ``` 쿼리 변수를 통해 동적으로 데이터를 할당 할 수 있습니다. ```graphql query GetFruit($country: String!) { fruit(country: $country) { name price } } ``` 동적으로 할당할 경우 $와 변수명을 붙여주고 타입을 지정해줍니다. GraphQL은 기본 내장 스칼라 타입 (String, Int, Float, Boolean, ID) 와 커스텀하게 타입을 지정할 수도 있습니다. ! 표시는 해당 값이 반드시 존재해야한다는 뜻이며 query를 보낼때 값이 undefined이거나 null일 경우 에러가 납니다. fruit타입의 인자 명이 country로 고정되어 있는데 이는 사전에 정의한 type으로서 변경할 수 없습니다. country필드의 값을 조건으로 받아오겠다고 정의한 후 해당 조건에 맞춰서 query를 보내야 합니다. 타입을 지정하는 방법에 대해서는 아래에서 살펴보겠습니다. mutation 역시 query와 동일한 구조로 사용할 수 있습니다. ```graphql mutation createFruit($country: String!, $name: String!, $price: Int!) { addFruit(country: $country, name: $name, price: $price) { name price } } ``` 생성하고자 하는 조건을 기입하고 리턴 값에 받고싶은 데이터 타입을 지정합니다. 개인적으로 이러한 방식은 매우 효율적이라는 생각이 드는데, 기존 REST API에서는 데이터를 생성하고자 할때 POST 메서드를 보낸 후 변경된 값을 받고 싶다면 (정의된 문서 상으로는) GET으로 다시 조회 해야합니다. 이는 HTTP Request를 두번 보내는 꼴이 되며 GET으로 조회한 데이터 중 사용자에게 성공적으로 생성되었다고 보여주는 화면에서 필요한 데이터가 일부라면 전체 데이터를 받아오는 꼴이 됨으로 위에서 언급한 REST API의 단점 중 하나인 오버페칭에 해당됩니다. 그러나 GraphQL에서는 한번의 Request로 데이터 생성과 생성된 데이터의 리턴값을 받아볼 수 있는데 이때 필요한 값만 정의 가능하므로 보다 효율적으로 설계할 수 있습니다. 만약 전달하는 변수가 많다면 하나씩 정의하기 보다는 input으로 정의한 후 해당 타입으로 명시할 수 있습니다. ```graphql mutation createFruit($input: AddFruitInput!) { addFruit(input: $input) { name price } } ``` input 타입 역시 사전에 정의하여 사용합니다. 예를 들어 위에서 사용한 `AddFruitInput`타입은 아래처럼 정의되어 있습니다. ```graphql input AddFruitInput { country: String! price: Int! name: String! desciprion: String } ``` 여기서 country, name, price는 필수 값이며 description은 String타입으로 포함되어도 되고 없어도 됩니다. 여기까지 GraphQL을 통해 조회, 생성, 수정, 삭제를 하는 방법을 익혔습니다. GraphQL은 추가로 실시간으로 데이터 변경 값을 받아올 수 있는 방법을 제공하고 있습니다. ```graphql subscription FruitChange { fruitChange { name price } } ``` subscription은 ws(Web Socket) 을 이용하여 데이터 변화를 감지합니다. 값의 변경이 일어나면 서버에서 이벤트를 발생시켜 데이터를 받아 온 후 UI 업데이트를 시킬 수 있습니다. ## GraphQL in Back-End 위에서 살펴본 GraphQL 쿼리는 스키마를 설계하여 정의해 주어야 동작합니다. 예를 들어 `query Fruit` 에서는 Fruit type을 정의해 주어야 하고 `addFruit` 은 Mutation에 `addFruit` 타입을 정의해 주어야 합니다. GraphQL은 스키마 우선주의라는 디자인 방법론에 따라 설계되는데 프론트이건 백이건 같은 스키마를 보고 데이터 구조를 이해함으로 보다 직관적이고 효율적으로 데이터 정의가 가능합니다. 가장 기본적인 타입은 아래와 같이 정의합니다. ```graphql type Fruit { name: String! price: Int! country: String count: Int description: String id: ID! } ``` Fruit이라는 객체 타입을 생성했습니다. 각 필드는 필드 명과 각 필드의 타입을 정의하였는데 기본적인 스칼라 타입 외에도, 커스텀 스칼라 타입, 객체 타입 등을 정의할 수도 있습니다. ```graphql type Fruit { name: String! price: Int! country: String count: Int description: String id: ID! } type FruitBox { id: ID! fruit: Fruit! } ``` 여기서 name, price, country, id는 전부 필수 값(non-nullable)입니다. 해당 값을 쿼리하거나 생성할 때 빈값이 있으면 에러를 발생시킵니다. ID는 고유 스칼라 타입으로서 String타입이지만 고유값을 가지고 있어야합니다. 중복된 값이 들어가는 경우 역시 에러를 리턴합니다. GraphQL은 편의를 위해 기본 스칼라 타입(Int, Float, String, Boolean, ID)을 제공하고 있지만 실제 어플리케이션을 개발하다 보면 추가로 필요한 타입이 존재합니다. 객체 타입이 아닌 값으로서 특정한 포맷을 지켜야 할 경우 커스텀 스칼라 타입을 정의할 수도 있습니다. 가장 기본적이고 자주 사용되는 커스텀 스칼라 타입으로 날짜 타입이 있습니다. ```graphql scalar DateTime type Fruit { name: String! id: ID! count: Int date: DateTime } ``` DateTime이라는 커스텀 scalar타입을 정의하고 Fruit 타입의 date 필드에 지정했습니다. 물론 DateTime이라는 스칼라 타입이 어떠한 조건에 맞춰서 입력되어야 하는지 GraphQL은 모르기 때문에 별도로 정의해주어야 합니다. 이는 아래에서 좀 더 자세히 보겠습니다. 우선은 DateTime은 날짜 (ex. 2021-08-08) 값에 맞춰서 저장된다고 가정해 보겠습니다. 따라서 Fruit을 생성할때 date에 날짜외에 다른 값이 온다면 (ex. "test") 에러를 리턴합니다. 객체, 스칼라 타입 외에도 리스트를 지정할 수도 있습니다. ```graphql type FruitList { id: ID! country: String! fruits: [Fruit!]! } ``` 위와 같이 정의하면 fruits 필드에는 Fruit타입의 값이 null이 아닌 리스트가 반드시 존재해야 한다는 뜻입니다. 만약 `[Fruit]!`라고 정의했을 경우 리스트는 반드시 존재해야 하지만 리스트 속에 null값이 들어가도 된다는 것을 의미합니다. 반대로 `[Fruit!]`라고 정의한다면 리스트가 있을 수도 있고 없을 수도 있지만 존재한다면 내부에는 반드시 null이 아닌 Fruit 값이 존재한다고 정의한 것입니다. GraphQL은 Graph 자료구조 처럼 1대1 관계, 1대다 관계, 다대다 관계를 타입으로 정의할 수도 있습니다. ```graphql # 다대다 type Cat { name: String! age: Int! sex: String neutering: Boolean! caregiver: [Person!]! } type Person { name: String! age: Int! sex: String address: String cats: [Cat!]! } ``` 위의 예시는 다대다로 연결한 것입니다. Cat 타입에는 다수의 Person 타입이 존재할 수 있고 Person 타입 역시 다수의 Cat 타입이 존재할 수 있습니다. ```graphql # 1대1 관계 type Cat { name: String! age: Int! sex: String neutering: Boolean! } type Person { name: String! age: Int! sex: String address: String cat: Cat! } # 1대다 관계 type Cat { name: String! age: Int! sex: String neutering: Boolean! caregiver: [Person!]! } type Person { name: String! age: Int! sex: String address: String } ``` 위와 같은 설계는 데이터베이스 설계와 유사하게 타입을 지정하고 관계를 정의함으로서 자료구조를 보다 간결하게 정의하고 사용할 수 있습니다. Query를 하기 위해서는 루트 타입인 Query 타입을 명시해 주어야 합니다. ```graphql type Query { totalFruits: Int! allFruits: [Fruit!]! } schema { query: Query } ``` 위와 같이 정의하면 프론트엔드에서 totalFruits query와 allFruits query를 사용할 수 있습니다. ```graphql query GetAllFruits { allFruits { name price } } ``` 여기서 GetAllFruits는 해당 쿼리의 이름이고 allFruits가 Query 타입에서 정의한 타입입니다. 그리고 리턴 되는 Fruit 중에 name과 price필드만 받아오도록 정의했습니다. 인자 역시 쿼리 조건과 마찬가지로 아래처럼 정의할 수 있습니다. ```graphql type Query { fruitList(country: String): [Fruit!]! } ``` fruitList 쿼리는 country 조건을 받을 수도 있고 없을 수도 있습니다. 조건을 넘기면 해당 조건에 맞는 Fruit 값들만 리턴 합니다. ```graphql query GetKoreaFruit { fruitList(country: KOREA) { name price } } ``` 인자 조건을 잘 이용한다면 페이지네이션과 같이 시작 부분과 리턴 갯수를 전달받아 구현할 수 있습니다. mutaion 역시 루트 타입으로서 아래와 같이 정의합니다. ```graphql type Mutation { addFruit(name: String!, country: String!, price: Int!): Fruit! deleteFruit(id: ID!): Fruit! } schema { mutation: Mutation } ``` 보통 mutation에는 인자값들이 다수 포함되므로 input 타입을 이용하여 별도로 정의하는 편의 간편합니다. ```graphql input FruitInput { name: String! country: String! price: Int! } type Mutation { addFruit(input: FruitInput!): Fruit! } ``` 타입에 기본값을 할당할 수도 있습니다. ```graphql input FruitInput { name: String! country: Country = KOREA price: Int! } ``` country 값이 없는 경우 KOREA를 넣어줍니다. 마지막으로 루트 타입인 Subscription은 값 변경시에 리턴되는 타입을 지정해줍니다. ```graphql type Subscription { newFruit: Fruit! } type schema { subscription: Subscription } ``` 이 외에도 enum, union, interface가 존재합니다. ```graphql # enum enum Country { KOREA CHINA AMERICA JAPAN } # union type Dog { name: String! age: Int! } type Cat { name: String! age: Int! } union HouseAnimal = Dog | Cat # interface interface Animal { name: String! age: Int! } type Cat implements Animal { name: String! age: Int! sex: String } ``` 기존에 정의되는 타입과 매우 유사하므로 쉽게 적용할 수 있습니다. union을 이용하면 편리한 부분 중 하나는 조건이 다양한 리스트 중 해당 조건만 받을 수 있도록 정의할 수 있습니다. ```graphql type Query { houseAnimal: [HouseAnimal!]! } ``` 위와 같이 정의하면 Animal 타입이 들어있는 리스트 중에서 HouseAnimal에 속한 타입들만 필터링 되어서 리턴 됩니다. 스키마 까지 성공적으로 설계되었다면 GrpahQL을 적용할 준비는 끝났습니다! 이제 마지막으로 실제로 값을 전달하는 방법을 정의한 Resolver(리졸버)만 정의해 주면 됩니다. GraphQL은 특정 언어를 지원하는 쿼리가 아닌 API를 가져오는 방식을 정의한 언어입니다. 따라서 프론트엔드와 백엔드의 언어가 달라도 상관 없으며 현재 사용중인 언어와 무관하게 동작합니다. 관련 라이브러리를 설치하여 정의를 하여도 되고 `*.graphql` 문서에 스키마를 정의하여 파일을 읽어온 후 할당해주어도 됩니다. 값을 전달하는 방식을 구현한 리졸버를 제외하고는 언어를 변경해야 하는 경우에도 값 변경 없이 정의된 타입 문서를 불러오기만 하면 됩니다. GraphQL을 이용하기 위해선 크게 `typeDefs` 와 `resolvers`를 정의해야 하는데 `*.graphql`이 `typeDefs`에 해당되는 파일이며 리졸버는 언어에 맞춰 별도로 정의해 주어야 합니다. 리졸버에서 실제로 DB에 접속하여 값을 가져오거나, 전달 받은 인수에 맞춰서 필터링 후 값을 보여주는 동작등을 합니다. 그 외에도 커스텀 스칼라 타입등을 지정하여 GraphQL이 타입 검사를 할 수 있도록 정의해주는 역활도 담당하고 있습니다. React 와 Redux에 익숙하시다면 마치 Action 과 Reducer를 정의하는 것과 같다고 볼 수 있습니다. `typeDefs`는 Action 정의에 해당 되며 실제 구현을 하고 있는 `resolvers`는 Reducer 인것입니다. GraphQL은 언어에도 무관하듯이 프론트엔트 프레임워크에도 무관하게 동작합니다. Angular, React, Vue 어떤 프레임워크에도 GraphQL을 통해 API 설계를 손쉽게 할 수 있습니다. 다만 GraphQL을 쉽게 구현하기 위해서는 Relay나 Apollo같은 라이브러리를 이용해야하는데 Relay는 React 전용으로 페이스북 팀에서 만든 라이브러리이다 보니 Apollo를 좀 더 자주 사용합니다. Apllo는 프론트엔드 뿐만 아니라 백엔드에서도 정의하여 양 쪽 모두 비슷한 환경에서 개발할 수 있다는 장점이 있습니다. 다음 포스트에서는 실제 리졸버 구현과 클라이언트 구현을 해보도록 하겠습니다. GraphQL에 대해 관심이 생겼고 좀 더 알아보고 싶다면 언제나 그렇듯 [공식 문서](https://graphql.org/learn/)가 가장 정확하고 잘 정리되어 있습니다. **참고** - [GraphQL 공식 문서](https://graphql.org/learn/) - 웹 앱 API 개발을 위한 GrpahQL 프로그래밍인사이트, 이브 포셀로, 알렉스 뱅크스 2018