GraphQL은 페이스북에서 만든 쿼리 언어로 Facebook, GitHub, Pinterest 등 다양한 서비스가 GraphQL
을 사용하고 있으나, 국내에는 아직 GraphQL을 사용한 Open API는 많지않다.
이름에서 알 수 있듯이 Query Language인데 이 때문에 SQL
과 같은 Database 질의 언어로 오해할 수 있으나, 백엔드 API 서버에 질의를 보내는 언어이다. 즉, GraphQL은 SQL 보다는 오히려 REST API와 더 유사하다고 할 수 있다.
일반적으로 RESTful API
라고 하면 명사로 이뤄진 URL(Endppoint)로 서비스를 구분하고 HTTP 메소드를 사용하여 요청을 보내면 해당 URI와 매핑된 메소드가 요청을 처리하는 형태의 서비스 방식을 REST 방식이라고 하는데 GraphQL
의 가장 큰 차이는 바로 Endpoint가 하나라는 점이다.
GraphQL
API에 요청을 보낼 때는 주로 /graphql
로 설정된 GraphQL
서버 주소로 요청을 보내는데, Query
또는 Mutation
의 요청을 보낸다. 여기서 Query
는 데이터를 조회하는 요청으로 REST
방식의 GET 요청과 유사하고, Mutation
요청은 데이터를 생성, 수정, 삭제 하는 요청으로 POST(PATCH/PUT/DELETE) 요청과 유사하다.
또 다른 GraphQL
방식의 가장 큰 특징중 하나는 요청 후 응답받을 데이터를 client 측에서 정할 수 있다는 것이다. GraphQL server
로 요청을 보낼 때는 요청이 query인지 mutation인지와 함께 요청명과 리턴받을 필드를 보내게 되는데, 그러면 서버는 해당 요청을 처리한 후 요청된 필드에 해당하는 값을 JSON 형태로 리턴해주게 된다.
GraphQL
에서 요청은 앞서 언급했듯이 Query
와 Mutation
두가지 종류가 있다.
데이터를 조회하는 요청으로 REST
방식의 GET 요청과 유사하다.
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
위와 같은 형태로 작성하는데 query
는 해당 요청이 query
임을 나타내며 생략해도 무관하다. (생략될 경우 query
인 것으로 인식한다.) 그 다음으로 오는 HeroNameAndFriends
는 요청의 이름(operation name)이다. 이 또한 없어도 무관하다. 이 부분이 흥미로운데 operation name
은 백엔드의 API 설계와 무관하게 프론트엔드에서 작성하고 관리할 수 있다. 즉, Schema의 형태만 지키면 REST
방식에 비해 API에 대한 의존도가 많이 내려가게 된다.
다음으로 나오는 ($episode: Episode)
는 변수이다 달러표시가 붙은 $episode
는 변수명이고 그 뒤에 나오는 Episode
는 해당 변수의 타입을 나타낸다. 그리고 이 오퍼레이션 안에 들어있는 hero가 server-side의 resolver
와 매핑되는 실제 쿼리이다. 즉 쿼리는 직접 파라미터를 받는게 아니라 오퍼레이션에게 파라미터를 전달하고 쿼리는 오퍼레이션이 전달받은 파라미터를 가져다 형태이다.
이러한 구조는 한번에 여러개의 쿼리를 전송하기 유용하게 만든다.
query getStudentInfomation($studentId: ID!, $year: Int!, $schoolCode: String){
personalInfo(studentId: $studentId) {
name
address1
address2
major
}
classInfo(year: $year, studentId: $studentId) {
classCode
className
teacher {
name
major
}
classRoom {
id
maintainer {
name
}
}
}
SATInfo(schoolCode: $schoolCode, studentId: $studentId) {
totalScore
dueDate
}
}
위와 같은 요청을 보내면 한번의 요청으로 personalInfo
, classInfo
, SATInfo
3개의 쿼리를 보낼 수 있다. $studentId
와 같이 복수의 쿼리에 사용되는 변수도 한번에 전달해줄 수 있다.
참고로 변수 타입 뒤에 붙은
!
(느낌표)는 해당 변수는 필수로 전달 되어야 함을 나타내는데 server-side의resolver
에서 해당 파라미터를 필수로 전달 받게 설계했다면, client-side에서 요청을 보낼 때 해당 파라미터를 보내더라도 operation에 느낌표를 붙임으로써 필수라는 것을 나타내지 않는다면 에러가 난다.client-side에게 자유도를 주고 API 의존도를 낮추지만 schema에 관해서는 철저하게 지켜져야하는 느낌이다.
변수를 동적으로 할당하여 쿼리의 구조와 형태를 동적으로 변경하는 방법도 있다.
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
// @include(if: Boolean) : 인자가 true 인 경우에만 이 필드를 결과에 포함
// @skip(if: Boolean) : 인자가 true 이면 이 필드를 건너뜀
데이터를 생성, 수정, 삭제 하는 요청으로 REST
방식의 POST, PATCH, PUT, DELETE 요청을 합친 요청이다.
Query
와 기본적인 형태는 똑같다.
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
Mutation
에서는 mutation
요청임을 반드시 명시해야하며, 변수의 사용법과 return 받을 필드를 입력하는 방식은 동일하다.
Typescript
와 마찬가지로 사용할 객체를 정의해줘야 하는데 이를 위해 TypeDef
를 작성해준다. 그리고 정의한 type
을 사용하는 Query
와 Mutation
들을 선언하는데 Java
의 추상메서드와 비슷하다. 이렇게 type
과 Query
/Mutation
을 선언한 것을 TypeDefs
라고 한다.
type Character {
name: String!
appearsIn: [Episode]!
}
위와 같이 type
을 정의해주면 Character
타입의 JSON Data를 파라미터로 받거나 return해줄 수 있게된다. 여기서도 마찬가지로 !
(느낌표)가 붙은 필드는 해당 타입에서 Null이 되면 안되는 값이고 대괄호[]
사이에 둠으로써 배열임을 나타낸다.
그리고 위에서 정의한 Character
타입을 사용한 Query
와 Mutation
을 아래와 같이 정의한다.
type Query {
character(id: Int!): Character
allCharacters: [Character]
}
type Mutation {
createCharacter(name: String!, appearsIn: [ID!]!): Character
}
이런식으로 작성해주면 된다.
참고로 정의된 Type이 아닌 Int나 String, ID 와 같은 기본적으로 제공되는 변수 타입들이 있는데 이를
Scalar
라고 한다. Int, Boolean, ID, String, Float 가 기본 스칼라로 제공되는데 Date와 DateTime이 없어서 얘네는custom scalar
로 만들어야한다. 공식 문서
참고로 GraphQL의 엄청난 점중 하나는 Documentation기능인데
""" Character 관련 Query """ type Query { """ 단일 Character 조회 """ character(id: Int!): Character """ 모든 Character 목록 조회 """ allCharacters: [Character] }
이런 식으로 작성해주면 graphql Playground에서 내용을 쉽게 확인 가능하다. 설명 포스트 참조
TypeDef
에서 선언한 Query
와 Mutation
을 구현한 구현체를 Resolver
라고 한다.
Query: {
character: async (_, { id }, context) => {
return await Character.findOne({ where: { id } })
},
allCharacters: async (_, __, context) => {
return await Character.findAll()
},
},
Mutation: {
createCharacter: async (_, { id, appearsIn }, context) => {
return await Character.create({
id,
appearsIn
})
}
}
이렇게 TypeDef
에 선언된 것들을 정의해주면 된다.
최대한 간단하게 정리해봤는데 사용이 편리하고 효율적이라는게 느껴지긴한다. 하지만 모든 기술이 그렇듯 실제로 GraphQL
을 사용해서 "잘" 개발하기는 어려울 것 같다. 기존의 REST
방식과는 정말 많이 다르고 코드 구현방식 부터해서 Back-End - Front-End 협업 방식까지 많이 다를것 같다. 대표적으로 Apollo 라이브러리를 함께 활용하는 듯 한데, GraphQL과 Apollo 자체가 언어의 제약없이 사용이 가능하다고는 하지만 Express 서버와 연동한 apollo-server-express
가 가장 많이 사용되는 것으로 보인다. (참고할 자료가 가장 많다.) Apollo 공식 문서에서도 server 쪽은 주로 Typescript로 예시가 작성되어 있다.