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

monorepo 프로젝트 구성

프로젝트의 디렉토를 생성해준다.

mkdir typeorm-apollo-server-example

 

yarn berry를 사용할 예정이기 때문에, 아래 명령어로 berry 버전으로 변경해준다.

yarn set version berry

 

아래에서 apollo server를 오류없이 원활하게 구축하기 위해, .yarnrc.yml의 nodeLinker를 node-modules로 수정해준다.

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

 

yarn init 명령어로 monorepo 프로젝트를 초기 설정한다.

yarn init -y

 

package.json이 생성이 되면, workspace 경로를 정의하고, 편의를 위해 scripts를 추가한다. 

// package.json
{
  "name": "typeorm-apollo-server-example",
  "version": "0.1.0",
  "packageManager": "yarn@3.2.3",
  "workspaces": {
    "packages": ["packages/**"]
  },
  "scripts": {
    "server": "yarn workspace @typeorm-apollo-server-example/server"
  }
}

 

monorepo 초기 설정은 완료!

 

apollo server 서버 구축

server 디렉토리를 생성하고 프로젝트 초기 설정을 해준다.

mkdir -p packages/server
cd packages/server
yarn init -y

 

server/package.json이 생성이 되면 아래와 같이 수정해준다.

// server/package.json
{
  "name": "@typeorm-apollo-server-example/server",
  "version": "0.1.0",
  "packageManager": "yarn@3.2.3",
  "scripts": {
    "dev": ""
  }
}

 

의존성 추가

apollo-express-server 구축을 위해 필요한 의존성 라이브러리들을 추가해준다.

yarn install
yarn server add apollo-server-express apollo-server-core express graphql

 

typescript를 사용할 예정이기 때문에 typescript와 관련된 라이브러리를 추가한다.

yarn server add --dev typescript @types/node ts-node

 

postgres와 apollo server 연동을 위해 typeorm을 사용할 예정이기 때문에, 의존성을 추가해준다. (postgres도 같이 추가)

yarn server add typeorm reflect-metadata
yarn server add pg

 

typegraphql도 사용하기 위해 의존성을 추가한다.

yarn server add class-validator type-graphql

 

개발을 편하게 하기 위해 nodemon과 환경변수 조작을 위해 dotenv를 설치한다.

yarn server add --dev nodemon dotenv

 

필요한 의존성을 모두 추가했다!

 

typescript 설정

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

// tsconfig.json
{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "lib": ["es2018", "esnext.asynciterable"],
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "outDir": "./dist",
    "sourceMap": true,
    "esModuleInterop": true,
    "removeComments": true,
    "isolatedModules": true
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "src"
  ]
}
  • typeorm과 typegraphql을 원활하게 사용하기 위해 아래 옵션은 반드시 추가해줘야 함
    • "target": "es2018"
    • "lib": ["es2018", "esnext.asynciterable"]
    • "emitDecoratorMetadata": true
    • "experimentalDecorators": true

 

서버 개발

src/index.ts를 생성하여 서버 실행 코드를 작성한다.

// server/index.ts
import 'reflect-metadata';
import { ApolloServer } from 'apollo-server-express';
import { ApolloServerPluginDrainHttpServer, ApolloServerPluginLandingPageLocalDefault } from 'apollo-server-core';
import express from 'express';
import http from 'http';

const startApolloServer = async () => {
  const app = express();
  const httpServer = http.createServer(app);
  const server = new ApolloServer({
    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}`));
};

startApolloServer();
  • 주의할점은 typeorm과 typegraphql을 사용하기 위해, import 'reflect-metadata'; 를 반드시 추가해줘야 함

 

typegraphql 을 이용하여 SDL과 리졸버 구현

SDL 구현

원래는 graphql 파일을 생성해서 SDL를 작성해야 하지만, typegraphql을 사용하면 객체 형태로 작성할 수 있다.

// src/entities
import { Field, ID, ObjectType } from 'type-graphql';

@ObjectType()
class User {
  @Field(() => ID)
  id: number;

  @Field(() => String)
  email: string;

  @Field(() => String)
  name: string;

  @Field(() => String, { nullable: true })
  address: string;
}

export default User;

 

리졸버 구현

마찬가지로 리졸버도 어노테이션을 이용하여 빠르게 구현이 가능하다.

// src/resolvers/user-resolver.ts
import { Arg, Mutation, ID, Query, Resolver } from 'type-graphql';
import User from '../entities/user';

// TODO 실제 DB에서 조회/삽입할 수 있도록 수정 예정
const mockData = [
  { id: '1', email: 'tester@gmail.com', name: 'tester', address: null },
  { id: '2', email: 'tester2@gmail.com', name: 'tester2', address: null },
  { id: '3', email: 'tester3@gmail.com', name: 'tester3', address: null },
];

@Resolver()
class UserResolver {
  @Query((returns) => User)
  async user(@Arg('id') id: string) {
    return mockData.find((user) => user.id === id);
  }

  @Query((returns) => [User])
  async users() {
    return mockData;
  }

  @Mutation((returns) => User)
  async createUser(
    @Arg('email') email: string,
    @Arg('name') name: string,
    @Arg('address', { nullable: true }) address?: string
  ) {
    const createdUser = {
      id: (mockData.length + 1).toString(),
      email,
      name,
      address,
    };
    mockData.push(createdUser);
    return createdUser;
  }
}

export default UserResolver;
  • Query 리졸버를 구현할때는 @Query 어노테이션을 사용하고 인자로 return 값을 지정
  • Mutation 리졸버를 구현할때는 @Mutation 어노테이션을 사용하고 인자로 return 값을 지정
  • request의 인자를 받아야한다면 @Arg 어노테이션을 사용
    • 만약, 필수 값이 아니라면 두번째 인자로 { nullable: true } 를 추가해주면 됨

 

이렇게 만든 SDL과 resolver를 apollo server에 넘겨주기 위해 index.ts 파일을 아래와 같이 수정한다.

// src/index.ts
import 'reflect-metadata';
import { ApolloServer } from 'apollo-server-express';
import { ApolloServerPluginDrainHttpServer, ApolloServerPluginLandingPageLocalDefault } from 'apollo-server-core';
import express from 'express';
import http from 'http';
import { buildSchema } from 'type-graphql';
import UserResolver from './resolvers/user-resolver';

const createSchema = () =>
  buildSchema({
    resolvers: [UserResolver],
  });

const startApolloServer = async () => {
  const schema = await createSchema();

  const app = express();
  const httpServer = http.createServer(app);
  const server = new ApolloServer({
    schema,
    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}`));
};

startApolloServer();

 

서버를 실행해서 제대로 동작하는지 확인해보기 위해, server/package.json의 scripts를 추가한다.

// server/package.json
{
  "name": "@typeorm-apollo-server-example/server",
  "version": "0.1.0",
  "packageManager": "yarn@3.2.3",
  "scripts": {
    "dev": "nodemon -w src src/index.ts"
  },
  ...
}
  • nodemon: 서버 파일에 변경사항이 있으면 지켜보고 있다가 알아서 서버를 재시작해주는 툴

 

실행 결과

yarn server dev

 

user 추가

 

특정 user 조회

 

user 전체 조회

 

docker로 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 컨테이너를 실행한다.

docker-compose up -d

 

postgres와 typeorm, apollo server 연동

이제 실제 DB와 연동해서 조회/삽입을 할 수 있도록 기능을 개선한다. datasource를 구현하기 전에 .env의 환경변수에 DB connect를 하기 위한 정보를 입력한다.

// .env
POSTGRES_HOST=127.0.0.1
POSTGRES_DB=gql_test_db
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=root

 

그리고, src/data-source.ts 파일을 만들고 datasource를 생성한다.

// src/data-source.ts
import dotenv from 'dotenv';
import { DataSource } from 'typeorm';
import User from './entities/user';

dotenv.config({ path: '.env' });
const { POSTGRES_HOST, POSTGRES_DB, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD } = process.env;

const PostgresDataSource = new DataSource({
  type: 'postgres',
  host: POSTGRES_HOST,
  port: parseInt(POSTGRES_PORT),
  username: POSTGRES_USER,
  password: POSTGRES_PASSWORD,
  database: POSTGRES_DB,
  synchronize: true,
  logging: true,
  entities: [User],
  subscribers: [],
  migrations: [],
});

export default PostgresDataSource;

 

그리고 서버가 실행되기 전에 datasource가 초기화 될 수 있도록 index.ts 코드를 수정한다.

// index.ts
// ...
import PostgresDataSource from './data-source';

// ...

const startApolloServer = async () => {
  await PostgresDataSource.initialize(); // DataSource 초기화

  const schema = await createSchema();

  const app = express();
  // ...
};

startApolloServer();

 

여기까지 하면 DB와 연동은 완료! 이제 조회/삽입 기능을 구현해보자

테이블과 entity를 매핑하기 위해 아래와 같이 어노테이션을 추가한다.

// src/entities/user.ts
import { Field, ID, ObjectType } from 'type-graphql';
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
@ObjectType()
class User extends BaseEntity {
  @PrimaryGeneratedColumn('uuid')
  @Field(() => ID)
  id: string;

  @Column({ length: '255' })
  @Field(() => String)
  email: string;

  @Column({ length: '50' })
  @Field(() => String)
  name: string;

  @Column({ length: '255', nullable: true })
  @Field(() => String, { nullable: true })
  address: string;
}

export default User;
  • @Entity를 class에 붙여주면 알아서 class 이름으로 테이블을 생성해줌. 인자로 문자열을 넣어주면 넣어준 문자열로 테이블이 생성
  • @PrimaryGeneratedColumn() 을 이용하면 primary key를 자동으로 생성해줌
  • @Column을 이용하여 테이블의 column 속성을 정의할 수 있음

 

그리고 방금 만든 Entity로 DB 조회/삽입을 할 수 있다. 

// src/resolvers/user-resolver.ts
import { Arg, Mutation, Query, Resolver } from 'type-graphql';
import User from '../entities/user';

@Resolver()
class UserResolver {
  @Query((returns) => User)
  async user(@Arg('id') id: string) {
    return await User.findOneBy({ id });
  }

  @Query((returns) => [User])
  async users() {
    return await User.find();
  }

  @Mutation((returns) => User)
  async createUser(
    @Arg('email') email: string,
    @Arg('name') name: string,
    @Arg('address', { nullable: true }) address?: string
  ) {
    const createdUser = await User.create({
      email,
      name,
      address,
    });
    await createdUser.save();
    return createdUser;
  }
}

export default UserResolver;

Java ORM의 JPA와 비슷하게 사용이 가능하다

  • findOnBy(): 특정 컬럼을 where 조건으로 넣고 조회할때 사용
  • find(): 전체 조회
  • create() & save(): 값을 insert 하고, commit

 

서버를 실행하면 테이블이 생성될때 어떻게 생성이 되는지 쿼리를 보여준다.

yarn server dev

 

결과

user 추가

 

특정 user 조회

 

user 전체 조회

 

한가지 더! 콘솔에 SQL 쿼리를 보여주기 때문에 디버깅도 편하다

실행된 쿼리를 콘솔에 보여줌

 

전체 코드는 아래 저장소 참고

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

 

GitHub - bluemiv/typeorm-apollo-server-example: typeorm을 사용하여 apollo express server 구축 샘플 코드

typeorm을 사용하여 apollo express server 구축 샘플 코드. Contribute to bluemiv/typeorm-apollo-server-example development by creating an account on GitHub.

github.com

 

Reference

 

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

memostack

@bluemiv_mm

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