9/6 Node.js 숙련 1주차 (2)

성준호·2024년 9월 6일
0

1. Raw Query 시작하기

Raw Query는 데이터베이스에 SQL을 이용하여 직접 쿼리를 요청하는 것

1) Raw Query 라이브러리 설치

# yarn으로 프로젝트를 초기화합니다.
yarn init -y

# express와 mysql 드라이버를 설치합니다.
yarn add express mysql2
  • mysql2 라이브러리
    MySQL 데이터베이스를 Node.js에서 사용할 수 있게 도와주는 라이브러리이다.
    데이터베이스와 개발 언어 사이를 연결해주기 때문에 데이터베이스 드라이버라고도 불린다.

2) 데이터베이스 연결하기

// app.js

import express from 'express';
import mysql from 'mysql2';

const connect = mysql.createConnection({
  host: 'express-database.clx5rpjtu59t.ap-northeast-2.rds.amazonaws.com', // AWS RDS 엔드포인트
  user: 'root', // AWS RDS 계정 명
  password: 'aaaa4321', // AWS RDS 비밀번호
  database: 'express_db', // 연결할 MySQL DB 이름
})
const app = express();
const PORT = 3017;

app.use(express.json());

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});
  • host

    • mysql2 데이터베이스 트라이버가 접속할 데이터베이스의 주소를 나타낸다.
  • user

    • AWS RDS 데이터베이스의 계정 명을 나타낸다.
  • password

    • AWS RDS 데이터베이스의 비밀번호를 나타냅니다.
  • database

    • AWS RDS 데이터베이스의 DB명을 나타낸다.

이외에도 timezone으로 시간대를 설정하거나, ssl로 ssl 인증서를 설정하는 것과 같은 옵션 설정을 할 수 있다.

3) 테이블 생성 API

// app.js

/** 테이블 생성 API **/
app.post('/api/tables/', async (req, res, next) => {
  const { tableName } = req.body;

  await connect.promise().query(`
      CREATE TABLE ${tableName}
      (
          id        INT         NOT NULL AUTO_INCREMENT PRIMARY KEY,
          name      VARCHAR(20) NOT NULL,
          createdAt DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP
      )`);

  return res.status(201).json({ message: '테이블 생성에 성공하였습니다.' });
});

mysql2 라이브러리에서 Raw Query는 connect.promise().query() 형식으로 사용한다.

4) 테이블 목록 조회 API

// app.js


/** 테이블 조회 API **/
app.get('/api/tables', async (req, res, next) => {
  const [tableList] = await connect.promise().query('SHOW TABLES');
  const tableNames = tableList.map(table => Object.values(table)[0]);

  return res.status(200).json({ tableList: tableNames });
});

Raw Query의 결과값을 const [tableList]의 형태로 할당하는 이유

Raw Query를 사용할 때, 데이터를 생성하는 명령어의 경우 반환하는 값이 존재하지 않지만, SHOW TABLES 또는 SELECT 문법의 조회 명령어는 반환값이 존재한다.
mysql2의 경우 Raw Query를 이용하여 조회된 결과값은 배열의 첫번째에 할당된다. 그렇기 때문에 배열 구조 분해 할당 문법을 이용해 배열의 첫번째 값만 tableList 변수에 할당하게 된다.

5) 데이터 삽입 API

// app.js

/** 데이터 삽입 API **/
app.post('/api/tables/:tableName/items', async (req, res, next) => {
  const { tableName } = req.params;
  const { name } = req.body;

  await connect.promise().query(`
      INSERT INTO ${tableName} (name)
      VALUES ('${name}')`);
  return res.status(201).json({ message: '데이터 생성에 성공하였습니다.' });
});

클라이언트로부터 Params로 전달받은 tableName에 해당하는 테이블에 Body 데이터인 name을 삽입한다

6) 데이터 조회 API

// app.js

/** 데이터 조회 API **/
app.get('/api/tables/:tableName/items', async (req, res, next) => {
  const { tableName } = req.params;

  const [itemList] = await connect.promise().query(`
      SELECT id, name, createdAt
      FROM ${tableName}`);

  return res.status(200).json({ itemList: itemList });
});

Params로 전달받은 tableName에 해당하는 테이블의 모든 데이터를 조회하는 API

2. Prisma 시작하기

1) Prisma 라이브러리 설치하기

# yarn 프로젝트를 초기화합니다.
yarn init -y

# express, prisma, @prisma/client 라이브러리를 설치합니다.
yarn add express prisma @prisma/client

# nodemon 라이브러리를 DevDependency로 설치합니다.
yarn add -D nodemon

# 설치한 prisma를 초기화 하여, prisma를 사용할 수 있는 구조를 생성합니다.
npx prisma init
  • prisma는 Prisma를 터미널에서 사용할 수 있도록 도구를 설치하는 패키지이다.
  • @prisma/client는 Node.js에서 Prisma를 사용할 수 있게 한다.
  • nodemon은 개발 코드가 변경되었을 때 자동으로 서버 재시작을 해주는 패키지이다.

폴더 구조

npx prisma init

프로젝트 폴더
├── prisma
│   └── schema.prisma
├── .env
├── .gitignore
├── package.json
└── yarn.lock
  • prisma.schema 파일
    Prisma가 사용할 데이터베이스를 설정하기 위해 사용하는 파일

  • .env 파일
    외부에 공유되어선 안 되는 비밀 정보들이 저장되는 파일

  • .gitignore 파일
    .env 파일이 git에 업로드되지 않도록 설정돼있다.

nodemon 명령어

# 형식
nodemon <실행할 JavaScript 파일명>

# nodemon을 이용해 app.js 파일 실행하기
nodemon app.js

터미널 명령어로 사용하는 것뿐만 아니라, package.jsonnodemon을 이용하여 서버를 실행하는 스크립트를 등록하여 간편하게 서버를 시작할 수 있다.

// package.json

...

"scripts": {
	"dev": "nodemon app.js"
},

이제 터미널에서 yarn run dev 명령어를 실행하면, nodemon을 이용하여 서버를 시작할 수 있다.

2) schema.prisma

Prisma를 처음 초기화했을 때, prisma.schema 파일에 아래 두 가지 구문이 작성된다.

  • datasource

    • 어떤 데이터베이스 엔진을 사용할 것인지, 데이터베이스의 위치(URL)는 어디인지 등의 정보 정의
  • generator

    • Prisma 클라이언트를 생성하는 방식을 설정한.

Prisma datasource

datasource 프로퍼티에 정의된 속성들을 수정하여 사용자 아이디, 비밀번호, 엔드 포인트 등 다양한 설정값을 입력해주어야 한다.

// schema.prisma

datasource db {
  // MySQL 데이터베이스 엔진을 사용합니다.
  provider = "mysql"
  // 데이터베이스 연결 정보를 .env 파일의 DATABASE_URL 로부터 읽어옵니다.
  url      = env("DATABASE_URL")
}
  • provider: Prisma가 사용할 데이터베이스 엔진
  • url: 데이터베이스를 연결하기 위한 URL

url 부분에서 데이터베이스의 주소가 노출되지 않게 작성하는 dotenv 문법을 사용하고 있다. 이는 .env 파일에 정의된 정보를 schema.prisma 파일로 불러온다.

# .env 파일

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

데이터베이스 URL

데이터베이스 URL은 크게 네 가지로 나뉜다.

  • Protocal

    • Prisma가 사용할 데이터베이스 엔진
    • postgresql, sqllite, mysql
  • Base URL

    • 데이터베이스의 엔드 포인트와 아이디, 패스워드, 포트 번호를 나타낸다.
    • <Id>:<Password>@<RDS Endpoint>:<Port>의 형태
  • Path

    • MySQL에서 사용할 데이터베이스 이름을 설정하는 구성요소
  • Argument

    • Prisma에서 데이터베이스 연결을 설정하는데 필요한 추가 옵션을 나타낸다.
    • 데이터베이스와 연결할 수 있는 최대 커넥션 개수, 타임아웃 시간 등

Prisma model

  • Prisma가 사용할 데이터베이스 테이블 구조를 정의
  • schema.prisma 파일의 model에 작성된 정보를 바탕으로 MySQL의 테이블을 조작할 수 있게 된다.

Products 테이블 예시

// schema.prisma

model Products {
  productId   Int     @id @default(autoincrement()) @map("productId")
  productName String  @unique @map("productName")
  price       Int     @default(1000) @map("price")
  info        String? @map("info") @db.Text

  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  @@map("Products")
}
  • Prisma에서 다양한 데이터 유형을 지원하는데, 위 예시에선 Int, String, DateTime 등이 쓰였다.
  • 데이터 유형 뒤에 ?가 붙으면 NULL을 허용하는 컬럼이 된다.
  • @@map("Product")Product 테이블을 MySQL에서도 Product란 이름으로 사용하겠단 뜻이다. 작성하지 않으면 테이블명이 소문자로 치환된다.

3) Prisma DB, Table 생성

# schema.prisma 파일에 설정된 모델을 바탕으로 MySQL에 정보를 업로드합니다.
npx prisma db push
  • prisma db push

    • schema.prisma 파일에 정의된 설정값을 실제 데이터베이스에 반영
    • 내부적으로 prisma generate가 실행된다.
    • 데이터베이스 구조를 변경하거나, 새로운 테이블을 생성할 수 있다.
  • prisma init

    • Prisma를 사용하기 위한 초기 설정을 생성한다.
    • schema.prisma 파일과 같은 필요한 설정 파일들이 생성된다.
  • prisma generate

    • Prisma Client를 생성하거나 업데이트한다.
    • schema.prisma 파일에 변경 사항이 생겼거나, 데이터베이스 구조가 변경되었을 때, 이명령어를 사용해 Prisma Client를 최신 상태로 유지할 수 있다.
  • prisma db pull

    • 현재 연결된 데이터베이스 구조를 prisma.schema 파일로 가져온다.
    • 데이터베이스에서 구조 변경이 발생했을 때, 이 명령어를 사용하면 Prisma Schema를 최신 상태로 유지할 수 있다.
    • 이후 prisma generate 명령어를 사용해 변경 사항을 Prisma Client에 반영할 수 있다.

4) Prisma Client

modelgenerate하면 해당 모델에 대한 정보가 node_modules 폴더 내에 있는 Prisma Client에 전달된다.

3. Prisma Method

1) Prisma Method 살펴보기

findMany(), findFirst(), findUnique() 등 다양한 메서드를 지원한다.


Posts 테이블은 게시글 제목(title), 내용(content), 비밀번호(password)총 3개의 컬럼을 가지고 있고, postId, createdAt, updatedAt 컬럼은 아무런 데이터를 입력하지 않더라도 기본값을 가질 수 있도록 구성되어 있다.
게시글을 생성 및 수정할 때 필수 인자값 3개를 이용해 권한 검증 및 데이터 생성을 구현할 수 있다.

먼저 routes/posts.router.js 파일을 생성하고, express 프로젝트를 초기화한다.

routes/posts.router.js

// routes/posts.router.js

import express from 'express';
import { PrismaClient } from '@prisma/client';

const router = express.Router(); // express.Router()를 이용해 라우터를 생성합니다.
const prisma = new PrismaClient({
  // Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해줍니다.
  log: ['query', 'info', 'warn', 'error'],

  // 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해줍니다.
  errorFormat: 'pretty',
}); // PrismaClient 인스턴스를 생성합니다.



export default router;

app.js

// app.js

import express from 'express';
import PostsRouter from './routes/posts.router.js';

const app = express();
const PORT = 3017;

app.use(express.json());
app.use('/api', [PostsRouter]);

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});

2) 게시글 생성 API

게시글 생성 API의 비즈니스 로직
1. title, content, passwordbody로 전달받는다.
2. title, content, password를 이용해 Posts 테이블에 데이터를 삽입 한다.
3. 생성된 게시글을 반환한다.

// routes/posts.router.js

// 게시글 생성
router.post('/posts', async (req, res, next) => {
  const { title, content, password } = req.body;
  const post = await prisma.posts.create({
    data: {
      title,
      content,
      password,
    },
  });

  return res.status(201).json({ data: post });
});

3) 게시글 조회 API

게시글 목록 조회 API의 경우 게시글의 내용(content)을 제외하고,
게시글 상세 조회 API일 경우에만 게시글의 전체 내용이 출력되도록 구현

게시글 목록 조회 API

// routes/posts.router.js

/** 게시글 전체 조회 API **/
router.get('/posts', async (req, res, next) => {
  const posts = await prisma.posts.findMany({
    select: {
      postId: true,
      title: true,
      createdAt: true,
      updatedAt: true,
    },
  });

  return res.status(200).json({ data: posts });
});

게시글 상세 조회 API

// routes/posts.router.js

/** 게시글 상세 조회 API **/
router.get('/posts/:postId', async (req, res, next) => {
  const { postId } = req.params;
  const post = await prisma.posts.findFirst({
    where: { postId: +postId },
    select: {
      postId: true,
      title: true,
      content: true,
      createdAt: true,
      updatedAt: true,
    },
  });

  return res.status(200).json({ data: post });
});
  • 게시글 목록 조회
    findMany() 메서드로 Posts 테이블이 가진 모든 데이터들을 배열 형태로 조회하였다.

  • 게시글 상세 조회
    findFirst() 메서드를 이용해 Posts 테이블에 특정한 데이터 1개를 조회하였다.

  • +postId
    +postId는 변수명 앞에 + 연산자가 붙은 경우, 문자열 타입을 숫자형 타입으로 변환해준다.

4) 게시글 수정 API

게시글 수정 API의 비즈니스 로직
1. Path Parameters로 어떤 게시글을 수정할 지 postId를 전달받습니다.
2. 변경할 title, content와 권한 검증을 위한 passwordbody로 전달받습니다.
3. postId를 기준으로 게시글을 검색하고, 게시글이 존재하는지 확인합니다.
4. 게시글이 조회되었다면 해당하는 게시글의 password가 일치하는지 확인합니다.
5. 모든 조건을 통과하였다면 게시글을 수정합니다.

// routes/posts.router.js

/** 게시글 수정 API **/
router.put('/posts/:postId', async (req, res, next) => {
  const { postId } = req.params;
  const { title, content, password } = req.body;

  const post = await prisma.posts.findUnique({
    where: { postId: +postId },
  });

  if (!post)
    return res.status(404).json({ message: '게시글이 존재하지 않습니다.' });
  else if (post.password !== password)
    return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });

  await prisma.posts.update({
    data: { title, content },
    where: {
      postId: +postId,
      password,
    },
  });

  return res.status(200).json({ data: '게시글이 수정되었습니다.' });
});

5) 게시글 삭제 API

게시글 삭제 API의 비즈니스 로직
1. Path Parameters로 어떤 게시글을 수정할 지 postId를 전달받습니다.
2. 권한 검증을 위한 passwordbody로 전달받습니다.
3. postId를 기준으로 게시글을 검색하고, 게시글이 존재하는지 확인합니다.
4. 게시글이 조회되었다면 해당하는 게시글의 password가 일치하는지 확인합니다.
5. 모든 조건을 통과하였다면 게시글을 삭제합니다.

// routes/posts.router.js

/** 게시글 삭제 API **/
router.delete('/posts/:postId', async (req, res, next) => {
  const { postId } = req.params;
  const { password } = req.body;

  const post = await prisma.posts.findFirst({ where: { postId: +postId } });

  if (!post)
    return res.status(404).json({ message: '게시글이 존재하지 않습니다.' });
  else if (post.password !== password)
    return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });

  await prisma.posts.delete({ where: { postId: +postId } });

  return res.status(200).json({ data: '게시글이 삭제되었습니다.' });
});

6) Prisma 리팩토링

PrismaClient는 Prisma를 사용하여 실제 데이터베이스와의 연결을 관리하는 객체이다.
new PrismaClient()를 이용해 Prisma를 사용할 수 있도록 인스턴스를 생성한다.

const prisma = new PrismaClient();

현재는 게시글(Posts) 라우터만 구현했지만, 이후에 사용자, 사용자 정보, 해시 태그 등의 라우터가 추가된다면 각각의 라우터 개수마다 데이터베이스와 연결하게 되는 문제가 생긴다.
이런 문제를 해결하기 위해 /utils/prisma/index.js 파일을 구현하여 하나의 파일에서 데이터베이스 커넥션을 관리하여 최초 1번만 MySQL과 커넥션을 생성한다.

utils/prisma/index.js 리팩토링

// utils/prisma/index.js

import { PrismaClient } from '@prisma/client';

export const prisma = new PrismaClient({
  // Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해줍니다.
  log: ['query', 'info', 'warn', 'error'],

  // 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해줍니다.
  errorFormat: 'pretty',
}); // PrismaClient 인스턴스를 생성합니다.

routes/posts.router.js 리팩토링

// routes/posts.router.js

import { prisma } from '../utils/prisma/index.js';
profile
안녕하세요

0개의 댓글