GraphQL - Node Tutorial - 05. Adding a Database

cadenzah·2019년 10월 12일
7

GraphQL - Node Tutorial

목록 보기
5/10
post-thumbnail

알립니다

이 번역 시리즈는 2019년 10월 경에 작성되었습니다. 원본인 GraphQL - Node 튜토리얼은 현재 새로운 버전으로 새롭게 작성되었습니다. 따라서 이 글은 Deprecated된 글임을 알려드립니다.

  • 본 시리즈에서는 How to GraphQL의 Tutorial 문서들을 차례대로 번역합니다.
  • 본 시리즈는 GraphQL Basic and Advanced 시리즈에서 이어집니다. GraphQL을 처음 접하는 분들은 해당 시리즈를 먼저 읽고 오시는 것을 추천드립니다.
  • 이 글은 GraphQL-Node Tutorial - Adding a Database을 번역한 글입니다.
  • 오역 또는 의역이 있을 수 있습니다. 양해 부탁드리며, 수정할 필요한 부분은 댓글로 요청해주세요.

데이터베이스 추가하기

이번 장에서는 우리의 GraphQL 서버에 연결될 데이터베이스를 Prisma를 사용하여 설정하도록 하겠습니다.

Prisma를 사용하는 이유

지금 쯤이면 GraphQL 서버가 동작하는 기본적인 방식을 이해하고 계실 겁니다. 놀라울 정도로 간단하죠? 이것이 GraphQL의 아름다운 부분입니다. 간단한 규칙 몇 가지만 따라가면 되는 것이죠. 강한 타입의 스키마, 그리고 서버에서 쿼리를 처리하는 GraphQL 엔진. 이 요소들을 잘 사용하면 API 개발에서 흔히 마주하게 되는 고통스러운 일들을 떨쳐낼 수 있습니다.

그렇다면, GraphQL 서버를 개발할 때 어려운 점은 무엇일까요?

실제 서비스를 개발할 때에는 리졸버들을 구현하는 것이 아주 복잡해지는 상황을 마주칠 가능성이 높습니다. 특히, GraphQL 쿼리가 여러 단계로 깊게 중첩될 때, 리졸버를 구현하는 것은 까다로워지고 성능 문제로 이어지기 십상입니다.

또한, 인증, 권한 부여, 데이터 매기기, 필터링, 실시간 기능, 외부 라이브러리 또는 레거시 시스템과의 통합 등... 이러한 추가적인 작업들에 많은 공과 시간을 들여야 할 것입니다.

리졸버를 구현하고 데이터베이스에 연결할 때, 일반적으로 두 가지 선택지가 존재합니다. 두 가지 선택지 모두 썩 좋지 않습니다.

  • 데이터베이스에 직접 접근 (SQL 작성 또는 기타 NoSQL 데이터베이스 API 사용)
  • 데이터베이스에 대한 추상화를 제공하는 ORM을 사용하고, 서버에서 사용하는 프로그래밍 언어를 이용하여 데이터베이스에 직접 접근

첫번째 선택지는 문제가 있는 것이, 리졸버를 사용하여 SQL을 다루는 것은 복잡하며 금새 감당할 수 없는 수준이 되어버립니다. 또다른 문제는 데이터베이스로 전송되는 SQL 쿼리는 흔히 평문 스트링(String)으로 전송된다는 점입니다. 스트링은 아무런 자료 구조도 아닌 단순한 문자값의 나열에 지나지 않습니다. 따라서, 쿼리에 어떤 문제가 있지 않은지 개발 환경 차원에서 검사를 해준다거나, 에디터 상의 자동 완성 기능을 지원하는 것이 불가능합니다. 따라서 SQL 쿼리를 작성하는 것은 지루하며 오류에 취약합니다.

두번째 선택지인 ORM 사용은 언뜻 보기에는 좋은 해결책인 것처럼 보입니다. 하지만, 이 접근 또한 좋지 않은 경우가 많습니다. ORM은 데이터베이스 접근에 필요한 간단한 해결책을 구현한 것에 불과합니다. 그래서 GraphQL에서 사용할 경우 쿼리의 복잡성과 여러 가지 비효율적인 경우들로 인하여 잘 동작하지 않기 때문에, 결국 문제가 발생하게 됩니다.

Prisma는 쿼리 리졸빙을 처리해주는 편리한 데이터 접근 계층을 제공하여 상기한 문제를 해결해줍니다. Prisma를 사용하면, 서버로 들어온 쿼리를 Prisma에 전달하고, Prisma는 이를 받아 실제 데이터베이스에 맞추어 쿼리를 리졸브하는 식으로 리졸버를 구현하게 됩니다. Prisma Client 덕분에, 리졸버 구현은 대부분의 경우 한두 줄로 구현이 가능할 정도로 간단명료한 과정입니다.

구조

아래는 Prisma를 사용하여 GraphQL 서버를 구축할 때에 사용되는 구조의 개관입니다.

Prisma 서버는 우리의 어플리케이션 구조 상에 데이터 접근 계층을 제공하여, API 서버가 Prisma를 거쳐서 데이터베이스와 상호작용하기 쉽게 만들어줍니다. Prisma 서버의 API는 우리의 API 서버 구현 안에서 작동하는 Prisma Client가 사용하게 됩니다(ORM과 유사합니다). API 서버는 지난 챕터부터 graphql-yoga를 사용하여 계속 만들어왔던 우리의 서버를 가리킵니다.

정리하면, Prisma는 API 서버의 GraphQL 리졸버가 쉽게 데이터베이스와 연결할 수 있도록 해줍니다.

데모 데이터베이스를 사용하여 Prisma 설정하기

본 튜토리얼에서는 아무 것도 없이 처음부터 다 만들어봅니다! 우리의 Prisma 구성은 가장 최소한의 설정을 가지고서 시작하도록 하겠습니다.

가장 먼저 할 일은 2개의 파일을 만드는 것입니다. 이 2개 파일은 prisma라는 새로운 디렉토리에 만들겠습니다.

우선, prisma라는 디렉토리를 만들고 그 안에 prisma.yml, datamodel.prisma을 생성합니다. 아래의 명령어를 터미널에서 실행하여 파일들을 생성합니다.
($ .../hackernews-node/)

mkdir prisma
touch prisma/prisma.yml
prisma/datamodel.prisma

prisma.yml은 Prisma를 설정에 사용되는 주요 구성 파일입니다. datamodel.prisma데이터 모델의 정의를 포함하고 있습니다. Prisma 데이터 모델은 우리 어플리케이션의 모델을 정의합니다. 각 모델은 데이터베이스 상의 한 테이블에 대응됩니다.

지금까지 Hacker News 앱의 데이터 모델은 단 하나의 데이터 타입, Link만을 포함하였습니다. Prisma는 모델을 정의하는 데에 GraphQL SDL을 사용하므로, Link의 정의를 schema.graphql에서 datamodel.prisma로 그대로 복사해도 됩니다.

datamodel.prisma 파일을 열고 아래의 코드를 추가합니다.
($ .../hackernews-node/prisma/datamodel.prisma)

type Link {
  id: ID! @id
  createdAt: DateTime! @createdAt
  description: String!
  url: String!  
}

schema.graphql에서의 Link와 비교했을 때 크게 2가지 차이점이 있습니다.

우선, id: ID! 필드에 추가한 @id 지시자입니다. 이것은 Prisma가 데이터베이스 상의 Link 레코드가 가지는 id 필드에 대하여 전역적으로 고유한 ID값을 자동 생성하고 저장하겠다는 의미입니다.

두번째로, createdAt: DateTime! @createdAt라는 새로운 필드를 추가했습니다. @createdAt 지시자 덕분에 이 필드는 Prisma가 관리하고, API 상에서 읽기-전용이 됩니다. 이 필드에는 특정 Link가 생성된 시간을 저장합니다. 어떤 레코드가 갱신되었을 때를 추적하기 위하여 @updatedAt 지시자를 필드에 사용할 수도 있습니다.

이제, prisma.yml을 작성해보도록 하겠습니다.

prisma.yml 파일을 열고 아래의 코드를 추가합니다.
($ .../hackernews-node/prisma/prisma.yml)

# 사용할 Prisma API의 HTTP 엔드포인드
endpoint: ''

# 데이터 모델을 포함하고 있는 파일의 이름
datamodel: datamodel.prisma

# 생성될 Prisma Client의 언어와 생성 위치 지정
generate:
  - generator: javascript-client
    output: ../src/generated/prisma-client

prisma.yml의 구조에 대하여 더 알아보려면 공식 문서를 확인하시기 바랍니다.

파일에 포함된 각 속성에 대하여 간단히 다루고 넘어가겠습니다.

  • endpoint: 사용할 Prisma API의 HTTP 엔드포인트.
  • datamodel: 데이터 모델이 포함된 파일을 지정합니다. 이 파일을 기반으로 API 서버에서 사용할 Prisma Client가 생성됩니다.
  • generate: 생성되는 Prisma Client의 사용 언어와 생성 위치를 지정합니다.

서비스를 배포하려면 Prisma CLI를 설치해야 합니다.

Yarn을 사용하여 Prisma CLI를 전역 설치하려면, 아래 명령어를 터미널에서 입력합니다.

yarn global add prisma

좋아요, 이제 Prisma 데이터 모델과 데이터베이스를 배포할 준비를 갖췄습니다! 🙌 본 튜토리얼의 경우, Prisma Cloud에서 제공하는 무료 데모 데이터베이스(AWS Aurora)를 사용한다는 점을 참고하시기 바랍니다. Prisma로 직접 데이터베이스를 연동하려면, 공식 문서를 참고하시기 바랍니다.

prisma deploy 명령어를 실행합니다.
($ .../hackernews-node/)

prisma deploy

prisma deploy 명령어에 의하여 상호작용형 셸 과정이 시작됩니다.

  • 우선, Demo server를 선택합니다. 브라우저가 열리면, Prisma Cloud에 회원가입하고, 터미널로 돌아옵니다.
  • 데모 서버의 region을 선택해야 합니다. 다음으로, 엔터 키를 두번 눌러서 servicestage의 기본값을 사용합니다.

참고: Prisma는 오픈 소스 프로젝트입니다. Docker를 사용하여 클라우드 서비스(Digital Ocean, AWS, Google Cloud 등) 상에 배포하는 것도 가능합니다.

명령어 실행을 완료하면, CLI가 Prisma API의 엔드포인트를 prisma.yml에 기록해줍니다. 이는 https://eu1.prisma.sh/john-doe/hackernews-node/dev와 같이 생긴 값입니다.

가장 마지막 단계는 데이터 모델에 대하여 Prisma Client를 생성하는 것입니다. Prisma Client는 Prisma API를 통하여 데이터베이스에 읽기 및 쓰기 작업을 할 수 있도록 해주는 클라이언트 라이브러리로, 자동 생성됩니다. prisma generate 명령어를 사용하여 생성할 수 있습니다. 이 명령어는 prisma.yml에서 정보를 읽고 이에 따라 Prisma 클라이언트를 생성해줍니다.

터미널에서 아래 명령어를 실행합니다.
($ .../hackernews-node/prisma)

prisma generate

이제 Prisma Client가 hackernews-node/src/generated/prisma-client 디렉토리에 생성되었습니다. 이 클라이언트를 사용하려면 방금 생성된 폴더로부터 prisma 인스턴스를 가져오면 됩니다. 아래는 Node.js 코드에서 사용할 수 있는 간단한 예시 코드입니다.

const { prisma } = require('./generated/prisma-client')

async function main() {

  // 새로운 링크 생성
  const newLink = await prisma.createLink({
    url: 'www.prisma.io',
    description: 'Prisma replaces traditional ORMs',
  })
  console.log(`Created new link: ${newLink.url} (ID: ${newLink.id})`)

  // 데이터베이스에서 모든 링크를 읽고 콘솔에 출력
  const allLinks = await prisma.links()
  console.log(allLinks)
}

main().catch(e => console.error(e))

생성된 디렉토리에는 TypeScript Definition 파일(index.d.ts)도 포함되어있다는 것에 유의하시기 바랍니다. 이 파일이 있어야 Prisma Client를 사용하여 읽기 또는 쓰기 작업을 할 때 IDE 상에서 자동 완성 기능을 사용할 수 있게 됩니다.

다음 장에서는 GraphQL 서버의 API를 진화시키고, Prisma Client를 사용하여 리졸버 함수 안에서 데이터베이스에 접근해보도록 하겠습니다.

Quiz

Prisma를 사용하는 GraphQL 서버 구조 상에서 2번째 GraphQL API(어플리케이션 스키마로 정의)가 필요한 이유로 적절한 것은?

  • 서버 성능을 향상시키기 위함이다.
  • 2가지 API를 사용할 때 GraphQL 서버가 확장이 용이하다.
  • Prisma API는 데이터베이스에 대한 인터페이스에 불과할 뿐, 대부분의 어플리케이션에서 필요한 비즈니스 로직을 적용할 수 없기 때문이다.
  • GraphQL 명세에서 요구한다.

6개의 댓글

comment-user-thumbnail
2020년 1월 18일

안녕하세요.
관심있는 분야 글 살펴보다가 아래 오타가 하나 발견했어요
graphql-yoha -> graphql-yoga

잘 읽고 갑니다!

1개의 답글
comment-user-thumbnail
2020년 5월 9일

음 허접한 질문 좀 ㅠㅠ..ㅋㅋ
글 잘 보고 있습니다.

const resolvers = {

// 조회(R)
Query: {
    info: () => `info 를 조회하다?`,
    feed: () => links,
    person: async () => {
        const query = `SELECT * FROM person`;
        let resultRow = [];
        await db.all(query, async (err, row) => {
           console.log(err);
           console.log(row);
           resultRow = await row;
        });

        return resultRow;

    }
},

// 생성, 변경, 삭제(CUD)
Mutation: {
    createLink: (parent, args) => {
        const link = {
            id: `link-${idCount++}`,
            description: args.description,
            url: args.url,
        };
        links.push(link);
        return link;
    },

    updateLink: (parent, {targetId, description, url}) => {
        const targetLink = links.find(link => link.id === targetId);
        targetLink.description = description ? description : '';
        targetLink.url = url ? url : '';
        return targetLink;
    },

    deleteLink: (parent, {targetId}) => {
        const targetLinkIndex = links.findIndex(link => link.id === targetId);
        if (targetLinkIndex != -1) {
            links.splice(targetLinkIndex, 1);
            return {
                msg: 'success'
            }
        } else {
            return {
                msg: 'undefined'
            }
        }

    }
},

// Link: {
//     id: parent => parent.id,
//     description: parent => parent.description,
//     url: parent => parent.url
// }

};

궁금한건
resolver 를 async - await 흐름으로 제어가 가능한가요?
그리고 저 위에 코드는 일단 학습으로 resolver 에서 바로 db 에 접근하는 형태로 일단 구현을 해본거라 무시하셔도 될거같습니다.

답글 달기
comment-user-thumbnail
2020년 5월 9일

자고 일어나서 좀 끄적여서 고쳤습니다 ^^;;
db 와 통신하는 코드부분을 new Promise() 로 감싸서 처리했습니다.
감사합니다.

1개의 답글