Apollo로 graphql 서버와 resolver 만들기

Irrationnelle
Prisma Korea
Published in
11 min readFeb 11, 2020

dooboolab 님께서 작성한 Creating graphql server and first resolver with Apollo 번역입니다. 피드백 환영합니다!

이번 포스팅에서는 apollo-server 와 함께 어떻게 첫 graphql 서버를 구현하는지 알아보려고 합니다.

그림 1. graphql playground 에서 확인할 수 있는 모습

서버를 구현하기 위해 선택한 기술 스택은 다음과 같습니다.

graphql 을 구현이 처음이라면, 더 깊은 개념을 이해하기 위해 서버 개발에 대한 개념을 먼저 숙지하고 오는 것을 추천합니다.

그림 2. 이번에 설치해야 하는 주요 package

apollo-server-expressexpress framework 위에 보다 더 쉽게 graphql resolver 를 구현할 수 있는 기능을 제공합니다. 이것은 express 를 얼마든지 사용하면서도 그 상단에 graphql resolver 를 구현할 수 있다는 것을 의미합니다. 어떻게 이것이 가능한지 함께 따라가면서 보겠습니다.

이미 위에서 명시했듯, 데이터베이스 ORM으로 sequelize 를 사용합니다. Prisma 를 다뤄본 적이 있다면, sequelizePrisma2 에서 photon 과 유사하다는 것을 알 수 있습니다. sequelize 또한 비슷한 기능들을 제공하고 있기 때문에 우리 graphql 서버에서 쉽게 대체할 수 있었습니다.

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

첫단계로 서버를 제작하기 전에 데이터베이스 스키마부터 먼저 살펴보는 것을 권장합니다.
graphql 은 SDL(Schema Definition Language) 가 주(主)이지만, 저는 SDL 가 구조화가 잘 이루어진 관계형 데이터베이스에 영향을 받는다고 생각합니다. 내부적으로는 결국 이 관계형 데이터베이스가 작동하고 있기 때문입니다.

1. 데이터베이스 모델 생성하기

그림 4. 데이터베이스 테이블 예시
그림 5. 관계형 데이터베이스 모델 예시

위 그림 4, 5 처럼 RDBMS 를 설계 했습니다. 그 후 sequelize ORM으로 설정합니다.
getting started guide from sequelize 를 참고하여 데이터베이스 모델을 생성하고 실제 데이터베이스와 연동하겠습니다.

그림 6. sequelize 를 이용한 User 모델

우리 프로젝트는 타입스크립트를 사용하기 때문에, User 모델을 나타내는 Model 파일이 꽤 길어졌습니다. 현재 sequelize 의 타입스크립트 지원은 보는 바와 같이 그리 좋진 않습니다.

그럼 Notification 모델과 Post 모델을 생성하겠습니다.

그림 7. sequelize 를 이용한 Notification 모델
그림 8. sequelize 를 이용한 Post 모델

위 과정을 전부 마쳤다면, ./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/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,
};

(Prisma2의 Lift와 비슷한) Sequelize migration 을 통해 실제 데이터베이스와 모델들을 연동할 수 있도록 config 파일은 순수 자바스크립트 파일(.js)로 작성합니다. 혹시나 config.jsconfig.ts 로 변경한다면, sequelize migration 커맨드가 마이그레이션 작업 실행을 위한 파일을 찾지 못합니다.

sequelize 는 주요 사용하는 다섯가지 RDB 인 PostgresMySQL, MariaDB, SQLite, Microsoft SQL 을 지원합니다. 우리는 이 포스팅에서 MySQL 을 사용합니다.

2. Apollo Server 설정하기

각 라인별로 위의 코드를 설명해보겠습니다.

17–33 라인에서는 사용자 인증(user authentication)을 확인하기 위해 JWT 토큰을 확인하는 함수들을 볼 수 있습니다.

35–73 라인에서는, Apollo Server를 초기화합니다.
이중 38–41 라인에서 context 를 이용하여 커스텀 미들웨어를 전달합니다.

getUser: () => Promise<User>;
models: ModelType;
pubsub: PubSub;
appSecret: string;
  • getUser 는 사용자가 로그인 했을 때 resolver 에서 인증하기 위해 사용합니다.
  • modelsSequelize 에서 가져온 것으로 실제 데이터베이스의 CRUD를 수행합니다.
  • pubsub 은 websocket 을 사용하는 graphql-subscription 을 위한 것입니다.
  • appSecret 은 자신의 JWT secret 입니다.

74 라인부터는, apollo server 를 subscriptions 과 함께 초기화하고 실행합니다.

3. graphql SDL로 스키마 만들기

./schemas 에 스키마를 정의하겠습니다.

그림 9. 스키마 파일 구조

schema 들의 루트 파일은 schema.graphql 입니다. 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 모델들을 SDL 로 정의 내려야합니다. 네, 같은 작업을 불만스럽게도 두 번 수행하는 것이지요.

그림 10. sequelize 모델을 SDL로 재정의한 User schema 의 간결한 형태입니다.

Primsa 를 사용하고 있다면 번거롭게 두 번 같은 작업을 하지 않아도 괜찮습니다. 하지만 Prisma2 가 안정적인 버전으로 나올 때까지는, 보수적인 방법으로 작업을 할 수 밖에 없습니다. 이런 접근은 Prisma 같은 graphql ORM 들을 이해할 수 있게 해줍니다.

그림10 을 좀 더 자세하게 보면, model 관계가 배열로 추가된 것을 볼 수 있습니다.

그림 11. graphql 을 이용한 relation 쿼리 예시

graphql 은 관계한 모델들을 위 그림11 처럼 fetch 하기 때문입니다. 조금 뒤에 resolver 에서 저 logic 을 만들어보겠습니다.

4. codegen 설정 및 실행

모든 스키마를 정의 했다면, SDL을 현재 사용 중인 프로그래밍 언어로 번역하기 위한 코드를 만들어야 합니다. 우리는 타입스크립트를 사용 중이기 때문에 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 은 context 를 위한 타입 정의입니다. 이 context는 server.tscontext 로 넘겨줍니다. 이 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. resolver 를 정의하지 않았다면, 타입스크립트가 알아서 error 를 던지는 모습입니다.

5. resolvers 구현

그림 15. resolvers 를 위한 폴더 구조

signUp mutation resolver 를 구현하겠습니다.

그림 16. signUp mutation 구현
그림 17. signUp resolver 결과

성공적으로 우리의 첫 resolver 를 만들 수 있었습니다 🎉

이 포스팅 이후에는 어떻게 다수의 resolver 와 query 관계를 효율적으로 추가할 수 잇는지 알아보겠습니다.

이 포스트 시리즈는 우리의 보일러플레이트 프로젝트인 ts-apollo-sequelize 에서 가져왔습니다.

--

--

Irrationnelle
Prisma Korea

Avec la violence suffisante, il faut faire ta vie être l’adversité