[GraphQL] 기본 구조 이해

냥린이·2022년 1월 14일
0

데이터베이스

목록 보기
4/4

GraphQL

GraphQL 개념잡기

GraphQL 뼈대를 이루는 네 가지 요소

#1. GraphQL 개념 및 개요

GraphQL개론

네스트JS 한국어 매뉴얼 사이트

GraphQL은 쿼리언어이며 웹 클라이언트가 데이터를 서버로부터 효율적으로 가져오기 위해 제작되었다. gql의 구문도 클라이언트 시스템에서 작성하고 호출한다.

gql은 특정 데이터베이스나 플랫폼, 네트워크 방식에 종속적이지 않다. http post(7)와 웹소켓 프로토콜(7)을 활용하지만 필요에 따라 TCP/UDP(4)나 이더넷 프레임(2)도 사용한다.

Screenshot from 2022-01-10 15-59-28.png

REST API는 다양한 엔드포인트로부터 url, method를 조합하여 사용하지만 gql은 url 체계가 아예 없고 엔드포인트가 단 하나다. REST API에서는 엔드포인트마다 쿼리가 다르지만, gql API는 gql 스키마 타입마다 쿼리가 달라진다.

네트워크를 여러번 호출할 필요가 없으며 원하는 데이터를 콕 찝어서 가져올 수 있기 때문에 오버패칭이나 언더패칭을 방지할 수 있다. 트리 핸들링을 통해 그래프를 횡단하여 json을 가져오는 구조이기 때문이다. REST API에서는 쪼개서 작성했을 쿼리문을 gql에서는 오퍼레이션 네임 쿼리를 통해 한 번 횡단할 때 여러 테이블로부터 원하는 모든 데이터를 가져올 수 있다.

Screenshot from 2022-01-10 16-47-28.png

  • query: read
  • mutation: create, update, delete

DB 프로시저는 전통적으로 백엔드 개발자 혹은 DBA가 작성하고 관리했지만 gql 오퍼레이션 네임 쿼리는 클라이언트 개발자가 작성하고 관리한다. 백엔드 의존성이 많이 사라지게 되었으나 데이터 스키마에 대한 협업은 여전히 필요하다.

Screenshot from 2022-01-10 16-57-36.png

구현 시 비즈니스 로직은 실제 리졸버 함수에 담지 않는다. 로직은 비즈니스 로직 레이어에 해당하는 별도의 파일에 작성하는 것이 권장된다.

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)
    }

Introspection

graphQL의 인스트로펙션 기능을 사용하면 현재 서버에 저장된 스키마의 정보를 실시간으로 확인할 수 있다. REST API에서는 swaggerul 같은 API 명세서를 작성해서 백엔드와 프론트엔드가 협업을 했으나, graphQL을 서버에서 사용하는 apllo server 라이브러리의 웹 IDE 화면에서 실시간으로 백엔드가 작성한 스키마 구조를 프론트도 확인할 수 있다.

모니터링에 관련해서는 아래 자료에서 자세하게 살펴볼 수 있다.

Resolver

리졸버는 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

  • parent: 연쇄적 리졸버 호출에서 부모 리졸버가 리턴한 객체. 이를 활용해 현재 리졸버가 내보낼 값을 조절
  • userId: 쿼리문에서 입력으로 넣은 인자 userId
  • context: 모든 리졸버에게 전달, 주로 미들웨어를 거쳐서 입력 값(로그인 정보, 권한 같은 주요 컨텍스트 관련 정보)
  • info: 스키마 정보 및 현재 쿼리의 특정 필드 정보

Aliases 별칭

별칭을 사용하는 경우 필드 결과의 이름을 지정할 수 있다.

{
	empiroHero: hero(episode: EMPIRE){
		name
	}
	jediHero: hero(episode: JEDI){
		name
	}
}

Fragments

fragment를 이용하여 재사용 가능한 단위를 묶고 데이터 요구사항을 작게 분할할 수 있다.

profile
홀로서기 기록장

0개의 댓글