Django에서 GraphQL 사용해보기

     

 

들어가기 전에

 

사이드 프로젝트 서버에서 DRF를 이용하여 API를 작성하던 중 최근에 핫한 GraphQL을 사용해보기로 했습니다. 국내에서 몇몇 기업이 사용하고 있는 것 같은데, 생각보다 레퍼런스가 적어서 공홈에 있는 문서나 외국 블로그를 참고했습니다.

 

일단 Django에서 하다 보니 Django용 패키지가 있는지를 먼저 검색했는데, 다행히 graphene이라는 python용 GraphQL패키지가 있다!!

 

아직까지 REST에 비해서 많이 사용되지 않아 레퍼런스가 부족한데 나름 한국에서도 정리된 글이 많이 있긴 합니다. 

 

https://tech.kakao.com/2019/08/01/graphql-basic/

 

GraphQL 개념잡기

GraphQL은 페이스북에서 만든 쿼리 언어입니다. GrpahQL은 요즘 개발자들 사이에서 자주 입에 오르내리고 있으나, 2019년 7월 기준으로 얼리스테이지(early-stage)임은 분명합니다. 국내에서 GraphQL API를 O

tech.kakao.com

https://www.apollographql.com/blog/graphql-vs-rest-5d425123e34b/

 

GraphQL vs. REST

Often, GraphQL is presented as a revolutionary new way to think about APIs. Instead of working with rigid server-defined endpoints, you can send queries to get exactly the data you’re looking for in one request. And it’s true — GraphQL can be transfo

www.apollographql.com

 

apollographql이라는 사이트가 아무래도 정리가 잘되어있는 편입니다. 특히 REST와의 비교를 잘해놔서 왜 GraphQL을 사용하는지에 대한 의문을 잘 긁어줍니다. 

 

내가 사이드 프로젝트를 하면서 GraphQL을 고민했던 내용은 아래 링크로..

https://cholol.tistory.com/492

 

 

이런 저런 이유로 GraphQL로 한번 만들어보려고 합니다. 왜냐하면 그냥 REST로 만들면 기존이랑 너무 똑같아서 지겨우니까... ㅋㅋ

 

시작.

 

 

일단 설치하기

 

GraphQL로 바꾸기 위해 일단 서버에 GraphQL을 쓸 수 있는 패키지를 설치해야 합니다. 저는 지금 Django로 하고 있어서 Django와 호환되는 패키지인 graphene-django를 설치할 예정입니다.

 

공식 홈피는 여기로..

https://docs.graphene-python.org/projects/django/en/latest/

 

 

일단 이곳 저곳에 많은 예제들과 공홈에 튜토리얼이 있지만, 

 

저는 그냥 내가 하던 프로젝트에 설치해서 기존 DRF로 되어있던걸 적용해볼 예정입니다.

왜냐하면 튜토리얼은 뭔가 샘플 Django 프로젝트 다운로드하여서 해보더라고요. 뭔가 남이 만든 거로 하려니 찝찝함!

 

암튼 pip로 graphene-django를 설치합니다.

 

pip install graphene-django

 

저는 윈도우 콘다 환경에서 설치를 했습니다. 잘되더라고요?

 

아무튼 설치를 하면 이제 시작해야 되는데 뭐부터 해야 되는지 막막합니다.

 

일단 아무 API나 하나 만들어봅시다.

 

# Create your views here.
class Plan(STView):
    def post(self, request):
        """
        :param request:
        :return:
        """

        data = dict(
            duration=0,
        )

        return stret(st_response.SUCCESS, data=data)

일단 이거는 제가 원래 만들어놓은 API 중에 하나인데 아직 모델을 만들지 않아서 일단 가짜로 duration값을 내려주는 API입니다. 지금은 모델을 만들어서 Seed라는 모델 안에 duration이라는 필드를 리턴하는 API를 만들 참이었습니다. 이 Seed라는 모델은 아래와 같이 정의되어 있습니다.

 

# Create your models here.
class Seed(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    start_time = models.DateTimeField(null=False)
    end_time = models.DateTimeField(null=False)
    duration = models.IntegerField(null=False, default=0)
    leafs_id = models.IntegerField(null=False, default=-1)
    st_id = models.CharField(verbose_name='id', max_length=20, null=False, default=False)

    class Meta:
        db_table = 'seed'
        verbose_name = '사용자 공부 시간 데이터'


class Leaf(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    st_id = models.CharField(verbose_name='id', max_length=20, null=False, default=False)
    name = models.CharField(max_length=100, null=False, default='New Leaf')
    goal_time = models.IntegerField(default=30)
    study_time = models.IntegerField(default=0)
    achievement_rate = models.IntegerField(default=0)
    species = models.CharField(max_length=100, null=True)

    class Meta:
        db_table = 'leaf'
        verbose_name = '사용자 공부 계획 데이터'

 

컨셉이 약간 꽃이라서 Seed -> Leaf -> Flower 이런 식으로 모델이 있고 seed는 시간 leaf는 플랜 flower는 일단위 이런식으로 구성했습니다. 지금 하고 있는 건 Seed모델이니까 이 모델을 GraphQL로 조회할 수 있도록 만들어봅시다.

 

일단 DRF에서는 models.py에서 모델을 만들고 Serializer를 이용해서 View에서 request데이터와 response데이터를 만들었습니다. 근데 GraphQL에서는 좀 다른 방식이더라고요. 

 

생소한 schema.py파일을 만듭니다.

 

schema.py

이제 이 schema.py안에 GraphQL을 사용하기 위한 이것저것을 만들 건데요, 이것저것이 뭔지는 용어는 잘 모름 ㅋㅋㅋ 일단은 쿼리를 사용할 수 있게 무엇인가 만들어 줘야 하겠죠?

 

import graphene
from graphene_django.types import DjangoObjectType

from .models import Seed, Leaf, Flower, FlowerGarden
from graphql import GraphQLError


class SeedType(DjangoObjectType):
    class Meta:
        model = Seed


class Query(object):
    seed = graphene.Field(SeedType,
                          st_id=graphene.String()
                          )
    def resolve_seed(self, info, **kwargs):
        st_id = kwargs.get('st_id')

        return Seed.objects.filter(st_id=st_id).first()

 

st_id라는 값으로 Seed라는 모델에 저장된 데이터중에 첫 번째 데이터를 가져오는 Query를 만들었습니다. 만들 때 두 가지가 필요한데, 첫 번째로 DjangoObjectType으로 선언한 SeedType과 그 SeedType을 Query 할 수 있게 만드는 Query 클래스 안에 seed입니다. 클라이언트에서 GraphQL로 seed를 호출했을 때 서버는 resolve_seed를 통해서 데이터를 내려줄 수 있는데, 위에 코드에서는 seed를 호출할 경우 Seed라는 모델에서 st_id값을 가지는 값 중 가장 첫 번째를 찾아서 리턴합니다.

 

class Query(object):
    seed = graphene.Field(SeedType,
                          st_id=graphene.String()
                          )

 

Query 클래스 안에 여러 개의 Query를 만들 수 있습니다.

 

seed를 보면 graphene field로 seedType과 st_id라는 값이 있는데, Query를 만들 때 안에 인자 값으로 어떤 Type의 데이터를 다루는지와 어떤 값을 input으로 사용하는지 정의할 수 있습니다. 여기서는 다루는 데이터는 SeedType(위에서 선언했던)이고 input으로는 st_id라는 String() 값을 사용합니다. 이렇게 선언하면 클라이언트에서 GraphQL을 올릴 때 st_id값을 넣어서 서버에 전달할 수 있습니다. 뭐 대충 아래와 같은 형식으로?

 

Query {
	seed(st_id: "abcd1234"){
    	duration
    }
}

 

input값들은 Query에서 정의하는데 output은 아까 만들어놓은 SeedType으로 정의할 수 있습니다.

 

class SeedType(DjangoObjectType):
    class Meta:
        model = Seed

 

이렇게 Meta에 model = Seed로 정의하면 SeedType은 Seed모델에 정의해놓은 필드들을 자동으로 output이라고 인지해서 값을 요청할 수 있습니다. model에 없는 값들은 따로 추가할 수 도 있습니다.

 

 

이제 만들었으니 postman이나 insomnia 같은 툴로 요청을 해볼게요

 

GraphQL Query

 

최신 Postman이나 insomnia는 GraphQL Query를 지원하기 때문에 옵션에서 GraphQL을 선택하면 쉽게 쿼리를 만들 수 있습니다.

 

아 근데 요청하기 전에 호출하는 url을 만들어줘야 합니다.

 

 

END POINT 만들기

 

REST랑 가장 다른 점이 이 부분인데 REST의 경우 API마다 호출하는 URL이 다르고 call 방식(get, post 등)도 다릅니다. 근데 신기하게 GraphQL은 하나의 URL만 사용합니다. 그냥 하나에다가 쿼리만 바꾸면 원하는 데이터를 주는 방식.

 

따라서 Django root폴더에 url하나만 만들면 다른 곳에는 만들 필요가 없습니다. 대신 root폴더에도 schema.py파일 만들어줘야 합니다.

 

# project/schema.py : [project]는 project 이름!
import graphene

# studydata라는 app에 있는 schema.py 읽어옴
from studydata.schema import Query as PlanQuery 

class Query(PlanQuery, graphene.ObjectType):
    pass

schema = graphene.Schema(query=Query)

 

root폴더에는 app폴더에서 만든 schema랑은 좀 다르게 각 app에서 사용한 schema를 연결하는 느낌?입니다. 마지막으로 urls.py에 추가하면 끝.

 

from django.contrib import admin
from django.urls import path, include
from django.conf.urls import url
from . import views

from graphene_django.views import GraphQLView
from django.views.decorators.csrf import csrf_exempt

urlpatterns = [
    path('admin/', admin.site.urls),
    path('user/', include('user.urls')),

    path(r'graphql/', csrf_exempt(GraphQLView.as_view()))
]

 

graphene에서 제공하는 GraphQLView를 이용해서 ~/graphql/이라는 API End Point 하나 만들어줍니다. 이름이 굳이 이게 아니어도 되는데 레퍼런스 보니까 다들 이 이름 사용함.. 대세인 듯?

 

아무튼 이렇게 등록하면 ~/graphql/로 query를 날릴 수 있습니다.

 

 

 

 

Query 날려보기

 

서버 쪽만 개발하다 보면 모를 수 있는 부분이 하나 있는데, GraphQL을 사용할 때 클라이언트는 가장 먼저 GraphQL의 Schema를 한번 쭉 긁어갑니다. apollo라는 GraphQL 라이브러리를 보니까 Json으로 긁어가던데 이걸 왜 긁어가냐면 클라이언트가 호출할 수 있는 GraphQL 구조를 알기 위함입니다. 

 

Postman이나 Insomnia를 써보면 알 수 있는데 메뉴에 Refresh Schema라는 게 있습니다. (Postman은 어떤 메뉴로 돼있는지는 모름)

 

Refresh Schema로 전체 GraphQL schema를 불러올 수 있음

이렇게 한번 불러가면 클라이언트 쪽에서 서버에 만들어진 GraphQL 쿼리들이 뭐 뭐 있는지 알 수 있습니다. (자동 완성도 잘됨)

 

요렇게 자동완성

 

이 GraphQL Schema라는 것 자체가 서버에서 정의한 모델이라서 GraphQL을 사용한다면 따로 API 문서화가 없어도 될 것같다는 생각도 듭니다. 아마 상세 정의까지는 안넘어가서 살짝 불편함은 있을 수 있는데, 기존에 REST API 하던거에 비하면 클라이언트가 알아서 작업할 수 있는 환경이 될것 같아요.

 

 

Seed model에 있는 값들 아무거나 호출 가능

따로 duration이나 stId 값을 준다고 서버에서 설정하지 않아도, 클라이언트가 알아서 값을 정해 호출할 수 있다. SeedType 자체가 Seed모델을 지정하고 있기 때문에 Seed 모델 안에 있는 어떤 필드도 요청할 수 있다.

 

 

 

이것저것 계속 날려보기

 

단건 조회는 그렇다 치고 List로 조회할 때는 어떻게 만들어야 될까?

 

class Query(object):
    all_seed = graphene.List(SeedType,
                             st_id=graphene.String()
                             )
                             
    def resolve_all_seed(self, info, **kwargs):
        st_id = kwargs.get('st_id')

        if st_id is not None:
            return Seed.objects.filter(st_id=st_id)

        return None

 

기존에 만들었던 seed를 살짝 변경해서 all_seed라는 쿼리를 만들고, resolve_all_seed라는 새로운 resolver를 만든다. 한건 조회에서는 model 하나를 넘겼다면, 여기서는 그냥 쿼리 셋으로 넘기면 알아서 인식한다. 

 

all_seed 호출

눈치가 있는 사람은 뭔가 발견했을 텐데, 서버에서는 분명 all_seed, st_id와 같이 "_"를 이용해서 클래스 이름과 변수 이름을 선언했다. 근데 GraphQL에서는 "_"를 허용하지 않는지 자동으로 "_"뒤에 있는 알파벳을 대문자로 바꿔서 인식한다. 처음엔 왜 이렇게 되지 고민했는데 그냥 Under Bar를 안 쓰는 게 정책인갑다.

 

아무튼 리스트 조회도 끝났고,

 

만약 커스텀 필드를 넣고 싶은 경우는? 예를 들면 결과 숫자의 2 배값을 출력하고 싶거나, 아예 다른 곳에서 값을 가져오고 싶을때는 어떻게 만들까?

 

class SeedType(DjangoObjectType):
    custom_param = graphene.String()
    
    class Meta:
        model = Seed
        
    def resolve_custom_param(self, info):
        return "this is custom param"

 

Seed 모델에는 없는 custom_param을 추가하기 위해서 SeedType 클래스 안에 custom_param이라는 필드를 추가했다. 필드를 추가할 때 항상 그 필드가 어떤 값인지 명시해줘야 한다. 여기서는 문자열이라서 graphene.String()으로 만들었다. 그리고 Query를 만들 때와 비슷하게 resolver를 하나 만들어야 한다. 여기에 어떤 값을 필드에 채울지 정하면 된다. 곱하든 더하든 다른곳에 있는 API를 호출하든 아무 짓이나 할 수 있다고 한다.

 

호출해보면 이렇게 바로 나온다.

 

만들다 보면 생각보다 복잡한 구조를 요구하는 API를 만들어야 하는데, 원하는 구조를 만들어서 쉽게 쉽게 추가할 수 있게 되어있다. 아래는 이것저것 Try 해보는 소스와 API 호출 시 나오는 값이다.

 

두 개의 Query를 한번에 쓰고, 각 Query에서 리스트-사전형 데이터를 호출하는 gql

import graphene
from graphene_django.types import DjangoObjectType

from .models import Seed, Leaf, Flower, FlowerGarden
from graphql import GraphQLError


class DailyState(graphene.ObjectType):
    index = graphene.Int()
    title = graphene.String()
    content = graphene.String()
    sub_content = graphene.String()


class DailyPlan(graphene.ObjectType):
    index = graphene.Int()
    title = graphene.String()
    study_time = graphene.String()
    goal_time = graphene.String()
    achievement_rate = graphene.Int()


class SeedType(DjangoObjectType):
    class Meta:
        model = Seed


class LeafType(DjangoObjectType):
    daily_plan = graphene.List(DailyPlan)

    class Meta:
        model = Leaf

    def resolve_daily_plan(self, info):
        ret = []

        ret.append(DailyPlan(0, '공부 제목이 보이는 공간입니다1', '00:11:12', '04:00:00', '4'))
        ret.append(DailyPlan(1, '책읽어서 책벌래되자', '03:40:12', '04:00:00', '96'))
        ret.append(DailyPlan(2, '소개팅 열심히 준비하자', '04:00:00', '04:00:00', '100'))

        return ret

class FlowerType(DjangoObjectType):
    class Meta:
        model = Seed


class FlowerGardenType(DjangoObjectType):
    test = graphene.String()
    daily_state = graphene.List(DailyState)

    class Meta:
        model = FlowerGarden

    def resolve_test(self, info):
        return "this is custom field"

    def resolve_daily_state(self, info):
        ret = []
        ret.append(DailyState(0,'title1','content1','sub_content1'))
        ret.append(DailyState(1,'title2','content2','sub_content2'))
        ret.append(DailyState(2,'title3','content3','sub_content3'))
        ret.append(DailyState(3,'title4','content4','sub_content4'))
        return ret


class Query(object):
    seed = graphene.Field(SeedType,
                          st_id=graphene.String()
                          )

    all_seed = graphene.List(SeedType,
                             st_id=graphene.String()
                             )

    error_test = graphene.Field(SeedType,
                                )

    daily_status = graphene.Field(FlowerGardenType,
                                  date=graphene.String())

    daily_plans = graphene.Field(LeafType,
                                 date=graphene.String())

    def resolve_all_seed(self, info, **kwargs):
        st_id = info.context.META.get('HTTP_ID')

        if st_id is not None:
            return Seed.objects.filter(st_id=st_id)

        return None

    def resolve_all_seed2(self, info, **kwargs):
        st_id = info.context.META.get('HTTP_ID')

        if st_id is not None:
            return Seed.objects.filter(st_id=st_id)

        return None

    def resolve_seed(self, info, **kwargs):
        st_id = info.context.META.get('HTTP_ID')

        return Seed.objects.filter(st_id=st_id).first()

    def resolve_error_test(self, info, **kwargs):
        raise GraphQLError("test error")
        return None

    def resolve_daily_status(self, info, **kwargs):
        st_id = info.context.META.get('HTTP_ID')

        return FlowerGarden.objects.first()


    def resolve_daily_plans(self, info, **kwargs):
        st_id = info.context.META.get('HTTP_ID')

        return Leaf.objects.first()

 

 

 

마무리하면서

 

일단 데이터 쪽은 쉽게 GraphQL을 적용할 수 있었는데, 로그인과 같이 보안 이슈가 있거나 데이터를 업데이트하는(gql에서는 mutation이라고 부르는) 쿼리는 어떻게 돌아가는지 확인하지 못했다. 뭐 쉽게 쉽게 할 수 있을 것 같긴 한데 아무래도 레퍼런스가 별로 없어서..

 

일단 사이드 프로젝트를 계속 진행하면서 GraphQL에 사용법에 익숙해진다면, 실제 사용화된 서비스에도 적용할 수 있을 것 같긴 하다. 이러다가 graphene-django 오픈소스 쪽도 건드릴 것 같은 예감이다 ㅋㅋ

 

 

 

 

 

 

 

반응형

댓글

Designed by JB FACTORY