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
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
결과
한가지 더! 콘솔에 SQL 쿼리를 보여주기 때문에 디버깅도 편하다
전체 코드는 아래 저장소 참고
https://github.com/bluemiv/typeorm-apollo-server-example
Reference
'Backend > GraphQL' 카테고리의 다른 글
apollo server와 knex를 이용하여 postgresql 연동하기 (with monorepo, typescript) (0) | 2022.08.26 |
---|---|
yarn berry와 Apollo Server를 이용한 GraphQL 서버 환경 구축 (0) | 2022.05.22 |