Express + Type-graphql + TypeDi + Typegoose
회사에서 Graphql을 사용할 일이 있어서 가볍게 Express에서 구현하려고 했었다. 공식문서와 구글링으로 기초적인 세팅과 구현은 완료해서 사용에는 문제가 없었지만, 타입 중복선언과 의존성 주입 부분에서 신경쓰이는 것들이 있어 해당 부분들을 해결하다보니 Express에 Type-graphql, TypeDi, Typegoose를 사용하게 되었다.
1. 타입 중복선언
일반적인 Graphql의 경우 입력이나 리턴되는 값들의 타입을 지정해줘야 한다. 보통 아래와 같이 typeDef를 선언해 할당해 준다
const typeDefs = gql`
type User {
loginId: String
userId: String
name: String
email: String
department: String
userRank: Int
joinDate: String
company: String
createdAt: String
}
type Query {
getUser(userId: Int): [User]
getUsers(userId: Int): [User]
}
type Mutation {
addUser(name: String!, email: String!, department: String!, userRank: Int!, company: String!): User
addCompany(company: String!): Company
}
`
// 보이는 것처럼 함수도 추상화해서 만들어줌
그리고 아래는 mongoose 의 스키마 선언 부분이다.
// /schema/User.ts
import mongoose from 'mongoose';
const { Schema } = mongoose;
const UsersSchema = new Schema({
loginId: { type: String, required: true },
userId: { type: String, required: true },
name: { type: String, required: true },
email: { type: String, required: true },
department: { type: String, required: true },
userRank: { type: Number, required: true },
joinDate: { type: String, required: true },
company: { type: String, required: true },
createdAt: { type: String, required: true },
});
// id라는 이름으로 _id 사용
UsersSchema.virtual('id').get(function () {
return this._id.toHexString();
});
export const Users = mongoose.model('Users', UsersSchema);
Graphql에 사용되는 typeDef부분과 mongoose의 UsersSchema 부분을 보면 동일한 의미의 코드가 중복으로 사용되고 있어 한 곳에서 관리하고 싶었고, 비지니스 로직 또한 최대한 구조적으로 짜고 싶었다.
이런 중복되는 부분들의 해결과 코드의 계층을 나누고 약한 의존성을 가지는 구조를 만들기 위해 Type-graphql, TypeDi을 도입하게 되었다.
2. Type-graphql (+ typeDi)
type-graphql은 Nest에서 자주 사용되는 데코레이터 패턴을 제공한다. 해당 데코레이터 패턴은 구조적인 아키텍처에 도움되는 기능들을 추상화해 제공하기 때문에 graphql로 이루어진 코드를 원하는 방식으로 만들 수 있게 도와준다.
(1) graphql 사용을 위한 기능, 의존성 주입
첫 번째로 graphql 사용을 위한 기능과 의존성 주입을 추상화해 지원한다.
import { Service, Container } from 'typedi';
@Service() // <-- 의존성 주입을 원하는 class에 @Service() 데코레이터를 사용한다.
class UserRepository {
async getUsers(_: any, userId: string) {
const [rows] = await User.find({ where: { userId: userId } });
return rows;
}
}
@Service() // <-- 의존성 주입을 원하는 class에 @Service() 데코레이터를 사용한다.
@Resolver() // <-- Resolver역할을 할 class에 @Resolver() 데코레이터를 사용한다.
class UserResolver {
constructor(private dependencies: UserRepository) {}
@Query(() => [User])
async getUsersByUserId(_: any, userId: string) {
const rows = await this.dependencies.getUsers(_, userId);
return rows;
}
}
Promise.resolve()
.then(() =>
buildSchema({
resolvers: [UserResolver],
container: Container, // <-- 이부분을 통해 최종적으로 의존성을 주입하게 된다.
}),
)
.then(schema => new ApolloServer({ schema }))
.then(apolloServer => {
apolloServer.start().then(res => {
apolloServer.applyMiddleware({
app,
path: '/graphql',
});
app.listen(process.env.PORT, () => {
console.log(`server listening on port ${process.env.PORT} `);
});
});
});
위 코드를 보면 Resolver layer와 Repository layer를 나눠 사용하고 있다. Resolver는 Repository를 의존하고 있기 때문에 constructor로 받아오고 typeDi의 @Service() 데코레이터와 Container를 사용해 의존성 주입을 도와준다. (typeDi가 DI와 IoC를 도와줌)
(+ 추가) Resolver 함수들에서 클라이언트로 부터 입력받는 데이터에 대한 DTO를 작성하려면 @InputType()이라는 데코레이터를 DTO에 추가해줘야한다. (entity와 중복사용도 가능함)
import { Field, InputType, ObjectType } from 'type-graphql';
@InputType() //<-- 입력값일 경우 @InputType()
export class PostUserDto {
@Field()
loginId: string;
@Field()
userId: string;
@Field()
name: string;
@Field()
email: string;
@Field()
department: string;
@Field(() => Number)
userRank: number;
@Field()
joinDate: string;
@Field()
company: string;
@Field()
createAt: string;
}
(2) @ObjectType() 데코레이터
두 번째로 graphql의 타입선언을 class로 사용할 수 있도록 도와주는 @ObjectType() 데코레이터이다.
// @ObjectType() 데코레이터
@ObjectType()
class User {
@Field(() => ID)
userId: string;
@Field()
loginId: string;
@Field()
name: string;
@Field({ nullable: true })
email: string;
@Field()
department?: string;
@Field(() => Int)
userRank?: number;
@Field()
joinDate: string;
@Field()
company: string;
@Field()
createdAt: string;
}
해당 데코레이터를 통해 typeorm과 같은 entity에 데코레이터만 추가해서 graphql의 타입으로 사용할 수 있게 된다.
mongoose 스키마 문제점
하지만 mongoose 스키마 선언의 경우 class를 사용하지 않고 객체를 바로 생성하기 때문에 적용할 수 없는 문제가 생겼다. 해당 문제를 해결하기 위해 Typegoose를 적용하게 되었다.
3. Typegoose
Typegoose는 데코레이터 패턴을 활용해 mongoose의 스키마를 class 형태로 작성할 수 있게 도와주는 라이브러리다. 아래 코드와 같이 entitiy를 작성한다.
import { Field, ID, Int, ObjectType } from 'type-graphql';
import { getModelForClass, prop } from '@typegoose/typegoose';
@ObjectType()
export class User {
@Field(() => ID, { nullable: true })
@prop()
userId: string;
@Field({ nullable: true })
@prop()
loginId: string;
@Field({ nullable: true })
@prop()
name: string;
@Field({ nullable: true })
@prop()
email: string;
@Field({ nullable: true })
@prop()
department?: string;
@Field(() => Int, { nullable: true })
@prop()
userRank?: number;
@Field({ nullable: true })
@prop()
joinDate: string;
@Field({ nullable: true })
@prop()
company: string;
@Field({ nullable: true })
@prop()
createdAt: string;
}
// versionKey: false와 같은 옵션은 해당 위치에 사용한다.
export const Users = getModelForClass(User, {
schemaOptions: {
versionKey: false,
},
});
이렇게 Typegoose를 사용해 class entity를 작성하게되면 type-graphql에서 제공하는 @ObjectType() 데코레이터를 추가해 DB entity와 graphql 타입을 하나의 class에서 활용할 수 있게 된다.
여기까지 Express위에서 Type-graphql과 TypeDi, Typegoose를 활용해 좀 더 구조적인 형태로 코드를 구현한 방법이다. 매번 느끼지만 Express는 자유도가 높아 구조적으로 코드를 짜는데 있어서 어려움(+ 귀찮음)이 있다. 하지만 계층간의 의존성 분리나 제어역전(IoC) 같은 개념들을 직접 구현해볼수 있다는 건 좋은 것 같다.
이제 Express로 직접 구현해봤으니 다음엔 Nest로 구조적인(사실 이미 짜여져있는) Graphql을 구현해볼 예정이다.
끝
'GraphQL' 카테고리의 다른 글
nestjs-query_02_Hook, Authorize (0) | 2024.01.14 |
---|---|
nestjs-query_01_세팅 (1) | 2024.01.10 |
Nest + Graphql 구현하기 (0) | 2022.09.29 |
TypeScript로 GraphQL 사용할 때의 문제 (0) | 2022.09.28 |