본문 바로가기

개발 이야기/실무 Recipe

[GraphQL] Connection, Edge, Node

source: datanami

 

Graphql Schema를 짤 때 List를 가져오는 쿼리의 Payload에 Connection이라는 이름을 본 적이 있을 것이다. List를 List라 부르지 못하고 왜 Connection이라고 부르는 걸까? infinite scroll을 구현하려고 구글을 서핑하던 도중 Apollo 블로그에서 이와 관련된 글을 읽게 되었다. 왜 Connection이라는 이름을 쓰고, 어떤 경우에 사용하는지 살펴보자.

 

 

*Apollo Blog 글: www.apollographql.com/blog/explaining-graphql-connections-c48b7c3d6976/

*SitePoint 글: www.sitepoint.com/paginating-real-time-data-cursor-based-pagination/

 

 


 

 

"Page vs Cursor"

 

페이지네이션에 두 종류가 있다. 책처럼 한장 한장 넘기는 limit/offset 기반 (줄여서 page기반이라고 하자.), 그리고 스크롤을 내리면서 다음 데이터를 가져오는 cursor 기반 방식이다. 사용자 입장에서 설명하면 page는 포장된 박스를 하나하나 열어보는 것이고, cursor는 데이터가 끝없이 흐르는 강을 따라 걷는 것과 같다. 어떤 느낌이냐면 page는 뚝뚝 끊기는 것, cursor는 끊임없이 흐르는 것이다. 

 

page가 안좋다는 게 아니다. 둘 다 장단점이 있다. 우리가 자주가는 커뮤니티 게시판에서 1 페이지를 보고 있었다고 하자. 게시판에서는 최신글을 먼저 보여줄 것이다. 너무 재밌는 나머지 1 페이지를 모두 읽어버렸고, 다음 버튼을 눌렀다. 그런데 아래의 오른쪽 이미지처럼 그새 1페이지에 5개의 글이 추가되었다. 2페이지에는 아까 봤던 5개의 글이 밀려들어왔고 우리는 새로운 글을 5개 밖에 보지 못한다. 

 

source: https://www.sitepoint.com/paginating-real-time-data-cursor-based-pagination/

 

 

cursor는 현재 위치에서 스크롤만 내리면 새로운 콘텐츠를 볼 수 있다. 버튼을 누를 필요도 없고 전에 봤던 콘텐츠는 다시 보지 않아도 된다. 그럼 'page 기반보다는 cursor 기반이 더 좋은 게 아닌가' 생각할 수 있는데, 꼭 그렇진 않다. 페이스북을 하면서 겪은 적 있겠지만 cursor 기반에서는 이전에 봤던 콘텐츠를 보려면 다시 스크롤을 올려가며 찾아야 한다(그래서 좋아요, 북마크 같은 기능들이 있지만.). 반면 page는 콘텐츠를 봤던 페이지만 기억하고 있으면 해당 페이지로 가서 콘텐츠를 찾으면 된다. 

 

우리가 자주 사용하는 SNS인 트위터, 페이스북, 인스타그램 등은 모두 cursor 기반이다. 스크롤을 내리면 끊임 없이 콘텐츠가 나온다. cursor를 사용하는 이유는 단순히 page보다 더 좋아서라기 보다는, 실시간으로 데이터를 보여주기에 적합해서이다. page 방식에서는 위 그림처럼 실시간으로 데이터가 추가되면 내가 보고 있는 페이지가 계속해서 밀릴 것이다.

 

 

 

"Graph와 Connection, Edge, Node"

 

cursor 기반 페이지네이션도 여러 방식으로 구현할 수 있다. 트위터의 경우 max_id를 cursor로서 사용한다. max_id를 전달하면 그 이후의 콘텐츠를 반환하는 형식이다. 페이스북에서는 after를 페이지의 마지막으로 설정한다. 이 방식들은 우리가 일반적으로 생각할 수 있는 방식 같다. 페이지마다 보여줄 콘텐츠의 count(offset)와 그 콘텐츠의 가장 마지막 id를 갖고 있다가, 그 페이지의 끝에 다다르면 마지막 id를 주고 그 id 이후의 콘텐츠를 가져오는 것이다. 

 

 

음, 그렇군. 하지만 아직 의문은 풀리지 않았다. GraphQL은 페이스북이 개발한 쿼리 언어이다. 페이스북은 왜 cursor 기반 페이지네이션에 connection이라는 이름을 사용한 걸까?

 

 

 

source: https://www.apollographql.com/blog/explaining-graphql-connections-c48b7c3d6976/

 

 

 

일반적으로 우리는 콘텐츠를 하나의 개체, 그리고 콘텐츠를 묶어놓은 것은 콘텐츠의 나열 List라고 본다. 하지만 DB 스키마를 설계하다보면 서로 연관되는 모델들이 있다. 페이스북 같은 SNS를 이용해보면 다른 사람을 팔로잉함으로써 '친구', '팔로워'로 연결되고 포스트에 like를 누름으로써 사용자와 포스트가 좋아요한 포스트로 연결된다. 

 

이렇듯 페이스북은 데이터를 서로 상관 관계가 있는 Graph로 보았다. 각 데이터를 Node, 그리고 두 Node를 연결하는 관계를 Edge, Connection은 노드에 연결된 모든 Edge를 가리킨다. 이는 단순한 데이터의 나열이 아니라 데이터들의 관계를 생각하게 만든다.  Connection, Node, Edge는 각 데이터가 무엇으로, 어떻게, 얼마나 많이 연결되어있는지 보여주는 방식이며 이름이다. 

 

 

 

"Connection을 이용한 스키마 설계와 네이밍"

 

GraphQL은 그래프 자료구조의 데이터를 쿼리하는 언어는 아니다. 다만 connection으로 그래프처럼 연관되어 있는 데이터들을 표현하고  해당 노드에 연결된 모든 노드를 쿼리할 수 있다. 즉, 관계를 나타내고 연결된 모든 노드를 쿼리할 때 사용한다.

 

User와 Post 노드가 있다. 이 둘 사이에는 Like라는 관계가 있다.

이 관계를 표현하고, Like로 User에 연결된 모든 Post 노드를 가져오려면 어떻게 스키마를 짜야할까?

 

: Node를 만들고, 둘 사이를 잇는 Edge, 그리고 Edge로 구성된 Connection을 만든다.

 

type Post {
  id: ID!
  title: String
  content: String
}

type User {
  id: ID!
  name: String
  likePostsConnection(first: Int, after: String, last: Int, before: String): UserLikePostsConnection
}

 

먼저 User와 Post 노드를 만들고, User가 좋아했던 Post를 보여주기 위해 User에 likePostsConnection 필드를 추가한다.

인자에 커서 기반 페이지네이션을 위해 *first, after, last, before를 넣었다. 

 

*before/after: 맨 처음 커서와 마지막 커서

*first/last: first에 5을 전달하면 before를 기준으로 앞의 5개 콘텐츠를 가져온다. last는 after를 기준으로 마지막 5개 콘텐츠를 가져올 것이다.

 

 

type PageInfo {
  startCursor: String
  endCursor: String
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
}

type UserLikePostsConnection {
  edges: [LikePostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int
}

type LikePostEdge {
  cursor: String!
  node: Post!
}

 

payload 타입명은 UserLikePostsConnection으로 지었는데, 이는 Apollo 블로그에서 권장한 네이밍 규칙을 따른 것이다. 블로그에서는 ${OriginType}${RelationType}Connection으로 지을 것을 권장한다. Edge 또한 ${OriginType}${RelationType}Edge로 짓는다. 이 저자가 그렇게 권한 건 맞는데, Apollo Graphql에서 권장하는 건지는 모르겠다.

 

여담으로 나는 OriginType(User)과 RelationType을 빼고 TargetType(Post)을 넣는 게 더 편하다. Like 외에도 Hate나 Ignore라는 관계가 추가되면 PostConnection과 PostEdge 타입을 다시 만들 필요 없이 재활용할 수 있기 때문이다. 큰 서비스거나 기능 별로 제약 사항이 많은 경우 OriginType과 RelationType을 추가하는 게 좋겠지만 그렇지 않다면 TargetType을 사용해도 괜찮을 것 같다. 네이밍은 자유니까✨

 

아무튼, 다시 본론으로 들어가서.. Connection은 노드에 연결된 모든 Edge, 그리고 cursor와 이전/다음 페이지 등 페이지 정보, 그리고 총 노드 수에 대한 데이터를 갖고 있다. Edge는 실질적인 데이터를 나타내는 Node 그리고 커서 정보를 갖고 있다. Edge에는 커서 외에도 노드(LikePost)에 대한 메타데이터를 추가할 수도 있다. 예를 들어 좋아요를 누른 시간, 좋아요를 누른 다른 유저 등과 같은 데이터를 추가할 수 있다.

 

 


 

사용하는 기술에 의문이 남는다면 뿌리부터 찾아보는 것도 좋은 것 같다.

기억도 할겸 공유도 할겸 좋은 내용인 것 같아 포스팅 했다.