본문 바로가기
Back-end/Python

[GraphQL] 무작정 시작하기 (3) - Object Field를 이용한 Pagination

by 허도치 2020. 4. 20.

2020/04/13 - [Back-end/Python] - [GraphQL] 무작정 시작하기 (1) - Schema & Query

2020/04/14 - [Back-end/Python] - [GraphQL] 무작정 시작하기 (2) - Mutation

 

 

 

0. 서론

 일반적으로 데이터 목록을 조회할 때, 목록 전체를 조회하는 것이 아니라 페이지 단위로 조회한다. GraphQL에서는 Cursor기반의 Connection으로 강력한 Pagination 기능을 제공하는데, 필자처럼 GraphQL을 시작한지 얼마안된 초보자라면 거부감이 들것이다. 그래서, ConnectionField대신 기본적인 ObjectField를 이용하여 Pagination을 구현해보았다.

 그래서 이번 포스트에서는 ObjectField를 이용한 Pagination을 다루어볼 계획이다. 추가로, Pagination을 처리하기 위해서는 Query와 함께 Variables를 받아서 처리해야하므로, 변수를 입력받고 처리하는 방법에 대해서도 함께 알아보도록 하겠다.


 

 

 

1. 프로젝트 준비
1-1. 프로젝트 구조

1) [ app.py ]
   : Flask Web Application 실행 파일.

2) [ api/__init__.py ]
   : Flask Web Application 설정 파일.

3) [ api/database.py ]
   : MongoDB 연결 및 기초 데이터 셋팅 파일.

4) [ api/models.py ]
   : MongoDB의 Document 구조를 정의하는 파일.

5) [ api/query.py ]
   : GraphQL에서 데이터를 조회하기위한 Field들을 정의하는 파일.

6) [ api/schema.py ]
   : GraphQL의 구조(Schema)를 정의하는 파일.

7) [ api/types.py ]
   : GraphQL에서 Database에 접근하는 객체를 정의하는 파일.

 

1-2. 디버깅 도구 설치

1) Altair GraphQL Client
  : https://altair.sirmuel.design/
  : GraphQL Server 디버깅 도구인데, graphene에서 기본적으로 제공하는 것보다 UI가 깔끔하고 여러개의 Widnow를 사용할 수 있어서 편함.
  : 직접 설치하는 것보단 Chrome이나 Safari의 확장 프로그램으로 사용하는 것을 추천.

 

Altair GraphQL Client

A beautiful feature-rich GraphQL Client IDE for all platforms. Enables you interact with any GraphQL server you are authorized to access from any platform you are on. Much like Postman for GraphQL, you can easily test and optimize your GraphQL implementati

altair.sirmuel.design

 

 

 

2. 기본 소스 작성

- 이번 포스트에서 중요하게 봐야할 부분은 'query.py' 파일이다. 나머지 파일들은 첫번째 포스트에서 작성한 내용을 그대로 사용하므로 자세한 설명은 해당 포스트를 참고하길 바란다.

2-1. api/database.py
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
# api/datdabase.py
import datetime
 
from mongoengine import connect
from api.models import RankModel
 
MONGO_DTATBASE="graphql-example"
MONGO_HOST="mongomock://localhost"
 
# Database 연결
conn = connect(MONGO_DTATBASE, host=MONGO_HOST, alias="default")
print( conn.server_info() )
 
# 기초 데이터 Insert 함수
def init_db():
  # 1000개의 데이터 생성
  for idx in range(1000):
    RankModel(
      name="heo"
      mode="4x4"
      score=idx, 
      is_mobile=False, 
      reg_dttm=datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    ).save()
 
cs

 

2-2. api/models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# api/models.py
from mongoengine import Document
from mongoengine.fields import (
  StringField, IntField, BooleanField
)
 
# MongoDB Document 객체 정의
class RankModel(Document):
  meta = {
    'collection''rank_list'
  }
  mode = StringField(description='2048 grame ranking.')
  name = StringField()
  score = IntField()
  is_mobile = BooleanField()
  reg_dttm = StringField()
  upd_dttm = StringField()
  
cs

 

2-3. api/types.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# api/types.py
import datetime
from graphene_mongo import MongoengineObjectType
from .models import RankModel
 
# MongoDB에서 데이터를 조회하는 GraphQL 객체 정의
class RankType(MongoengineObjectType):
  class Meta:
    model = RankModel
  
  # reg_dttm을 출력할 때, 처리하는 로직
  def resolve_reg_dttm(parent, info, **input):
    return datetime.datetime.strptime(parent.reg_dttm, "%Y%m%d%H%M%S").strftime("%Y-%m-%d %H:%M:%S")
 
  # upd_dttm을 출력할 때, 처리하는 로직
  def resolve_upd_dttm(parent, info, **input):
    if parent.upd_dttm is not None:
      return datetime.datetime.strptime(parent.upd_dttm, "%Y%m%d%H%M%S").strftime("%Y-%m-%d %H:%M:%S")
    else:
      return parent.upd_dttm
 
cs

 

2-4. api/schema.py
1
2
3
4
5
6
7
8
9
10
11
12
13
# api/schema.py
import graphene
 
from .types import RankType
from .query import Query
 
# Schema 생성
schema = graphene.Schema(
  query=Query,
  types=[
    RankType
  ]
)
cs

 

2-5. api/__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# api/__init__.py
from flask import Flask
from flask_graphql import GraphQLView
from api.schema import schema
 
def create_app():
  # Flask Application 생성
  app = Flask(__name__)
  
  # /graphql EndPoint 설정
  app.add_url_rule(
    '/graphql',
    view_func=GraphQLView.as_view(
      'graphql',
      schema=schema,
      graphiql=True   # GraphQL UI 제공
    )
  )
  
  return app
  
cs

 

2-6. app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app.py
import dotenv
from api import create_app
from api.database import init_db
 
if __name__ == '__main__':
  # 환경변수 설정
  dotenv.load_dotenv(dotenv_path=".env")
  
  # MongoDB 접속 및 기초 데이터 입력
  init_db()
  
  # Flask App 실행
  app = create_app()
  app.run(host="localhost", port=3000)
  
cs

 

2-7. .env
1
2
3
# .env
FLASK_ENV=devlopment
FLASK_DEBUG=true
cs

 

 

 

3. Query 소스 작성
3-1. api/query.py
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
31
32
33
34
35
36
37
38
39
40
41
42
43
# api/query.py
import graphene
 
from .models import RankModel
from .types import RankType
 
# 조회 조건의 인자값을 Json 형태로 받기위한 객체 정의
class InputPagination(graphene.InputObjectType):
  page = graphene.Int(default_value=1)
  count_for_rows = graphene.Int(default_value=10)
  
 
# Query Field 객체 정의
class Query(graphene.ObjectType):
  # 전체 랭킹 목록 필드 설정, 반환 결과( List )
  rank_list = graphene.List(
    RankType,
    mode = graphene.String(required=True, default_value=None),
    name = graphene.String(),
    order=graphene.List(graphene.String),
    pagination=InputPagination()
  )
 
  # 전체 랭킹 목록
  def resolve_rank_list(parent, info, mode=None, name=None, pagination=None, **input):
    # 선택 조건 처리
    cond = dict()
    if name is not None:
      code["name"= name
    
    # 정렬 처리
    order = input.get("order"if "order" in input else list()
    
    # 페이징 처리
    if pagination is not None:      
      page = pagination.page if pagination.page > 0 else 1
      count_for_rows = pagination.count_for_rows if pagination.count_for_rows > 0 else 10
      skip = (page-1* count_for_rows
      
      return RankModel.objects(mode=mode, **cond).order_by(*order).skip(skip).limit(count_for_rows)
    
    return RankModel.objects(mode=mode, **cond).order_by(*order)
    
cs

1) [ 8~10 ln ] - 조회 조건의 인자값을 Json형태로 받기위한 객체 정의.
   : 현재 페이지를 의미하는 'page' 정수형 변수.
   : 페이지당 보여줄 목록의 수를 의미하는 'count_for_rows' 정수형 변수.

2) [ 14~49 ln ] - 데이터 조회를 위한 Query Field 정의.
   : [ 16~22 ln ] - 반환 결과가 List인 'rank_list' 필드를 정의.
     : [ 17 ln ] - 리스트 안의 데이터 타입 설정.
     : [ 18 ln ] - 필수 조건 Field 설정.
     : [ 19~20 ln ] - 선택 조건 Field 설정.
     : [ 21 ln ] - Json 형태로 입력받을 조건 Field 설정.

   : [ 25~42 ln ] - 'rank_list'에 대한 Resolver 정의.
     : [ 27~29 ln ] - 선택 조건은 값이 'None'인 경우, 조회 조건에서 제외.
     : [ 32 ln ] - 선택 조건은 Keyword Arguments에서 가져와서 사용 가능.
     : [ 36~39 ln ] - 페이징 처리를 위한 연산.
     : [ 40 ln ] - MongoDB에서 skip과 limit를 이용하여 페이징처리된 조회된 결과를 반환.
     : [ 42 ln ] - 페이징처리가 되지않은 결과를 반환.

* Mongoengine에서 'sort_by'함수는 인자값으로 컬럼명을 나열하며 기본적으로 오름차순(Ascending, ASC)으로 정렬하고, 컬럼명 앞에 '-'를 붙일경우 내림차순(Descending, DESC)으로 정렬함.
  예) .order_by("-score")


 

 

 

4. Pagination Query 테스트

4-1. Query 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
query PaginationQuery(
 $mode: String!
  $name: String
  $order: [String]
  $pagination: InputPagination
){
  rankList(
    mode: $mode
    name: $name
    order: $order
   pagination: $pagination
  ) {
    mode
    name
    score
  }
}
cs

1) [ 1~6 ln ] - Query Operation 설정.
   : [ 1 ln ] - Operation Name 설정.

   : [ 2~5 ln ] - 변수 설정( $변수명:데이터타입[필수여부] )
     : [ 2 ln ] - 변수명 mode는 데이터타입이 문자열필수조건으로 설정.
     : [ 3 ln ] - 변수명 name은 데이터타입이 문자열선택조건으로 설정.
     : [ 4 ln ] - 변수명 order는 데이터타입이 문자열 배열선택조건으로 설정. 
     : [ 5 ln ] - 변수명 pagination은 데이터타입이 'InputPagination'인 선택조건으로 설정.

   : [ 7~12 ln ] - 'rank_list' 필드 조회 및 조건 적용.
     : GraphQL은 기본적으로 모든 필드에 대해서 Camel-Case 기법이 적용됨.

   : [ 13~15 ln ] - 출력 필드 설정.

 

4-2. Variables 작성
1
2
3
4
5
6
7
8
9
10
{
  "mode""4x4",
  "order": [
      "-score"
  ],
  "pagination": {    
      "page"1,
      "countForRows"3
  }
}
cs

1) [ 3~5 ln ] - 정렬 변수 설정.
   : 'order'는 반환타입이 문자열 배열( [String] )임.
   : [ -score ]는 score를 기준으로 내림차순 정렬을 의미.

2) [ 6~9 ln ] - 페이징 변수 설정.
   : pagination은 반환타입이 'api/query.py'에서 정의한 객체( InputPagination )임.
   : 사전에 정의한 변수 외에 다른 것을 사용할 경우 오류가 발생함.

 

 

 

마치며

- 처음에는 Relay를 이용한 Pagination에 비해서 성능이 어떨지는 모르겠지만, GraphQL을 처음 접했을 때는 ConnectionField보다 이렇게 구현하는 좀 더 접근하기가 쉬울 것 같다.
- 필자도 처음에 이렇게 직접 구현을 해보고나서 Connection을 사용해보았는데, Edge, Node, Connection, Relay 등 공부해야하는 범위가 넓어졌지만, 알고나면 Pagination에 있어서는 Connection를 이용하는 방법이 편하다. 다만, Pagination 이외에 정렬이나 좀 더 세부적인 데이터 조작을 원할 경우에는 다소 복잡해질 수 있다.
- 다음 포스트에서는 Relay와 Connection에 대해서 알아보고 이용한 Connection을 이용한 Pagination을 구현해보도록 하겠다.

댓글