GraphQL 을 배워보자 - 2

준형정·2023년 10월 9일

GraphQL

목록 보기
2/2

스키마 설계하기

GraphQL 은 스키마 정의를 위해 SDL(Schema Definition Language, 스키마 정의 언어)를 지원한다.
쿼리 언어처럼 SDL 도 애플리케이션에서 사용중인 프로그래밍 언어 및 프레임워크와는 상관 없이 사용 방법이 항상 동일하다.

GraphQL 스키마 문서 - 애플리케이션에서 사용할 타입을 정의해 둔 텍스트 문서

타입 정의하기

타입(Type) - GraphQL 에서 핵심 단위, 타입은 커스텀 객체
타입은 애플리케이션 데이터를 상징

스키마 텍스트 파일의 주요 확장자는 .graphql 이다.

#Type 정의 예시
type Photo {
	id: ID!
    name: String!
    url: String!
    description: String
}

필드 타입에 붙은 느낌표는 'null 값을 허용하지 않음(non-nullable)'을 뜻함
ID 스칼라 타입은 고유 식별자 값이 반환되어야 하는곳에 사용
id 필드 반환 값은 문자열 타입이지만 고유한 값인지 유효성 검사를 받음

커스텀 스칼라 타입

스칼라 타입은 객체 타입이 아니기 때문에 필드를 가지지는 않음
커스텀 스칼라 타입의 유효성 검사 방식을 지정할 수 있음

# DateTime으로 지정된 필드는 JSON 문자열이 값으로 반환됨
# 반환 문자열 값이 직렬화와 유효성 검사 과정을 거쳤는지,
# 공식 날짜 및 시간으로 형식이 맞춰졌는지 검사 가능
scalar DateTime

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

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

열거 타입

열거 타입(Enumeration Type)은 스칼라 타입에 속하며, 필드에서 반환하는 문자열 값을 세트로 미리 지정

enum PhotoCategory {
	SELFIE
    PORTRAIT
    ACTION
    LANDSCAPE
    GRAPHIC
}
# 사용
type Photo {
	id: ID!
    name: String!
    url: String!
    description: String
    created: DateTime!
    category: PhotoCategory!
}

연결과 리스트

리스트는 대괄호를 감싸서 만듬. [String], [PhotoCategory]

일대일 연결

type User {
	githubLogin: ID!
	name: String
    avatar: String
}
// 아래 타입 선언에서 postedBy 가 두 노드(타입)을 이어주는 엣지(패스) 이다
type Photo {
	id: ID!
    name: String!
    url: String!
    description: String
    created: DateTime!
    category: PhotoCategory!
    postedBy: User!
}

일대다 연결

GraphQL 서비스는 최대한 방향성이 없도록 유지하는 편이 좋음
그렇게 해야 클라이언트 쪽에서 쿼리를 자유롭게 만들 수 있다

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

일대다 관계는 어떤 객체(부모) 필드 목록중 다른 객체 리스트(자식)를 반환하는 필드를 보유하고 있을 때 나타나는 관계

흔히 루트 타입에 일대다 관계를 정의함

type Query {
	totalPhotos: Int!
    allPhotos: [Photo!]!
    totalUsers: Int!
    allUsers: [User!]!
}

schema {
	query : Query
}

다대다 연결

노드 리스트를 다른 노드 리스트와 연결지어야 하는 경우
ex) 사진속의 여러명 사용자 태그 <-> 사용자 한 명이 태그될 수 있는 사진은 여러장

// 다대다 연결 관계를 만들려면 User와 Photo 타입 양쪽 모두에 리스트 타입 필드를 추가하면 된다
type User {
	... 
    inPhotos: [Photo!]!
}
// 
type Photo {
	...
    taggedUsers: [User!]!
}

하나의 다대다 관계는 두 개의 일대다 관계로 이루어져 있음

통과 타입

관계 자체에 대한 정보를 담고 싶을 경우
SQL 의 관계 테이블과 비슷

type User {
	friends: [User!]!
}
# 만약 친구 관계에 대한 정보(기간 등등) 도 넣고싶을 경우
type User {
	friends: [Friendship!]!
}
# 엣지를 커스텀 객체 타입(통과 타입)으로 정의해야 함
# Friendship 이 통과타입이다.
type Friendship {
	friend_a: User!
    friend_b: User!
    howLing: Int!
    whereWeMet: Location
}
# 친구를 리스트로 넣고싶을 경우
type Friendship {
	friends: [User!]!
    howLing: Int!
    whereWeMet: Location
}

여러 타입을 담는 리스트

유니언 타입

union AgendaItem = StudyGroup | Workout
//
type StudyGroup {
	name: String!
    subject: String
    students: [User!]!
}
//
type Workout {
	name: String!
    reps: Int!
}
//
type Query {
	agenda: [AgendaItem!]!
}

인터페이스

인터페이스 역시 한 필드 안에 타입을 여러 개 넣을 때 사용한다.
객체 타입을 만드는 용도로 사용하는 추상 타입
타입이 반환하는 데이터 타입에 구애받지 않고 사용 가능

query schedule {
	agenda {
    	name
        start
        end
        ... on Workout {
        	reps
        }
    }
}

모든 일정 아이템에 반드시 들어가야 하는 필드에는 인터페이스에 존재해야 함
ex) name, start, end

다음과 같이 GraphQL 스키마에서 사용할 인터페이스를 만듬

scalar DateTime

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

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

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

type Query {
	agenda: [AgendaItem!]!
}

AgendaItem 라는 추상 타입(인터페이스)를 가지고 확장하여 다른 타입 만듬
인터페이스로 타입을 만드려면 인터페이스에 정의된 필드가 무조건 들어가야 함

유니언 타입, 인터페이스 둘 다 타입을 여럿 수용하는 필드를 만들떄 사용한다.
객체에 따라 필드가 완전히 달라져야 한다면 유니언 타입을 쓰는편이 좋음
특정 필드가 반드시 들어가야 한다면 인터페이스가 더 적절함

인자

type Query {
	User(githubLogin: ID!) : User!
    Photo(id: ID!) : Photo!
}
# 쿼리에서 사용할떄는
query {
	User(githubLogin: "MoonTahoe") {
    	name
        avatar
    }
}

query {
	Photo(id: "14TH5B6NS4KIG3H4S") {
    	name
        description
        url
    }
}

특정 객체에 대한 정보를 얻기 위해서는 인자를 반드시 넣어주어야 하며
인자가 필수이기 때문에 인자로는 null 값을 반환할 수 없는 필드로 정의한다.(non-nullable 타입 인자)

데이터 필터링

옵션 인자 - nullable 타입 인자

type Query {
	allPhotos(category: PhotoCategory) : [Photo!]!
}
# 특정 카레고리를 인자로 넣으면 이 카테고리에 속하는 사진만 거른 후 목록에 담아 반환
query {
	allPhotos(category: "SELFIE") {
    	name
        description
        url
    }
}

데이터 페이징

GraphQL 쿼리에 인자를 전달해 한 페이지에 나올 데이터의 양을 정하는 과정
데이터 페이징 기능을 추가하려면 인자를 두 개 더 써야 함
first : 데이터 페이지 한 장 당 들어가는 레코드 수
start : 첫 번째 레코드가 시작되는 인덱스

# 클라이언트에서 쿼리를 보낼 때 인자를 따로 넣지 않는다면 미리 정해 둔 기본값을 인자로 사용
type Query {
	allUsers(first: Int=50 start: Int=0) : [User!]!
	allPhotos(first: Int=25 start: Int=0) : [Photo!]!
}

정렬

enum SortDirection {
	ASCENDING
    DESCENDING
}

enum SortablePhotoField {
	name
    description
    category
    created
}

type Query {
	allPhotos (
    	sort: SortDirection = DESCNDING
        sortBy: SortablePhotoField = created
    ) : [Photo!]!
}

# 쿼리
query {
	allPhotos(sortBy: name) {
    	...
    }
}

Query 타입 이외 다른 타입에도 인자를 추가할 수 있음

type User {
	postedPhotos(
    	first: Int = 25
        start: Int = 0
        sort: SortDirection = DESCENDING
        sortBy: SortablePhotoField = created
        category: PhotoCategory
    ) : [Photo!]!
}

뮤테이션

뮤테이션은 반드시 스키마 안에 정의해 두어야 함
사용자가 GraphQL 서비스를 가지고 할 수 있는 일을 정의해야 함

type Mutation {
	postPhoto (
    	name: String!
        description: String
        category: PhotoCategory = PORTRAIT
    ) : Photo!
}

schema {
	query : Query
    mutation: Mutation
}

# 쿼리
# 사진 데이터 생성 후에 새롭게 만들어진 필드 정보가 모두 반환됨
# 게시자에 대한 정보는 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
        name
        email
    }
}

인풋 타입

인풋 타입을 사용하면 인자 관리를 더 체계적으로 할 수 있음
GraphQL 객체 타입과 비슷하나, 인풋 타입은 인자에서만 쓰임

input PostPhotoInput {
	name: String!
    description: String
    category: PhotoCategory = PORTRAIT
}
// 객체 타입과 비슷하나 전달할 인자에만 사용
// name 과 description 필드는 필수이지만(초기화값이 없어서), category 필드는 // 꼭 넣지 않아도 된다.
type Mutation {
	postPhoto(input: PostPhotoInput!): Photo!
}
// 쿼리
mutation newPhoto($input: PostPhotoInput!) {
	postPhoto(input: $input) {	
    	id
        url
        created
    }
}
// $input 변수 타입은 PostPhotoInput 와 같아야 함
// input 변수 선언
{
	"input" : {
    			"name" : "Hanging at the Arc",
                "description" : "Sunny on the deck of the Arc",
                "category" : "LANDSCAPE"
			}
}

인풋타입으로 정렬 및 필터링 필드와 관련된 코드 구조를 체계화

input PhotoFilter {
	category: PhotoCategory
    createdBetween: 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 {
	...
    postedPhotos(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!]!
}

만들어둔 인풋 타입을 기반으로 복잡한 인풋 데이터를 받는 쿼리 작성

query getPhotos($filter: PhotoFilter, $page: DataPage, $sort: DataSort) {
	allPhotos(filter: $filter, paging: $page, sorting: $sort) {
    	id
        name
        url
    }
}
# 그에 따른 쿼리 변수
{
	"filter" : {
    	"category" : "ACTION",
        "taggedUsers" : ["MoonTahoe", "EvePorcello"],
        "createdBetween": {
        	"start" : "2018-5-31",
            "end" : "2018-11-6"
        }
    },
    "page" : {
    	"first" : 100
    }
    // 옵션 인자이기 때문에 sort 는 없어도 된다.
}

인풋 타입을 사용하면 스키마 구조를 정리하고, 인자를 재사용할 수 있음
GraphQL 인터페이스에서 자동으로 만들어주는 스키마 문서의 질도 더 좋아진다.
API도 사용하기 더 편해짐

리턴 타입

페이로드 데이터 말고도 쿼리나 뮤테이션에 대한 메타 정보를 같이 받아야 할때 사용

type AuthPayload {
	user: User!
    token: String!
}

type Mutation {
	...
    githubAuth(code: String!) : AuthPayload!
}

단순한 페이로드 데이터 외에 추가적으로 데이터가 더 필요할 때는 필드에 커스텀 객체 타입(커스텀 리턴 타입)을 사용

서브스크립션

type Subscription {
	newPhoto: Photo!
    newUser: User!
}

schema {
	query: Query
    mutation: Mutation
    subscription: Subscription
}

서브스크립션에서도 인자를 활용할 수 있음

# 새로운 사진중 ACTION 카테고리에 속하는 사진만 받는 필터를 newPhoto에 추가하고 싶을 경우
type Subscription {
	newPhoto(catagory: PhotoCategory): Photo!
    newUser: User!
}

subscription {
	newPhoto(category: "ACTION") {
    	id
        name
        url
        postedBy {
        	name
        }
    }
}

스키마 문서화

# 주석 위 아래로 인용 부호를 붙여 각 타입 혹은 필드에 추가
"""
깃허브에서 한 번 이상 권한을 부여받은 사용자
"""
type User {
	"""
	사용자의 깃허브 로그인 ID
    """
    githubLogin: ID!
}

# 인자 역시 문서화할 수 있음
type Mutation {
	"""
    깃허브 사용자 권한 부여
    """
	githubAuth(
    	"사용자 권한 부여를 위해 깃허브에서 받아 온 유니크 코드"
        code: String!
    ): AuthPayload!
}

# 인풋 타입에 대한 주석

"""
postPhoto 뮤테이션과 함ㄲ ㅔ전송되는 인풋 값
"""
input PostPhotoInput {
	"신규 사진명"
    name: String!
    "(옵션) 사진에 대한 간략한 설명"
    description: String
    "(옵션) 사진 카테고리"
    category: PhotoCategory=PORTRAIT
}

Mutation {
	postPhoto(
      "인풋: 신규 사진 이름, 설명, 카테고리"
      input: PostPhotoInput!
	): Photo!
}

주석은 모두 아래와 같이 GraphQL 인터페이스 툴의 스키마 문서에 나오게 됨

스키마는 GraphQL 프로젝트의 핵심
개발 로드맵이 되어주며, 프론트엔드와 백엔드가 서로 공유하는 일종의 계약서

profile
접니다

0개의 댓글