GraphQL 기본 개념 및 코드 예시

steve·2024년 10월 4일

Nestjs

목록 보기
3/3

개요

  • GraphQL 기본 개념
  • GraphQL CRUD 코드 예제
  • 고급 GraphQL 기능과 개념

1. GraphQL 기본 개념

GraphQL은 Facebook에서 개발한 데이터 쿼리 언어로, RESTful API의 한계를 극복하고 클라이언트가 요청한 데이터만 받아올 수 있게 해준다. 주요 특징은 다음과 같다:

  • 단일 엔드포인트: 모든 요청이 하나의 엔드포인트에서 처리된다.
  • 명시적 데이터 요청: 클라이언트가 원하는 데이터 구조를 정의하여, 필요한 데이터만을 정확하게 가져올 수 있다.
  • 타입 시스템: 강력한 타입 시스템을 통해 데이터 구조와 유효성을 검증한다.

2. GraphQL CRUD 코드 예제

NestJS와 GraphQL을 사용하여 기본 CRUD API를 구현하는 방법을 예시 코드로 보여준다.

2.1 User 모델 정의

import { ObjectType, Field, Int } from '@nestjs/graphql';

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

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

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

  @Field(() => Int)
  status: number;
}

2.2 GraphQL CRUD Resolver

기본 CRUD 기능 정의: Resolver

import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UsersService } from './users.service';
import { NewUserInput } from './dto/new-user.input';
import { User } from './models/user.model';
import { UpdateUserInput } from './dto/update-user.input';
import { UsersArgs } from './dto/users.args';

@Resolver()
export class UsersResolver {
  constructor(private readonly service: UsersService) {}

  @Query(() => User, { nullable: true })
  async user(
    @Args('id', { type: () => Int }) id: number,
    @Args('name', { type: () => String, nullable: true }) name?: string
  ): Promise<User | null> {
    const result = this.service.getUserByIdOrName(id, name);
    return result;
  }

  @Query(() => [User])
  users(@Args() usersArgs: UsersArgs): Promise<User[]> {
    return this.service.getUsers(usersArgs);
  }

  @Mutation(() => User)
  async addUser(
    @Args('newUserData', { type: () => NewUserInput }) newUserData: NewUserInput
  ): Promise<User> {
    const user = await this.service.addUser(newUserData);
    return user;
  }

  @Mutation(() => User)
  async updateUser(
    @Args('updateUserData', { type: () => UpdateUserInput })
    updateUserData: UpdateUserInput
  ): Promise<User> {
    const user = await this.service.updateUser(updateUserData);
    return user;
  }

  @Mutation(() => Boolean)
  async deleteUser(
    @Args('id', { type: () => Int }) id: number
  ): Promise<boolean> {
    return this.service.deleteUser(id);
  }
}

2.3 CRUD 기능을 위한 서비스와 레포지토리

서비스 레이어: Service

import {
  ConflictException,
  Injectable,
  InternalServerErrorException,
  NotFoundException,
} from '@nestjs/common';
import { UsersRepository } from './users.repository';
import { User } from './models/user.model';
import { NewUserInput } from './dto/new-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { UsersArgs } from './dto/users.args';

@Injectable()
export class UsersService {
  constructor(private readonly repository: UsersRepository) {}

  async getUserByIdOrName(id: number, name?: string): Promise<User | null> {
    const user = await this.repository.findUserByIdOrName(id, name);
    if (!user) {
      return null;
    }

    return user;
  }

  async getUsers(usersArgs: UsersArgs): Promise<User[]> {
    const { skip, take } = usersArgs;
    const users = await this.repository.findUsers(skip, take);
    return users;
  }

  async addUser(data: NewUserInput): Promise<User> {
    try {
      const user = await this.repository.create(data);
      return user;
    } catch (err) {
      if (err.name === 'QueryFailedError') {
        if (err.code === 23505) {
          throw new ConflictException(err);
        }
      }
      throw new InternalServerErrorException(err);
    }
  }

  async updateUser(data: UpdateUserInput): Promise<User> {
    const { id, name, email } = data;

    const existingUser = await this.repository.findUserByIdOrName(id);
    if (!existingUser) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    if (name) existingUser.name = name;
    if (email) existingUser.email = email;

    const updatedUser = await this.repository.updateUser(existingUser);
    return updatedUser;
  }

  async deleteUser(id: number): Promise<boolean> {
    const existingUser = await this.repository.findUserByIdOrName(id);
    if (!existingUser) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    await this.repository.deleteUser(id);
    return true;
  }
}

레포지토리 레이어: Repository

import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { User } from './models/user.model';
import { NewUserInput } from './dto/new-user.input';

@Injectable()
export class UsersRepository {
  constructor(private readonly dataSource: DataSource) {}

  async findUserByIdOrName(id: number, name?: string): Promise<User> {
    let condition = '';
    let bindNum = 0;
    const params = [];

    if (name) {
      params.push(name);
      condition = `AND name = $${++bindNum}`;
    }
    params.push(id);

    let sql = `SELECT id, name, email, status FROM users WHERE id = $${++bindNum} ${condition}`;
    const [result] = await this.dataSource.query(sql, params);

    return result;
  }

  async findUsers(skip: number, take: number): Promise<User[]> {
    const result = await this.dataSource.query(
      `SELECT id, name, email, status FROM users ORDER BY id LIMIT $1 OFFSET $2`,
      [take, skip]
    );

    return result;
  }

  async create(data: NewUserInput): Promise<User> {
    const [result] = await this.dataSource.query(
      `INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email, status`,
      [data.name, data.email]
    );

    return result;
  }

  async updateUser(user: User): Promise<User> {
    const { id, name, email } = user;

    await this.dataSource.query(
      'UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *',
      [name, email, id]
    );

    return user;
  }

  async deleteUser(id: number): Promise<void> {
    await this.dataSource.query('DELETE FROM users WHERE id = $1', [id]);
  }
}

3. 고급 GraphQL 기능

3.1 Scalars

스칼라 타입은 GraphQL의 기본 데이터 타입이다. 예를 들어, 커스텀 스칼라 타입을 정의하여 특정 형식을 강제할 수 있다.

import { Scalar, CustomScalar } from '@nestjs/graphql';
import { GraphQLScalarType, Kind } from 'graphql';

@Scalar('Date')
export class DateScalar implements CustomScalar<number, Date> {
  description = 'Date custom scalar type';
  // 구현 생략...
}

3.2 Directives

디렉티브는 스키마나 쿼리에서 특정 조건을 적용할 때 사용된다.

directive @upper on FIELD_DEFINITION

type User {
  name: String @upper
}

3.3 Interfaces

인터페이스는 공통 필드를 가진 타입을 정의한다.

@InterfaceType()


abstract class Animal {
  @Field()
  name: string;
}

3.4 Unions and Enums

유니언은 여러 타입 중 하나를 반환하고, 열거형은 특정 값 집합을 정의한다.

const AnimalUnion = UnionType('AnimalUnion', () => [Cat, Dog]);

enum Role {
  ADMIN = 'ADMIN',
  USER = 'USER'
}

3.5 Field Middleware

필드 미들웨어는 특정 필드에 로직을 적용한다.

const loggingMiddleware: FieldMiddleware = async (ctx, next) => {
  console.log('Accessing field:', ctx.info.fieldName);
  return next();
};

3.6 Mapped Types

Mapped Types는 기존 타입을 기반으로 새로운 타입을 생성한다.

@ObjectType()
class UpdateUserInput extends PartialType(User) {}

3.7 Plugins

플러그인은 GraphQL 서버의 기능을 확장한다.

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
});

3.8 Complexity

쿼리의 복잡도를 측정하고 제한을 설정한다.

@Complexity(1, (childrenComplexity, args) => childrenComplexity + 10)

3.9 Federation

여러 GraphQL 서비스 간에 스키마를 결합한다.

import { buildFederatedSchema } from '@apollo/federation';
const schema = buildFederatedSchema([{ typeDefs, resolvers }]);

결론

GraphQL과 NestJS를 사용하여 API를 설계하는 데 필요한 기본 개념과 CRUD 코드 예제를 통해 GraphQL의 강력한 기능을 활용할 수 있는 방법을 이해할 수 있다. 또한, 고급 기능들을 통해 더욱 복잡한 애플리케이션의 요구사항을 만족시킬 수 있다.

0개의 댓글