본 포스트는 공식 레퍼런스를 참고해 GraphQL을 공부하며 직접 작성한 가이드 입니다. 본 포스트는 2021년 7월 최신 버전인 v16.2를 기준으로 작성되어 있습니다.
GrpahQL
에서는 Pagintaion
을 제공하고 있다. Facebook에서 만든 API Query 언어 답게 offset 기반의 Infinite Scroll
방식의 페이지네이션을 모티브로 제공하고 있다. 물론 구현에 따라 다르겠지만 서빙하는 API 응답의 형태가 그러하다. 공식 문서를 살펴보면 size, after
라는 개념을 사용해 after
이후 size
개수 만큼의 응답을 Edge
에 반환한다. 또한 pageInfo
에는 이전, 다음 페이지 여부와 요청을 쉽게 보내기 위한 Edge
의 처음과 마지막 curosr
를 담아준다.
먼저 페이지네이션을 위한 스키마부터 정의하자. Type Definition Factory
로 웹툰 커넥션 스키마를 정의하면 된다.
1 2 3 4 5 type Query { webToon(id: ID!): WebToon webtoons(first: Int!, after: String): WebToonConnection @connection (for: "WebToon") }
그 다음엔 리졸버에 쿼리를 등록하자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Slf4j @Component @RequiredArgsConstructor public class WebToonResolver implements GraphQLQueryResolver { private final WebToonBO webToonBO; public Connection<WebToon> webtoons (int first, @Nullable String after) { List<Edge<WebToon>> edges = webToonBO.getWebtoons(first, after) .stream() .map(webtoon -> new DefaultEdge <>(webtoon, CursorUtil.from(webtoon.getId()))) .collect(toList()); return new DefaultConnection <>(edges, new DefaultPageInfo ( CursorUtil.getFirstCursorFrom(edges), CursorUtil.getLastCursorFrom(edges), Objects.nonNull(after), edges.size() == first )); } }
코드를 보면 커넥션에 edge
와 pageInfo
를 넣어준다. 각 edge
는 node
와 cursor
를 담고있는데 공식 문서에 따르면 커서에 대한 인식과 비신뢰성 을 위해 base64
인코딩을 권장하고 있다. 따라서 공식 문서 스펙에 따라 CursorUtil
을 구현했다. WebToonBO
를 잠깐 살펴보면 다음과 같이 구현해 두었다. offset
이 null
인경우에는 처음부터 first
개의 검색을, non null
이라면 offset 이후 first
개의 검색을 수행한다. 저장소의 페이지네이션 구현은 이 포스트의 주제와 벗어나니 여기까지만 설명한다.
1 2 3 4 5 6 7 public List<WebToon> getWebtoons (int first, String after) { return Optional.ofNullable(after) .map(CursorUtil::decodeFrom) .map(uuid -> getWebtoonsAfter(first, uuid)) .orElseGet(() -> getWebtoons(first)); }
pageInfo
에는 현재 페이지에 대한 정보를 담는다. 처음과 마지막 커서 정보, 이전 페이지 존재 여부, 다음 페이지 존재 여부에 대해 담아주면 된다. 요청에 대한 인-메모리 Mock 데이터는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 webtoons = Arrays.asList( WebToon.builder().id(UUID.randomUUID()).title("여신강림" ).webToonType(WebToonType.Originals).build(), WebToon.builder().id(UUID.randomUUID()).title("스퍼맨" ).webToonType(WebToonType.Originals).build(), WebToon.builder().id(UUID.randomUUID()).title("제로게임" ).webToonType(WebToonType.Originals).build(), WebToon.builder().id(UUID.randomUUID()).title("열렙전사" ).webToonType(WebToonType.Originals).build(), WebToon.builder().id(UUID.randomUUID()).title("신도림" ).webToonType(WebToonType.Originals).build(), WebToon.builder().id(UUID.randomUUID()).title("헬퍼" ).webToonType(WebToonType.Originals).build(), WebToon.builder().id(UUID.randomUUID()).title("귀곡의 문" ).webToonType(WebToonType.Originals).build(), WebToon.builder().id(UUID.randomUUID()).title("은주의 방" ).webToonType(WebToonType.Originals).build(), WebToon.builder().id(UUID.randomUUID()).title("엔딩 후 서브남을 주웠다" ).webToonType(WebToonType.Originals).build() );
이제 쿼리를 돌려보자. 간단하게 2개의 웹툰 정보만 페이지네이션 해서 가져오자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 웹툰 페이지네이션 query GET_WEBTOONS_PAGINATION { webtoons(first: 2) { edges { cursor node { id title webToonType } } pageInfo { hasPreviousPage hasNextPage startCursor endCursor } } }
응답에서 pageInfo
정보와 edges
에 요청한 웹툰 정보들이 나열되어 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 { "data" : { "webtoons" : { "edges" : [ { "cursor" : "ZjE0OGNjODAtNDA5Zi00ZjU1LWJmNjctMTYwMmM5MDg5Mzkz" , "node" : { "id" : "f148cc80-409f-4f55-bf67-1602c9089393" , "title" : "여신강림" , "webToonType" : "Originals" } } , { "cursor" : "MjQ5NWFkOWEtZGQxYy00YjY1LWJhNzktZjg4ZWFjNTg1OGEw" , "node" : { "id" : "2495ad9a-dd1c-4b65-ba79-f88eac5858a0" , "title" : "스퍼맨" , "webToonType" : "Originals" } } ] , "pageInfo" : { "hasPreviousPage" : false , "hasNextPage" : true , "startCursor" : "ZjE0OGNjODAtNDA5Zi00ZjU1LWJmNjctMTYwMmM5MDg5Mzkz" , "endCursor" : "MjQ5NWFkOWEtZGQxYy00YjY1LWJhNzktZjg4ZWFjNTg1OGEw" } } } }
이제 pageInfo
의 endCursor
를 사용해서 스퍼맨 이후의 데이터를 요청해보자.
1 2 3 4 5 6 # 웹툰 페이지네이션 query GET_WEBTOONS_PAGINATION { webtoons(first: 2, after: "MjQ5NWFkOWEtZGQxYy00YjY1LWJhNzktZjg4ZWFjNTg1OGEw") { ... } }
응답에서는 Mock 데이터 내의 스퍼맨 다음에 있는 제로게임과 열랩전사 웹툰 정보가 내려오는걸 볼 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 { "data" : { "webtoons" : { "edges" : [ { "cursor" : "NDc4YTRjN2EtMzcxYS00ZWMxLWI0MzktYTk1MDA3NmEwMzBj" , "node" : { "id" : "478a4c7a-371a-4ec1-b439-a950076a030c" , "title" : "제로게임" , "webToonType" : "Originals" } } , { "cursor" : "NmNmNWM5MTctZTc4My00ZTE3LWJkOGItY2MyNDFlNWNiODRm" , "node" : { "id" : "6cf5c917-e783-4e17-bd8b-cc241e5cb84f" , "title" : "열렙전사" , "webToonType" : "Originals" } } ] , "pageInfo" : { "hasPreviousPage" : true , "hasNextPage" : true , "startCursor" : "NDc4YTRjN2EtMzcxYS00ZWMxLWI0MzktYTk1MDA3NmEwMzBj" , "endCursor" : "NmNmNWM5MTctZTc4My00ZTE3LWJkOGItY2MyNDFlNWNiODRm" } } } }
Repository
모든 가이드의 예제 코드는 SongHayoung/springboot-graphql-tutorial 에서 확인할 수 있습니다.