[번역] 아폴로를 이용한 그래프 큐엘 서버 구현

Stark Studio
Prisma Korea
Published in
12 min readFeb 2, 2020

해당 글은 두부랩(dooboolab)과 그래프 큐엘-서울에서 쓰여진 Creating graphql server and first resolver with Apollo 을 번역한 글입니다.

이 글은 Apollo-Server를 이용해 그래프 큐엘 서버를 구현하기 위한 프로젝트 글입니다.

[그림 1] 그래프 큐엘 플레이 그라운드

GraphQL 구현을 위한 서버의 기술 스택은 다음과 같습니다.

  • Node.js : V8 엔진 기반의 자바스크립트 런타임.
  • Express : Node.js 기반의 서버 구축 프레임워크.
  • Typescript : Javascript에 타입을 제공하는 컴파일 언어.
  • Sequelize : ORM으로 데이터 베이스 조작을 쉽게 해주는 라이브러리.
  • Jest : 테스트를 위한 라이브러리.

그래프 큐엘 서버를 처음 구현한다면, 최대한 서버 프로그래밍에 대한 지식을 먼저 익혀야 합니다. Graph QL를 깊게 이해하기 위해서는 다양한 서버 프로그래밍 지식이 필요하기 때문입니다.

(개인적 생각) REST API, Over/Under Fetching, HTTPS, 스키마, 서버, 데이터 베이스에 대한 학습이 기본적으로 선행되어야 합니다.

[그림 2] 해당 프로젝트의 메인 패키지는 apollo-server-express입니다.

Express 프레임워크 위에서그래프 큐엘 리졸버(Resolver)를 쉽게 사용하기 위해 apollo-server-express 라이브러리를 쓸 수 있습니다. 이러한 방법을 통해 express 자체에서 제공하는 기능을 함께 사용할 수 있습니다.

처음 기술 스택에서 언급한 것 처럼, 해당 프로젝트에서는 데이터 베이스 ORM(Object-Relational-Mapping)으로 sequelize 라이브러리를 사용합니다. 만약, Prisma(프리즈마)를 이미 써보셨다면, Prisma2(프리즈마2)에서 소개하는 photon과 비슷한 역할을 하는 라이브러리 입니다.

Sequelize는 photon과 비슷한 기능을 제공하기 때문에, 해당 프로젝트에서는 sequelize를 사용하여 데이터베이스를 조작할 것입니다.

[그림 3] ORM 예제( 출처 : https://mixedcode.com/Article/Index?aidx=1076)

본격적으로 GraphQL서버 구현에 앞서, 데이터 베이스 스키마(모델링)를 정하는 것이 좋습니다. 그래프 큐엘에서 데이터를 정의하기 위해 SDL에 초점을 두지만, SDL에 정의한 리졸버를 구현할 때 잘 설계된 데이터 베이스 모델이 도움이 되기 때문입니다.

01. 데이터베이스 모델 생성

[그림 4] 데이터베이스 테이블 예제
[그림 5] 데이터 베이스 관계도(ERD)

해당 프로젝트를 위해 위와같이 RDB를 작성하고, 이를 통해 Sequelize orm 환경 설정을 해야합니다. sequelize 공식 문서의 getting started guide를 따라하며 데이터 베이스 모델을 생성하고 실제 데이터베이스와 연동할 수 있습니다.

[그림 6] Sequelize를 이용한 User 테이블 모델 정의

시퀄라이즈에 타입스크립트(Typescript)를 도입하려다 보니 User 테이블을 정의하는 파일이 다소 길어졌습니다. 현재로서 시퀄라이즈의 타입스크립트 지원이 조금 까다로워, 불가피하게 코드가 길어졌습니다.

User 모델 외에도 NotificationPost 모델을 다음과 같이 정의합니다.

[그림 7] Sequelize를 이용한 Notification 테이블 모델 정의
[그림 8] Sequelize를 이용한 Post테이블 모델 정의

이후 실제 데이터베이스와 ORM의 연동을 위한 ./db/index.ts 파일을 생성해주어야 합니다.

import { Sequelize } from 'sequelize';
import config from '../../config/config';
const sequelize = new Sequelize(
config.database,
config.username,
config.password,
config,
);if (process.env.NODE_ENV !== 'test') {
sequelize.sync();
}
export default sequelize;

데이터 베이스 설정 정보는 아래와 같이 ./config/config.js 파일에 작성합니다.

const config = {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: process.env.DB_CONNECTOR,
};

이때, config.js는 최대한 타입스크립트 파일이 아닌, js 확장자를 가지는 것이 좋습니다. 왜냐하면 Sequelize에서 제공하는 마이그레이션(Migration)기능을 사용할 때, config.ts를 인식할 수 없기 때문입니다.

Sequelize Migration 기능은 Prisma2의 Lift와 비슷한 기능으로 데이터베이스에 새로운 속성을 추가할 때 기존 데이터베이스의 데이터를 삭제하는 일 없이 추가해주는 기능입니다.

Sequelize는 Postgres, MySQL, MariaDB, SQLite, Microsoft SQL, MySQL등 주로 사용되는 관계형 데이터 베이스(RDB)를 지원하며 해당 프로젝트에서는 MySQL을 사용합니다.

02. 아폴로 서버 설정

코드에 대한 설명을 간략하게 한다면,

17~ 33 번 줄 : 유저 인증을 위한 JWT를 검증하는 코드

35~73 번 줄 : 아폴로 서버를 초기화 하는 코드이며 38번 줄과 41번줄은 커스텀 미들웨어를 context에 추가하는 코드입니다.

getUser: () => Promise<User>;
models: ModelType;
pubsub: PubSub;
appSecret: string;
  • getUser 유저가 로그인 했는지를 검증하는 미들웨어.
  • models 실제 데이터베이스에 시퀄라이즈를 이용해 CRUD 작업을 수행하기 위한 미들웨어.
  • pubsub 웹 소켓 기반의 graphql-subscription을 위한 미들웨어
  • appSecret JWT 비밀키

이후 나머지는 Apollo 서버와 graphql-subscription을 함께 Initialize 해주는 코드입니다.

03. Graph QL SDL과 함께 스키마 만들기

Graph QL을 이용하기 위해 ./schemas 디렉토리에 스키마를 정의해야 합니다.

[그림 9] 스키마 파일 구조

스키마 파일의 root 파일(진입점)은 schema.graphql 입니다. 이 부분에 QueryMutation ,Subscription 에 대한 리졸버(Resolver)를 정의합니다.

# import User from "user.graphql"
# import Notification from "notification.graphql"
# import Post from "post.graphql"type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
notifications: [Notification!]!
}type Mutation {
signInGoogle(socialUser: SocialUserInput!): AuthPayload!
signInFacebook(socialUser: SocialUserInput!): AuthPayload!
signInApple(socialUser: SocialUserInput!): AuthPayload!
signUp(user: UserInput!): AuthPayload!
addNotificationToken(notification: NotificationInput!): Notification
updateProfile(user: UserInput!): User
}type Subscription {
userAdded: User
userUpdated: User
}

이후에 Sequelize와 Graph QL 서버를 구현할 때 귀찮은 SDL을 작성해야 합니다. (이 부분은 모델에 대한 정의를 2번씩 써주어야 해 귀찮은 부분입니다)

[그림 10] Sequelize에서 정의된 User 스키마(왼쪽)와 GraphQL 스키마(오른쪽)

만약 Prisma를 사용하고 있다면, 위와 같이 두 번씩 모델을 정의할 필요가 없지만, Prisma가 아직 과도기적 단계에 있기에 좀 더 안정적인(전통적인) 방법을 사용할 것입니다.

아마도 Prisma또한 내부적으로 이와 비슷하게 구현될 것이기에, 이를 이해한다면 Prisma와 같은 Graph QL ORM을 이해하는데 이런 접근법이 도움이 될 것이라 생각합니다.

[그림 10]을 좀더 자세히 보면, GraphQL 스키마(오른쪽) 부분에 Notification 모델이 배열과 같이 [Notification]으로 추가된 것을 확인할 수 있습니다.

[그림 11] 그래프 큐엘 플레이 그라운드를 통해 쿼리를 날린 예제. 쿼리(왼쪽)와 그에 대한 반환값(오른쪽)

이는 [그림 11] 에서 보는 것과 같이 Graph QL에서 관계형 모델(1:N, N:M)을 위와 같이 Fetching 하기 때문입니다.

04. Codegen 설정 및 실행

모든 스키마를 정의한 후에, SDL을 현재 사용하는 프로그래밍 언어로 변환해주는 코드를 생성해야합니다. 현재 프로젝트에서는 Typescript를 사용하고 있기에 Typescript 플러그인을 사용할 것입니다.

codegen.yml 파일을 아래와 같이 작성하고, graphql-codegen을 통해 실행시켜봅시다.

overwrite: true
schema: './schemas/schema.graphql'
documents: null
generates:
src/generated/graphql.ts:
config:
contextType: ../context #MyContext
plugins:
- 'typescript'
- 'typescript-resolvers'
[그림 12] graphql-codegen을 실행한 후 결과
[그림 13] `graphql.ts`가 생성된 모습

codegen.yml에 구현된 contextType 는 server.ts에 있는 context 에 대한 타입 정의 입니다. ./src/context 에 아래와 같이 작성합니다.

import { ModelType } from './models';
import { PubSub } from 'graphql-subscriptions';
import { User } from './models/User';export interface MyContext {
getUser: () => Promise<User>;
models: ModelType;
pubsub: PubSub;
appSecret: string;
}
[그림 14] 리졸버가 정의되지 않았을 경우 타입스크립트 측에서 Error 표시를 해줄 것입니다.

05. 리졸버(Resolver) 구현

[그림 15] 리졸버 폴더 구조

signUp mutation에 대한 리졸버(Resolver)를 구현해 보면 아래와 같습니다.

[그림 16] “signUp” mutation에 대한 리졸버
[그림 17] 플레이 그라운드를 통한 “signUp” 리졸버 결과 확인

드디어, 첫 번째 리졸버를 완성했습니다.

해당 포스트는 앞으로 어떻게 다양한 리졸버를 추가할 것인지와 어떻게 쿼리를 효과적으로 구성할 것인지에 대한 내용을 꾸준히 업로드 할 것입니다.

해당 포스트 시리즈에 대한 보일러플레이트는 ts-apollo-sequelize 프로젝트를 참고했습니다.

--

--