memostack
article thumbnail
블로그를 이전하였습니다. 2023년 11월부터 https://bluemiv.github.io/에서 블로그를 운영하려고 합니다. 앞으로 해당 블로그의 댓글은 읽지 못할 수 도 있으니 양해바랍니다.
반응형

1. 환경

  • nodejs: v16.15.1 (lts)
  • yarn version: berry
  • 프로젝트 구조: monorepo
  • Framework: apollo-server-express
  • 기타 사용 모듈: typescript, Knex 
  • DB: postgresql

 

2. monorepo 프로젝트 생성

graphql 서버 구축 전에 yarn workspace를 활용하여 monorepo 프로젝트 생성을 한다.

<shell />
mkdir knex-apollo-server-example

 

yarn berry를 사용하기 위해 버전을 변경한다. 그리고 yarn init 명령어로 프로젝트 초기 설정을 한다.

<shell />
yarn set version berry yarn init -y

 

.yarnrc.yml에서 nodeLinker에 node-modules를 추가한다.

<typescript />
nodeLinker: node-modules yarnPath: .yarn/releases/yarn-3.2.3.cjs

 

생성된 package.json에 workspace를 지정한다. (본 글에서는 packages 디렉토리를 workspace로 사용할 예정)

<javascript />
// package.json { "name": "knex-apollo-server-example", "packageManager": "yarn@3.2.3", "version": "0.1.0", "workspaces": { "packages": [ "packages/**" ] } }

 

workspace로 사용할 packages 디렉토리와 하위에 server 디렉토리를 생성한다

<shell />
mkdir -p packages/server

 

graphql server 초기 설정을 해준다.

<shell />
cd packages/servere yarn init -y

 

server/package.json 설정을 한다. (scripts 부분은 추후에 작성 예정)

<javascript />
// 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를 추가한다.

<javascript />
// 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 프로젝트 설정 완료!

 

3. graphql server 설정

3.1. 의존성 추가

3.1.1. graphql 관련 모듈 설치

monorepo 기반의 프로젝트가 완성이 됐으므로, 본격적으로 graphql 서버 설정을 한다. apollo-express-server를 사용할 예정이기 때문에 관련된 dependency를 추가해야 한다.

 

의존성 추가하는 명령어를 수행하기 전에 프로젝트의 root 위치에서 수행 할 수 있도록 디렉토리를 변경해준다

<shell />
cd ../../

 

dependency를 추가한다.

<typescript />
yarn install yarn server add apollo-server-express apollo-server-core express graphql

 

3.1.2. typescript 관련 모듈 설치

typescript 기반으로 서버를 구축할 예정이기 때문에, typescript와 관련된 타입 모듈을 설치한다.

<shell />
yarn server add --dev typescript @types/node ts-node
  • ts-node 모듈은 typescript 기반의 프로젝트를 nodemon으로 실행을 위해 필요함

 

개발을 편하게 하기위해 nodemon을 같이 설치해준다. (nodemon은 프로젝트에 변경된 사항이 있으면 알아서 서버를 재시작해주는 역할을 함)

<shell />
yarn server add --dev nodemon

 

3.1.3. DB 연동 관련 모듈 설치

마지막으로 postgresknex, 그리고 apollo server와 datasource를 연결해주기 위해 datasource-sql 를 설치한다.

<typescript />
yarn server add pg knex datasource-sql

 

환경변수 설정을 위해 dotenv도 함께 설치한다.

<shell />
yarn server add --dev dotenv

 

3.2. typescript 설정

tsconfig.json을 생성하여 typescript 설정을 한다.

<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" ] }

 

필요한 모듈 설치와 기타 설정을했으니 서버 개발을 진행해보자

 

4. graphql server 개발

4.1. 기본 코드 작성

src/index.ts를 생성하여 아래 코드를 작성한다. 공식문서에 나오는 코드에서 맞지않는 코드를 일부 수정 했다.

<typescript />
// 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 파일을 생성하고, 스키마를 정의했다.

<typescript />
// 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.tssrc/resolvers/user.ts를 생성하여 리졸버를 작성한다.

<typescript />
// 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, };

 

<typescript />
// src/resolvers/index.ts import { userQueryResolver, userMutationResolver } from './user'; const resolver = { Query: { ...userQueryResolver, }, Mutation: { ...userMutationResolver, }, }; export default resolver;

 

스키마와 리졸버를 모두 구현했으니, graphql server에 넘겨준다.

<typescript />
// src/index.ts // ...중략... const typeDefs = fs.readFileSync(path.resolve(__dirname, './schema/index.graphql')).toString(); startApolloServer(typeDefs, resolver);

 

graphql server가 제대로 동작하는지 확인해보기 위해, server/package.json에 scripts를 추가한다.

<javascript />
// server/package.json { ... "scripts": { "dev": "nodemon -w src src/index.ts", "build": "" }, ... }

 

설정 후 프로젝트 root 경로에서 아래 명령어를 수행하면 서버가 실행된다.

<shell />
yarn server dev

서버 실행

 

그리고 localhost:4000/graphql 로 접속하면 아래와 같은 화면이 나온다. 테스트로 쿼리를 작성하여 요청하여 응답이 제대로 오는지 확인해보자

gql 테스트

 

4.2. Postgres 연동

연동하기 전에 docker-compose로 postgres 컨테이너를 띄운다. docker-compose.yml 파일을 생성한다

<python />
# 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 컨테이너를 띄어놓자

<shell />
docker-compose up -d

 

4.2.1. Knex 설정

Knex로 datasource를 생성하여 apollo server에 추가해줘야 한다. 우선 환경변수를 생성하여, dotenv를 이용하여 DB 정보를 불러오자

 

우선 server/.env 파일을 생성한다.

<javascript />
// 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를 생성한다.

<typescript />
knex init -x ts

 

아래와 같이 내용을 수정한다.

 

<typescript />
// 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를 생성한다.

<typescript />
// src/db/knex-datasource.ts import { SQLDataSource } from 'datasource-sql'; export class KnexDatasource extends SQLDataSource {}

 

apollo server에 생성한 datasource를 추가한다.

<typescript />
// 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 파일을 생성한다.

<typescript />
// 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를 추가한다.

<javascript />
// 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 테이블 생성한다.

<shell />
yarn server migration:up

 

반대로 삭제할때는 server migration:down을 사용한다.

<shell />
yarn server migration:down

 

그리고, 다시 apollo server를 실행했을때, 오류 없이 실행되면 1차적으로 성공

 

4.3. insert, select 구현

Knex 설정까지 완료되었으므로, user insert 기능과 select 기능을 만들어보자

 

우선 knex-datasource.ts에 select하는 메소드와 insert하는 메소드를 생성한다.

<typescript />
// 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 리졸버를 정의한다.

<typescript />
// 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, };

 

 

결과

user 추가

 

특정 user 조회

 

모든 user 조회

 

전체 코드는 아래 git 참고

https://github.com/bluemiv/knex-apollo-server-example

 

GitHub - bluemiv/knex-apollo-server-example: knex를 이용하여 postgres와 apollo-express-server 연동 샘플 코드

knex를 이용하여 postgres와 apollo-express-server 연동 샘플 코드 - GitHub - bluemiv/knex-apollo-server-example: knex를 이용하여 postgres와 apollo-express-server 연동 샘플 코드

github.com

 

5. Reference

반응형
블로그를 이전하였습니다. 2023년 11월부터 https://bluemiv.github.io/에서 블로그를 운영하려고 합니다. 앞으로 해당 블로그의 댓글은 읽지 못할 수 도 있으니 양해바랍니다.
profile

memostack

@bluemiv_mm

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!