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

환경

  • 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 연동 관련 모듈 설치

마지막으로 postgresknex, 그리고 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.tssrc/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 로 접속하면 아래와 같은 화면이 나온다. 테스트로 쿼리를 작성하여 요청하여 응답이 제대로 오는지 확인해보자

gql 테스트

 

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

 

 

결과

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

 

Reference

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

memostack

@bluemiv_mm

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