GraphQL 스터디

스키마 설계하기

막이86 2023. 11. 10. 16:21
728x90

웹 앱 API 개발을 위한 GraphQL 을 요약한 내용입니다.

  • GraphQL을 사용하면 API 설계 과정이 바뀔 수도 있습니다.
  • API가 REST 엔드포인트의 집합이 아니라 타입 집합으로 보이게 됩니다.
  • API에서 반환할 데이터 타입에 대한 생각을 보고, 같이 이야기를 나누어 보고, 이를 재대로 정의해 두는 일이빈다.
  • 데이터 타입의 집합을 스키마라고 부릅니다.
  • **스미카 우선주의(Schema First)**는 디자인 방법론의 일종입니다.
    • 모든 팀원이 애플리케이션 안의 데이터 타입을 같은 선상에서 이해할 수 있습니다.
    • 백엔드 팀은 스키마를 보고 어떤 데이터를 저장하고 전달해야 하는지 정확히 이해할 수 있습니다.
    • 프론트엔드 팀은 사용자 인터페이스 작업을 할 때 필요한 데이터를 정의할 수 있습니다.
  • GraphQL은 스키마 정의를 위해 SDL(Schema Definition Languate, 스키마 정의 언어)를 지원합니다.
  • 쿼리 언어처럼 SDL 역시 애플리케이션에서 사용중인 프로그래밍 언어나 프레임워크와 상관없이, 사용법이 동일합니다.
  • GraphQL 스키마 문서는 애플리케이션에서 사용할 타입을 정의해 둔 텍스트 문서입니다.
  • 여기서 정의한 타입은 클라이언트와 서버에서 GraphQL 요청에 대한 유효성 검사를 할때 사용됩니다.

4.1 타입 정의하기

  • 예시로 만들 사진 공유 애플리케이션은 사용자가 깃허브계정으로 로그인하여 사진을 게시하고 사진에 사람을 태그할 수 있는 기능이 있습니다.

4.1.1 타입(Type)

  • 타입은 GraphQL의 핵심 단위 입니다.
  • GraphQL에서 타입은 커스텀 객체이며 이를 보고 애플리케이션의 핵심 기능을 알 수 있습니다.
    • 소셜 미디어 애플리케이션은 Users와 Posts로 구성됩니다.
    • 블로그라면 Categories와 Articles로 구성됩니다
  • 타입에는**필드(field)**가 들어갑니다.
  • 필드는 각 객체의 데이터와 관련이 있습니다. 필드는 특정 종류의 데이터를 반환합니다.
  • 스키마에는 타입 정의를 모아 둡니다. 자바스크립트 파일에문자열로 작성하거나, 따로 텍스트 파일로 작성해 둘 수도 있습니다.
    • 텍스트 파일의 주요 확장자는 .graphql 입니다.
  • 스키마 파일에 첫 GraphQL 객체 타입을 정의 해봅시다.
type Photo {
	id: ID!
	name: String!
	url: String!
	description: String
}
  • 중괄호 사이에 Photo 타입의 필드를 정의 했습니다.
    • url 필드는 이미지 파일 경로가 들어갑니다.
    • name과 description 필드를 사용해 Photo 타입에 대한 메타 데이터 정보를 넣습니다.
    • id 필드는 사진에 접근할 때 키 값으로 사용할 수 있습니다.
  • 필드 쿼리 요청을 보냈을 때 돌아오는 응답 데이터의 형식은 JSON 문자열입니다.
  • 필드에 붙은 느낌표는 필드에서 'null 값을 허용하지 않음(non-nullable)'을 뜻합니다.
    • name과 url 필드는 쿼리가 왔을 때 반드시 데이터를 반환해야 합니다.
    • description은 'null 값이 허용되므로(nullable)', 사진 설명은 필수 값이 아닙니다.
    • id 필드에는 각 사진에 대한 고유 식별자가 들어갑니다.
    • id 필드 반환 값은 문자열 타입이지만, 고유한 값인지 유효성 검사를 받습니다.

4.1.2 스칼라 타입

  • 내장 스칼라 타입(Int, Float, String Boolean, ID)는 무척 유용하기는 하나, 스칼라 타입을 직접 만들고 싶은 경우도 있습니다.
  • 스칼라 타입은 객체 타입이 아니기 때문에 필드를 가지지는 않습니다.
  • 커스텀 스칼라 타입의 유효성 검사 방식을 지정할 수 있습니다.
  • 커스텀 스칼라 타입인 DateTime을 만들었습니다.
scalar DateTime

type Photo {
	id: ID!
	name: String!
	url: String!
	description: String
	created: DateTime!
}
  • DateTime으로 지정된 필드는 JSON 문자열이 값으로 반환됩니다.
  • 커스텀 스칼라 타입을 사용하면 반환 문자열 값이 직격화와 유효성 검사 과정을 거쳤는지, 공식 날짜 및 시간으로 형식이 맞춰졌는지 검사할 수 있습니다.

graphql-custom-type은 Node.js GraphQL 서비스에서 자주 사용할 법한 커스텀 스칼라 타입을 모아 둔 npm 패키지 입니다.

4.1.3 열거 타입(Enumeration Type)

  • 스칼라 타입에 속하며, 필드에서 반환하는 문자열 값을 세트로 미리 지정해 둘 수 있습니다.
  • 미리 정해 둔 셋트에 속한 값만 필드에서 반환하도록 만들고 싶다면 열겨타입을 사용하면 됩니다.
  • PhotoCategory라는 enum 타입을 만들어 보겠습니다.
    • SELFIE, PORTRAIT, ACTION, LANDSCAPE, GRAPHIC 중 하나만 값으로 반환할 수 있습니다.
scalar DateTime

enum PhotoCategory {
	SELFIE
	PORTRAIT
	ACTION
	LANDSCAPE
	GRAPHIC
}

type Photo {
	id: ID!
	name: String!
	url: String!
	description: String
	created: DateTime!
	category: PhotoCategory!
}

개발 언어의 열거 타입 지원 여부는 걱저앟지 않아도 됩니다. 언어와 상관없이 사용할 수 있습니다.

4.2 연결과 리스트

  • 스키마 필드에서는 GraphQL 타입이 담긴 리스트 반환도 가능합니다.
  • 리스트는 GraphQL 타입을 대괄호로 감싸서 만듭니다.
    • [String] 은 문자열 리스트를 정의한 것입니다.
    • [PhotoCategory] 는 사진 카테고리 리스트를 정의한 것입니다.
  • '3.2 절 GraphQL 쿼리'에서 봤듯이 union이나 interface 타입을 사용하면 리스트에 여러 개의 타입을 한 번에 담을 수 있습니다.
  • 가끔으 느낌표 때문에 리스트를 정의하기 까다로울 수 있습니다.
    • 닫는 대괄호 다음에 느낌표를 쓰면 필드에서는 null 값을 반환할 수 없음(non-nullable)을 뜻합니다.
    • 느낌표가 닫는 대괄호 앞에 오면 리스트에 담긴 값 자체가 null 값이 될 수 없습니다.

리스트 null 적용 규칙

  • 리스트 안에 담기는 값은 대부분 null이 될 수 없으며, 리스트 자체도 null이 될 수 없는 경우가 대다수입니다.
  • 리스트 안의 갑이 null을 허용하는 경우가 드물기 때문입니다.
  • 리스트에 값이 전혀 없다면 그냥 [] 같이 빈 JSON 배열을 반환하면 됩니다.
  • 데이터와 데이터 사이의 관계 정립이 자유로우며, 데이터를 요청할 때 이와 관련된 데이터의 필드까지 요청할 수 있는 쿼리 기능은 GraphQL의 핵심적인 부분입니다.

4.2.1 일대일 연결

  • 커스텀 객체 타입으로 필드를 만들면 두 객체가 서로 연결됩니다.
  • 그래프 이론에 의하면 두 객체 사이의 연결, 혹은 링크를 일컽어 **엣지(edge)**라고 합니다.
  • 사진은 사용자가 게시하므로, 시스템상의 모든 사진에는 게시자와 사진 사이를 연결해 주는 엣지 정보가 들어 있어야 합니다.
  • 그림 4-1에 Photo와 User 사이의 단방향 관계가 그려져 있습니다.
  • 두 노드를 이어주는 엣지는 postedBy라고 합니다.
  • 스키마에 User 타입을 새로 추가합니다.
    • 사진 공유 앱 사용자는 깃허브를 통해 로그인 합니다.
    • githubLogin 값을 가지고 사용자 정보 식별자로 사용합니다.
    • 사진 객체에 postedBy 필드를 추가해 관계를 표시했습니다.
    • 사진은 반드시 게시자가 있으므로 User! 타입으로 필드를 정의 합니다.
scalar DateTime

enum PhotoCategory {
	SELFIE
	PORTRAIT
	ACTION
	LANDSCAPE
	GRAPHIC
}

type User {
	githubLogin: ID!
	name: String
	avatar: String
}

type Photo {
	id: ID!
	name: String!
	url: String!
	description: String
	created: DateTime!
	category: PhotoCategory!
	postedBy: User!
}

4.2.2 일대다 연결

  • GraphQL 서비스는 최대한 방향성이 없도록 유지하는 편이 좋습니다.
  • 방향이 없다면 아무 노드에서 그래프 횡단을 시작할 수 있으므로, 클라이언트 쪽에서 쿼리를 최대한 자유롭게 만들 수 있기 때문입니다.
  • 그러기 위해서는 Photo 타입에서 User 타입으로 되돌아갈 수 있는 패스가 있어야 합니다.
  • User 쿼리를 작성할 때 해당 사용자가 게시한 사진 정보 역시 모두 받을 수 있어야 한다는 뜻입닏니다.
  • User 타입에 postedPhotos 필드를 축가하여 사용자에서 Photo로 돌아가는 경로를 만들었습니다.
    • postedPhotos 필드는 사용자가 게시간 Photo 타입 리스트를 반환합니다.
    • 사용자 한 명이 사진을 여러 장 올릴 수 있으므로 이들 사이의 관계를 일대다로 정립했습니다. [그림 4-2 일대다 관계]
scalar DateTime

enum PhotoCategory {
	SELFIE
	PORTRAIT
	ACTION
	LANDSCAPE
	GRAPHIC
}

type User {
	githubLogin: ID!
	name: String
	avatar: String
	postedPhotos: [Photo!]!
}

type Photo {
	id: ID!
	name: String!
	url: String!
	description: String
	created: DateTime!
	category: PhotoCategory!
	postedBy: User!
}
  • 사진이나 사용자 정보에 대한 쿼리 요청을 만드려면 Query 루트 타입에 필드를 정의해야 합니다.
  • 루트 Query 타입에 커스텀 타입을 새로 추가하려면 다음과 같이 합니다.
    • 각 타입의 총 레코드 수를 추가 하였습니다.
    • 모든 사진리스트를 요청을 추가 하였습니다.
    • Query 타입을 schema에 필드로 추가 했습니다.
    • GraphQL API에서 쿼리를 사용할 수 있습니다.
type Query {
	totalPhotos: Int!
	allPhotos: [Photo!]!
	totalUsers: Int!
	allusers: [User!]!
}

schema {
	query: Query
}
  • 쿼리 요청 방법 입니다.
query {
	totalPhotos
	allPhotos {
		name
		url
	}
}

4.2.3 다대다 연결

  • 가끔은 노드 리스트를 다른 노드 리스트와 연결지어야 할 때도 있습니다.
  • 예를 들면 사용자가 사진을 게시하면 이 사진속의 다른 사용자를 태그하는 기능이 있습니다.
  • 한 장의 사진 안에는 사용자 여러 명 태그되어 있을 수 있으며, 사용자 한 명이 태그될 수 있는 사진 역시 여러 장이 될 수 있습니다.
  • 다대다 연결 관계를 만드려면 User와 Photo 타입 양쪽 모두에 리스트 타입 필드를 추가하면 됩니다.
  • 하나의 다대다 관계는 두개의 일대다 관계로 이루어 져 있습니다.
    • 사진 한 장에 사용자를 여러 명 태그할 수 있습니다.
    • 사용자는 사진 여러 장에 태그될 수 있습니다.
scalar DateTime

enum PhotoCategory {
	SELFIE
	PORTRAIT
	ACTION
	LANDSCAPE
	GRAPHIC
}

type User {
	githubLogin: ID!
	name: String
	avatar: String
	postedPhotos: [Photo!]!
	inPhotos: [Photo!]!
}

type Photo {
	id: ID!
	name: String!
	url: String!
	description: String
	created: DateTime!
	category: PhotoCategory!
	postedBy: User!
	taggedUsers: [User!]!
}

통과 타입(Through Type)

  • 다대다 연결을 만들 경우, 관계 자체에 대한 정보를 담고 싶을 때도 있습니다.
  • 사용자 사이의 친구 관계를 예로 들어 통과 타입을 설명 하겠습니다.
  • 사용자 사이의 관계를 만들기 위해 User 타입 하위에 다른 사용자를 모아 둔 리스트를 추가합니다.
type User {
	friends: [User!]!
}
  • 타입에 친구 리스트가 들어갑니다.
  • 친구 관계에 대한 정보(서로 어디서 만났는지, 혹은 얼마나 알고 지냈는지 등등)도 넣어야 하는 상황이라 가정 해봅시다.
    • 엣지를 커스텀 객체 타입으로 정의 해야합니다.
    • 엣지가 두 노드를 연결하기 위해 만들어진 노드이므로 통과 타입이라고 부르겠습니다.
    • 친구 사이를 연결하면서 어디서 어떻게 만났는가도 알려 주는 Friendship을 통과 타입으로 정의해봅시다.
  • friend 필드를 User 타입에 직접 만들지 않고, Friendship 타입을 만들어 friends를 연결 하였습니다.
    • 친구 사이인 friend_a, friend_b 필드를 추가
    • 우정의 지속 기간을 나타내는 howLong 필드 추가
    • 만남 정보를 가지고 있는 whereWeMet 필드 추가
type User {
	friends: [Friendship!]!
}

type Friendshp {
	friend_a: User!
	friend_b: User!
	howLong: Int!
	whereWeMet: Location
}
  • 여러 명의 절친을 신입생 때 한 번에 만난 경우도 있을 것입니다.
    • 따라서 친구 두명 이상을 한 번에 친구 관계로 몰아넣을 수 있도록 friends 필드를 만듭니다.
    • 우정 관계를 그룹으로 표현할 수 있습니다.
type User {
	friends: [Friendship!]!
}

type Friendshp {
	friends: [User!]!
	howLong: Int!
	whereWeMet: Location
}

4.2.4 여러 타입을 담는 리스트

  • 스키마에 두가지 타입을 추가하는 법을 알아봅시다.
  • 일정 앱을 예시로 사용이 됩니다.
  • 일정에는 여러 종류의 이벤트가 들어가며, 이벤트마다 데이터 필드가 달라질 수 있습니다.
    • 스터디 그룹 모임, 운동과 같이 필드는 완전히 다를 수도 있습니다.
    • 일정 앱에는 이렇게 상이한 이벤트를 모두 추가할 수 있어야합니다.
    • 일정 스키마를 만들려면 유니언 타입이나 인터페이스를 사용하면 됩니다.

유니언 타입(Union Type)

  • 유니언 타입을 사용하면 여러 타입 가운데 하나를 반환할 수 있습니다.
  • 일정이 스터디 그룹과 운동일 때 각각 반환되는 쿼리 입니다.
query {
	agenda {
		... on Workout {
			name
			reps
		}
		... on StudyGroup {
			name
			subject
			students
		}
	}
}
  • 학생들이 매일 일정을 계획할 때 사용할 수 있도록 AgendaItem이라는 union 타입을 만들겠습니다.
    • AgendaItem에서 스터디 그룹 일정과 운동일정을 하나의 타입으로 묶어 두었습니다.
    • 유니언 타입에 타입을 원하는 만큼 결합할 수 있습니다.
    union AgendaItem = StudyGroup | Workout | Class | Meal | Meeting | FreeTime
    
union AgendaItem = StudyGroup | Workout

type StudyGroup {
	name: String!
	subject: String
	students: [User!]!
}

type Workout {
	name: String!
	reps: Int!
}

type Query {
	agenda: [AgendaItem!]!
}

인터페이스(Interface)

  • 인터페이스 역시 한 필드 안에 타입을 여러개 넣을 때 사용합니다.
  • 객체 타입 용도로 만드는 추상 타입이며, 스키마 코드의 구조를 조직할 때 아주 좋은 방법입니다.
  • 인터페이스를 통해 특정 필드가 무조건 특정 타입에 포함되도록 만들 수 있으며, 이들 필드는 쿼리에서 사용 할 수 있습니다.
  • 인터페이스는 타입이 반환하는 데이터 타입에 구애받지 않고 사용할 수 있습니다.
  • 인터페이스를 사용하여 작성한 agenda 쿼리 예시 입니다.
    • name, start, end 필드 등이 필수로 들어 갑니다.
    • 일정 종류와는 상관없이 필수 필드는 반드시 들어가야합니다.
query schedule {
	agenda {
		name
		start
		end
		... on Workout {
			reps
		}
	}
}
  • AgendaItem이라는 인터페이스를 만들었습니다.
    • 인터페이스 안에 정의된 필드가 무조건 들어가야 합니다.
    • StudyGroup, Workout 타입은 AgendaItem 인터페이스를 기반으로 만들었기 때문에 name, start, end 필드가 들어가야 합니다.
    • agenda 필드가 반환하는 리스트 안에는 AgendaItem 인터페이스를 사용해 만든 타입이 모두 들어갑니다.
    • 타입에는 인터페이스에 정의된 필드 외에 다른 필드도 넣을 수 있습니다.
scalar DateTime

interface AgendaItem {
	name: String!
	start: DateTime!
	end: DateTime!
}

type StudyGroup implements AgendaItem {
	name: String!
	start: DateTime!
	end: DateTime!
	participants: [User!]!
	topic: String!
}

type Workout implements AgendaItem {
	name: String!
	start: DateTime!
	end: DateTime!
	reps: Int!
}

type Query {
	agenda: [AgendaItem!]!
}
  • 유니언 타입과 인터페이스 둘 다 타입을 여럿 수용하는 필드를 만들때 사용합니다.
  • 둘 중 무엇을 사용할지는 프로젝트의 상황에 따라 달라집니다.
  • 일반적으로 필드가 완전히 달라져야 한다면 유니언 타입을 쓰는 편이 좋습니다.
  • 특정 필드가 반드시 들어가야 한다면 유니언 타입 대시에 인터페이스가 더 적절합니다.

4.3 인자

  • 필드에는 인자를 추가할 수 있습니다.
  • 인자를 사용하면 데이터를 전달할 수 있기 때문에 GraphQL 요청 결과 값이 바뀔 수도 있습니다.
  • 쿼리와 큐테이션에 인자를 전달 했습니다. 스키마에 인자를 정의하는 법에 대해서 알아봅시다.
  • allUsers와 allPhotos를 목록으로 반환하는 필드가 있는데, User 한 명, 혹은 Photo 한 장만 선택하고 싶다면 어떡할까요?
type Query {
	totalPhotos: Int!
	allPhotos: [Photo!]!
	totalUsers: Int!
	allusers: [User!]!
	User(githubLogin: ID!): User!
	Photo(id: ID!): Photo!
}
  • MoonTahoe의 이름과 아바타를 선택하는 쿼리 입니다.
query {
	User(githubLogin: "MoonTahoe") {
		name
		avatar
	}
}
  • 사진 한 장의 세부 정보를 받아 보려면 사진의 ID를 전달하면 됩니다.
query {
	Photo(id: "14TH") {
		name
		description
		url
	}
}
  • 특정 사용자나 사진의 정보를 얻으려면 인자를 반드시 넣어주어야 합니다.
  • 인자가 필수이기 때문에 이들은 null 값을 반환할 수 없는 필드로 정의합니다.
  • 쿼리 요청 시 id나 githubLogin 값을 넣어주지 않으면 GraphQL 파서가 에러를 반환합니다.

4.3.1 데이터 필터링

  • 반드시 인자가 값을 받도록 만들 필요는 없습니다.
  • null을 반환할 수 있는 필드를 만들고 인자는 옵션으로 받아도 됩니다.
  • 부가적인 파라미터로 인자를 넘겨 쿼리 요청이 수행됩니다.
  • allPhotos 쿼리에서 사진 카테고리에 대한 정보를 옵션 인자로 넘기면 특정 카테고리 사진만 걸러 내어 그 목록을 받아 봅시다.
type Query {
	totalPhotos: Int!
	allPhotos(category: PhotoCategory): [Photo!]!
	totalUsers: Int!
	allusers: [User!]!
	User(githubLogin: ID!): User!
	Photo(id: ID!): Photo!
}
  • SELFIE 카테고리로 분류된 사진의 name, description, url을 반환하는 쿼리 입니다.
query {
	allPhotos(category: "SELFIE") {
		name
		description
		url
	}
}

데이터 페이징(Data Paging)

  • Users와 Photos 데이터가 많이 쌓이게 되면 요청이 올때마다 모든 User와 Photo를 응답으로 돌려줄 수는 없을 것 같습니다.
  • 따라서 GraphQL 쿼리에 인자를 전달해 반환 데이터의 양을 조절합니다.
  • 이때 한 페이지에 나올 데이터의 양ㅇ을 정한다는 의미에서 데이터 페이징이라 합니다.
  • 데이터 페이징을 위해서는 first인자와 start 인자를 추가 해야합니다.
  • first는 페이지 한 장당 들어가는 레코드 수를 지정하고, start는 첫번째 레코드가 시작되는 인덱스를 지정 합니다.
  • first와 start 두 인자를 추가하였습니다.
  • 만약 클라이언트 쪽에서 쿼리 요청을 보낼 때 인자를 따로 넣지 않는다면 미리 정해 둔 기본 값을 인자로 사용합니다.
type Query {
	totalPhotos: Int!
	allPhotos(first: Int=25 start: Int=0): [Photo!]!
	totalUsers: Int!
	allusers(first: Int=50 start: Int=0): [User!]!
	User(githubLogin: ID!): User!
	Photo(id: ID!): Photo!
}
  • 90번째 사용자에서 시작해서 10명까지만 추려보내고 싶다면 쿼리를 다음처럼 작성합니다.
query {
	allUsers(first: 10 start: 90) {
		name
		avarta
	}
}
  • 총 페이지 수를 계산하려면 전체 아이템 수를 페이지 당 아이템 수로 나누면 됩니다.
    • pages = total / pageSize

정렬

  • 데이터 리스트가 반환되는 쿼리를 작성할 때는 리스트의 정렬 방식을 지정할 수 있습니다.
  • enum을 사용하면 Photo 객체의 정렬 기준이 될 필드를 지정하고 정렬 방식도 지정할 수 있습니다.
  • sort 인자의 값을 ASCENDING, 혹은 DESCENDING 둘 중 하나로 제한하기 위해 SortDirection 이라는 Enum Type을 만들었습니다.
  • 정렬 기준으로 아무 필드나 사용하고 싶지는 않으므로 sortBy 인자 값으로 name, description, category, created 이 네 값 중 하나만 받도록 만듭니다.
enum SortDirection {
	AECENDING
	DESCENDING
}

enum SortablePhotoField {
	name
	description
	category
	created
}

type Query {
	allPhotos(
		sort: SortDirection = DESCENDING 
		sortBy: SortablePhotoField = created
	): [Photo!]!
}
  • 이름 필드 기준으로 내림차순 정렬한 사진 리스트가 반환되는 쿼리 입니다.
query {
	allPhotos(sortBy: name)
}
  • 사진 필드에 필터, 정렬, 페이지 인자를 추가한 코드 입니다.
type Query {
	allPhotos(
		first: Int=25 
		start: Int=0 
		sort: SortDirection = DESCENDING 
		sortBy: SortablePhotoField = created
	): [Photo!]!
}

4.4 뮤테이션

  • 뮤테이션은 반드시 스키마 안에 정의해 두어야 합니다.
  • 쿼리를 정의할 때처럼 커스텀 타입으로 정의한 다음에 스키마에 추가합니다.
  • 엄밀히 말하자면 스키마 안에서 쿼리와 뮤테이션 작성법은 차이가 없습니다. 유일한 차이는 구문 작성 의도에서 발생합니다.
  • 애플리케이션 상태를 바꿀 액션이나 이벤트가 있을 때만 뮤테이션을 작성해야 합니다.
  • 사용자가 GraphQL로 만든 애플리케이션에서 취할 수 있느 ㄴ동작을 일단 모두 목록으로 만들어보면, 대부분 뮤테이션일 확률이 높습니다.
  • 사진 공유 앱에서 사용자는 깃허브 계정으로 로그인하며, 사진을 게시하고 사진 태그도 합니다. 이런 행동은 전부 애플리케이션 상ㅌ에 영향을 끼칩니다.
  • 사용자가 사진을 게시하면 사진 데이터가 시스템에 추가 됩니다.
  • Mutation 타입 하위에 postPhoto 필드를 넣어서 사용자가 사진을 게시할 수 있도록 만듭니다.
type Mutation {
	postPhoto(
		name: String!
		description: String
		category: PhotoCategory = PORTRAIT
	): Photo!
}

schema {
	query: Query
	mutation: Mutation
}
  • 사진을 게시할 때 다른 것은 몰라도 사지의 name은 필수 값으로 들어오도록 만들었습니다.
  • description과 category는 필수 값이 아닙니다.
  • category 값을 인자로 넣지 않으면 기본 값으로 지정한 PORTRAIT가 들어갑니다.
  • 사진을 게시할 때 다음과 같은 뮤테이션 요청이 전송됩니다.
    • 사진을 게시하고 난 뒤에는 이 사진에 대한 정보를 받아 볼 수 있습니다.
    • 응답 받은 필드를 보고 정상적으로 게시되었는지 알아볼 수 있습니다.
    • 신규 사진 ID는 데이터베이스에서 생성해 부여합니다.
    • 쿼리 셀렉션 세트안에는 사진 게시자에 대한 정보도 들어가 있습니다.
      • 만약 현재 로그인한 사용자가 없다면 뮤테이션은 에어를 반환합니다.
      • 사용자가 로그인 상태라면 게시자에 대한 정보가 postedBy 필드에 들어갑니다.
mutation {
	postPhoto(name: "Sending the Palisades") {
		id
		url
		created
		postedBy {
			name
		}
	}
}

뮤테이션 변수

  • 뮤테이션을 작성할 떄는 변수를 선언하는 편이 좋습니다.
  • 사용자 데이터를 대량으로 만들어야 한다면 변수를 활용해 뮤테이션을 반복 요청하면 됩니다.
mutation postPhoto ($name: String! $description: String $category: PhotoCategory) {
	postPhoto(name: $name, description: $description, category: $category) {
		id
		url
		created
		postedBy {
			name
		}
	}
}

4.5 인풋 타입(Input Type)

  • 인풋 타입을 사용하면 인자 관리를 조금 더 체계적으로 할 수 있습니다.
  • 인풋 타입은 객체 타입과 비슷하나, 인풋 타입은 인자에서만 쓰입니다.
input PostPhotoInput {
	name: String!
	description: String
	category: PhotoCategory = PORTRAIT
}

type Mutation {
	postPhoto(input: PostPhotoInput!): Photo!
}
  • postPhoto 뮤테이션을 작성하려면 신규 사진에 대한 정보를 하나의 객체에 다 담아야 합니다.
    • $input 변수 타입은 PostPhotoInput 인풋 타입과 같아야합니다.
    • 사진을 새로 추가하려면 input.name 필드 값이 필요하므로 인풋 타입은 null이 될 수 없습니다.
    • $input 필드 변수 값으로는 새로운 사진 데이터가 들어가야합니다.
mutation newPhoto($input: PostPhotoInput!) {
	postPhoto(input: $input) {
		id
		url
		created
	}
}
  • 인풋 객체는 JSON 객체 안의 'input' 키 값으로 묶여 뮤테이션의 쿼리 변수와 함께 전송됩니다.
    • 쿼리 변수의 형식은 JSON 입니다.
    • 카테고리 키 값은 문자열이 되어야 하며, PhotoCategory 타입 값 중 하나가 들어 있어야 합니다.
{
	"input": {
		"name": "Hanging at the Arc",
		"description": "Sunny on the deck of the Arc",
		"category": "LANDSCAPE"
	}
}
  • 인풋 타입을 사용하면 GraphQL 스키마를 깔끔하게 작성하고 유지할 수 있습니다.
  • 인풋 타입은 모든 필드에서 인자로 사용할 수 있습니다.
  • 애플리케이션 데이터 페이징 및 필터링 기능을 개선할 때도 사용가능합니다.
  • 인풋 타입으로 상당량의 필드를 체계화했습니다.
  • 스키마 이곳저곳에서 인풋 타입을 인지로 재사용할 수 있게 만들었습니다.
input PhotoFilter {
	category: PhotoCategory
	createBetween: DateRange
	taggedUsers: [ID!]
	searchText: String
}

input DateRange {
	start: DateTime!
	end: DateTime!
}

input DataPage {
	first: Int = 25
	start: Int = 0
}

input DataSort {
	sort: SortDirection = DESCENDING
	sortBy: SortablePhotoField = created
}

type User {
	
	postedPhotods(
		filter: PhotoFilter
		paging: DataPage
		sorting: DataSort
	): [Photo!]!
	inPhotos(filter: PhotoFilter, paging: DataPage, sorting: DataSort): [Photo!]!
}

type Photo {

	taggedUsers(sorting: DataSort): [User!]!
}

type Query {

	allUsers(paging: DataPage, sorting: DataSort): [User!]!
	allPhotos(filter: PhotoFilter, paging: DataPage, sorting: DataSort): [Photo!]!
}
  • 이미 만들어 둔 인풋 타입을 조압하여 상당히 복잡한 인풋 데이터를 받는 쿼리도 작성할 수 있습니다.
  • 쿼리에서 $filter, $page, $sort 새 인풋 타입을 옵션 인자로 받도록 했습니다.
  • 필요한 사진에 대한 세부 사항은쿼리 변수를 사용해 지정합니다.
query getPhotos($filter: PhotoFilter, $page: DataPage, $sort: DataSort) {
	allPhotos(filter: $filter, paging: $page, sorting: $sort) {
		id
		name
		url
	}
}
  • 깃허브 사용자 MoonTahoe와 EvePorcello가 태그된 ACTION 사진 중, 11월 6일과 5월 31일 사이에 생성된 것만 찾아줍니다.
  • 생성된 사진을 전부 보내지는 않고, 앞에서 100장까지만 보내 줍니다.
{
	"filter": {
		"category": "ACTION",
		"taggedUsers": ["MoonTahoe", "EvePorcello"],
		"createdBetween": {
			"start": "2018-5-31",
			"end": "2018-11-6"
		}
	},
	"page": {
		"first": 100
	}
}
  • 인풋 타입을 사용하면 스키마 구조를 정리하고 인자를 재사용할 수 있습니다.
  • GraphiQL이나 GraphQL 플레이그라운드에서 자동으로 만들어 주는 문서의 질도 더 좋아집니다.
  • API도 사용하기 더 편해지며, 배우거나 이해하기도 쉬워집니다.
  • 클라이언트 쪽에서도 인풋 타입을 사용해 체계적으로 궈키를 작성하고 사용할 수 있습니다.

4.6 리턴 타입(Return Type)

  • 가끔은 페이로드 데이터 말고도 쿼리나 뮤테이션에 대한 메타 정보를 함께 받아야 할 떄가 있습니다.
  • 사용자가 로그인 상태이거나 인증을 거친 상태라면 User 페이로드에 관련 토큰을 같이 반환해야 합니다.
  • 깃허브 OAuth를 통해 로그인하려면 깃허브에서 OAuth 코드를 받아와야 합니다.
  • githubAuth 뮤테이션에 넣어 사용자가 로그인할 수 있도록 만듭니다.
  • 인증이 잘 된다면 로그인이 성곡적으로 잘 되었다는 정보와 함께 토큰이 커스텀 객체 타입에 담겨 반환됩니다.
type AuthPayload {
	user: User!
	token: String!
}

type Mutation {
	
	githubAuth(code: String!): AuthPayload!
}
  • 단순한 페이로드 데이터 외에 추가적으로 데이터가 더 필요할 떄는 필드에 커스텀 객체 타입을 사용합니다.
  • 쿼리에서 응답까지 걸린 시간이 궁굼할 수도 있고, 응답에 들어있는 항목의 개수를 알고 싶을 때도 있습니다.
  • 이런 경우 커스텀 리턴 타입을 사용하면 됩니다.

4.7 서브스크립션(Subscription)

  • Subscription 타입은 GraphQL 스키마 정의 언어에 존재하는 다른 타입과 별반 차이가 없습니다.
  • Photo나 User 타입이 생성될 떄마다 그 소식을 클라이언트에서 받아 볼 수 있도록 만들 수 있습니다.
  • 사진이 새로 게시되면 newPhoto를 구독 중인 클라이언트는 모두 새로운 사진에 대한 알림을 받습니다.
  • 신규 사용자가 생성된다면 그 소식을 구독하는 클라이언트는 모두 해당 사용자에 대한 정보를 받습니다.
type Subscription {
	newPhoto: Photo!
	newUser: User!
}

schema {
	query: Query
	mutation: Mutation
	subscription: Subscription
}
  • 서브스크립션에서도 인자를 활용할 수 있습니다.
  • 새로운 사진 중에 ACTION 카테고리에 속하는 사진만 받는 필터를 newPhoto에 추가하고 싶으면 다음과 같이 하면 됩니다.
type Subscription {
	newPhoto(category: PhotoCategory): Photo!
	newUser: User!
}
  • 새로 등록된 ACTION 사진만 받고 싶다면 GraphQL API에 다음과 같은 코드를 보내면 됩니다.
subscription {
	newPhoto(category: "ACTION") {
		id
		name
		url
		postedBy {
			name
		}
	}
}

4.8 스키마 문서화

  • GraphQL 스키마를 작성할 때는 옵션으로 각 필드에 대한 설명을 적어 넣을 수 있습니다.
  • 스미마 타입과 필드에 대한 부가정볼르 제공할 수 있습니다.
  • 설명을 잘 적어 두면 자기 자신뿐 아니라 팀의 다른 사람들 그리고 API 사용자들이 스키마를 이해하는 데 도움이 됩니다.
  • 주석 위, 아래로 인용 부호를 붙여 각 타입 혹은 필드에 추가합니다.
"""
깃허브에서 한 번 이상 권한을 부여받은 사용자
"""
type User {
	"""
	사용자의 깃허브 로그인 ID
	"""
	githubLogin: ID!

	"""
	사용자의 이름(성 포함)	
	"""
	name: String

	"""
	사용자의 깃허브 프로필 사진 url
	"""
	avatar: String

	"""
	사용자가 올린 모든 사진
	"""
	postedPhotos: [Photo!]!

	"""
	사용자가 들어간 모든 사진
	"""
	inPhotos: [Photo!]!
}
  • 타입과 필드뿐 아니라 인자 역시 문서화 할 수 잇습니다.
type Mutation {
	"""
	깃허브 사용자 권한 부여 
	"""
	githubAuth(
		"사용자 권한 부여를 위해 깃허브에서 받아 온 유니크 코드"
		code: String!
	): AuthPayload!
}
  • 인자에 주석을 써 주면 인자명과 필드의 옵션 여뷰가 자동으로 들어가게 됩니다.
  • 인풋 타입에 대한 주석은 다른 타입과 같은 방식으로 써주면 됩니다.
"""
postPhoto 뮤테이션과 함께 전송되는 인풋 값
"""
input PostPhotoInput {
	"신규 사진명"
	name: String!
	"(옵션) 사진에 대한 간략한 설명"
	description: String
	"(옵션) 사진 카테고리"
	category: PhotoCategory=PORTRAIT
}

postPhoto(
	"인풋: 신규 사진 이름, 설명, 카테고리"
	input: PostPhotoInput!
): Photo!
  • 주석은 GraphQL 플레이그라운드, 혹은 GraphiQL 툴 스키마 문서에 나오게 됩니다.
  • 타입에 대한 설명은 인트로스펙션 쿼리로도 검색할 수 있습니다.
  • 견고하게 잘 정의된 스키마는 모든 GraphQL 프로젝트의 핵심입니다.
  • 스키마는 개발 로드맵이 되어 주며, 프론트엔드와 백엔드가 서로 공유하는 일종의 계약서의 역활을 해 줍니다.
  • 이를 통해 두 팀에서는 서로가 동의한 스키마를 항상 사용할 것이라 확신할 수 있습니다.
728x90

'GraphQL 스터디' 카테고리의 다른 글

GraphQL API 만들기  (0) 2023.11.10
GraphQL 쿼리어  (0) 2023.11.10
GraphQL에 오신 것을 환영합니다.  (0) 2023.11.10