쿠팡이츠 카테고리 분류하기

shooting star·2024년 5월 10일
0
post-thumbnail

들어가며

Nest.js는 웹 애플리케이션 개발을 위한 모듈식 아키텍처를 제공하는 프레임워크입니다. GraphQL과 함께 사용하면 데이터 관리를 유연하고 효율적으로 할 수 있는데요. 이번 글에서는 카테고리 분류 기능을 구현하기 위한 GraphQL API 개발을 소개하겠습니다.

프로젝트 구조 개요

카테고리 분류 기능을 구현하기 위해 다음 네 가지 핵심 요소가 필요합니다.

  1. Entity: 데이터베이스에 저장될 데이터의 구조를 정의
  2. DTO (Data Transfer Object): 데이터 요청과 응답의 형식 정의
  3. Service: 데이터베이스 접근과 로직 구현
  4. Resolver: GraphQL 요청을 받아 서비스를 호출

1. 카테고리 엔티티 정의

엔티티(Entity)는 데이터베이스에 저장될 데이터의 구조를 정의합니다. 예를 들어, Category 엔티티는 다음과 같이 구현됩니다.

// category.entity.ts

@InputType('CategoryInputType', { isAbstract: true })
@ObjectType()
@Entity()
export class Category extends CoreEntity {
  @Field(type => String)
  @Column({ unique: true })
  name: string;

  @Field(type => String, { nullable: true })
  @Column({ nullable: true })
  coverImg?: string;

  @Field(type => String)
  @Column({ unique: true })
  slug: string;

  @Field(type => [Restaurant], { nullable: true })
  @OneToMany(type => Restaurant, restaurant => restaurant.category)
  restaurants?: Restaurant[];
}
  • name: 카테고리 이름
  • coverImg: 카테고리 커버 이미지
  • slug: 고유 식별자
  • restaurants: 해당 카테고리에 속한 식당들

2. DTO 정의

DTO(Data Transfer Object)는 데이터 요청과 응답의 형식을 정의합니다. GraphQL API에서 CategoryInput, CategoryOutput, AllCategoriesOutput DTO는 다음과 같이 작성됩니다.

// category.dto.ts

@ArgsType()
export class CategoryInput {
  @Field(type => String)
  slug: string;
}

@ObjectType()
export class CategoryOutput extends CoreOutput {
  @Field(type => Category, { nullable: true })
  category?: Category;
}

@ObjectType()
export class AllCategoriesOutput extends CoreOutput {
  @Field(type => [Category], { nullable: true })
  categories?: Category[];
}
  • CategoryInput: 특정 카테고리를 식별하기 위한 slug 필드
  • CategoryOutput: 응답 데이터 구조에 category 필드를 포함
  • AllCategoriesOutput: 모든 카테고리를 포함한 배열 필드를 포함

3. 서비스 구현

서비스(Service)는 데이터베이스에 접근하고, 필요한 비즈니스 로직을 처리합니다. 카테고리 관련 로직은 CategoriesService에서 처리합니다.

// categories.service.ts
  async findCategoryBySlug({ slug }: CategoryInput): Promise<CategoryOutput> {
    try {
      const category = await this.categories.findOne({ slug });
      if (!category) {
        return { ok: false, error: 'Category not found' };
      }
      return { ok: true, category };
    } catch {
      return { ok: false, error: 'Could not load category' };
    }
  }

  async allCategories(): Promise<AllCategoriesOutput> {
    try {
      const categories = await this.categories.find();
      return { ok: true, categories };
    } catch {
      return { ok: false, error: 'Could not load categories' };
    }
  }

  countRestaurants(category: Category): Promise<number> {
    return this.restaurants.count({
      where: { category: { id: category.id } },
    });
  }
  • findCategoryBySlug: 특정 슬러그로 카테고리를 검색
  • allCategories: 모든 카테고리 반환
  • countRestaurants: 해당 카테고리에 속한 식당 개수 계산

4. 리졸버 정의

리졸버(Resolver)는 GraphQL 쿼리를 처리하고 서비스를 호출해 데이터를 반환합니다. CategoryResolver는 다음과 같이 작성됩니다.

// category.resolver.ts

import { Resolver, Query, Args, ResolveField, Parent, Int } from '@nestjs/graphql';
import { CategoryInput, CategoryOutput, AllCategoriesOutput } from './dtos/category.dto';
import { CategoriesService } from './categories.service';
import { Category } from '../entities/category.entity';

@Resolver(of => Category)
export class CategoryResolver {
  constructor(private readonly categoriesService: CategoriesService) {}

  @Query(returns => AllCategoriesOutput)
  async allCategories(): Promise<AllCategoriesOutput> {
    return this.categoriesService.allCategories();
  }

  @Query(returns => CategoryOutput)
  async category(
    @Args('input') categoryInput: CategoryInput,
  ): Promise<CategoryOutput> {
    return this.categoriesService.findCategoryBySlug(categoryInput);
  }

  @ResolveField(type => Int)
  async restaurantCount(@Parent() category: Category): Promise<number> {
    return this.categoriesService.countRestaurants(category);
  }
}
  • @Query: allCategories 쿼리를 통해 모든 카테고리를, category 쿼리를 통해 특정 카테고리를 반환합니다.
  • @ResolveField: 각 카테고리 객체에 계산된 restaurantCount 필드를 추가합니다.

@ResolveField 데코레이터 사용 방법

@ResolveField 데코레이터를 사용하면 매 요청마다 계산된 필드를 가져올 수 있습니다. 예를 들어, 각 카테고리에 속한 식당 수를 계산하는 restaurantCount 필드를 구현할 수 있습니다.

@ResolveField(type => Int)
async restaurantCount(@Parent() category: Category): Promise<number> {
  return this.categoriesService.countRestaurants(category);
}
  • @Parent: 부모 객체(여기서는 Category)를 받아 해당 데이터를 기반으로 필드를 계산합니다.

마치며

이번 글에서는 GraphQL API에서 카테고리 분류 기능을 어떻게 구현하는지 살펴보았습니다. @ResolveField를 활용해 계산된 필드를 각 카테고리마다 추가하고, 엔티티(Entity), DTO, 서비스(Service), 리졸버(Resolver)의 핵심 구조를 이해해 효율적인 카테고리 분류 기능을 구현할 수 있습니다.

0개의 댓글