환경
- nodejs: v16.15.1 (lts)
- yarn version: berry
- 프로젝트 구조: monorepo
- Framework: apollo-server-express
- 기타 사용 모듈: typescript, Knex
- DB: postgresql
monorepo 프로젝트 생성
graphql 서버 구축 전에 yarn workspace
를 활용하여 monorepo
프로젝트 생성을 한다.
mkdir knex-apollo-server-example
yarn berry
를 사용하기 위해 버전을 변경한다. 그리고 yarn init 명령어로 프로젝트 초기 설정을 한다.
yarn set version berry
yarn init -y
.yarnrc.yml에서 nodeLinker에 node-modules를 추가한다.
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.2.3.cjs
생성된 package.json에 workspace를 지정한다. (본 글에서는 packages 디렉토리를 workspace로 사용할 예정)
// package.json
{
"name": "knex-apollo-server-example",
"packageManager": "yarn@3.2.3",
"version": "0.1.0",
"workspaces": {
"packages": [
"packages/**"
]
}
}
workspace로 사용할 packages 디렉토리와 하위에 server 디렉토리를 생성한다
mkdir -p packages/server
graphql server 초기 설정을 해준다.
cd packages/servere
yarn init -y
server/package.json
설정을 한다. (scripts 부분은 추후에 작성 예정)
// packages/server/package.json
{
"name": "@knex-apollo-server-example/server",
"version": "0.1.0",
"packageManager": "yarn@3.2.3",
"scripts": {
"dev": "",
"build":""
}
}
마지막으로 편의를 위해 프로젝트 root 경로의 package.json에 scripts를 추가한다.
// package.json
{
"name": "knex-apollo-server-example",
"packageManager": "yarn@3.2.3",
"version": "0.1.0",
"workspaces": {
"packages": [
"packages/**"
]
},
"scripts": {
"server": "yarn workspace @knex-apollo-server-example/server"
}
}
monorepo 프로젝트 설정 완료!
graphql server 설정
의존성 추가
graphql 관련 모듈 설치
monorepo 기반의 프로젝트가 완성이 됐으므로, 본격적으로 graphql 서버 설정을 한다. apollo-express-server
를 사용할 예정이기 때문에 관련된 dependency를 추가해야 한다.
의존성 추가하는 명령어를 수행하기 전에 프로젝트의 root 위치에서 수행 할 수 있도록 디렉토리를 변경해준다
cd ../../
dependency를 추가한다.
yarn install
yarn server add apollo-server-express apollo-server-core express graphql
typescript 관련 모듈 설치
typescript 기반으로 서버를 구축할 예정이기 때문에, typescript와 관련된 타입 모듈을 설치한다.
yarn server add --dev typescript @types/node ts-node
ts-node
모듈은 typescript 기반의 프로젝트를nodemon
으로 실행을 위해 필요함
개발을 편하게 하기위해 nodemon을 같이 설치해준다. (nodemon은 프로젝트에 변경된 사항이 있으면 알아서 서버를 재시작해주는 역할을 함)
yarn server add --dev nodemon
DB 연동 관련 모듈 설치
마지막으로 postgres
와 knex
, 그리고 apollo server와 datasource를 연결해주기 위해 datasource-sql
를 설치한다.
yarn server add pg knex datasource-sql
환경변수 설정을 위해 dotenv도 함께 설치한다.
yarn server add --dev dotenv
typescript 설정
tsconfig.json
을 생성하여 typescript 설정을 한다.
// tsconfig.json
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"removeComments": true,
"isolatedModules": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": [
"src"
],
"exclude": [
"node_modules"
]
}
필요한 모듈 설치와 기타 설정을했으니 서버 개발을 진행해보자
graphql server 개발
기본 코드 작성
src/index.ts
를 생성하여 아래 코드를 작성한다. 공식문서에 나오는 코드에서 맞지않는 코드를 일부 수정 했다.
// src/index.ts
import { ApolloServer } from 'apollo-server-express';
import { ApolloServerPluginDrainHttpServer, ApolloServerPluginLandingPageLocalDefault } from 'apollo-server-core';
import express from 'express';
import http from 'http';
const startApolloServer = async (typeDefs: string, resolvers: { Query?: {}; Mutation?: {} }) => {
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
csrfPrevention: true,
cache: 'bounded',
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
],
});
await server.start();
server.applyMiddleware({ app });
httpServer.listen(4000, () => console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`));
};
이제 resolver와 graphql 스키마 파일을 생성해보자.
src/schema/index.graphql
파일을 생성하고, 스키마를 정의했다.
// src/schema/index.graphql
type User {
id: ID!
email: String!
name: String!
address: String
}
type Query {
user(id: ID): User
users: [User]!
}
type Mutation {
createUser(email: String!, name: String!, address: String): User
}
src/resolvers/index.ts
와 src/resolvers/user.ts
를 생성하여 리졸버를 작성한다.
// src/resolvers/user.ts
const mockData: { id: string; email: string; name: string; address?: string }[] = [
{ id: '1', email: 'test-user1@gmail.com', name: 'test-user1' },
{ id: '2', email: 'test-user2@gmail.com', name: 'test-user2' },
{ id: '3', email: 'test-user3@gmail.com', name: 'test-user3' },
];
// TODO DB에서 가지고 올 수 있도록 수정
const user = (parent, args, context, info) => mockData.find((u) => u.id === args.id);
// TODO DB에서 가지고 올 수 있도록 수정
const users = (parent, args, context, info) => mockData;
// TODO 실제 DB에 insert 되도록 수정
const createUser = (parent, args, context, info) => {
const { email, name, address } = args;
const createdUser = { id: (mockData.length + 1).toString(), email, name, address };
mockData.push(createdUser);
return createdUser;
};
export const userQueryResolver = {
user,
users,
};
export const userMutationResolver = {
createUser,
};
// src/resolvers/index.ts
import { userQueryResolver, userMutationResolver } from './user';
const resolver = {
Query: {
...userQueryResolver,
},
Mutation: {
...userMutationResolver,
},
};
export default resolver;
스키마와 리졸버를 모두 구현했으니, graphql server에 넘겨준다.
// src/index.ts
// ...중략...
const typeDefs = fs.readFileSync(path.resolve(__dirname, './schema/index.graphql')).toString();
startApolloServer(typeDefs, resolver);
graphql server가 제대로 동작하는지 확인해보기 위해, server/package.json
에 scripts를 추가한다.
// server/package.json
{
...
"scripts": {
"dev": "nodemon -w src src/index.ts",
"build": ""
},
...
}
설정 후 프로젝트 root 경로에서 아래 명령어를 수행하면 서버가 실행된다.
yarn server dev
그리고 localhost:4000/graphql 로 접속하면 아래와 같은 화면이 나온다. 테스트로 쿼리를 작성하여 요청하여 응답이 제대로 오는지 확인해보자
Postgres 연동
연동하기 전에 docker-compose로 postgres 컨테이너를 띄운다. docker-compose.yml
파일을 생성한다
# docker-compose.yml
version: '3.4'
services:
gql_test_db:
container_name: "gql_test_db"
image: postgres
ports:
- "5432:5432"
expose:
- "5432"
volumes:
- "./pgdata:/var/lib/postgresql/data"
environment:
- "POSTGRES_DB=gql_test_db"
- "POSTGRES_USER=postgres"
- "POSTGRES_PASSWORD=root"
- "PGDATA=/var/lib/postgresql/data/pgdata"
postgres의 기본 포트번호는 5432이다. 동일하게 host에서도 5432로 사용할 수 있게 mapping한다. volumes의 경우는 현재 프로젝트의 root 경로에 pgdata/ 디렉토리로 생성되도록했다.
docker-compose 명령어를 실행하여 postgres 컨테이너를 띄어놓자
docker-compose up -d
Knex 설정
Knex로 datasource를 생성하여 apollo server에 추가해줘야 한다. 우선 환경변수를 생성하여, dotenv를 이용하여 DB 정보를 불러오자
우선 server/.env 파일을 생성한다.
// server/.env
POSTGRES_HOST=127.0.0.1
POSTGRES_DB=gql_test_db
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=root
src/db 디렉토리를 생성하고, 그 위치에서 아래 명령어를 사용하여 knexfile.ts
를 생성한다.
knex init -x ts
아래와 같이 내용을 수정한다.
// src/db/knexfile.ts
import type { Knex } from 'knex';
import dotenv from 'dotenv';
dotenv.config({ path: '.env' });
const { NODE_ENV, POSTGRES_HOST, POSTGRES_DB, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD } = process.env;
const knexConfig: { [key: string]: Knex.Config } = {
development: {
client: 'pg',
connection: {
host: POSTGRES_HOST,
port: parseInt(POSTGRES_PORT),
user: POSTGRES_USER,
password: POSTGRES_PASSWORD,
database: POSTGRES_DB,
},
migrations: {
directory: './migrations',
},
},
};
export default knexConfig[NODE_ENV?.toLowerCase() || 'development'];
src/db/knex-datasource.ts
를 생성하여 dataSource를 생성한다.
// src/db/knex-datasource.ts
import { SQLDataSource } from 'datasource-sql';
export class KnexDatasource extends SQLDataSource {}
apollo server에 생성한 datasource를 추가한다.
// src/index.ts
// ...
import { KnexDatasource } from './db/knex-datasource';
import knexConfig from './db/knexfile';
const startApolloServer = async (typeDefs: string, resolvers: { Query?: {}; Mutation?: {} }, dataSources) => {
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
csrfPrevention: true,
cache: 'bounded',
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
],
dataSources: () => dataSources, // apollo server에 dataSource 추가
});
// ...
};
const typeDefs = fs.readFileSync(path.resolve(__dirname, './schema/index.graphql')).toString();
const dataSources = { db: new KnexDatasource(knexConfig) }; // DB DataSource 생성
startApolloServer(typeDefs, resolver, dataSources);
DB 연결 설정은 끝냈고 테이블이 있어야 조회하거나 삽입이 가능하므로, migration 작업을 한다. src/db/migrations 디렉토리를 생성하고, create_table.ts 파일을 생성한다.
// src/db/migrations/create_table.ts
import type { Knex } from 'knex';
export const up = async (knex: Knex): Promise<void> => {
return knex.schema.createTable('user', (t) => {
t.increments('id').primary();
t.string('email').notNullable();
t.string('name').notNullable();
t.string('address').nullable();
t.timestamps(false, true);
});
};
export const down = async (knex: Knex): Promise<void> => {
return knex.schema.dropTableIfExists('user');
};
그리고, 편의를 위해 server의 package.json에 scripts를 추가한다.
// server/package.json
{
...
"scripts": {
"dev": "nodemon -w src src/index.ts",
"migration:up": "env $(cat .env) knex migrate:up --knexfile src/db/knexfile.ts",
"migration:down": "env $(cat .env) knex migrate:down --knexfile src/db/knexfile.ts"
},
...
}
아래 명령어 수행하여 user 테이블 생성한다.
yarn server migration:up
반대로 삭제할때는 server migration:down
을 사용한다.
yarn server migration:down
그리고, 다시 apollo server를 실행했을때, 오류 없이 실행되면 1차적으로 성공
insert, select 구현
Knex 설정까지 완료되었으므로, user insert 기능과 select 기능을 만들어보자
우선 knex-datasource.ts에 select하는 메소드와 insert하는 메소드를 생성한다.
// knex-datasource.ts
import { SQLDataSource } from 'datasource-sql';
export class KnexDatasource extends SQLDataSource {
insertUser = async (email, name, address = null) => {
const createdUsers = await this.knex()
.insert({ email, name, address })
.into('user')
.returning(['id', 'email', 'name', 'address']);
return createdUsers[0];
};
findAllUser = () => {
return this.knex.select('*').from('user');
};
findUserById = async (id: string) => {
const users = await this.knex.select('*').from('user').where({ id });
return users[0];
};
}
이제 방금 만든 메소드를 이용하여 리졸버를 통해 user를 생성하거나 조회할 수 있다. 이제 필요없는 mockData 를 삭제하고, user
, users
, createUser
리졸버를 정의한다.
// src/resolvers/user.ts
const user = (parent, args, context, info) => {
const { db } = context.dataSources;
const { id } = args;
return db.findUserById(id);
};
const users = (parent, args, context, info) => {
const { db } = context.dataSources;
return db.findAllUser();
};
const createUser = (parent, args, context, info) => {
const { email, name, address } = args;
const { db } = context.dataSources;
return db.insertUser(email, name, address);
};
export const userQueryResolver = {
user,
users,
};
export const userMutationResolver = {
createUser,
};
결과
전체 코드는 아래 git 참고
https://github.com/bluemiv/knex-apollo-server-example
Reference
'Backend > GraphQL' 카테고리의 다른 글
typegraphql + typeorm + postgres을 사용하여 monorepo apollo server 구축 (0) | 2022.08.28 |
---|---|
yarn berry와 Apollo Server를 이용한 GraphQL 서버 환경 구축 (0) | 2022.05.22 |