GraphQL + MongoDB + Prisma 를 이용하여 REST API 를 대체할 수 있는 서버 만들기
REST API 보다 GraphQL 이 더 낫다고 할 순 없지만, 요즘 트렌드 기술인 듯 하여 현재 운영중인 서버에 테스트겸 구축해보면서 기록
전체적인 구조는 아래 이미지와 같다.
Prisma 를 사용하는 이유 : GraphQL 서버를 구축해서 사용하려면 리졸버들을 구현해야하는데 이 리졸버를 구현하는거 자체가 까다로운데, Prisma를 이용하면 실제 데이터베이스에 맞는 리졸버를 자동으로 생성하여 연결해주기에 실제로 우리가 리졸버를 구현할때는 한두줄로 해결할 수 있다.
일단 구축하기 전 상황은 아래와 같다.
- AWS EC2 인스턴스에 REST API 서버와 MongoDB 가 돌아가고 있다.
- MongoDB 에는 이미 데이터들이 채워져 있다.
- GraphQL 서버를 만들고, Prisma를 기존에 있는 MongoDB 와 연결해서 사용하려고 한다.
따라서 진행하다보면 MongoDB의 Database 이름과 Collection 명이 필요한데 알아보기 쉽게하기 위해 아래처럼 DB가 구성되어 있다고 가정하고 진행한다.
Database : School
Collection : Student
Student 의 데이터 예
{
_id: ObjectId("5aei2124aef34"),
name: "Dean",
phone: "010-3333-2222"
}
1) 폴더 생성 및 프로젝트 초기화
아래의 명령어를 통해 폴더 생성 및 초기화 수행
mkdir graphql-server
cd graphql-server
yarn init -y
2) GraphQL Server의 진입점 생성
graphql-server 디렉토리로 이동해서 아래 명령어 실행
mkdir src
touch src/index.js
3) index.js 코드 입력
const { GraphQLServer } = require('graphql-yoga');
const { prisma } = require('./generated/prisma-client');
const Query = require('./resolvers/Query');
const resolvers = {
Query
};
const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers,
context: request => {
return {
...request,
prisma
};
}
});
server.start(() => console.log('http://localhost:4000'));
4) resolvers 폴더 생성
src 폴더 안에 resolvers 이름으로 폴더를 하나 생성
* resolvers 폴더 : GraphQL 사용 시 필요한 리졸버들을 정의해놓은 곳
5) resolvers 폴더 안에 Query.js 파일 생성
내용은 나중에 입력
touch Query.js
6) 의존성 추가
* graphql-yoga : express.js 기반으로 개발된 GraphQL 서버. GraphQL 이용시 필요한 기능들 왠만한건 다 지원 ( Playground 지원 )
* prisma-client-lib : Prisma 서버와 연결하기 위해 필요함
yarn add graphql-yoga prisma-client-lib
7) Prisma
글로벌로 Prisma CLI 를 설치해야 한다.
graphql-server 폴더 안에서 아래 명령어 실행
이 명령어를 실행하게되면 다음과 같이 항목들을 선택할 수 있는 메시지가 뜨는데 아래처럼 선택한다.
1. Use existing database 를 선택
2. MongoDB 선택
3. MongoDB Connection String 입력 ( db id = admin , password = admin 이라고 가정하고 )
mongodb://admin:admin@ec2서버주소:27017/School 입력
* ec2서버주소 에는 실제 서버 주소를 입력해준다. 현재 prisma 를 돌리려고 하는 서버에 mongodb 가 있다고 해서 localhost로 적으면 안됨 -> 해당 Prisma 서버는 docker 를 이용하여 가상 컨테이너 안에서 돌릴거기 때문
4. 정상적으로 연결이 되면 Database 리스트가 나타나고 사용할 데이터베이스를 선택한다.
5. Prisma Client 를 생성할지 물어보는 창이 뜨는데 Don't generate 를 선택한다.
6. 정상적으로 생성이 완료되면, 아래와 같은 메시지가 뜨고 3개의 파일이 생성된 것을 알 수 있다.
8) datamodel.prisma
데이터베이스의 모델 구조에 맞게 작성해야 한다.
이미 존재하는 MongoDB에 연동하여 사용 할 경우 Collection 이름과 동일하게 정의해야 한다.
Collection 이름이 Student 이므로 아래와 같이 작성한다.
type Student {
id: ID! @id
name: String!
phone: String!
}
9) prisma.yml
생성된 prisma.yml 파일을 보면 맨위 3줄밖에 작성되어 있지 않은데, generate 부분과 hooks 부분을 추가해준다.
endpoint : Prisma Server의 주소 ( 처음 deploy 하게되면 그때 알아서 입력되므로 지금 입력 안해도 됨 )
datamodel : Prisma 가 사용할 datamodel 파일
generate : Prisma Client 파일을 생성할때 참고하는 옵션
- generator :
endpoint: http://localhost:4466
datamodel: datamodel.prisma
# Specifies language & location for the generated Prisma client
generate:
- generator: javascript-client
output: ../src/generated/prisma-client
hooks:
post-deploy:
- prisma generate
10) docker-compose.yml
생성된 docker-compose 파일의 내용을 보면 databases: default: 부분에 Schema 라고 해서 추가된 부분이 있는데 그 부분을 지우고 default 부분을 아래처럼 입력한다.
version: '3'
services:
prisma:
image: prismagraphql/prisma:1.34
restart: always
ports:
- '4466:4466'
environment:
PRISMA_CONFIG: |
port: 4466
# uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
# managementApiSecret: my-secret
databases:
default:
connector: mongo
database: School
uri: mongodb://admin:admin@ec2주소:mongodb포트/School
11) docker compose 설치
docker 를 이용해서 가상화된 공간에 Prisma 서버를 돌리기 위해 docker compose 를 설치해준다.
apt install docker-compose
일단 여기까지하면 Prisma 관련 세팅이 끝난다.
12) Graphql Schema 작성
src 폴더 안에 schema.graphql 파일을 만들고 아래와 같이 작성한다.
Query 안에 보면 students 라는 필드가 있고, 해당 필드는 [Student!]! 로 되어 있는데 Graphql 에서 [] 는 Array를 의미하고, [] 안에는 해당 Array 의 타입을 적어준다. 또한, ! 는 해당 타입에 맞는 데이터가 오는지 체크하겠다는 의미이다.
따라서 students 필드는 Student 타입의 Array로 이루어진 데이터를 반환한다는 의미가 된다.
type Query {
students: [Student!]!
}
type Student {
id: ID!
name: String!
phone: String!
}
* 위의 Schema 파일은 Prisma 에서 datamodel.prisma 에 정의된 값들만 이용 가능하기에 데이터 모델의 변경이 생기면 datamodel.prisma 파일과 schema.graphql 파일을 수정하고 맞춰주어야 한다.
13) Query.js
schema 에서 type Query{} 를 작성하였고 해당 Query 안에 있는 필드들을 사용하기 위해 리졸버를 구현해줘야 한다. 따라서 우리는 students 라는 이름을 가진 리졸버 함수를 구현해야 한다.
이전에 만들어놓은 resolvers/Query.js 파일에 아래와 같이 입력한다.
function students(parent, args, context, info) {
return context.prisma.students();
}
module.exports = {
students
};
* context.prisma.students(); 해당 부분에 적혀있는 students는 type Query 안에 우리가 작성한 students 와는 다른 개념이다. Prisma 에서 datamodel.prisma 를 작성하고 나중에 prisma deploy 명령어를 입력해 deploy 하게되면 prisma client 가 실행되고, datamodel.prisma 를 기반으로 해서 CRUD 처럼 간단한 Query 들에 대한 리졸버들이 생성된다.
* 그 중에 find 쿼리에 해당하는 리졸버들이 생성되는데 해당 리졸버 함수의 이름은 datamodel.prisma 에 정의한 type 이름에 s 를 붙인 형태가 된다. 반드시 다 이렇진 않으며 es가 뒤에 붙는경우도 있고, 대문자로 바뀌는 경우도 있는데 확인하는 방법은 아래에서 다시 설명.
* 어쨌든 type Student 라고 datamodel.prisma 에 선언했기에, students 라는 find 쿼리에 해당하는 리졸버가 자동 생성되고 이를 호출하기 위해서 사용하는게 context.prisma.students() 이다.
14) Prisma Server 실행
prisma 폴더로 이동한 후 아래 명령어 실행
해당 명령어를 입력하면, 이전에 작성했던 docker-compose.yml 파일을 읽고 해당 정보를 기반으로 이미지를 다운받고 가상의 공간을 생성한 후 서버를 작동시킨다. ( -d 옵션은 Demon 으로 백그라운드에서 서버가 실행되게 한다. )
15) Prisma Client 생성
graphql-server 폴더로 이동한 후 아래 명령어 실행
prisma deploy
deploy 명령어는 이전에 docker-compose 로 실행해 둔 서버에 변경사항을 적용하는 역할을 하게된다.
실질적으로 client 를 생성하려면 prisma generate 라는 명령어를 실행해야하는데 deploy 하고 generate 하는게 번거로움이 있어서 9) prisma.yml 항목을 보게되면 맨 마지막에 아래와 같이 작성되어 있는 부분이 있다.
hooks:
post-deploy:
- prisma generate
이 부분이 deploy 명령어를 실행하면 deploy 후에 prisma generate 명령어를 자동으로 실행하게 해주는 부분이다.
따라서 모든 작업이 완료되면, src 폴더에 generated 라는 폴더가 생기고 그 안에 prisma client 관련 파일들이 생성된 것을 확인할 수 있다.
16) GraphQL Server 시작
graphql-server 폴더로 이동한 후 아래 명령어 실행
위 명령어를 실행하면 localhost:4000 이라는 메시지가 뜨게 되고 해당 주소로 접속하면 Graphql Playground 사이트가 열리는 것을 확인 할 수 있다.
* Prisma Client 가 자동으로 생성한 스키마 정보 확인 방법
src/generated 폴더에 보면 prisma-schema.js 라는 파일이 있으며 해당 파일을 열어보면 자동으로 생성된 Schema 들을 확인할 수 있다.
그 중 type Query 부분을 살펴보면 어떤 이름으로 호출을 해야 하는지 알 수 있다.
type Query {
comment(where: CommentWhereUniqueInput!): Comment
comments(where: CommentWhereInput, orderBy: CommentOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Comment]!
commentsConnection(where: CommentWhereInput, orderBy: CommentOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): CommentConnection!
link(where: LinkWhereUniqueInput!): Link
links(where: LinkWhereInput, orderBy: LinkOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Link]!
linksConnection(where: LinkWhereInput, orderBy: LinkOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): LinkConnection!
user(where: UserWhereUniqueInput!): User
users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection!
node(id: ID!): Node
}