Deview2023의 graphQL 발표영상을 보고 정리한 문서입니다. 네이버 MyPlace 팀에서 graphQL을 어떤식으로 사용하고 있는지 예시와 함께 설명해주고 graphQL의 다양한 기능을 소개하고 있습니다. GraphQL 잘 쓰고 계신가요? (Production-ready GraphQL)
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인데, 필요하지 않은 다른 데이터를 받아오게 됩니다.
Union
타입을 사용하여 해결union Result = Succeed | Error
// 닉네임중복X
type DuplicatedNicknameError {
message: String!
}
// 금칙어사용X
type PwordError {
words: [String!]!
message: String
}
union CheckNicknameOutput = NicknameSucceed | DuplicatedNicknameError | PwordError
→ ✅ (새로운 스펙과 에러가 추가되지 않는다면) 간단하게 해결할 수 있는 방법
→ 🤔 문제점: 만약 새로운 스펙과 에러가 추가된다면? 기존에 클라이언트에서 사용하던 유니언 타입으로는 알 수 없습니다. (유니언 타입은 확장에 닫혀있음)
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 준비물
Follow Mutation → Response → ? → UI Update <== 이 상황에서 필요했던 것?
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)