스키마 정의와 리졸버 구현
GraphQL 스키마는 API의 타입 시스템을 정의하며, 리졸버는 이 스키마에 따라 데이터를 실제로 가져오거나 수정하는 함수입니다.
NestJS에서는 이들을 타입스크립트 클래스와 데코레이터를 사용하여 우아하게 구현할 수 있습니다.
GraphQL 스키마 기본 구성 요소
- 타입
@ObjectType()
class User {
@Field(type => ID)
id: string;
@Field()
name: string;
@Field(type => [Post])
posts: Post[];
}
- 쿼리
@Resolver(of => User)
class UserResolver {
@Query(returns => User)
async user(@Args('id') id: string) {
return this.userService.findById(id);
}
}
- 뮤테이션
@Resolver(of => User)
class UserResolver {
@Mutation(returns => User)
async createUser(@Args('input') input: CreateUserInput) {
return this.userService.create(input);
}
}
- 서브스크립션
@Resolver(of => Post)
class PostResolver {
@Subscription(returns => Post)
postAdded() {
return this.pubSub.asyncIterator('postAdded');
}
}
@nestjs/graphql 주요 데코레이터
- @ObjectType() : GraphQL 객체 타입 정의
- @InputType() : 입력 타입 정의
- @Field() : 필드 정의
- @Args() : 인자 정의
- @Resolver() : 리졸버 클래스 정의
- @Query(), @Mutation(), @Subscription() : 각각의 작업 정의
예시
@InputType()
class CreateUserInput {
@Field()
name: string;
@Field(type => [String])
roles: string[];
}
@Resolver(of => User)
class UserResolver {
@Query(returns => [User])
async users(@Args('roles', { type: () => [String] }) roles: string[]) {
return this.userService.findByRoles(roles);
}
}
리졸버 구현
- 쿼리 리졸버
@Resolver(of => User)
class UserResolver {
constructor(private userService: UserService) {}
@Query(returns => User)
async user(@Args('id') id: string) {
return this.userService.findById(id);
}
}
- 뮤테이션 리졸버
@Resolver(of => User)
class UserResolver {
constructor(private userService: UserService) {}
@Mutation(returns => User)
async updateUser(
@Args('id') id: string,
@Args('input') input: UpdateUserInput
) {
return this.userService.update(id, input);
}
}
- 필드 리졸버
@Resolver(of => User)
class UserResolver {
constructor(private postService: PostService) {}
@ResolveField()
async posts(@Parent() user: User) {
return this.postService.findByUserId(user.id);
}
}
스키마와 TypeScript 타입 자동 생성
GraphQLModule
설정
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: 'schema.gql',
definitions: {
path: join(process.cwd(), 'src/graphql.ts'),
},
}),
이 설정으로 schema.gql
파일과 src/graphql.ts
파일이 자동 생성됩니다.
복잡한 타입 관계 모델링
- 일대다 관계
@ObjectType()
class User {
@Field(type => ID)
id: string;
@Field(type => [Post])
posts: Post[];
}
@ObjectType()
class Post {
@Field(type => ID)
id: string;
@Field(type => User)
author: User;
}
- 다대다 관계
@ObjectType()
class Product {
@Field(type => ID)
id: string;
@Field(type => [Category])
categories: Category[];
}
@ObjectType()
class Category {
@Field(type => ID)
id: string;
@Field(type => [Product])
products: Product[];
}
리졸버 구현
@Resolver(of => User)
class UserResolver {
@ResolveField()
async posts(@Parent() user: User) {
return this.postService.findByUserId(user.id);
}
}
인터페이스와 유니온 타입
- 인터페이스
@InterfaceType()
abstract class Node {
@Field(type => ID)
id: string;
}
@ObjectType({ implements: Node })
class User implements Node {
@Field(type => ID)
id: string;
@Field()
name: string;
}
- 유니온 타입
@ObjectType()
class Book {
@Field()
title: string;
}
@ObjectType()
class Movie {
@Field()
director: string;
}
@createUnionType({
name: 'SearchResult',
types: () => [Book, Movie],
})
export class SearchResultUnion {}
GraphQL 지시어(Directive) 정의 및 사용
- 지시어 정의
@Directive('@upper')
export class UpperDirective {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function(...args) {
const result = await resolve.apply(this, args);
if (typeof result === 'string') {
return result.toUpperCase();
}
return result;
};
}
}
``
2. 지시어 사용
```typescript
@ObjectType()
class User {
@Field()
@Directive('@upper')
name: string;
}
Best Practices 및 성능 최적화 전략
- 데이터 로더 사용
import DataLoader from 'dataloader';
@Injectable()
export class UserDataLoader {
constructor(private readonly userService: UserService) {}
public readonly batchUsers = new DataLoader(async (userIds: string[]) => {
const users = await this.userService.findByIds(userIds);
return userIds.map(id => users.find(user => user.id === id));
});
}
- 필드 수준 인증
@Resolver(of => User)
class UserResolver {
@ResolveField()
@UseGuards(RolesGuard)
async secretInfo(@Parent() user: User) {
return this.userService.getSecretInfo(user.id);
}
}
- 스키마 분할
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
include: [UsersModule, PostsModule],
}),
UsersModule,
PostsModule,
],
})
export class AppModule {}
- 쿼리 복잡도 제한
GraphQLModule.forRoot({
validationRules: [
depthLimitRule(5),
complexityLimiter({
maxComplexity: 20,
estimators: [
fieldConfigEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
}),
],
}),
- 응답 캐싱
@Resolver(of => User)
class UserResolver {
@Query(returns => User)
@CacheKey('user')
@CacheTTL(30)
async user(@Args('id') id: string) {
return this.userService.findById(id);
}
}
NestJS에서는 복잡한 타입 관계를 모델링할 때는 일대다, 다대다 관계를 필드 데코레이터와 리졸버 함수를 통해 표현할 수 있습니다.
이는 데이터베이스의 관계를 GraphQL 스키마에 효과적으로 매핑할 수 있게 해줍니다.
인터페이스와 유니온 타입을 사용하면 더욱 유연한 스키마 설계가 가능합니다.
인터페이스를 통해 공통 필드를 정의하고, 유니온 타입으로 여러 타입 중 하나를 반환하는 필드를 만들 수 있습니다.
추가적으로 GraphQL 지시어를 사용하면 스키마에 메타데이터를 추가하고 런타임 동작을 수정할 수 있습니다.
이는 권한 체크, 데이터 변환 등 다양한 용도로 활용될 수 있습니다.