【AWS Amplify 노하우 시리즈】 4. amplify codegen 시에 depth 설정에 대해 주의해야할 점

Amplify 시리즈 네번째 글입니다! 이번 글에서는 amplify codegen 이라는 기능을 활용할 때 반드시 설정해야하는 depth 항목에 대해 주의해야할 점을 설명합니다.
2020.07.25

안녕하세요!ㅎㅎ Classmethod 컨설팅부 소속 김태우입니다.

Amplify 시리즈 네번째 글입니다! 이번 글에서는 amplify codegen 이라는 기능을 활용할 때 반드시 설정해야하는 depth 항목에 대해 주의해야할 점을 설명하려고 합니다. AWS Amplify 에 국한된 내용이 아니라 GraphQL 의 일반적인 codegen 기능을 활용할 시에 주의해야할 점이기도 하므로, depth 설정과 관련하여 어느정도 경험과 지식이 있으신 분들은 이번 편은 스킵하셔도 괜찮습니다.

그럼 시작하겠습니다! :)

amplify codegen 이란?

amplify codegen 이란 AWS Amplify 에서 제공하는 CLI 커맨드의 한 종류로, GraphQL 스키마에 작성된 내용을 바탕으로 프론트엔드 프로젝트에서 사용될 수 있는 API 요청 라이브러리를 만들어주는 기능을 제공합니다. 예를 들어, 아래의 스키마가 있다고 가정하고

type Post @model {
  id: ID!
  authorID: ID!
  author: User @connection(fields: ["authorID"])
  title: String!
  content: String!  
}

type User @model {
  id: ID!
  name: String!
  address: Address 
}

type Address {
  zipcode: String!
  country: String!
  state: String!
  city: String!
  street: String!
  building: String!
  etc: String
}

이 스키마를 기반으로 amplify codegen 해주게 되면 Angular 를 기준으로 (제가 Angular 를 좋아합니다ㅎㅎ) 아래와 같은 코드가 자동으로 generate 됩니다.

/* tslint:disable */
/* eslint-disable */
//  This file was automatically generated and should not be edited.
import { Injectable } from "@angular/core";
import API, { graphqlOperation } from "@aws-amplify/api";
import { GraphQLResult } from "@aws-amplify/api/lib/types";
import * as Observable from "zen-observable";

export type CreatePostInput = {
  id?: string | null;
  authorID: string;
  title: string;
  content: string;
};

export type ModelPostConditionInput = {
  authorID?: ModelIDInput | null;
  title?: ModelStringInput | null;
  content?: ModelStringInput | null;

...
  
  async UpdateUser(
    input: UpdateUserInput,
    condition?: ModelUserConditionInput
  ): Promise<UpdateUserMutation> {
    const statement = `mutation UpdateUser($input: UpdateUserInput!, $condition: ModelUserConditionInput) {
        updateUser(input: $input, condition: $condition) {
          __typename
          id
          name
          address {
            __typename
            zipcode
            country
            state
            city
            street
            building
            etc
          }
        }
      }`;
    const gqlAPIServiceArguments: any = {
      input
    };
    if (condition) {
      gqlAPIServiceArguments.condition = condition;
    }
    const response = (await API.graphql(
      graphqlOperation(statement, gqlAPIServiceArguments)
    )) as any;
    return <UpdateUserMutation>response.data.updateUser;
  }

...

OnDeleteUserListener: Observable<OnDeleteUserSubscription> = API.graphql(
    graphqlOperation(
      `subscription OnDeleteUser {
        onDeleteUser {
          __typename
          id
          name
          address {
            __typename
            zipcode
            country
            state
            city
            street
            building
            etc
          }
        }
      }`
    )
  ) as Observable<OnDeleteUserSubscription>;
}

본 포스팅의 지면 관계상, 상당부분을 생략했지만 정확히 790 라인의 코드가 생성되었습니다. 실제 상용 애플리케이션에서 사용될 스키마 타입의 수가 수십~수백개까지 늘어날 것을 감안하면 수만~수십만 라인까지도 자동으로 생성시켜줄 수 있는 강력한 기능입니다.

이러한 강력한 기능을 제대로 활용하기 위해서는 depth 라고 하는 항목의 설정이 매우!! 매우!! 중요합니다.

codegen 의 depth 에 대하여

사실, codegen 기능이 AWS Amplify 만의 독자적인 기능은 아닙니다. GraphQL 생태계와 GraphQL 클라이언트 CLI 를 제공하는 툴들 중에서는 이미 codegen 은 필수적인 기능으로 자리잡고 있습니다.

직접 커스텀 쿼리를 작성하는 경우에는 해당되지 않지만, 쿼리를 자동으로 생성해줄 때, 특히 아래와 같은 순환 참조를 해야하는 모델의 경우 필수적으로 depth 를 설정하여 무한히 쿼리의 depth 가 깊어지지 않도록 설정해주는 것이 매우 중요합니다.

type Post @model 
  @key(name: "byUser", fields: ["authorID"])
{
  id: ID!
  authorID: ID!
  author: User @connection(fields: ["authorID"])
  title: String!
  content: String!  
}

type User @model {
  id: ID!
  name: String!
  address: Address 
  posts: [Post] @connection(keyName: "byUser", fields: ["id"])
}

위와 같은 스키마의 경우, User 타입과 Post 타입이 서로를 참조하는 식으로 구성되어 있기때문에 아래와 같이 쿼리를 무한히 깊이 작성할 수 있습니다.

query InfinitelyDeepQuery {
  getUser(id: "USER_ID") {
    posts {
      items {
        author {
          posts {
            items {
              author {
                ...
              }
            }
          }
        }
      }
    }
  }
}

따라서, 퍼포먼스 저하 및 쓸데없는 금전적 비용 낭비를 방지하기 위해 depth 는 반드시 적절히 조정해야합니다.

적절한 depth 를 설정하는 것으로 클라이언트 사이드에서의 코드 작성이 훨씬 편해지기도 하지만, depth 가 깊어짐에 따라 서버사이드에 걸리는 부하와, 특히 Amplify 의 경우 DynamoDB 를 기본 데이터소스로 활용하기 때문에 Request 수에 따른 금전적 비용에 대한 고려도 반드시 해야합니다.

특정 페이지의 니즈에 모든 쿼리를 희생시키지 말자

그런데 애플리케이션을 직접 작성하다보면 특정 페이지에서는 다른 페이지들에 비해 상당히 깊은 레벨에서의 쿼리를 요구하기도 합니다. 쿼리의 depth 를 해당 페이지의 니즈보다 얕게 설정해두면, 해당 페이지에서 쿼리를 여러번 호출해야하므로 코드 관리 및 스테이트 관리를 깔끔하게 유지하는 것이 좀 더 어려워집니다. 예를 들어, 다른 페이지들에서는 depth = 5 로 설정해둔 것으로 다 만족스러웠는데 특정페이지에서 좀 더 깊은 레벨의 depth 를 요구한다면?

아래와 같은 쇼핑몰 서비스에서 다른 상품 추천과 추천상품의 리뷰 하나가 보이는, 꽤나 복잡한 상품상세 페이지를 떠올려봅시다.

- product (상품 모델)
  - recommendedProducts (추천 상품)
    - items
      - product (상품 모델)
        - name
        - image 
        - price
        - reviews (상품 리뷰 모델)
          - items
            - content
            - author
              - profileThumbnailImageURL
              - username

이렇게만 생각해도 벌써 depth 는 8 까지 늘어납니다. 극단적으로 복잡하게 구성한것 처럼 보이나요? 실제로는 10 depth 를 넘어가는 쿼리들도 종종 요구되게 됩니다.

이럴땐 어떻게 해야할까요? 물론, 해당 페이지에서 depth 5 짜리 쿼리를 여러번에 나눠서 요청하는 방법도 괜찮을 수 있지만, 직접 백엔드를 구축하지 않고 굳이 AWS Amplify 를 선택한 이유에 대해 다시한번 생각해 볼 필요가 있다고 생각합니다.

백엔드 개발의 부담을 덜고, 초기 서비스의 MVP 를 빠르게 구축하기 위해 AWS Amplify 를 도입하는 경우가 일반적일 것이라는 전제로 위 상황을 살펴보면, 가능한 공수를 줄이기 위해 복잡한 코드를 직접 작성하기보다는 쉽고 빠르고 효과적으로 한번에 모든 데이터를 다 가져오는 쿼리를 더 선호하게 될 것입니다. 즉, 위의 시나리오 대로라면 8 depth-level 의 쿼리를 어떻게해서든 작성해야한다는 것이죠.

그럼, 해당 페이지의 니즈를 충족시키기 위해 codegen 의 depth 레벨을 8 로 설정해야할까요?

저는 그렇지 않다고 생각합니다. 전체적인 퍼포먼스를 위해서 codegen 의 depth 는 5 로 그대로 두고, 해당 페이지에서 필요로하는 쿼리만 "직접" 작성해서 사용하는거죠. 이렇게 함으로써 전체적인 depth 를 얕게 유지시킬 수 있음과 동시에 특정 쿼리만 깊은 depth 의 쿼리를 작성하는 것이 가능해집니다.

이번 글에서는 위 시나리오가 아닌, 클라이언트 사이드에서 어떻게 원하는 쿼리를 직접 작성할 수 있는지를 Angular 를 기반으로 소개합니다. 매우 간단하므로 React 나 Vue 와 같이 다른 웹 플랫폼 상에서도 쉽게 작성할 수 있습니다.

Angular 에서 직접 쿼리를 작성하는 방법

type Blog @model {
  id: ID!
  name: String!
  posts: [Post] @connection(keyName: "byBlog", fields: ["id"])
}
type Post @model @key(name: "byBlog", fields: ["blogID"]) {
  id: ID!
  title: String!
  blogID: ID!
  blog: Blog @connection(fields: ["blogID"])
  comments: [Comment] @connection(keyName: "byPost", fields: ["id"])
}
type Comment @model @key(name: "byPost", fields: ["postID", "content"]) {
  id: ID!
  postID: ID!
  post: Post @connection(fields: ["postID"])
  content: String!
}

위와 같은 모델이 있다고 가정하고, amplify codegen 의 depth 를 기본값인 2로 설정해두었을때, 그 이상의 depth 수준을 요구하는 쿼리를 어떻게 직접 작성할 수 있는지 알아보겠습니다.

이번글에서는 Angular 새 프로젝트를 만들고, amplify init 을 한 후에 위 스키마를 배포하고 amplify push 해둔 상황에서 아래의 작업을 진행합니다. Amplify 의 기초적인 부분은 다 알고, 좀 더 유용하게 활용해보고자 하시는 분들이 이번 글의 타겟층이다보니 Angular 위에 어떻게 amplify 세팅을 하는지는 스킵하도록 하겠습니다. (사실 Amplify 공식 문서에 다 친절히 스텝별로 적혀있습니다ㅎㅎ)

src/app/app.component.ts 파일에 아래와 같이 작성합니다.

import { Component } from '@angular/core';
import { APIService } from './API.service';
import API, { graphqlOperation } from "@aws-amplify/api";
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  constructor(public apiService: APIService) {}
  ngOnInit() {
    const blogID = "02de1fc1-d07c-420d-b338-54c898e58436"
    this.GetBlogDepthFive(blogID).then((result) => {
      console.log(result);
    })
  }
  async GetBlogDepthFive(id: string): Promise<any> {
    const statement = `query GetBlog($id: ID!) {
        getBlog(id: $id) {
          posts {
            items {
              comments {
                items {
                  content
                }
              }
            }
          }
        }
      }`;
    const gqlAPIServiceArguments: any = {
      id
    };
    const response = (await API.graphql(
      graphqlOperation(statement, gqlAPIServiceArguments)
    )) as any;
    return <any>response.data.getBlog;
  }
}

여기서 포인트는 19번째줄의 getBlog 쿼리는 codegen 을 통해 자동으로 생성된 resolver 를 사용한다는 점입니다. 따라서, 커스텀 리졸버를 개발해야하는 것이 아니라 단순히 기존 리졸버를 활용하여 depth 만 더 깊숙히 가져오는 형태가 되므로 이렇게 간단하게 메서드 하나만 정의해서 사용해서 쉽게 깊은 depth 레벨의 쿼리를 작성하고 사용할 수 있습니다.

실제로 위 코드를 동작시키면 아래와 같은 결과를 얻어올 수 있습니다.

마치며

이번 글에서는 amplify codegen 의 depth 설정과 관련한 주의사항과, depth 를 직접 커스텀한 쿼리를 작성하는 방법에 대해 설명했습니다. codegen 을 위한 depth 를 적절한 수준에서 관리하고, 필요할 때마다 직접 쿼리를 작성함으로써, 전체적인 애플리케이션의 퍼포먼스와 비용까지 최적화 할 수 있게해주는 방법을 통해, AWS Amplify 에 좀 더 한발짝 다가갈 수 있게 되었다면 이번글은 대성공이라고 생각합니다! :D

이상, 컨설팅부의 김태우였습니다!