본문 바로가기
Back-end/기초부터 따라하는 nest.js

시즌 2 #10. 기초부터 따라하는 Nest.js 2 : Prisma와 DB 연결 (MySQL)

by hsloth 2024. 7. 7.

지난번 포스팅에서는 Pipe에 대해서 배웠습니다.

 

https://suloth.tistory.com/206

 

시즌 2 #9. 기초부터 따라하는 Nest.js 2 : Pipe - 파이프

지난번 포스팅에서는 Nest.js에서 Query와 Param, Body를 다루는 법에 대해서 배웠습니다.https://suloth.tistory.com/204 시즌 2 #8. 기초부터 따라하는 Nest.js 2 : Request 객체와 Query, Param, Body 사용법 (feat. Response)

suloth.tistory.com

 

 

이번 시간에는 드디어! 데이터베이스(MySQL)를 연결하는 방법에 대해서 배워보도록 하겠습니다.


과제 정답


 

[지난 과제]

CommentService에서 comment list를 리턴하는 함수를 만들되, 페이지네이션으로 댓글을 페이지별로 일정 개수만큼 가져오도록 함수를 만들어봅시다.

그리고 CommentService에서 만든 함수를 PostController의 getPost함수에서 사용하도록 합시다.

즉, N번째 게시글을 가져오면 게시글 내용뿐만 아니라 게시글에 달린 댓글까지 가져오도록 만드는 겁니다.

이 때, 심심하니까 Query로 perCommentPage curCommentPage를 사용해서 가져올 댓글 개수를 가져오도록 해봅시다. (원래는 이런식으로 코드를 작성하지는 않습니다...ㅎㅎ) ParseIntPipe 사용하는 것도 잊지 마시구요.

 

먼저 과제의 조건대로, CommentService에서 comment list를 리턴하는 함수를 만들어봅시다.

// comment.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class CommentService {
  async getCommentMainPage(): Promise<string> {
    return 'Comment Main Page';
  }

  async getCommentList(perPage: number, curPage: number): Promise<any> {
    return {
      perPage,
      curPage,
    };
  }
}

 

getCommentList라는 함수를 만들어주었습니다.

 

그러면 이제 다음 조건인 getPost함수에서 getCommentList함수를 사용하도록 해봅시다.

getCommentList함수를 사용하기 위해서는 CommentService를 PostController에 주입해줄 필요가 있습니다.

생성자의 인자에 CommentService를 적어줍시다.

기본적으로 Module, Controller, Provider(Service)는 Nest.js에서 관리를 해주기 때문에, 저런식으로 생성자에 적어주기만하면 자동으로 Nest.js가 의존성을 주입해줍니다. (즉, commentService = new CommentService() 와 동일하게 동작합니다)

// post.controller.ts
@Controller('post')
export class PostController {
  constructor(
    private readonly postService: PostService,
    private readonly commentService: CommentService,
  ) {}
  
  ...
}

 

그리고 getPost함수를 다음과 같이 수정해줍시다.

// post.controller.ts

  ...
  
  @Get('/:postId')
  async getPost(
    @Param('postId', ParseIntPipe) postId: number,
    @Query('perCommentPage', ParseIntPipe) perCommentPage: number,
    @Query('curCommentPage', ParseIntPipe) curCommentPage: number,
  ): Promise<any> {
    const commentList = await this.commentService.getCommentList(
      perCommentPage,
      curCommentPage,
    );

    return {
      title: `${postId} 번째 게시글입니다.`,
      content: `${postId} 번째 게시글의 내용입니다.`,
      commentList,
    };
  }

 

그리고 npm run start 명령어로 서버를 실행시켜보면...!

아래와 같은 오류가 뜰겁니다 ㅎㅎ

 

무엇이 문제일까요? 에러 메시지를 잘 한 번 읽어보시기 바랍니다.

다 읽어도 원인을 찾지 못했다면... 에러 메시지 읽는 연습을 해보는 걸 추천합니다. 계속 읽어보세요.

 

자, 그대로 읽으면 됩니다.

Nest can't resolve dependencies of the PostController (PostService, ?).

  • Nest가 PostController의 의존성을 해결하지 못했다는 메시지입니다.

여기까지는 뭐가 문제인지 정확히는 모르겠죠? 이제 Potential solutions쪽을 읽어봅시다.

잠재적인 해결책은 다음과 같다고 합니다.

  • PostModule이 유효한 NestJS module이니? - PostModule이 AppModule과 연결이 되어있니? 라는 뜻입니다. (AppModule에 직접적으로 명시되어있지는 않아도, 모듈간의 연결로 인해 연결되는 것도 연결이 되어있다고 생각하시면 됩니다. 즉, AppModule-B-C 면, AppModule과 C는 연결되어있으므로 C는 유효한 NestJS Module입니다) -> 우리는 AppModule에 PostModule을 명시했으므로 문제없습니다.
  • CommentService가 Provider면, PostModule에 포함되어 있니? - 이 부분이 문제입니다. 우리는 CommentService를 PostController에서 가져다 사용했기 때문에, CommentService를 PostModule에 등록을 해주어야합니다.
  • CommentService가 분리된 다른 Module에서 exported되어있다면, 해당 모듈이 PostModule에 imports되어 있니? - 이 부분도 마찬가지입니다. CommentService를 CommentModule에서 사용하고 있기 때문에 CommentModule에서 CommentService를 exports해주고, CommentModule을 PostModule에 등록해달라는 이야기입니다.

자 그러면 먼저, CommentModule에서 CommentService를 export해줍시다. 모듈에서 provider를 export해주어야 다른 서비스에서 해당 모듈이 가지고 있는 서비스를 사용할 수 있습니다. (만약, export하지 않는다면, 따로 provider를 생성해주어야 합니다)

// comment.module.ts
import { Module } from '@nestjs/common';
import { CommentService } from './comment.service';
import { CommentController } from './comment.controller';

@Module({
  controllers: [CommentController],
  providers: [CommentService],
  exports: [CommentService],
})
export class CommentModule {}

 

exports 부분에 CommentService를 추가해주었습니다.

그리고 PostModule에서 CommentModule을 import해줍시다.

// post.module.ts
import { Module } from '@nestjs/common';
import { PostService } from './post.service';
import { PostController } from './post.controller';
import { CommentModule } from '../comment/comment.module';

@Module({
  imports: [CommentModule],
  controllers: [PostController],
  providers: [PostService],
})
export class PostModule {}

 

이제 PostModule에서 CommentService를 사용할 때, CommentModule에 있는 CommentService를 참조하여 사용하게 됩니다.

 

그리고 서버를 실행시키면 정상 실행됩니다.

브라우저에 http://127.0.0.1:3000/post/1?perCommentPage=10&curCommentPage=2

를 입력하면 다음과 같은 화면을 볼 수 있습니다.

 

생각보다 간단합니다.


Prisma를 사용하여 Nest.js와 MySQL 연동


자, 이제 Nest.js에서 사용되는 ORM중 하나인 Prisma를 사용하여 MySQL을 연동해보도록 하겠습니다.

먼저, MySQL 세팅을 아래 포스팅에서 진행하였습니다.

https://suloth.tistory.com/198

 

시즌 2 #3. 기초부터 따라하는 Nest.js 2 : Express 배우기(2)

지난 시간에는 간단하게 express 서버 구축, router, query, param, body, middleware에 대해서 배웠습니다.https://suloth.tistory.com/197 시즌 2 #2. 기초부터 따라하는 Nest.js 2 : Express 배우기(1)지난 시간에는 HTTP Meth

suloth.tistory.com

 

 

그리고, 챕터5에서 우리가 프로젝트를 위해 DB 구조를 작성하였습니다.

https://suloth.tistory.com/200

 

시즌 2 #5. 기초부터 따라하는 Nest.js 2 : Nest.js 프로젝트 DB 구조 설명

지난 시간에는 Nest.js의 구조에 대해서 알아봤습니다.https://suloth.tistory.com/199 시즌 2 #4. 기초부터 따라하는 Nest.js 2 : 이제 Nest.js를 배워봅시다!지난 시간에는 Express와 MySQL을 연동하는 방법을 알아

suloth.tistory.com

 

 

먼저 아래 명령어를 통해 mysql 서비스가 실행중인지 확인해봅시다.

brew services list

 

그러면 현재 homebrew를 통해 실행되고 있는 서비스 목록이 나올텐데, mysql의 상태가 started가 아니라면 아래 명령을 입력해서 실행시켜줍시다.

brew services start mysql

 

 

Prisma를 사용하는 이유?

우리는 Prisma 라는 ORM을 사용해서 DB와 Nest.js 서버를 연결해줄 예정입니다.

 

Node.js 생태계에서 사용되는 ORM은 상당히 많습니다.

TypeORM, Prisma, Drizzle ORM, Kysely(쿼리빌더), MikroORM, Sequelize, Knex 등이 있지만, 이중에서 제일 자주 사용되는 ORM은 TypeORM과 Prisma입니다.

 

 

여기서 잠깐! ORM이 무엇인지 모른다면?

더보기

ORM이란 Object Relational Mapping의 약자로

우리가 프로그래밍할 때 사용하는 객체(Class)와 DB에서 사용하는 Table(Relation)을 Mapping하여 사용할 수 있도록 만든 라이브러리라고 생각하면 됩니다.

 

장점

SQL Query를 이용하지 않고 자신이 사용하는 프로그래밍 언어로 DB를 조작할 수 있다는 장점이 있습니다.

DBMS에 대한 종속성이 줄어들어 개발자는 DB의 테이블이 아닌, 객체(class)에 중점을 두고 프로그래밍을 할 수 있습니다.

코드의 재사용성이 높고 유지보수에 용이합니다. (객체의 재활용, 객체를 통한 명확한 코드의 이해)

타입스크립트의 경우, ORM을 통한 타입추론이 가능하기 때문에 편의성이 높다.

 

단점

raw query에 비해 상대적으로 속도가 느립니다.

복잡한 쿼리의 경우 ORM만으로는 구현하기 힘들 수 있고, 구현이 되더라도 성능이 좋지 않을 수도 있습니다.

데이터베이스에 대한 정밀한 제어를 할 수 없습니다. 기능지원 제한 등의 이유로 말이죠.

 

개인적으로 Prisma를 사용할 때, 자동적으로 타입 추론이 되어서 사용하기 편했고 Prisma에서 자체적으로 쿼리 결과에 대한 타입도 지원이 되어서 Prisma를 이용하여 강의를 진행하기로 결정했습니다.

현재는 현업에서 TypeORM을 많이 사용하지만, 추세가 Prisma로 넘어가는 추세이기도 해서 Prisma를 선택한 이유도 있습니다.

 

 


Prisma 초기설정


 

먼저, 항상 공식문서를 애용합시다 ㅎㅎ

아래는 프리즈마 공식문서 사이트입니다.

https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases-typescript-mysql

 

Start from scratch with Prisma ORM using TypeScript and MySQL (15 min) | Prisma Documentation

Learn how to create a new TypeScript project from scratch by connecting Prisma ORM to your MySQL database and generating a Prisma Client for database access.

www.prisma.io

 

여기에서는 이미 Nest.js 프로젝트 기본세팅이 되어있으니, prisma를 설치하고 설정파일관련 세팅만 진행하면 됩니다.

 

먼저, prisma 패키지를 설치합니다. prisma는 오직 db랑 데이터를 동기화 시킬 때만 사용하기 때문에 개발 의존성(devDependency)로 넣어줍시다. --save-dev 옵션을 넣어주면 개발환경에서만 필요한 라이브러리임을 package.json의 devDependencies에 명시해줍니다.

npm install prisma --save-dev

 

그리고 prisma 관련 설정 파일을 만들어줍시다.

npx prisma init

 

여기서 잠깐!! 갑자기 npx가 뜬금없이 왜 나오죠?

더보기

npx는 간단히 말하면, 프로젝트 내의 패키지 명령어를 사용할 수 있게 해주는 명령어라고 보시면 됩니다.

우리가 npm으로 prisma를 install했지만, prisma라는 명령어를 터미널에 입력하면 없는 명령어라고 나오게 됩니다.

터미널에서 prisma를 명령어로 입력하기 위해서는 prisma를 전역적으로 (--global 옵션을 사용해서) 설치해야하는데, 프로젝트에서만 사용되는 prisma를 굳이 전역적으로 설치할 필요도 없고, 오히려 공간을 낭비할 수 있기 때문입니다.

 

그래서 npx라는 명령어를 이용하여 프로젝트 내의 prisma라는 패키지의 명령어를 사용할 수 있게 만들어 놓은 것입니다.

 

다음과 같은 화면이 보여야합니다. 여기서 Next steps 부분만 잘 따라가셔도 됩니다.

 

그러면 이렇게 prisma 폴더가 생기고 그 안에 schema.prisma 파일이 생길겁니다. (프로젝트 최상위에 .env파일도 생깁니다)

 

그러면 이제 schema.prisma 파일을 다음과 같이 수정을 해줍시다.

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

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

 

우리는 mysql을 사용할 것이기 때문에 datasource db의 provider 부분을 mysql로만 바꿨습니다.

그리고 url은 .env파일의 DATABASE_URL 이라는 녀석의 값을 가져오게 됩니다.

 

그러면 .env 파일로 가볼까요? (만약 생성되지 않았다면, 프로젝트 폴더 최상위에 .env 파일을 생성해주세요)

DATABASE_URL="mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/DATABASE_NAME"

 

위와 같이 .env파일을 작성해주세요.

 

예를들면, 다음과 같이 작성하시면 됩니다. 저의 경우 MySQL의 Username은 root이고, 비밀번호는 없으므로 비워놨구요. Host는 로컬에서 MySQL을 실행하고 있기 때문에 localhost로 설정하고, Port는 MySQL의 기본포트인 3306, DB_NAME은 우리가 이전에 study라는 이름의 DB를 만들었기 때문에 study로 해주었습니다.

DATABASE_URL="mysql://root:@localhost:3306/study"

 

 

여기서 중요한 점!

.env 파일의 경우 보안에 민감한 정보들이 담겨있기 때문에 github에 올라가서는 안됩니다.

이를 방지하기 위해 .gitignore 파일에 (없다면 프로젝트 최상위 폴더에 생성해주세요) .env를 추가해주어야 합니다.

Nest.js는 미리 .gitignore 파일을 생성해주기 때문에 해당 파일에 .env를 추가해줍시다.

# compiled output
/dist
/node_modules

# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# OS
.DS_Store

# Tests
/coverage
/.nyc_output

# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

# env
.env

 

 

 

Prisma로 DB 스키마 작성하기

schema.prisma 파일에 DB 스키마를 작성해두면 (= DB 테이블에 관련된 코드를 작성해놓으면)

prisma 명령을 통해 DB에 해당 스키마(테이블)를 바로 적용할 수 있습니다. (즉, 코드로 작성된 테이블 구조가 실제로 MySQL에서 생성된다는 뜻)

 

우리가 챕터 5에서 작성한 DB 구조는 다음과 같습니다.

 

 

위 구조에 맞게 schema.prisma 파일을 작성해봅시다.

아! schema.prisma를 작성하기 전에, 유용한 익스텐션 하나를 소개해드리겠습니다.

더보기

vscode의 extension 탭에서 prisma를 검색해서 다운로드 받으면 됩니다! schema.prisma 파일의 자동완성 기능을 지원해줍니다.

뭔지 잘 모르겠으면... 일단 깔아봅시다 ㅎㅎ

 

다음과 같이 작성해줍시다.

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

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

model User {
  id Int @id @default(autoincrement())
  email String @unique
  password String
  nickname String @unique

  createdAt DateTime @db.Timestamp(3) @default(now())
  updatedAt DateTime? @db.Timestamp(3) @updatedAt()
  deletedAt DateTime? @db.Timestamp(3)

  post Post[]
  comment Comment[]
}

model Post {
  id Int @id @default(autoincrement())
  title String
  content String

  createdAt DateTime @db.Timestamp(3) @default(now())
  updatedAt DateTime? @db.Timestamp(3) @updatedAt()
  deletedAt DateTime? @db.Timestamp(3)

  userId Int

  user User @relation(fields: [userId], references: [id])

  comment Comment[]
}

model Comment {
  id Int @id @default(autoincrement())
  content String

  createdAt DateTime @db.Timestamp(3) @default(now())
  updatedAt DateTime? @db.Timestamp(3) @updatedAt()
  deletedAt DateTime? @db.Timestamp(3)

  parentId Int?
  userId Int
  postId Int

  parent Comment? @relation("ParentChild", fields: [parentId], references: [id])
  user User @relation(fields: [userId], references: [id])
  post Post @relation(fields: [postId], references: [id])

  child Comment[] @relation("ParentChild")
}

 

일단 이렇게 작성해줍시다. 문법이 궁금하다면, Prisma 공식문서에서 찾아보시기 바랍니다..

간단하게 설명을 하자면,

// 테이블 구조 정의
model 테이블명 {
  칼럼명 타입(Prisma문법의 타입) @옵션 @옵션2 ... 
  
  // ex
  title String @unique // @unique 옵션을 사용해서 유니크한 속성임을 명시한다.
  content String? // 타입뒤에 ?를 붙여서 nullable한 속성임을 명시한다.
  
  // 외래키가 있다면
  속성명 "외래키가 속한 테이블명" @relation(fields: [현재 테이블에서 사용되는 외래키의 칼럼명], references: [외래키의 테이블에서 사용되는 외래키의 칼럼명])
}

 

대충 이런 식이라고 생각하시면 됩니다. 제가 작성해놓은 schema.prisma 파일만 잘 분석해도 어떤식으로 작성하는지 감이 올겁니다...!

 

이제 schema.prisma에 작성한 모델의 스키마를 이용하여 DB에 테이블을 생성해봅시다.

아래 명령어만 입력하면 됩니다!

npx prisma db push

중간에 warning이 뜬다면 y를 눌러서 진행해주시면 됩니다.

 


테이블 생성 확인


 

테이블이 잘 생성되었는지 확인해봅시다.

 

먼저, 터미널에서 확인하는 방법입니다.

터미널을 켜주세요. (brew services list 명령을 통해 mysql이 실행되고 있는지 확인도 해주세요)

아래 명령을 통해 mysql에 접속을 해주시기 바랍니다.

mysql -u <유저네임> -p

 

 

그리고 다음 명령을 차례대로 입력해주세요.

use study;

show tables;

desc User;
desc Post;
desc Comment;

 

정상적으로 출력이 된다면, 잘 생성된겁니다!

 

MySQL Workbench에서 확인하는 방법입니다.

MySQL Workbench를 실행해주세요.

 

그리고 MySQL Connections 옆에 + 버튼을 눌러주세요.

 

 

다음과 같이 설정을 입력해주고 OK를 누르면 됩니다.

 

이때, Connection Name은 자신이 식별하기 편한 이름을 아무거나 지어주면 되고

Password의 Store in Keychain에 비밀번호를 입력해두면 됩니다.

 

 

그러면 이런식으로 네모 박스가 생기는데, 이를 더블클릭해서 DB에 접속할 수 있습니다.

 

 

여기서 Schemas 탭을 들어가면 다음과같이 DB목록이 나오는데, 우리는 study를 DB에 테이블을 생성하였으니 들어가봅시다.

그러면 테이블이 뜨는데, 여기서 Comment 옆에 느낌표와 스패너 모양 옆에 번개가 붙은 표 모양을 클릭하면

 

다음과 같이 테이블 구조와 어떠한 데이터가 들어있는지가 나옵니다...!

 

 


Prisma Client 설정


Prisma를 이용하여 DB의 스키마를 정의해주고 DB에 테이블 생성까지 했으니, 이제 DB를 Nest.js에서 사용할 수 있게끔 만들어봅시다.

여기서는 Prisma Client라는 패키지가 필요합니다.

@prisma/client 패키지를 설치해줍시다.

npm install @prisma/client

 

 

그리고 src폴더에 databases폴더를 만들고, 그 안에 prisma폴더를 만들어준 후 prisma.service.ts파일을 만들어줍시다.

 

prisma.service.ts 파일을 다음과 같이 작성해주도록 합시다.

// databases/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 enableShutdownHooks() {
    process.on('beforeExit', async () => {
      await this.$disconnect();
    });
  }
}

 

이 부분은 그냥 "아, 이렇게 작성하면 되는구나~" 하고 넘어가셔도 됩니다.

궁금하신 분들을 위해 코드를 설명하자면, (이해 안되면 일단 넘어가세요!)

1. OnModuleInit이라는 Interface를 implements 해서 onModuleInit 메서드를 구현하게끔 만들었습니다.

2. onModuleInIt 메서드를 통해 PrismaService가 초기화되면(인스턴스가 생성되면) $connect함수를 통해 DB와 연결되도록 하였습니다.

3. enableShutdownHooks 메서드를 통해 process가 종료되기 전에 DB와 연결을 끊도록 하였습니다. (이 메서드는 main.ts에서 사용됩니다)

 

prisma.service.ts파일을 작성했다면, PrismaService를 관리하는 모듈도 필요하겠죠? 하나 만들어보겠습니다.

// databases/prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Global()
@Module({
  imports: [],
  controllers: [],
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './res/user/user.module';
import { PostModule } from './res/post/post.module';
import { CommentModule } from './res/comment/comment.module';
import { PrismaModule } from './databases/prisma/prisma.module';

@Module({
  imports: [PrismaModule, UserModule, PostModule, CommentModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 

@Global 데코레이터를 통해서, 한 번만 선언해도 글로벌로 적용되도록 만들어주었습니다. (이해 안되면 일단 넘어가셔도 됩니다)

그리고 app.module.ts에 PrismaModule을 등록해주었습니다.

 

 

prisma.module.ts파일을 작성했다면, main.ts파일을 다음과 같이 작성해줍시다.

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { PrismaService } from './databases/prisma/prisma.service';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const prismaService = app.get(PrismaService);
  await prismaService.enableShutdownHooks();

  await app.listen(3000);
}
bootstrap();

 

중간에 prisma관련 코드만 추가되었습니다.

 

 

이제 PrismaService를 이용해서 DB에 쿼리를 날릴 수 있습니다!

 

PrismaService를 이용하는 부분은 다음 포스팅에서 다루도록 하겠습니다.

 

이상으로 Prisma와 DB연결 포스팅을 마치겠습니다!