open:graphql-pagination

GraphQL Pagination

서로 다른 페이지 매김 모델은 서로 다른 클라이언트 기능을 가능하게 합니다.1)

GraphQL의 일반적인 사용 사례는 개체 집합 간의 관계를 탐색하는 것입니다. 이러한 관계를 GraphQL에 노출할 수 있는 여러 가지 방법이 있어 클라이언트 개발자에게 다양한 기능 세트를 제공합니다.2)

객체 간의 연결을 노출하는 가장 간단한 방법은 복수형을 반환하는 필드를 사용하는 것입니다. 예를 들어, R2-D2의 친구 목록을 얻으려면 모든 친구를 요청할 수 있습니다.3)

{
  hero {
    name
    friends {
      name
    }
  }
}

{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

그러나 클라이언트가 원할 수 있는 추가 동작이 있음을 빠르게 인식합니다. 클라이언트는 가져올 친구 수를 지정할 수 있기를 원할 수 있습니다. 아마도 그들은 처음 두 개만 원할 것입니다. 그래서 우리는 다음과 같은 것을 노출하고 싶습니다:4)

{
  hero {
    name
    friends(first:2) {
      name
    }
  }
}

그러나 처음 두 개를 가져왔다면 목록도 페이지 매김을 하고 싶을 것입니다. 클라이언트가 처음 두 친구를 가져오면 다음 두 친구를 요청하기 위해 두 번째 요청을 보낼 수 있습니다. 어떻게 그 행동을 가능하게 할 수 있습니까?5)

페이지 매김을 할 수 있는 방법에는 여러 가지가 있습니다.6)

  • friends(first:2 offset:2)와 같은 작업을 수행하여 목록에서 다음 두 개를 요청할 수 있습니다.7)
  • 우리는 friends(first:2 after:$friendId)와 같은 것을 할 수 있습니다. 우리가 가져온 마지막 친구 다음에 다음 두 친구를 요청할 수 있습니다.8)
  • friends(first:2 after:$friendCursor)와 같은 작업을 수행할 수 있습니다. 여기서 마지막 항목에서 커서를 가져와 페이지 매김에 사용합니다.9)

일반적으로 커서 기반 페이지 매김이 설계된 것 중 가장 강력하다는 것을 발견했습니다. 특히 커서가 불투명한 경우 커서 기반 페이지 매김을 사용하여 오프셋 또는 ID 기반 페이지 매김(커서를 오프셋 또는 ID로 만들기)을 구현할 수 있으며 커서를 사용하면 향후 페이지 매김 모델이 변경될 경우 추가적인 유연성을 제공합니다. 커서가 불투명하고 형식에 의존해서는 안 된다는 점을 상기시키기 위해 base64 인코딩을 제안합니다.10)

그것은 우리를 문제로 이끕니다. 그렇지만; 객체에서 커서를 얻는 방법은 무엇입니까? 커서가 사용자 유형에 있는 것을 원하지 않습니다. 그것은 개체가 아니라 연결의 속성입니다. 그래서 우리는 간접 참조의 새로운 레이어를 도입하고 싶을 수도 있습니다. 친구 필드는 가장자리 목록을 제공해야 하며 가장자리에는 커서와 기본 노드가 모두 있어야 합니다.11)

{
  hero {
    name
    friends(first:2) {
      edges {
        node {
          name
        }
        cursor
      }
    }
  }
}

모서리의 개념은 객체 중 하나가 아닌 모서리에 특정한 정보가 있는 경우에도 유용합니다. 예를 들어, API에서 “우정 시간”을 노출하고 싶다면 엣지에 라이브를 두는 것이 자연스러운 위치입니다.12)

이제 커서를 사용하여 연결을 통해 페이지 매김을 할 수 있지만 연결 끝에 도달했을 때를 어떻게 알 수 있습니까? 우리는 빈 목록을 다시 얻을 때까지 계속 쿼리해야 하지만, 연결이 끝에 도달했을 때 알려주고 싶기 때문에 추가 요청이 필요하지 않습니다. 마찬가지로 연결 자체에 대한 추가 정보를 알고 싶다면 어떻게 해야 할까요? 예를 들어, R2-D2의 총 친구 수는 몇 명입니까?13)

이 두 가지 문제를 모두 해결하기 위해 친구 필드는 연결 개체를 반환할 수 있습니다. 그런 다음 연결 개체에는 가장자리에 대한 필드와 기타 정보(예: 총 개수 및 다음 페이지 존재 여부에 대한 정보)가 있습니다. 따라서 최종 쿼리는 다음과 같이 보일 수 있습니다.14)

{
  hero {
    name
    friends(first:2) {
      totalCount
      edges {
        node {
          name
        }
        cursor
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

PageInfo 개체에 endCursorstartCursor를 포함할 수도 있습니다. 이렇게 하면 가장자리에 포함된 추가 정보가 필요하지 않은 경우 pageInfo에서 페이지 매김에 필요한 커서를 얻었으므로 가장자리를 쿼리할 필요가 전혀 없습니다. 이는 연결에 대한 잠재적인 사용성 향상으로 이어집니다. 에지 목록만 노출하는 대신 간접 계층을 피하기 위해 노드의 전용 목록을 노출할 수도 있습니다.15)

분명히 이것은 복수형을 갖는 원래 디자인보다 더 복잡합니다! 그러나 이 디자인을 채택하여 클라이언트를 위한 여러 기능을 잠금 해제했습니다.16)

  • 목록을 페이지 매김하는 기능.17)
  • totalCount 또는 pageInfo와 같은 연결 자체에 대한 정보를 요청할 수 있는 기능.18)
  • cursor 또는 friendTime과 같은 에지 자체에 대한 정보를 요청할 수 있는 기능입니다.19)
  • 사용자가 불투명한 커서만 사용하기 때문에 백엔드가 페이지 매김을 수행하는 방식을 변경할 수 있는 기능20)

이를 실제로 확인하기 위해 예제 스키마에 이러한 모든 개념을 노출하는 friendsConnection이라는 추가 필드가 있습니다. 예제 쿼리에서 확인할 수 있습니다. 페이지 매김이 어떻게 영향을 받는지 보려면 friendsConnection에 대한 after 매개변수를 제거해 보십시오. 또한 edge 필드를 연결의 helper friends 필드로 교체해 보십시오. 그러면 클라이언트에게 적절한 경우 간접 참조의 추가 에지 레이어 없이 친구 목록으로 직접 이동할 수 있습니다.21)

{
  hero {
    name
    friendsConnection(first:2 after:"Y3Vyc29yMQ==") {
      totalCount
      edges {
        node {
          name
        }
        cursor
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friendsConnection": {
        "totalCount": 3,
        "edges": [
          {
            "node": {
              "name": "Han Solo"
            },
            "cursor": "Y3Vyc29yMg=="
          },
          {
            "node": {
              "name": "Leia Organa"
            },
            "cursor": "Y3Vyc29yMw=="
          }
        ],
        "pageInfo": {
          "endCursor": "Y3Vyc29yMw==",
          "hasNextPage": false
        }
      }
    }
  }
}

이 패턴의 일관된 구현을 보장하기 위해 Relay 프로젝트에는 커서 기반 연결 패턴을 사용하는 GraphQL API를 빌드하기 위해 따를 수 있는 공식 사양이 있습니다.22)


1)
Different pagination models enable different client capabilities
2)
A common use case in GraphQL is traversing the relationship between sets of objects. There are a number of different ways that these relationships can be exposed in GraphQL, giving a varying set of capabilities to the client developer.
3)
The simplest way to expose a connection between objects is with a field that returns a plural type. For example, if we wanted to get a list of R2-D2's friends, we could just ask for all of them:
4)
Quickly, though, we realize that there are additional behaviors a client might want. A client might want to be able to specify how many friends they want to fetch; maybe they only want the first two. So we'd want to expose something like:
5)
But if we just fetched the first two, we might want to paginate through the list as well; once the client fetches the first two friends, they might want to send a second request to ask for the next two friends. How can we enable that behavior?
6)
There are a number of ways we could do pagination:
7)
We could do something like friends(first:2 offset:2) to ask for the next two in the list.
8)
We could do something like friends(first:2 after:$friendId), to ask for the next two after the last friend we fetched.
9)
We could do something like friends(first:2 after:$friendCursor), where we get a cursor from the last item and use that to paginate.
10)
In general, we've found that cursor-based pagination is the most powerful of those designed. Especially if the cursors are opaque, either offset or ID-based pagination can be implemented using cursor-based pagination (by making the cursor the offset or the ID), and using cursors gives additional flexibility if the pagination model changes in the future. As a reminder that the cursors are opaque and that their format should not be relied upon, we suggest base64 encoding them.
11)
That leads us to a problem; though; how do we get the cursor from the object? We wouldn't want cursor to live on the User type; it's a property of the connection, not of the object. So we might want to introduce a new layer of indirection; our friends field should give us a list of edges, and an edge has both a cursor and the underlying node:
12)
The concept of an edge also proves useful if there is information that is specific to the edge, rather than to one of the objects. For example, if we wanted to expose “friendship time” in the API, having it live on the edge is a natural place to put it.
13)
Now we have the ability to paginate through the connection using cursors, but how do we know when we reach the end of the connection? We have to keep querying until we get an empty list back, but we'd really like for the connection to tell us when we've reached the end so we don't need that additional request. Similarly, what if we want to know additional information about the connection itself; for example, how many total friends does R2-D2 have?
14)
To solve both of these problems, our friends field can return a connection object. The connection object will then have a field for the edges, as well as other information (like total count and information about whether a next page exists). So our final query might look more like:
15)
Note that we also might include endCursor and startCursor in this PageInfo object. This way, if we don't need any of the additional information that the edge contains, we don't need to query for the edges at all, since we got the cursors needed for pagination from pageInfo. This leads to a potential usability improvement for connections; instead of just exposing the edges list, we could also expose a dedicated list of just the nodes, to avoid a layer of indirection.
16)
Clearly, this is more complex than our original design of just having a plural! But by adopting this design, we've unlocked a number of capabilities for the client:
17)
The ability to paginate through the list.
18)
The ability to ask for information about the connection itself, like totalCount or pageInfo.
19)
The ability to ask for information about the edge itself, like cursor or friendshipTime.
20)
The ability to change how our backend does pagination, since the user just uses opaque cursors.
21)
To see this in action, there's an additional field in the example schema, called friendsConnection, that exposes all of these concepts. You can check it out in the example query. Try removing the after parameter to friendsConnection to see how the pagination will be affected. Also, try replacing the edges field with the helper friends field on the connection, which lets you get directly to the list of friends without the additional edge layer of indirection, when that's appropriate for clients.
22)
To ensure a consistent implementation of this pattern, the Relay project has a formal specification you can follow for building GraphQL APIs which use a cursor based connection pattern.
  • open/graphql-pagination.txt
  • 마지막으로 수정됨: 2022/09/05 02:27
  • 저자 127.0.0.1