sbyeol3 / articles

Learn.. Run.. 🏃
34 stars 1 forks source link

[번역] GraphQL 페이지네이션 지침서 : Offset vs. Cursor vs. Relay 스타일 페이지네이션 #3

Open sbyeol3 opened 3 years ago

sbyeol3 commented 3 years ago

원문 GraphQL Pagination Primer: Offset vs. Cursor vs. Relay-Style Pagination

이 포스팅은 제가 저술한 Advanced GraphQL with Apollo & React의 Relay 스타일 페이지네이션에 대한 챕터에서 발췌했습니다.

여러분이 GraphQL API를 개발할 때 데이터 목록을 반환해야 하는 데 모든 결과를 한 번에 검색하기에는 너무 많은 데이터가 있는 경우가 있습니다. 또한 GraphQL 쿼리 내에서 중첩된 리스트 필드들에도 해당하는 얘기입니다. 그래서 페이지네이션이 필요한 것이죠.

우리가 매일 사용하는 어플리케이션에서 페이지네이션은 너무 흔한 기능이지만, 잘 구현하기에는 까다로운 기능이 될 수 있습니다. 한 술 더 떠서 GraphQL 쿼리가 어떤 페이지네이션 방법을 선택할지에 대한 여러 선택지들이 존재하고 있습니다.

이전에 페이지네이션을 구현한 적이 있다면 아마 offset 기반이나 cursor 기반 방식에 더 친숙하실 겁니다. GraphQL API를 사용한다면 우리에게는 Relay-style 페이지네이션이라는 하나의 옵션이 더 있습니다.

Relay-style 페이지네이션은 페이징 GraphQL 쿼리에서는 꽤 인기있는 선택지입니다. 하지만 GraphQL 스키마가 Relay 명세를 준수하는지 확인하기 위해 몇 가지 고려할 사항이 있습니다.

이 포스팅에서는 각 페이지네이션 방식에 따른 장점과 단점을 살펴볼 것입니다. 그 후에 여러분에게 더 적합한 방식을 선택하시면 됩니다.

오프셋 기반

역사적으로 오프셋 기반 페이지네이션은 데이터베이스로부터 결과값들을 페이징하는 인기있는 선택지였습니다. 이 방법으로 페이징을 하면, 클라이언트는 페이지 당 받고자 하는 항목의 값에 대한 정보(limit이라 불림)와 뛰어 넘을 항목들의 개수에 대한 정보(offset이라 불림)를 서버에 보냅니다. 서버는 이 기준을 사용하여 데이터베이스에 특정 데이터 셋을 쿼리해옵니다. (필요하다면 기본 limit, offset을 설정해둡니다.)

어떻게 동작하는지 시각적으로 보기 위해, 5개 항목으로 이루어진 데이터 셋이 있다고 가정해보세요. 그리고 각 페이지 당 2개씩, 내림차순으로 정렬되어 있는 항목들의 두 번째 페이지를 가져올 겁니다.

오프셋 기반 페이지네이션은 사용 가능한 전체 페이지 수를 알고 있을 때 유용합니다. 또한 양방향 페이지네이션을 잘 지원하죠. 양방향 페이지네이션은 여러분들이 페이지 앞으로, 뒤로 이동하거나 특정 페이지로 이동하는 것이 가능합니다. 블로그와 같은 곳에서 많이 보이는 방식이죠.

그러나 쿼리를 날리는 데이터베이스에 너무 많은 데이터가 있는 경우에 이 방식을 사용하게 되면 성능 저하 문제가 있습니다. 게다가 새로운 데이터가 빈번하게 추가되는 경우에는 실시간으로 페이지 window가 잘못 매칭되어 데이터가 중복되어 보이거나 유실될 수 있습니다.

이 위험을 설명하기 위해 데이터셋에서 첫 번째 페이지를 가져오는 상황을 가정해보겠습니다. 여러분이 해당 결과를 보는 동안 다음 페이지에 넘어가기 전에 6번째 데이터가 추가되었습니다. 갑자기 페이지 윈도우가 한 자리씩 밀리게 되어 4번째 데이터가 첫 번째 페이지의 마지막이자 두 번째 페이지의 처음에 보이게 됩니다.

커서 기반

커서 기반 페이지네이션은 데이터셋에서 결과물들을 가져오는 것을 진행하기 위한 커서를 사용합니다. 커서는 데이터셋에서 특정 결과를 가리키는 포인터입니다. 고유하고 순차적인 값들에 한해서 백엔드 어플리케이션에 적합한 어떤 값이든 될 수 있습니다. 클라이언트가 이후 페이지를 탐색할 때 서버는 커서 값으로 표시된 항목 뒤에 있는 결과값들을 반환해줍니다.

5개의 데이터셋에서 커서 기반 페이지네이션을 사용하면 아래 그림과 같습니다.

커서 특성 자체가 클라이언트에게 중요한 것이 아닙니다. 클라이언트는 이후 다음 값들에 대해 요청을 보낼 때 커서 값을 서버로 다시 보내기만 하면 서버가 어느 시점부터 다음 데이터를 검색해야 하는지 알 수 있습니다.

커서 기반 페이지네이션은 오프셋 기반 방식에서 발생했던 페이지 윈도우의 부정확성 문제를 해결하기 때문에 빠르게 갱신되는 데이터셋에 아주 적합한 방식입니다. 첫 번째 페이지를 가져오고 6번째 아이템이 추가되었어도 커서를 사용하면 다음 페이지를 시작하는 것에 혼란의 여지가 없습니다.

이 방식은 단점도 있습니다. 커서 기반의 접근은 특정 페이지 넘버로 점프하거나 전체 페이지 수를 계산할 수 없습니다. 그러나 빠르게 업데이트되고 컨텐츠를 브라우징하는 무한 스크롤을 구현해야 할 때는 페이지 넘버나 전체 페이지 수가 없는 것이 전혀 문제가 되지 않습니다.

Relay 스타일

Relay-style 페이지네이션은 GraphQL API에서 커서 기반 페이지네이션에 약간의 향이 첨가된 방식입니다. Relay 자체는 GraphQL API에서 데이터를 검색하고 캐싱하는 클라이언트로 사용할 수 있는 JavaScript 프레임워크입니다. 이 방식은 페이스북에 의해 만들어졌고 페이스북 수준으로 데이터가 많은 앱을 염두에 두고 설계되었습니다.

Relay는 Apollo Client와 같은 다른 옵션들보다는 좀 더 진입 장벽이 높습니다. 그래서 GraphQL을 시작할 때 Relay 자체가 개발자들에게 첫 번째 패키지는 아니죠. 그러나 Relay는 GraphQL API에서 어떻게 페이징된 데이터를 관리할 것인지에 대한 유용한 로드맵을 제공하는데, 이를 "커서 커넥션 규격(GraphQL Cursor Connections Specification)"이라고 부릅니다. 여러분은 이 링크에서 Relay의 전체 규격을 읽어보실 수 있습니다.

비록 여러분의 클라이언트 앱에서 Relay를 사용하려고 하지는 않아도, 여러분은 GraphQL API에서 페이지네이션을 구현할 때 여전히 이 규격을 따를 수 있습니다. 또한 Relay를 사용하는 다른 클라이언트가 나중에 여러분의 API에 요청을 보내려는 경우 이 페이지네이션 방식을 사용할 때 여러분의 API를 좀 더 미래지향적으로 만들어줄 수도 있습니다.

Relay-style 페이지네이션에서 기억해야 할 중요한 점은 단방향성입니다. 여러분이 앱에서 "이전 페이지" 버튼과 "다음 페이지" 버튼으로 컨텐츠 이동을 구현하고자 한다면 Relay 방식은 아마 여러분에게 적합한 방식이 아닐 겁니다. (구글링을 하면 양방향 페이징을 지원하기 위한 몇 가지 방법이 있기는 합니다.) 그러나 추가적인 페이지를 보여줄 때 무한 스크롤의 UI를 구현한다면, 이 방식은 아주 잘 맞을 겁니다.

이전에 언급했듯이, Relay는 페이징된 쿼리 데이터의 아웃풋 뿐만 아니라 인수를 통해 요청이 이루어지는 방식에 대해 매우 확고합니다. Relay 방식의 페이지네이션으로 구현된 가상 쿼리의 예를 봅시다. following 필드는 User 객체의 리스트를 반환합니다.

query {
  user(username: "bob") {
    fullName
    following(first: 20, after: "someProfileId") {
      edges {
        cursor
        node {
          fullName
          username
        }
      }
      pageInfo {
        hasPreviousPage
        hasNextPage
      }
    }
  }
}

여러분들은 이 쿼리의 몇 가지 재밌는 특징에 대해 알아차리셨을 겁니다. 먼저, 쿼리에 edge 타입을 리스트로 갖고 있는edges가 있습니다. 이 edge 타입은 nodecursor로 불리는 최소 2개의 필드로 이루어진 객체 타입입니다.

node 필드는 객체 자체이고 리스트를 제외한 GraphQL 어떤 타입이든 될 수 있습니다. (우리의 경우에는 User 타입이 되겠습니다.) cursor 필드는 각 edge를 식별하는 고유하고 순차적인 값에 해당하는 문자열입니다.

마지막으로 pageInfohasPreviousPagehasNextPage 필드를 무조건 가지는 객체입니다. 두 필드 모두 null이 될 수 없는 boolean 값입니다.

또한 쿼리의 first, after 인수에 대해 주목해야 하는데 이 값들은 정방향 페이지네이션을 위한 인수 값들입니다. 정방향이 아니라 역방향으로 페이지네이션을 하고 싶다면 last, before 인자를 사용할 겁니다.

참고! 역방향 페이지네이션은 내림차순 정렬과는 다릅니다. 데이터셋의 끝에서부터 시작해서 처음으로 데이터를 가져오는 방식을 의미합니다. 내림차순 정렬은 높은 값부터 낮은 값으로 페이지를 이동하는 것을 의미하는 것이고요. 데이터셋은 보통 먼저 정렬이 되고, 정렬된 값들의 페이지를 검색할 때 정방향 또는 역방향 페이지네이션을 적용하게 됩니다.

위 쿼리 예시에서 명확하지 않은 점은 상위 수준의 "connection type"이 객체 유형으로 구현되고(접두사로 Connection이 붙는) following 필드가 User 객체 타입으로 이루어진 리스트 대신 하나의 커넥션 객체로 반환된다는 것입니다.

쿼리 결과나 문법에 대한 이 개념적 모델이 처음에는 부담스러울 수도 있지만 어플리케이션에서 페이지네이션을 다루는 데 깔끔하고 표준화된 방식을 제공합니다.

요약

이 포스트에서는 오프셋, 커서, Relay 기반 페이지네이션의 3가지 방식에 대해 알아보았습니다.

오프셋 기반의 페이지네이션은 (보통은) 빠르게 구현되고 양방향 페이지네이션을 지원하는 데 유용합니다. 또한 전체 페이지 수를 계산할 때도 좋습니다. 다만 성능 이슈가 있고 빠르게 데이터가 업데이트되는 경우에는 잘 맞지 않습니다.

커서 기반 페이지네이션은 아주 빠르게 데이터가 업데이트되고 일반적으로 단방향을 지원할 때 오프셋 기반 페이지네이션을 사용했을 때의 문제에 대한 해결책을 제공할 수 있습니다. Relay 방식의 페이지네이션은 GraphQL API로 페이지네이션을 구현할 때 커서 기반 페이지네이션에 약간의 조미료를 첨가한 또 하나의 선택지입니다.

읽어주셔서 감사합니다! 이 포스트가 마음에 드셨다면 Advanced GraphQL with Apollo & React의 전체 챕터를 무료로 다운로드 받으실 수 있습니다.