(apollo-graphql) Apollo-graphQL & Usage

SuKyoung·2023년 3월 19일
5

TIL

목록 보기
2/3
post-thumbnail

Deview2023의 graphQL 발표영상을 보고 정리한 문서입니다. 네이버 MyPlace 팀에서 graphQL을 어떤식으로 사용하고 있는지 예시와 함께 설명해주고 graphQL의 다양한 기능을 소개하고 있습니다. GraphQL 잘 쓰고 계신가요? (Production-ready GraphQL)


GraphQL? Query Language - 필요한 데이터만 요청할 수 있다. - 단일 요청으로 많은 데이터 가져오기 - 가능한 케이스를 타입 시스템으로 표현하기

graphQL을 사용하면, 뷰에서 필요한 데이터만 요청할 수 있습니다. 만약에 client에서 너비가 200px인 이미지만 필요한 경우에 Field Argument에 width 200을 전달해주면 됩니다.

type User {
  name: String
  image(width: Int): String
}

query {
  user(id: "1234") {
    name
    image(width: 200) // <== 요렇게
  }
}

Case. 하지만 만약에 client 에서 필요한 이미지 사이즈를 모른다면? → Enum

// 지원하는 이미지 사이즈를 enum 타입으로 지정
enum ImageWidth {
  W_42
  W_60
  W_100
}
type User {
  name: String
  image(width: ImageWidth = W_42): String! // <== 기본값 42px로 지정
}
query {
  user(id: "1234") {
    name
    image(width: W_42)
  }
}

👇🏻 기존 스키마와 비교해보면…

as-is

type Profile {
  name: String
  thumbnail42: String
  thumbnail60: String
  thumbnail100: String
  thumbnail120: String
  backgroundImage42: String
  backgroundImage60: String
  backgroundImage100: String
  backgroundImage150: String
  backgroundImage200: String
}

to-be

type Profile {
  name: String
  thumbnailImage(width: ImageWidth = W_42): String!
  backgroundImage(width: ImageWidth = W_100): String!
}

→ ✅ enum을 사용하여 필요한 데이터만 명확하게 표현함으로써 실수를 줄일 수 있습니다.

Case. 암시적인 API Parameter → Enum

hasMedia

true : review with media

false: turn off filter(x), reviews without media (O)

empty: turn off filter (hidden value)

→ 좀 더 명시적으로 드러낼 순 없을까?

MediaFilter enum 도입

as-is

hasMedia
- true : review with media
- false: turn off filter(x), 
         reviews without media (O)
- empty: turn off filter(hidden value)

to-be

enum MediaFilter {
  WITH_MEDIA
  WIDHOUT_MEDIA
  ALL // <- empty인 경우
}

👇🏻 enum 적용 후

type Query {
  placeReviews(placeId: ID!, mediaFilter: MediaFilter = ALL): [Review!]!
}
// 사진이 있는 리뷰
query {
  placeReview(placeId: "123123", mediaFilter: WITH_MEDIA) {
    id
    mediaItems {
      id
      type
      url
    }
  }
}
// 사진이 없는 리뷰
query {
  placeReview(placeId: "123123", mediaFilter: WITHOUT_MEDIA) {
    id
    mediaItems {
      id
      type
      url
    }
  }
}
// 필터링이 없는 모든 리뷰
query {
  placeReview(placeId: "123123") {
    id
    mediaItems {
      id
      type
      url
    }
  }
}

→ ✅ empty가 의미를 가지는 경우, Enum을 사용함으로써 API 사용자 관점으로 스키마를 표현할 수 있게 됩니다.

Case. Error Handling → Union 또는 interface

닉네임 중복x

금칙어 사용x

1일 5회 제한x <== 만일 새로운 조건이 추가된다면

type ValidateNicknameResult {
  success: Boolean!
  isDuplicate: Boolean!
  words: [String!]!
  isCountOver: Boolean! // <== 타입이 추가가 될 것이고,
  todayCount: Int! // <==
}
{
  "data": {
    "success": true,
    "isDuplicate": true,
    "words": ["네이버"],
    "isCountOver": true, // <== 불필요한 데이터를 받아옵니다.
    "todayCount": 5 // <==
  }
}

→ 🤔 문제점: 분명 성공은 true인데, 필요하지 않은 다른 데이터를 받아오게 됩니다.

  1. Union 타입을 사용하여 해결
union Result = Succeed | Error
// 닉네임중복X
type DuplicatedNicknameError {
  message: String!
}
// 금칙어사용X
type PwordError {
  words: [String!]!
  message: String
}
union CheckNicknameOutput = NicknameSucceed | DuplicatedNicknameError | PwordError

→ ✅ (새로운 스펙과 에러가 추가되지 않는다면) 간단하게 해결할 수 있는 방법

→ 🤔 문제점: 만약 새로운 스펙과 에러가 추가된다면? 기존에 클라이언트에서 사용하던 유니언 타입으로는 알 수 없습니다. (유니언 타입은 확장에 닫혀있음)

  1. interface를 사용하여 해결
interface BaseError {
  message: String!
}
type DuplicatedNickname implements BaseError {
  message: String!
}
type PwordError implements BaseError {
  words: [String!]!
  message: String!
}
type CountOverError implements BaseError { // 새로운 스펙이 추가되어도 확장이 쉬워짐!
  count: Int!
  message: String!
}
query {
  checkNickname (nickname: "어쩌구") {
    __typename,
    ... on NicknameSuccess {
      nickname
    }
    ... on DuplicatedNickname {
      isDuplicated
    }
    ... on PwordError {
      words
    }
    ... on BaseError {
      message
    }
  }
}
{
  "data" : {
    "__typename": "CountOverError",
    "message": "일일 변경을 초과했습니다."
  }
}

→ ✅ Interface를 사용하면, 1일5회 제한 타입(CountOverError)을 query에 명시하지 않더라도 유연하게 에러 핸들링을 할 수 있습니다.

Custom Scalar

const GraphQLYearMonth = new GraphQLScalarType({
  name: "YearMonth",
  description: "년월을 `YYYY-MM` 포맷의 스트링으로 받고 유효한 날짜인지 검증합니다.",
  serialize,
  parseValue,
})
type Query {
  missions(yearMonth: YearMonth!): [Mission!]!
}

구현: Server에서는 Dayjs, Client에서는 String으로 전달

const GraphQLYearMonth = new GraphQLScalarType({
  name: "YearMonth",
  description: "년월을 `YYYY-MM` 포맷의 스트링으로 받고 유효한 날짜인지 검증합니다.",
  serialize: (value: Dayjs): string => {
    return value.toISOString()
  },
  parseValue: (value: unknwon): Dayjs => {
    if (typeod value !== "string") throw new GraphQLError("Provided YearMonth is not a string")

    if (!YEAR_MONTH_REGEX.test(value)) throw new GraphQLError('Provided YearMonth is not 'YYYY-MM' format string

    const parsedValue = dayjs(value)
    if (!parsedValue.isValid()) throw new GraphQLError("provided YearMonth is invalid date format")
    return parsedValue;
  }
})

→ ✅ 유효하지 않은 값을 넣으면 parseValue 과정에서 에러 리턴! Scalar ensures type-safety by validation!

→ graphql-scalar라는 유용한 라이브러리가 있음


Case. 클라이언트에서 복잡한 조건을 다룰 때 → Field Resolver

클라이언트의 관심사? 리뷰노출조건 → 서버에서 리뷰노출조건만 내려준다면 좋을 것! → Field Resolver를 사용하여 필요한 조건만 내려줍니다.

Rest에서도 가능은 하지만 문제는…

  • 필요에 상관없이 항상 계산 (무거운 연산이라면?)
  • 필요 시 받을 수 있게 하려면 파라미터 또는 앤드포인트를 추가해야함

👇🏻 graphQL에서 field resolver를 사용하면, field를 요청할때만 계산합니다.

// field를 요청하지 않을 때, isShowable() 실행안함
query Review($id: ID!) {
  review(id: $id) {
    id
    text
  }
}
// field를 요청할 때, isShowable() 실행
query Review($id: ID!) {
  review(id: $id) {
    id
    text
    isShowable
  }
}

→ ✅ graphQL은 RestAPI에 비해서 훨씬 퍼포먼스에 유리함

→ ✅ field resolver를 사용하면 웹, ios, Android에서 리뷰노출조건을 일일히 기억할 필요없이 서버쪽에서 내려준 값만 사용하면 되므로 간편해집니다.


Case. 팔로잉 후 멀리 떨어진 UI 업데이트 문제 → Database Normalization

Follow Mutation → Response → ? → UI Update

Client-Side Normalization 준비물

  • 서버로부터 받는 모든 데이터의 Schema를 미리 알고 있어야 한다.
  • 타입이름: 아이디! unique cache id로 "__typename: id"를 이용

Follow Mutation → Response → ? → UI Update <== 이 상황에서 필요했던 것?

  • Global state manager (X)
  • Refetch (X)
  • Normalization (O) → ✅ 데이터의 중복을 관계와 참조로서 제거함. 무결성 & 일관성을 지킬 수 있음.

Case. 쿼리를 보내는 곳과 쓰는곳 사이의 거리가 멀어진다면 → fragment

데이터를 호출하는 곳과 실제로 사용하는 곳에서 거리가 길어지는 경우, 호출하는 곳에서 실제로 사용하지 않는 것이 어떤 것인지 추적이 어려워짐.

→ Component가 자신이 필요한 데이터 의존성을 선언만 하고, Bottom-up으로 최상위 Query에 전달된다면 best 일것

type User {
  id
  nickname
  thumbnailUrl // => Thumbnail user.thumbnailUrl이 필요해!
  stat {
    followingCount
    followerCount
  }
}
fragment Thumbnail_User on User {
  thumbnailUrl
}

→ ✅ Fragment를 사용하면 변경이 쉬워진다! (fragment: reusable units for data requirement)

profile
👩‍💻 Frontend Engineer | 📝 Hobby Blogger

0개의 댓글