- GraphQL
Relay Connection에 totalCount는 포함되어야 할까?
필터링을 고려하지 않는 하위항목 수는 그냥 `post { repliesCount }` 처럼 구현하는 것이 더 편리합니다. 고급 필터링이 필요하면 그 때 커넥션 totalCount를 구현하면 됩니다.
커넥션 내 totalCount
현재 커넥션이 페이지네이션하고 있는 전체 항목의 갯수는 유용한 정보입니다.
fragment FeedDetailReplies_feed on Feed {
id
allReplies: replies { totalCount }
replies(first: $count, after: $cursor, tag: $tag) @connection(key: "FeedDetailReplies_replies") {
totalCount # 현재 태그 조건에 매치한 댓글의 갯수
edges{
cursor
node {
...Reply_reply
}
}
}
}
Spec 관점에서 보기
Relay Connection Spec 에는 totalCount 자체가 포함되기 어렵게 되어 있습니다.
커넥션 엣지가 0개일 때 커넥션 자체가 null로 전달되지 않으면 스펙을 지킬 수 없게 되어 있는데, 그 경우 항목이 0개인 커넥션은 메타데이터를 반환하지 못하게 됩니다. 클라이언트는 엣지 0개인 커넥션의 메타데이터를 적절하게 추론할 수도 있지만 서버가 명시적으로 zero-value를 제공하는 것이 더 편리합니다.
한편 이 totalCount 필드는 GraphQL 문서에 소개되어 있기도 하고 다른 구현에서도 많이 등장하는 필드입니다.
Facebook, GitHub 등 많은 커넥션 구현체들이 totalCount
필드를 구현해서 쓰고 있습니다.
GraphQL: A query language for APIs.
Frontend 관점에서는 alias가 (거의) 강제된다
커넥션 메타데이터는 아래와 같은 방식으로 사용될 수 있습니다. 아래는 피드 하위의 댓글들의 수를 포함하는 프래그먼트 정의입니다.
export default createFragmentContainer(FeedHeader, {
feed: graphql`
fragment FeedHeader_feed on Feed {
id
author { username }
replies { totalCount }
}
`,
})
그런데 보통은 이런 프래그먼트는 페이지네이션이 적용된 다른 프래그먼트와 함께 쿼리되곤 합니다.
export default createPaginationContainer(FeedDetailReplies, {
feed: graphql`
fragment FeedDetailReplies_feed on Feed
@argumentDefinitions(
count: {type: "Int", defaultValue: 20}
cursor: {type: "String"}
) {
id
replies(first: $count, after: $cursor) @connection(key: "FeedDetailReplies_replies") {
edges{
cursor
node {
...Reply_reply
}
}
}
}
`
}, {
// ...
})
위 두 fragment는 아래처럼 쿼리에 함께 쓰입니다.
query SomeQuery($id: ID!) {
node(id: $id) {
...FeedHeader_feed
...FeedDetailReplies_feed
}
}
하지만 이 경우 invalid 한 GraphQL 쿼리가 됩니다. Fragment를 펼쳐보면 문제가 잘 보입니다.
query SomeQuery($id: ID!) {
node(id: $id) {
# ...FeedHeader_feed
replies { totalCount }
# ...
# ...FeedDetailReplies_feed
replies(first: $count, after: $cursor) @connection(key: "FeedDetailReplies_replies") {
# ...
}
}
위 코드처럼 다른 인자를 가지고 있는 replies 필드 셀렉션의 이름이 중복되기 때문입니다.
전체를 쿼리하는 커넥션 필드를 allReplies
처럼 alias를 부여하면 해결됩니다.
export default createFragmentContainer(FeedHeader, {
feed: graphql`
fragment FeedHeader_feed on Feed {
id
author { username }
allReplies: replies { totalCount } # 변경
}
`,
})
Backend 관점에서는 리졸버가 살짝 복잡해진다
SomeQuery
를 서버에서 실행할 때는 어떨까요?
query SomeQuery($id: ID!) {
node(id: $id) {
# ...FeedHeader_feed
allReplies: replies { totalCount }
# ...
# ...FeedDetailReplies_feed
replies(first: $count, after: $cursor) @connection(key: "FeedDetailReplies_replies") {
# ...
}
}
커넥션에 메타데이터 필드를 포함시킬 때, 페이지네이션 된 데이터를 가져오는 시점은 edges
, pageInfo
를 호출하는 때여야 합니다.
위의 allReplies
에서 totalCount
필드를 호출할 때 백엔드가 전체 항목을 데이터베이스에서 쿼리하는 것은 낭비가 됩니다.
또한, 서비스에서는 커넥션이 가져올 수 있는 최대 엣지 count
를 제약할 때가 많습니다.
그런 제약이 걸려 있다고 하더라도 totalCount
처럼 메타데이터의 쿼리는 허용해야 하는데 이 때 리졸버의 구조가 복잡해지는 단점이 있었습니다.
메타데이터를 고려하지 않았을 때 count
가 너무 큰 경우 replies
필드 리졸버가 바로 거절해버렸을 것입니다.
하지만 메타데이터가 있으면 혹시 이 쿼리가 totalCount
를 가져오고자 하는 쿼리일 수도 있으니 edges
로 검사를 미루게 됩니다.
그래서?
구현하는 서비스의 클라이언트가 필터된 항목의 갯수를 가져와야 한다면 ReplyConnection.totalCount
를 구현합니다.
꼭 그럴 필요가 없다면 그냥 Post.repliesCount
로 구현합니다.
나중에 필요에 의해 ReplyConnection.totalCount
를 구현해서 Post.repliesCount
가 동시에 있다고 하더라도 문제되지 않습니다.