GraphQL은 Facebook에서 처음으로 개발했고, 오픈 소스로 제공된 쿼리 언어.
Graph + Query Language의 줄임말로 Query Language 중에서도 Server API 를 통해 정보를 주고받기 위해 사용하는 Query Language를 뜻함.
쉽게 말해 API를 위한 쿼리 언어라고 할 수 있음.
GraphQL의 아이디어는 그래프로 생각하기에서부터 출발함.
그래프라는 자료구조는 인간의 뇌 구조 및 언어적인 설명과 비슷하기 때문에 실제 현실 세계의 많은 현상들을 모델링할 수 있는 강력한 도구임.
따라서 그래프 자료구조를 살펴보면 우리가 특정 개념을 학습하고 이를 다른 개념과 연관시킬 때 자연스럽게 사용하는 마인드맵과 유사한 데이터 구조를 가진다는 것을 알 수 있음.
그래프는 여러 개의 점들이 서로 복잡하게 연결되어 있는 관계를 표현한 자료구조를 뜻함.
하나의 점을 그래프에서는 Node 또는 정점(vertex)이라고 표현하고, 하나의 선은 간선(edge) 이라고 함.
직접적인 관계가 있는 경우 두 점 사이를 이어주는 선이 있으며 간접적인 관계라면 몇 개의 점과 선에 걸쳐 이어짐.
또한 각 노드간의 간선을 통해 특정한 순서에 따라 그래프를 재귀적으로 탐색할 수 있음.
GraphQL에서는 모든 데이터가 그래프 형태로 연결되어 있다고 전제함.
일대일로 연결된 관계도, 여러 계층으로 이루어진 관계도 모두 그래프임.
트리나 그래프나 노드와 노드를 연결하는 간선으로 구성된 자료구조이기 때문임.
단지 그 그래프를 누구의 입장에서 정렬하느냐(클라이언트가 어떤 데이터를 필요로 하느냐)에 따라 트리 구조를 이룰 수 있음.
이를 통해 GraphQL은 클라이언트 요청에 따라 유연하게 트리 구조의 JSON 데이터를 응답으로 전송할 수 있음.
다시 말해 GraphQL은 REST API 방식의 고정된 자원이 아닌 클라이언트 요청에 따라 유연하게 자원을 가져올 수 있다는 점에서 엄청난 이점을 갖음.
Overfetch: 필요 없는 데이터까지 제공함
Underfetch: endpoint 가 필요한 정보를 충분히 제공하지 못함
클라이언트 구조 변경 시 엔드포인트 변경 또는 데이터 수정이 필요함
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를 호출하여 작업을 처리합니다.
/graphql
이라는 하나의 endpoint 로 요청을 받고 그 요청에 따라 query , mutation을 resolver 함수로 전달해서 요청에 응답합니다. 모든 클라이언트 요청은 POST
메소드를 사용합니다.서버로부터 데이터를 조회(Read)하는 경우, REST API에선 GET 요청이 있었다면 GraphQL에서는 Query를 이용해 원하는 데이터를 요청할 수 있음. 또한 Create, Delete와 같이 저장된 데이터를 수정하는 경우에는 Mutation을 이용해 이를 수행할 수 있음.
더 나아가 GraphQL에서는 구독(Subscription)이라는 개념을 제공하며 이를 이용해 실시간 업데이트를 구현할 수 있음.
Subscription는 전통적인 Client(요청)-Server(응답) 모델을 따르는 Query 또는 Mutation과 달리, 발행/구독(pub/sub) 모델을 따름.
클라이언트가 어떤 이벤트를 구독하면, 클라이언트는 서버와 WebSocket을 기반으로 지속적인 연결을 형성하고 유지하게 됨.
그 후 특정 이벤트가 발생하면, 서버는 대응하는 데이터를 클라이언트에 푸시해줌.
매우 간단한 query(데이터 조회, 이하 쿼리)와 실행 했을 때 얻은 결과
{
hero {
name
}
}
//[코드] hero의 name을 쿼리
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
//[코드] 쿼리를 실행했을 때의 결과
필드의 name
은 String 타입을 반환함.
위의 경우 hero
의 name
이 “R2-D2”임을 알 수 있음.
보이는 것처럼 쿼리와 결과가 정확하게 같은 모양을 하고 있음을 확인할 수 있는데, 이 부분은 GraphQL에 있어서 필수적이라고 볼 수 있음.
GraphQL은 서버에 요청했을 때 예상했던 대로 돌려받고, 서버는 GraphQL을 통해 클라이언트가 요구하는 필드를 정확히 알기 때문임.
다른 예시
{
hero {
name
# 이런 식으로 GraphQL 내에서 주석도 작성할 수 있습니다.
friends {
name
}
}
}
//[코드] 히어로의 이름과 히어로의 친구 이름을 같이 쿼리
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
//[코드] 히어로의 이름과 히어로의 친구의 이름이 조회되어 나옵니다.
이런 식으로 원하는 필드를 중첩하여 쿼리하는 것도 가능함.
위의 예에서 freinds
필드는 배열을 반환함.
GraphQL 쿼리는 관련 객체 및 필드를 순회할 수 있기 때문에 고전적인 REST API에서 그러했듯 다양한 endpoint를 만들어 각기 요청을 보내는 대신 클라이언트가 하나의 요청을 보냄으로써 관련 데이터를 가져올 수 있음.
필드에 인수를 전달하는 부분을 추가하게 되면 쿼리의 필드 및 중첩된 객체들에 전달하여 원하는 데이터만 받아올 수 있음.
{
human(id: "1000") {
name
height
}
}
//[코드] id가 1000인 human의 name과 height를 쿼리
{
"data": {
"human": {
"name": "Luke Skywalker",
"height": 1.72
}
}
}
//[코드] 쿼리 결과
이런 식으로 id가 1000인 human의 이름과 키를 쿼리해 올 수 있음.
REST와 같은 시스템에서는 단일 인수 집합(요청의 쿼리 매개변수 및 URL 세그먼트)만 전달할 수 있음.
예를 들어 REST API를 이용한다면 ?id=1000
이거나 /1000(/:id)
일 때와 같은 목적으로 쿼리할 수 있습니다.
필드 이름을 중복해서 사용할 수 없으므로, 필드 이름을 중복으로 사용해서 쿼리를 해야 할 때는 별명을 붙여서 쿼리를 함.
{
hero(episode: EMPIRE) {
name
}
hero(episode: JEDI) {
name
}
}
//[코드] 이런 식으로 중복해 쿼리할 수 없습니다.
{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
//[코드] 앞에 알아볼 수 있는 별명을 붙여주면 쿼리할 수 있습니다.
{
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}
//[코드] 쿼리 결과를 받아볼 수 있습니다.
위와 같이 다른 이름으로 별명을 지정하면 한 번의 요청으로 두 개의 결과를 모두 얻어낼 수 있음.
여태껏 쿼리와 쿼리 네임을 모두 생략하는 축약형 구문을 사용했지만, 실제 앱에서는 코드를 모호하지 않게 작성하는 것이 중요함.
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
//[코드] 이런 식으로 query keyword와 query name을 작성합니다.
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
앞의 query
는 오퍼레이션 타입임.
오퍼레이션 타입에는 query
뿐만 아니라 mutation
, subscription
, describes
등이 있음.
쿼리를 약식으로 작성하지 않는 한 이런 오퍼레이션 타입은 반드시 필요함.
오퍼레이션 네임을 작성할 때는 오퍼레이션 타입에 맞는 이름으로 작성하는 것이 가독성이 좋음.
여태껏 고정된 인수를 받아 쿼리했지만, 실제 앱을 사용할 때는 고정된 인수를 받는 것보다는 동적으로 인수를 받아 쿼리하는 경우가 대다수임.
변수는 그런 인수들을 동적으로 받고 싶을 때 사용함.
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
//[코드] 변수를 써서 작성된 쿼리
오퍼레이션 네임 옆에 변수를 $변수 이름: 타입 형태 로 정의함. 위의 예시처럼 $ep: Episode
일 때, 뒤에 !
가 붙는다면 ep는 반드시 Episode여야 한다는 뜻임. !
는 옵셔널한 사항임.
GraphQL은 대개 데이터를 가져오는 데에 중점을 두고 있지만 서버측 데이터를 수정하기도 함.
REST API에서 GET
요청을 사용하여 데이터를 수정하지 않고,
POST
혹은 PUT
요청을 사용하는 것처럼 GraphQL도 유사함.
GraphQL은 mutation
이라는 키워드를 사용하여 서버 측 데이터를 수정함.
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
GraphQL 스키마의 가장 기본적인 구성 요소는 서비스에서 가져올 수 있는 객체의 종류, 그리고 포함하는 필드를 나타내는 객체 유형.
type Character {
name: String!
appearsIn: [Episode!]!
}
Character
는 GraphQL 객체 타입이며, 즉 필드가 있는 타입임을 의미합니다. 스키마에 있는 대부분의 타입은 객체 타입입니다.
name
과 appearIn
은 Character 타입의 필드 입니다. 즉 name
과 appearIn
은 GraphQL 쿼리의 Character
타입 어디서든 사용할 수 있는 필드입니다.
String
은 내장된 스칼라 타입 중 하나입니다. 이는 단일 스칼라 객체로 확인되는 유형이며 쿼리에서 하위 선택을 가질 수 없습니다. 스칼라 타입에는 ID, Int도 있습니다.
!
가 붙는다면 이 필드는 nullable하지 않고 반드시 값이 들어온다는 의미입니다. 이것을 붙여 쿼리한다면 반드시 값을 받을 수 있을 것이란 예상을 할 수 있습니다.
[ ]
는 배열을 의미합니다. 배열에도 !
가 붙을 수 있습니다. 여기서는 ! 이 뒤에 붙어 있어 null 값을 허용하지 않으므로 항상 0개 이상의 요소를 포함한 배열을 기대할 수 있게 됩니다.
요청에 대한 응답을 결정해주는 함수로써 GraphQL의 여러 가지 타입 중 Query, Mutation, Subscription과 같은 타입의 실제 일하는 방식 즉 로직을 작성함.
다시 말해 위와 같이 스키마를 정의하면 그 스키마 필드에 사용되는 함수의 실제 행동을 Resolver에서 정의함.
또한 이러한 함수들이 모여 있기 때문에 보통 Resolvers라 부름.
const db = require("./../db")
const resolvers = {
Query: { // **Query :** 저장된 데이터 가져오기 (REST 에 GET 과 비슷합니다.)
getUser: async (_, { email, pw }) => {
db.findOne({
where: { email, pw }
}) ... // 실제 디비에서 데이터를 가져오는 로직을 작성합니다.
...
}
},
Mutation: { // **Mutation :** 저장된 데이터 수정하기 ( Create , Update , Delete )
createUser: async (_, { email, pw, name }) => {
...
}
}
Subscription: { // **Subscription :** 실시간 업데이트
newUser: async () => {
...
}
}
};
GraphQL에서는 데이터를 가져오는 구체적인 과정을 직접 구현해야 하는데 이와 같은 작업(e.g. 데이터베이스 쿼리, 원격 API 요청)을 Resolver가 담당하게 됨.