GraphQL(Apollo)

차분한열정·2022년 3월 22일
0

GraphQL

목록 보기
1/7

1. 기초

Apollo는 프론트엔드와 백엔드 사이의 communication layer(=a unified graph)를 만들기 위한 플랫폼이다. graph의 중심에는 GraphQL이라고 불리우는 query language가 있다.

GraphQL을 쓰는 다양한 이유가 있지만 그 중에 가장 큰 것은 역시 백엔드에서 아무리 API가 바뀌더라도 프론트엔드 관점에서는 딱히 크게 변하는 게 없다는 점 때문이다. 즉 백엔드에서 API에 새로운 타입이나 필드를 추가하기가 용이하고 클라이언트에서도 새롭게 추가/수정된 필드들을 사용하기가 용이하다는 점이다. 뿐만 아니라 REST API에서는 두 번 요청해야만 하는 경우도 GraphQL을 쓰면 한번에 딱 조회가 가능한 경우도 많다.

그리고 딱 필요한 필드만 백엔드에서 내려주기 때문에 네트워크 비용이 절약되는 효과도 있다.

하지만 GraphQL을 쓰면 학습 비용이 들고, 프론트엔드에서 요구하는 대로 백엔드에서 데이터를 준비하려다 보면 성능에도 안 좋은 영향을 미칠 수 있기 때문에 스키마 설계를 잘 하는 것이 중요하다. 그리고 웹 브라우저에서는 보통 URL을 기준으로 캐싱을 수행하는데 GraphQL을 웹 클라이언트에서 사용하게 되면 같은 URL임에도 다른 쿼리를 보낼 수 있기 때문에 캐싱된 결과를 믿을 수 없다는 단점이 있다.

GraphQL에서 타입에는 크게 scalar typeobject type이 있는데 이 둘만 잘 구분해서 사용하면 스키마를 정의할 때 크게 어려울 건 없다. 그리고 당신이 만들 graph에서 지원하는 데이터 조회용 쿼리들은 Query라고 하는 special type(이면서 root type), 지원하는 데이터 변경용 쿼리들은 Mutatioin라는 special type(이면서 root type) 선언하면 된다는 사실만 기억하면 된다.

참고로 또 기억해둬야할 것은 Query의 경우 서버에서 여러 개를 받았을 때 동시에 실행되지만, Mutation의 경우 순차 실행된다는 점이다. 즉, 특정 필드 값을 +1 하는 operation을 2번 수행하면 정확히 +2가 되는 것이다. 이런 부분은 GraphQL에서 관리를 해준다. 정확한 내용은 다음 공식 문서 원문을 참조..!

A single mutation operation can include multiple top-level fields of the Mutation type. This usually means that the operation will execute multiple back-end writes (at least one for each field). To prevent race conditions, top-level Mutation fields are resolved serially in the order they're listed (all other fields can be resolved in parallel).

2. 타입의 종류

GraphQL이 지원하는 타입에는 다음과 같은 것들이 있다.

(1) Scalar types

보통 프로그래밍 언어로 치면 primitive type이라고 보면 된다.

1) Int: A signed 32-bit intger

2) Float: A signed double-precision floating-point value

3) String: A UTF-8 character sequence

4) Boolean: true or false

5) ID(serialized as a String): refetch나 cache key로 사용되는 고유 identifier로 String으로 표현되지만 human-readable하지는 않다

(2) Object types

우리가 GraphQL 스키마에서 정의하는 대부분의 타입이 이 타입이다. object type은 각각 저마다의 타입을 갖고있는 필드들로 구성된다.

참고로 여러분이 작성한 스키마의 모든 object type은 자동으로 __typename이라는 필드를 가지는데 이 필드는 해당 object type의 이름을 String으로 리턴한다. (ex. Book, Author...)

GraphQL 클라이언트는 이 __typename을 캐싱에 사용하거나, 다양한 타입을 리턴할 수 있는(union or interface) 필드의 타입을 판단하기 위해서 사용한다.

(3) Query type

이 타입은 그냥 Special object type이라고 보면된다. 클라이언트가 서버에 요청할 수 있는 쿼리의 모든 top-level entry point를 나타낸다.

(4) Mutation type

Query type과 똑같은데 단지 Query type이 read성 opeartion을 위한 것이라면 Mutation type은 write성 operation을 위한 Special object type이다.

(5) Input type

Input type은 필드에 대한 argument로 넣을 hierarchical data를 제공하기 위해 사용되는 Special object type이다. 예를 들어 다음과 같은 식으로 Input type을 정의해볼 수 있다.

input BlogPostContent {
  title: String
  body: String
  media: [MediaDetails!]
}

input MediaDetails {
  format: MediaFormat!
  url: String!
}

enum MediaFormat {
  IMAGE
  VIDEO
}

type Mutation {
  createBlogPost(content: BlogPostContent!): Post
  updateBlogPost(id: ID!, content: BlogPostContent!): Post
}

참고로 Query 타입과 Mutation 타입의 같은 필드에 대해서 같은 Input을 공유하는 것보다는 아예 별도로 분리하는 것이 더 낫다.

(6) Enum types

특정 값들 중에서만 값을 선택할 수 있는 타입이다.

enum AllowedColor {
  RED
  GREEN
  BLUE
}

Enum은 scalar 타입이 유효한 어느 곳에도 사용할 수 있다. 왜냐하면 항상 string으로 직렬화되기 때문이다.

(7) Unions and Interfaces

이 둘은 추상 타입으로 GraphQL에서 특정 필드가 다양한 object type 중에 하나를 리턴할 수 있도록 해준다.

(1) Uniton type

union Media = Book | Movie

type Query {
  allMedia: [Media] # This list can include both Book and Movie objects
}

union에 포함된 타입은 반드시 object type이어야 한다. (scalar type X, input type X ...)
union에 포함된 타입들이 반드시 특정 필드들을 공유해야할 필요는 없다.

그렇다면 Union type을 리턴하는 쿼리의 경우 어떻게 요청을 해야할까? 아래를 보면

query GetSearchResults {
  search(contains: "Shakespeare") {
    # Querying for __typename is almost always recommended,
    # but it's even more important when querying a field that
    # might return one of multiple types.
    __typename
    ... on Book {
      title
    }
    ... on Author {
      name
    }
  }
}

이렇게 각 타입에 따라 원하는 필드를 다르게 설정할 수 있다.(현재 여기에는 inline fragment가 사용되어 있다)

(2) Interface

말 그대로 인터페이스로 인터페이스를 구현한 타입은 반드시 인터페이스에 있는 모든 필드를 포함하고 있어야 한다.

interface Book {
  title: String!
  author: Author!
}

type Textbook implements Book {
  title: String!
  author: Author!
  courses: [Course!]!
}

type ColoringBook implements Book {
  title: String!
  author: Author!
  colors: [String!]!
}

type Query {
  books: [Book!]!
}

이 상황에서 books라는 쿼리를 날리면 TextBook이나 ColoringBook이 배열에 담겨서 올 것이다.

그리고 Union type과 마찬가지로 실제 object type을 확인해서 각 object type마다 다른 원하는 필드를 조회할 수도 있다.

query GetBooks {
  books {
    # Querying for __typename is almost always recommended,
    # but it's even more important when querying a field that
    # might return one of multiple types.
    __typename
    title
    ... on Textbook {
      courses { # Only present in Textbook
        name
      }
    }
    ... on ColoringBook {
      colors # Only present in ColoringBook
    }
  }
}

Union type이나 Interface type을 사용하려면 다음과 같이 실제 type을 resolve할 수 있도록 __resolveType이라는 함수를 정의해줘야 한다.

const resolvers = {
  Book: {
    __resolveType(book, context, info){
      // Only Textbook has a courses field
      if(book.courses){
        return 'Textbook';
      }
      // Only ColoringBook has a colors field
      if(book.colors){
        return 'ColoringBook';
      }
      return null; // GraphQLError is thrown
    },
  },
  Query: {
    books: () => { ... }
  },
};

3. Descriptions(docstrings)

스키마에는 다음과 같은 Markdown 형태의 주석을 달 수 있다.

"Description for the type"
type MyObjectType {
  """
  Description for field
  Supports **multi-line** description for your [API](http://example.com)!
  """
  myField: String!

  otherField(
    "Description for argument"
    arg: Int
  )
}

4. GraphQL을 사용할 때 주의점

(1) GraphQL 스키마는 백엔드에서 데이터들이 어떤 방식으로 저장되었는지와는 전혀 무관하게 프론트엔드에서 필요한 데이터가 무엇인지를 중심으로 설계되어야 한다. 예를 들어, 필드마다 실제로 서버에서 저장된 데이터 스토어는 다를 수 있지만 프론트엔드에서는 굳이 그걸 알 필요는 없다. 그것이 GraphQL의 목적이기 때문이다.

(2) Mutation을 사용할 때는 해당 operation으로 생성되거나 수정된 데이터를 응답으로 받게하는 식으로 스키마 설계를 하는 것이 좋다. 그래야 프론트엔드에서 굳이 follow-up 조회 콜을 안 보내도 바로 생성/갱신된 데이터를 볼 수 있기 때문이다. 그리고 이때 바로 해당 객체 타입을 바로 리턴 타입으로 설정하기보다는

type LikePostMutationResponse {
	code: String!
    success: Boolean!
    message: String!
    user: User
}

이런 식으로 성공/실패 여부와 상세 내용을 담은 메타 필드를 추가해서 정의하는 것이 좋다.

profile
성장의 기쁨

0개의 댓글

관련 채용 정보