GraphQL은 쿼리언어이며 웹 클라이언트가 데이터를 서버로부터 효율적으로 가져오기 위해 제작되었다. gql의 구문도 클라이언트 시스템에서 작성하고 호출한다.
gql은 특정 데이터베이스나 플랫폼, 네트워크 방식에 종속적이지 않다. http post(7)와 웹소켓 프로토콜(7)을 활용하지만 필요에 따라 TCP/UDP(4)나 이더넷 프레임(2)도 사용한다.
REST API는 다양한 엔드포인트로부터 url, method를 조합하여 사용하지만 gql은 url 체계가 아예 없고 엔드포인트가 단 하나다. REST API에서는 엔드포인트마다 쿼리가 다르지만, gql API는 gql 스키마 타입마다 쿼리가 달라진다.
네트워크를 여러번 호출할 필요가 없으며 원하는 데이터를 콕 찝어서 가져올 수 있기 때문에 오버패칭이나 언더패칭을 방지할 수 있다. 트리 핸들링을 통해 그래프를 횡단하여 json을 가져오는 구조이기 때문이다. REST API에서는 쪼개서 작성했을 쿼리문을 gql에서는 오퍼레이션 네임 쿼리를 통해 한 번 횡단할 때 여러 테이블로부터 원하는 모든 데이터를 가져올 수 있다.
DB 프로시저는 전통적으로 백엔드 개발자 혹은 DBA가 작성하고 관리했지만 gql 오퍼레이션 네임 쿼리는 클라이언트 개발자가 작성하고 관리한다. 백엔드 의존성이 많이 사라지게 되었으나 데이터 스키마에 대한 협업은 여전히 필요하다.
구현 시 비즈니스 로직은 실제 리졸버 함수에 담지 않는다. 로직은 비즈니스 로직 레이어에 해당하는 별도의 파일에 작성하는 것이 권장된다.
requestPaymentSession: async (parent, {
pgId, name, sex, birthDay, phoneNumber, amount, productName, ref
}, context, info) => {
const ret = await requestPaymentSession({ pgId, name, birthDay, phoneNumber, sex, amount, productName, ref })
return removeSymbol(ret)
},
requestPaymentApprove: async (parent, {
sessionKey, authNumber
}, context, info) => {
const ret = await requestApprovePayment({ sessionKey, authNumber })
return removeSymbol(ret)
}
graphQL의 인스트로펙션 기능을 사용하면 현재 서버에 저장된 스키마의 정보를 실시간으로 확인할 수 있다. REST API에서는 swaggerul 같은 API 명세서를 작성해서 백엔드와 프론트엔드가 협업을 했으나, graphQL을 서버에서 사용하는 apllo server 라이브러리의 웹 IDE 화면에서 실시간으로 백엔드가 작성한 스키마 구조를 프론트도 확인할 수 있다.
모니터링에 관련해서는 아래 자료에서 자세하게 살펴볼 수 있다.
리졸버는 graphql의 쿼리, 뮤테이션, 구독을 데이터로 변환하기 위한 지침을 제공한다. 스키마에 지정한 것과 같은 형태의 데이터를 동기적으로 반환한다. 리졸버 맵을 수동으로 만들기도 하지만 type-graphql 패키지는 데코레이터가 제공하는 메타 데이터를 사용하여 자동으로 리졸버 맵을 생성한다.
// 오퍼레이션 네임 쿼리
query HeroNameAndFriends($episode: Episode) {
// 필드
hero(episode: $Episode) {
// 서브필드
name
friends{
// 서브서브필드
name
}
}
}
위와 같은 쿼리문은 파싱되어 리졸버에게 전달된다. 리졸버의 역할은 각 필드에 해당하는 데이터를 실제로 내주는 것이다. 필드 1개당 리졸버 1개가 존재한다. 필드의 타입이 개발자가 정의한 타입일 경우 해당 타입의 리졸버를 호출하게 된다. 리졸버가 스칼라값(문자열이나 숫자)를 리턴하기 전까지 리졸버는 계속 실행된다.
// id, amount 필드에 해당하는 리졸버 호출하게 됨
{
paymentByUser(userId: 10){
id
amount
}
}
// id, amount, user 필드와 name, phoneNumber 리졸버 호출하게 됨
{
paymentByUser(userId: 10){
id
amount
user {
name
phoneNumber
}
}
}
위의 예시를 보면 쿼리명은 paymentByUser로 동일하지만 아래 쿼리문이 더 많은 리졸버를 호출한다. 리졸버 함수는 다음과 같이 구현할 수 있다.
// 쿼리 필드 정의
{
paymentByUser(userId: 10){
id
amount
}
}
// 리졸버 구현
Query: {
paymentByUser: async (parent, { userId }, context, info) => {
const limit = await Limit.findOne({where: {UserId: userId}})
const payments = await Payment.findAll({where: {LimitId: limit.id}})
return payments
},
}
paymentByUser의 인자는 총 4개이다. parent, userId, context, info
별칭을 사용하는 경우 필드 결과의 이름을 지정할 수 있다.
{
empiroHero: hero(episode: EMPIRE){
name
}
jediHero: hero(episode: JEDI){
name
}
}
fragment를 이용하여 재사용 가능한 단위를 묶고 데이터 요구사항을 작게 분할할 수 있다.