Node.js/NestJS

[NestJS] 설치 및 Prisma, GraphQL 적용하기(+MySQL)

SongMinu 2023. 12. 2. 20:29
728x90

예전부터 NestJS가 자바스크립트로 스프링을 경험해 볼 수 있다고 해서 사용해보고 싶은 프레임워크 중 하나였다.

하지만 나중에 공부해봐야지....하면서 계속 미루고 미루고 있었다.

그러던 중에 최근 NestJS를 사용해서 백엔드 개발 일을 할 수 있는 좋은 기회가 생겨 하게되었다.

일로 접한다면 미룰 수 없다는 생각이 들었고 이때 아니면 또 계속 미룰 것 같다는 생각에 바로 한다고 했다.

프로젝트는 NestJS에 ORM으로 Prisma를 사용하고 있었고, 클라이언트와 서버 간의 데이터 요청과 전달을 해주는 쿼리 언어 및 런타임으로 GraphQL을 사용하고 있었다.

3개다 들어보기만 하고 처음 만져보는 거라 집에 와서 설치부터 세팅을 직접 해봤다.


NestJS설치 및 프로젝트 생성

# NestJS cli 설치
npm i -g @nestjs/cli

# NestJS 프로젝트 생성
nest new nest-prisma-graphql

프로젝트 생성을 위한 명령어를 치면 아래처럼 출력된다.

본인이 원하는 걸로 선택하면 되고, 난 npm으로 했다.
선택하면 설치가 시작되고 완료된 모습

설치가 완료되면 생성된 프로젝트로 들어가서 npm i로 패키지들을 설치하고 npm start로 프로젝트가 정상적으로 실행되는지 확인한다.

그리고 localhost:3000으로 접속해서 화면에 hello world가 뜨면 nest는 끝

Prisma 패키지 설치 및 초기화

# Prisma 필요 패키지 설치
npm i -D prisma
npm i @prisma/client

# Prisma 초기화
npx prisma init

 

@prisma/client의 경우에는 런타임에 필요하기 때문에 일반 종속성으로 설치하고, prisma는 개발 중에만 필요하기 때문에 개발 종속성으로 설치한다.

프리즈마 클라이언트는 Type-safe ORM으로 데이터베이스와 소통하는 도구라고 볼 수 있다.
내가 작성한 프리즈마 스키마를 가지고 ORM을 만들어주고 프로그래밍 단계에서 쿼리를 작성할 때 더 쉽고 직관적으로 작성할 수 있게 해준다.

그리고 타입-세이프라는 특징을 가지고 있어 프로그래밍 단계에서 오류를 미리 감지하고 줄여준다.

 

npx prisma init 으로 초기화 명령어를 치면 프로젝트 내부에 prisma디렉터리가 생성되고 그 안에 schema.prisma라는 파일이 생성된다.

schema.prisma 파일을 열어서 수정한다.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

처음 생성하면 db의 provider가 postgresql로 되어있는데  mysql로 바꿔 준다.

그리고 .env 파일도 생성된다.

열어보면 DATABASE_URL이 postgresql로 되어 있다.

이걸 mysql 방식으로 수정한다.

DATABASE_URL="mysql://userID:userPassword@localhost:3306/DataBaseName"

여기까지 작성 후 만약 연결할 mysql이 구동 중이 아니라면 구동시켜 놓자.

구동 중이 아닌 상태로 실행하면 커넥션 에러가 뜬다.

스키마 추가

이제  schema.prisma 파일 안에 추가적으로 데이터베이스에 생성할 테이블 정보를 작성한다.

model User {
  id    Int     @default(autoincrement()) @id
  email String  @unique
  name  String?
  posts Post[]

  @@map("tb_test_user")
}

model Post {
  id        Int      @default(autoincrement()) @id
  title     String
  content   String?
  published Boolean? @default(false)
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?

  @@map("tb_test_post")
}

여기에 작성하는 정보는 데이터베이스 모델을 정의하는 것으로 데이터베이스의 테이블과 관계를 표현하는 데 사용된다.

 

User 안의 posts Post[]는 Post 모델과의 관계를 나타낸다.(1명의 유저가 여러 개의 게시글을 가질 수 있다는 의미)

@id는 PK라고 보면된다.

 

Post안의 author User는 User 모델과의 관계를 나타내며, @relation을 통해 이 필드가 User 모델의 id 필드와 연결되어 있음을 나타낸다. (FK 관계를 정의)

 

이렇게 데이터에이스의 테이블 간의 관계를 정의하는 PK, FK를 작성을 할 수 있다고 보면 된다.


@@이렇게 @가 2개가 붙는 건 데이터베이스와 상호작용을 정의한다.

@@map은 프리즈마 모델이 데이터베이스 내에서 사용하는 실제 테이블 이름, 컬럼명을 지정한다.
위 예제 소스에는 테이블명만 작성되어 있지만 User 모델의 id가 실제 테이블에는 user_id라고 저장되어 있으면 

  id    Int     @default(autoincrement()) @id @@map("user_id")

이렇게 작성하면 된다.

이러면 프리즈마를 이용해 로직을 작성할 때는 id를 사용해서 쿼리나 로직을 작성하고, 실제 데이터베이스에는 user_id로 접근한다.

작성된 필드명과 데이터베이스의 필드명이 일치하면 별도로 작성할 필요 없고, 다르다면 작성해줘야 한다.

 

작성이 완료되면 prisma가 제공하는 명령어를 이용해 테이블을 생성해야 한다.

생성하는 명령어를 사용하기 전에 확인을 해야 할 게 있다.

env에 작성된 데이터베이스에 이미 만들어져 있는 테이블들이 있고, 현재 schema.prisma에 작성된 정보와 일치하지 않는 테이블들을 모두 삭제시킬 수도 있다.

삭제해도 상관없는 테이블들이면 그냥 진행해도 되고, 아니면 데이터베이스를 변경해야 한다

실행할 때 상황에 따라 데이터가 삭제되거나, 테이블이 삭제될 수도 있으니 항상 출력되는 로그를 잘 확인해봐야 한다.

 

모두 작성되면 npx prisma migrate dev --name init을 입력하면 테이블이 생성된다.

위 명령어를 실행하면 이렇게 파일이 생성되는데 

sql 파일을 열어보면 schema.prisma를 토대로 테이블을 생성하는 쿼리들을 볼 수 있다.

schema.prisma를 수정하고 migrate 명령어를 실행하면 계속해서 디렉터리가 생성되는데 이는 이력관리에 용이하다고 한다.

Prisma 모듈 만들기

src 디렉터리 안에 prisma 디렉터리를 만들고 그 안에 prisma.service.ts를 생성 후 아래와 같이 작성.

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }
  async onModuleDestroy() {
    await this.$disconnect();
  }
}

프리즈마 클라이언트를 초기화하고 관리를 하기 위한 서비스 클래스이다.

이 파일을 통해 데이터베이스에 연결하고, 쿼리를 실행할 수 있다.

 

@Injectable() 데코레이터는 PrismaService가 NestJS의 의존성 주입 시스템에 의해 관리되어야 함을 나타낸다.

이를 통해 다른 NestJS 컴포넌트에서 이 프리즈마서비스를 주입하고 사용할 수 있다.

onModuleInit은 NestJS 모듈이 초기화될 때 자동으로 호출되고, await this.$connect()를 통해 프리즈마클라이언트를 데이터베이스에 연결한다.

onModuleDestroy는 NestJS 모듈이 종료되면 호출되고, await this.$disconnect()를 통해 프리즈마클라이언트를 데이터베이스와의 연결을 해제한다.

 

여기까지 하면 이제 NestJS가 Prisma를 이용해 데이터베이스와의 연결과 프리즈마가 제공하는 쿼리를 통해 CRUD를 모두 할 수 있다.

GraphQL 패키지 설치 및 모듈 설정

 npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql

설치 후 app.module.ts 파일을 열어서 코드를 수정

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

GraphQL의 모듈을 추가한다.

User 로직 구현

src밑에 user 디렉터리를 생성 후 user.module.ts, user.resolver.ts, user.serivce.ts를 생성한다.

module은 이 user디렉터리에서 만든 resolver와 service를 NestJS 모듈로 묶는 역할을 한다.

resolver는 GraqhQL의 쿼리와 뮤테이션을 처리한다. 클라이언트의 요청을 받고 적절한 데이터를 응답하는 곳이다.

service에서는 이제 prisma를 통해 실제 데이터베이스와 상호작용을 담당한다. resovler에서 요청을 받으면 실제 데이터베이스 작업을 실행하고 결과를 resolver로 반환한다.

 

추가적으로 user 디렉터리 안에 dto 디렉터리 생성 후 user.dto.ts 파일을 생성한다.

dto는 클라이언트에서 GraphQL로 요청을 보낼 때 파라미터가 필요하다면 어떤 정보를 입력해야 하는지 데이터 구조를 정의할 수 있다.

그리고 user 디렉터리 안에 models 디렉터리를 생성 후 user.model.ts 파일을 생성한다.

이 파일은 클라이언트로 응답을 보낼 데이터의 구조를 정의하는 파일이다.

 

사용자 등록 기능 구현

먼저 user.dto.ts에 클라이언트로부터 받을 데이터 구조를 만들어 준다.

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

@InputType({ description: '사용자 등록 DTO' })
export class UserCreatetDto {
  @Field(() => String, { description: '사용자 이름' })
  name: string;
  @Field(() => String, { description: '이메일' })
  email: string;
}

@InputType 데코레이터를 이용해 입력 타입을 정의해 준다.

@Field 데코레이터는 해당 프로퍼티가 GraphQL 스키마의 필드로 사용된다.

그리고 user.model.ts에다 사용자 모델을 만들어 준다.

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

@ObjectType({ description: '사용자' })
export class UserModel {
  @Field(() => Int, { description: '사용자 아이디' })
  id: number;
  @Field(() => String, { description: '사용자 이름' })
  name: string;
  @Field(() => String, { description: '사용자 이메일' })
  email: string;
}

@ObjectType 데코레이터를 사용해 출력 타입을 정의해 준다.

@Field 데코레이터는 해당 프로퍼티가 GraphQL 스키마의 필드로 사용된다.

 

이렇게 작성된 dto와 model들은 graphql에서 제공되는 플레이그라운드를 통해 요청하는 API가 어떤 파라미터를 필요로 하는지, 어떤 구조로 데이터를 반환하는지 쉽게 알 수 있게 도와주기도 한다.

확인하는 방법은 생성된 schema.gql 파일을 확인해 보면 된다.

GraphQL에서 이 정보를 확인하며 쉽게 쿼리를 작성할 수 있다.

 

이제 user.service.ts 파일에다가 사용자 리스트 조회와 등록 로직을 만든다.

import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { UserCreatetDto } from './dto/user.dto';
import { UserModel } from './models/user.model';

@Injectable()
export class UserService {
  constructor(private readonly prisma: PrismaService) {}

  // 모든 사용자 조회
  async findManyUserAll(): Promise<UserModel[]> {
    return this.prisma.user.findMany();
  }

  // 사용자 등록
  async createUser(dto: UserCreatetDto): Promise<UserModel> {
    return this.prisma.user.create({
      data: dto,
    });
  }
}

 

 

다음으로 user.resolver.ts 파일에 GraphQL의 쿼리와 뮤테이션을 작성한다.

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { UserService } from './user.service';
import { UserCreatetDto } from './dto/user.dto';
import { UserModel } from './models/user.model';

@Resolver()
export class UserResolver {
  constructor(private readonly userService: UserService) {}

  @Query(() => [UserModel], { description: '사용자 목록' })
  async findManyUserAll(): Promise<UserModel[]> {
    return this.userService.findManyUserAll();
  }

  @Mutation(() => UserModel, { description: '사용자 등록' })
  async createUser(@Args('data') data: UserCreatetDto): Promise<UserModel> {
    return this.userService.createUser(data);
  }
}

데이터 조회에는 @Query 데코레이터를, 데이터 조작에는 @Mutaion 데코레이터를 사용한다고 생각하면 된다.

 

user.module.ts도 작성

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserResolver } from './user.resolver';

@Module({
  imports: [],
  providers: [UserService, UserResolver],
  exports: [],
})
export class UserModule {}

 

 

이제 마지막으로 이 모듈을 사용할 수 있게 app.module.ts의 imports에 등록해 준다.

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    }),
    UserModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 

여기까지 작성하고 실행하면 될 것 같지만, 에러가 발생한다.

이 에러는 모듈 간의 의존성 관계가 잘못되었을 때 발생한다.

user.service.ts에서 UserService 클래스에 주입하려는 PrismaService 때문에 발생한 문제이다.

(PrismaService가 @Injectable로 되어 있어서)
user.module.ts에서 providers에 등록해야 한다.



이렇게 하면 정상적으로 실행이 된다.

 

만약 다른 모듈에서(@Module 어노테이션으로 정의된 모듈) 가져올 경우에는 imports에 넣어줘야 한다고 한다.

imports와 providers 부분은 아직도 헷갈린다.

 

GraphQL로 데이터 등록과 조회

이제 데이터를 등록하고 조회를 해볼 건데 localhost:3000/graphql을 접속해 보면

이런 화면을 볼 수 있다.

 

여기서 이제 사용할 수 있는 쿼리, 뮤테이션을 확인할 수 있고 작성할 수 있다.

윈도우에선 컨트롤+스페이스바, 맥에선 쉬프트+스페이바를 통해 사용할 수 있는 정보들을 볼 수 있다.

 

이 정보들은 @Resolver, @InputType, @ObjectType, @Field 어노테이션을 이용해 작성된 클래스들, 속성들의 정보들이 출력된다.

description을 작성하면 저렇게 설명도 나온다.

작성된 정보들은 schema.gql에 만들어지고 이 파일을 토대로 출력이 된다.

사용자 등록

이렇게 사용할 수 있는 정보들을 보면서 빠르게 만들 수 있다.

 

뮤테이션을 작성하고, 실행하면 이렇게 결과를 받게 된다.(user.model.ts에 작성한 UserModel 클래스 구조)

 

사용자 조회

내가 만든 사용자 조회의 경우 별도의 파라미터를 받지 않고 요청만 받고 데이터를 전달하기 때문에 쿼리가 좀 많이 단순하다.

 

이 GraphQL 쿼리는 아직 나도 습득하고 있는 단계라 어떤 방식으로 사용하는지는 이제 감은 조금 잡았는데, 아직 명확하게 설명을 하진 못하겠다..

파라미터가 필요하냐 아니냐에 따라 방식도 좀 다르고, 지금 findMayUserAll을 보면 안에 id name email이 있는데 저기서 name을 빼면 name을 제외하고 결과 데이터를 받는다.

 

서버에서 제공해 주는 데이터의 타입을 다 줄 수 있게 만들어도 필요한 타입만 요청해서 받을 수도 있고, 한 번에 여러 개의 쿼리를 요청에서 받을 수도 있는 등 사용법이 정말 다양하다.

 

Resolver 단에서 반환 타입을 어떻게 지정하느냐에 따라 출력되는 것도 다르기 때문에 기능을 구현할 때 명확하게 정의하고 로직을 구현해야 한다.

InputType, ObjectType도 마찬가지로 구현하고자 하는 기능에 대해 명확하게 하고 만들어야해서 두루뭉실하게 구현하다가는 수정하는데 꽤 고생할 수도 있다....


본문에 대한 소스코드는 아래 링크에 있습니다.

https://github.com/smw0807/typescript/tree/main/nest-prisma-graphql

추가적으로 계속 작업하는건 아래 링크에...

https://github.com/smw0807/nest-prisma-graphql

 

GitHub - smw0807/nest-prisma-graphql: NestJS Prisma GraphQL

NestJS Prisma GraphQL. Contribute to smw0807/nest-prisma-graphql development by creating an account on GitHub.

github.com

 

만드는데 참고한 공식문서

https://docs.nestjs.com/

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

https://docs.nestjs.com/recipes/prisma

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

https://www.prisma.io/docs/concepts/database-connectors/mysql

 

MySQL database connector (Reference)

This page explains how Prisma can connect to a MySQL database using the MySQL database connector.

www.prisma.io

https://www.prisma.io/docs/concepts/components/prisma-client/crud

 

CRUD (Reference)

How to perform CRUD with Prisma Client.

www.prisma.io

https://docs.nestjs.com/graphql/quick-start

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

https://graphql.org/learn/schema/#the-query-and-mutation-types

 

Schemas and Types | GraphQL

Schemas and Types On this page, you'll learn all you need to know about the GraphQL type system and how it describes what data can be queried. Since GraphQL can be used with any backend framework or programming language, we'll stay away from implementation

graphql.org

제가 만든 소스코드는 정말 간략하게 만든 거라 위 공식문서들을 보면 더 자세한 정보를 확인할 수 있습니다.

반응형

'Node.js > NestJS' 카테고리의 다른 글

[NestJS] winston을 이용한 커스텀로거  (0) 2024.03.19
[NestJS] 환경변수 설정(env)  (0) 2024.03.18