본문 바로가기
내일배움 강의/강의- Node.js 입문, 숙련

Node.js 숙련주차 3.4 ORM과 Prisma

by GREEN나무 2024. 11. 25.
728x90

1. Raw Query와 ORM의 차이점을 알아봅니다. 
2. Prisma를 구성하는 schema.prisma 파일을 알아봅니다.
3. Prisma의 대표적인 Method를 이용하여 CRUD 기능을 구현해봅니다.

 

01. Raw Query 시작하기

1) Raw Query란?  

= 원시쿼리

Raw Query는 데이터베이스에 SQL(Structured Query Language)을 이용하여 직접 쿼리(Query)를 요청하는 것을 뜻합니다.
Raw Query는 이전에 배웠던 SQL을 Node.js에서 사용하여 데이터베이스에 쿼리(Query)를 요청할 수 있는 방법입니다. SQL만 알고 있더라도 다양한 데이터베이스에 연결하여 테이블을 생성하거나 데이터를 조회하는 등 다양하게 데이터베이스와 상호작용을 할 수 있습니다.
더불어, 엄청나게 긴 쿼리를 수행하거나 트랜잭션을 직접적으로 관리하는 등 데이터베이스가 지원하는 대다수의 기능을 SQL만으로 간편하게 사용할 수 있는 아주 엄청난 장점을 가지고 있습니다.

2) Raw Query 시작하기

 폴더를 생성, 라이브러리 설치, 프로젝트 구성
Node.js에서 Raw Query를 사용하기 위해서는 AWS RDS에서 대여받은 MySQL에 연결을 도와주는 데이터베이스 드라이버가 필요합니다. 이번에는 데이터베이스에 직접 SQL를 요청하여, 테이블을 생성하거나, 데이터를 삽입하는 다양한 API를 mysql2 라이브러리를 이용하여 구현해보도록 하겠습니다.


📚  Raw Query API 명세서

기능 Methoed API URL Request Response
테이블 생성 POST /api/tables { "tableName": "rawQueryTable" } { "message": "테이블 생성에 성공하였습니다." }
테이블 목록 조회 GET /api/tables { } { "tableList": [ "rawQueryTable", "rawQueryTable1" ] }
데이터 삽입 POST /api/tables/:tableName/items { "name":"Hello" } { "message": "데이터 생성에 성공하였습니다." }
데이터 조회 GET /api/tables/:tableName/items { } { "itemList": [ { "id": 1, "name": "Hello", "createdAt": "2024-01-01T04:17:57.000Z" }, { "id": 2, "name": "Hello", "createdAt": "2024-01-01T04:17:57.000Z" }, { "id": 3, "name": "Hello", "createdAt": "2024-01-01T04:17:58.000Z" } ] }

POST  localhost:3018/api/tables/:tableName/items : /api/tables안에서 params에 특정 테이블 하나를 /:tableName 이런식으로 tableName 데이터를 전달받아서 거기에 해당하는 테이블 내에 items를 집어넣는 API 입니다.


 [Raw Query] 라이브러리 설치하기
package.json 파일에 "type":"module" 설정하기

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

# express와 mysql 드라이버를 설치합니다.
yarn add express mysql2


mysql2 라이브러리 는 우리가 Node.js 에서 MySQL을 사용 수 있게 데이터베이스와 개발 언어 사이를 연결해주는   데이터베이스 드라이버의 역할을 담당합니다. 데이터베이스 드라이버라는 이름으로도 불립니다.

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

Node.js 에서 AWS RDS에서 대여받은 MySQL과 연결하기 위해 mysql2 라이브러리를 사용할 예정입니다.

AWS RDS :  https://ap-northeast-2.console.aws.amazon.com/rds?region=ap-northeast-2

 

app.js 파일을 만들고, mysql2 라이브러리를 사용하여 AWS RDS의 MySQL과 연결을 설정해야 합니다.

[Raw Query] app.js 데이터베이스 연결하기
mysql2의 createConnection 속성은 여러분들이 대여받은 AWS RDS 정보로 변경야합니다.
port는 기본값이 3306으로 설정되어 있기 때문에, 설정하지 않으셔도 됩니다. 

// 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, '포트로 서버가 열렸어요!');
});

database: "express_db", // 연결할 MySQL DB 이름

mysql2 데이터베이스 연결 속성 알아보기

mysql2 라이브러리에서 데이터베이스를 연결하기 위한 속성은 크게 4가지로 구성됩니다.
◆ host
   mysql2 데이터베이스 드라이버가 접속할 데이터베이스의 주소를 나타냅니다.
◆ user
   AWS RDS 데이터베이스의 계정 명을 나타냅니다.
◆ password
   AWS RDS 데이터베이스의 비밀번호를 나타냅니다.
◆ database
   AWS RDS 데이터베이스의 DB 명을 나타냅니다.
이 외에도, timezone으로 시간대를 설정하거나, ssl로 SSL 인증서를 설정하는 것과 같은 다양한 옵션을 설정할 수 있습니다. 

4) 테이블 생성 API

Raw Query를 이용하여 테이블을 생성하는 AP
테이블을 생성하는 명령어 CREATE TABLE
클라이언트로부터 생성할 테이블 이름을 tableName으로 전달받아, 새로운 테이블을 생성해보도록 하겠습니다.
테이블 생성 API의 테이블 구조

Name Type NULL 제약조건 default
id(PK) INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT
name STRING NOT NULL    
createdAT DATETIME NOT NULL   현재 시간


[Raw Query] app.js 테이블 생성 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: '테이블 생성에 성공하였습니다.' });
});
더보기
// app.js

import express from "express";
import mysql from "mysql2";

// AWS RDS설정과 동일해야합니다.
const connect = mysql.createConnection({
    host: "express-database.cvyiaeceu603.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());

/** 테이블 생성 API **/
app.post("/api/tables/", async (req, res, next) => {
    const { tableName } = req.body;
    // connect.promise : promise로 반환하도록 구현
    // 여기에 쿼리 구현
    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: "테이블 생성에 성공하였습니다." });
});
app.listen(PORT, () => {
    console.log(PORT, "포트로 서버가 열렸어요!");
});


mysql2 라이브러리에서 Raw Query connect.promise().query() 형식으로 사용합니다. 여기서, 데이터 정의어(DDL), 데이터 조작어(DML) 등 다양한 SQL 명령어를 사용할 수 있습니다.

5) 테이블 목록 조회 API

테이블을 조회하는 명령어 SHOW TABLES  : 현재까지 생성된 여러가지의 테이블을 확인할 수 있습니다.

[Raw Query] app.js 테이블 목록 조회 API

// app.js

/** 테이블 조회 API **/
app.get("/api/tables", async (req, res, next) => {
    // SHOW TABLES  : 현재까지 생성된 여러가지의 테이블을 조회
    const [tableList] = await connect.promise().query("SHOW TABLES");
    const tableNames = tableList.map((table) => Object.values(table)[0]); // 하나만 뽑아냄 

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

 

더보기
/** 테이블 조회 API **/
app.get("/api/tables", async (req, res, next) => {
    // SHOW TABLES  : 현재까지 생성된 여러가지의 테이블을 조회
    const [tableList] = await connect.promise().query("SHOW TABLES");
    //const tableNames = tableList.map((table) => Object.values(table)[0]); // 하나만 뽑아냄

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

// app.js

import express from "express";
import mysql from "mysql2";

// AWS RDS설정과 동일해야합니다.
const connect = mysql.createConnection({
    host: "express-database.cvyiaeceu603.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());

/** 테이블 생성 API **/
app.post("/api/tables/", async (req, res, next) => {
    const { tableName } = req.body;
    // connect.promise : promise로 반환하도록 구현
    // 여기에 쿼리 구현
    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: "테이블 생성에 성공하였습니다." });
});

/** 테이블 조회 API **/
app.get("/api/tables", async (req, res, next) => {
    // SHOW TABLES  : 현재까지 생성된 여러가지의 테이블을 조회
    const [tableList] = await connect.promise().query("SHOW TABLES");
    // 모든 테이블의 값이 .map((table)의 table에 들어간다. 모든 테이블의 값 중에 첫번째 하나를 tableLis에 할당함
    const tableNames = tableList.map((table) => Object.values(table)[0]); // 하나만 뽑아냄

    // 테이블 이름 목록을 JSON 형태로 클라이언트에게 응답합니다.
    return res.status(200).json({ tableList: tableNames });
});

/** 데이터 삽입 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: "데이터 생성에 성공하였습니다." });
});

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

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

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

6) 데이터 삽입 API

생성된 테이블의 데이터를 할당하는 데이터 삽입 API를 구현
테이블에 데이터를 삽입하는 SQL  : INSERT INTO 
Raw Query] app.js 데이터 삽입 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: '데이터 생성에 성공하였습니다.' });
});

 

7) 데이터 조회 API

테이블의 데이터를 조회하는 API를 구현해보도록 하겠습니다.
테이블 내에 존재하는 데이터를 조회하는 쿼리는 어떤것이었을까요? 바로 SELECT 문법입니다. 해당하는 명령어는 이제부터 여러분들이 가장 많이 사용하게 될 것이고, 연관관계, 서브 쿼리, 인덱싱 처리와 같이 배울거리가 많이 존재하는 문법입니다.
그렇다면, 마지막으로 Params로 전달받은 tableName 에 해당하는 테이블의 모든 데이터를 조회하는 API를 구현해보도록 할까요?
[코드스니펫] [Raw Query] app.js 데이터 조회 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 });
});
더보기
// app.js

import express from "express";
import mysql from "mysql2";

// AWS RDS설정과 동일해야합니다.
const connect = mysql.createConnection({
    host: "express-database.cvyiaeceu603.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());

/** 테이블 생성 API **/
app.post("/api/tables/", async (req, res, next) => {
    const { tableName } = req.body;
    // connect.promise : promise로 반환하도록 구현
    // 여기에 쿼리 구현
    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: "테이블 생성에 성공하였습니다." });
});

/** 테이블 조회 API **/
app.get("/api/tables", async (req, res, next) => {
    // SHOW TABLES  : 현재까지 생성된 여러가지의 테이블을 조회
    const [tableList] = await connect.promise().query("SHOW TABLES");
    // 모든 테이블의 값이 .map((table)의 table에 들어간다. 모든 테이블의 값 중에 첫번째 하나를 tableLis에 할당함
    const tableNames = tableList.map((table) => Object.values(table)[0]); // 하나만 뽑아냄

    // 테이블 이름 목록을 JSON 형태로 클라이언트에게 응답합니다.
    return res.status(200).json({ tableList: tableNames });
});

/** 데이터 삽입 API **/
app.post("/api/tables/:tableName/items", async (req, res, next) => {
    const { tableName } = req.params;  // tableName을 클라이언트로부터 받습니다.
    const { name } = req.body;

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

/** 데이터 조회 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 });
});

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

8) Raw Query는 계속 사용해도 괜찮을까?

Node.js에서 SQL을 직접 사용하는 Raw Query를 이용하여 더욱 빠르고 효율적으로 백엔드 서버를 개발할 수 있게 되었습니다.

그러나 구현한 API의 테이블 컬럼을 수정하려면  관련된 모든 코드를 수정해야 합니다. 또한, 사용자가 전달한 데이터를 데이터베이스에 직접 요청하게 되므로 SQL 인젝션의 취약점을 가지게 될 수 있습니다. (해킹위험높음)
→ SQL 인젝션 : https://www.bugbountyclub.com/pentestgym/view/52
 

이러한 문제를 해결하기 위해, ORM이라는 기술이 나타나게 되었습니다.
ORM을 통해 여러분들은 Node.js에서 SQL을 직접 작성하지 않고, Javascript 만으로 데이터베이스를 조작할 수 있게 되어 코드의 유지보수성이 증가하게 되고, 최종적으로 더욱 객체지향적인 프로그래밍을 가능하게 만들 수 있게 됩니다. 

 Node.js 진영에서 최근 가장 많이 각광받고 있는 Prisma 라는 ORM에 대해 학습합시다.

 

02. ORM과 Prisma

1) Prisma란?

PrismaORM(Object Relational Mapping)으로써 Javascript 객체(Object)와 데이터베이스의 관계(Relation)연결(Mapping) 해주는 도구입니다.
Node.js 환경에서는 TypeORM, Prisma, Sequelize 등 다양한 ORM이 존재합니다. 하지만 저희는 Javascript 환경뿐만 아니라 TypeScript에서도 사용할 수 있고, ORM 개념을 학습하기 쉬운 Prisma를 바탕으로 프로젝트를 진행해보도록 하겠습니다.
Prisma와 같은 ORM은 여러가지의 관계형 데이터베이스(RDB)를 사용할 수 있습니다. 예를들어 MySQL이나 강의에서 다루지 않는 Oracle, MariaDB, PostgreSQL와 같은 다양한 데이터베이스를 사용할 수 있습니다.

2) Prisma vs mongoose

mongoose의 경우 ODM(Object Document Mapping)으로 Javascript의 객체를 Document와 연결하지만, Prisma는 ORM(Object Relational Mapping)으로 Javascript의 객체와 데이터베이스의 관계(Relation)를 연결해주는 차이점이 있습니다.


RDBMS(관계형 DB) vs MongoDB
mongoose는 지원하는 데이터베이스는 MongoDB 밖에 존재하지 않았지만, Prisma의 경우 RDBMS에 해당하는 다양한 데이터베이스를 사용할 수 있다는 장점있습니다.
  미약하지만, Prisma는 MongoDB도 지원합니다. 
그리고 mongoose의 경우 Schema의 형태 컬렉션(Collection)에 대한 속성을 설정하였다면, Prisma의 경우 Model의 형태로 테이블(Table)의 속성을 설정할 수 있습니다. 
 MongoDB의 컬렉션(Collection)과 MySQL의 테이블(Table)은 동일한 위상을 가지고 있습니다.

3) ORM의 장단점

프로젝트를 구현할 때 Raw Query가 아닌 ORM을 사용하는 이유 : 수정 용이
Prisma와 같은 ORM을 사용하는 가장 큰 이유는 대표적으로 2가지가 있습니다.

 

1️⃣  :  프로덕션에서 사용하는 데이터베이스가 언제바뀔 지 알 수 없습니다.
하나의 스타트업을 가정해보겠습니다.
처음은 가장 보편적으로 사용하는 MySQL을 사용하고 있었습니다. 그러나 시간이 지날수록 개발자들의 유지보수 만으로 DB를 관리하기 어려워져 기술지원이 활발한 Oracle로 DB를 변경하려할 때 ORM을 사용하지 않는 개발자들은 두가지의 선택의 기로에 서게되는데요. 
1. 서비스 중인 프로덕션의 모든 Raw Query코드를 MySQL에서 Oracle로 변경하는 것을 고려하거나,
2. Oracle로 변경하지 않고, 어려운 현재 상황을 감내하고 계속 MySQL을 쓴다거나 말이죠.
하지만, ORM을 도입하였을 경우 여러분들은 이런 상황을 겪지 않고, 단순히 ORM의 속성값만 변경할 경우 언제든지 자유롭게 DB를 변경할 수 있게 되어 개발할 때 선택의 폭이 넓어지게 됩니다. 

 

2️⃣  : 데이터베이스에서 사용하는 DB 또는 Table 속성이 변경되었을 때 빠르게 수정이 가능합니다.
DB를 사용하는 코드를 모두 Javascript에서 Raw Query로 구현하였다고 가정해보겠습니다.
프로덕션에서 서비스를 진행 중 갑작스럽게 클라이언트의 요구사항으로 게시글을 생성할 때 공개, 비공개 속성을 추가하게 되었을 경우 아래와 같은 SQL이 생성되게 됩니다.
visivility 속성값이 추가되기 전

INSERT INTO Posts (title, content)
       VALUE ("제목", "내용");


visivility 속성값이 추가된 후 

INSERT INTO Posts (title, content, visibility)
       VALUE ("제목", "내용", "visible");
# 게시글의 visivility Column이 추가되었습니다.


우선 Javascript 코드 만으로는 게시글을 관리하는 테이블이 어떤 테이블을 나타내는지 인지하기도 어려울 뿐더러, 모든 API에서 사용하는 Raw Query에서 visibility Column에 대한 내용을 하나씩 수정해야됩니다. 
단순히 조회를 하는 API 뿐만 아니라 생성, 수정, 삭제에 해당하는 모든 API를 해당하는 쿼리에 맞게 수정해야하는 불상사가 발생하게 됩니다.
(프로젝트를 시작한지 얼마되지 않았다면 API를 수정하기 쉽겠지만, 사용하는 API가 수십, 수백개가 된다고 했을 때, 모든 코드를 수정하는 것이 쉽지 않겠죠?)
하지만, 여기서 ORM을 사용하였을 경우 여러분들은 테이블을 나타내는 Prisma의 model을 수정하기만 하더라도 수많은 API에서 Raw Query를 수정하지 않고도 visibility Column 값에 대한 정보를 추가할 수 있습니다.
하지만, 이러한 장점을 가지고 있는 ORM도 만능은 아닙니다.
JOIN과 UNION 연산자를 동시에 사용하는 복잡한 쿼리를 작성할 경우, ORM으로 구현하기 위해 SQL 보다는 ORM을 더 깊게 이해해야 하는 상황이 발생할 수 있고, 이로인해 원인과 결과가 뒤집힌 상황이 발생할 수도 있습니다.
이 뿐만 아니라, 서브 쿼리를 포함하는 복잡한 쿼리를 작성하거나, ORM의 SQL로 변환해주는 시간 조차 아까운 극한의 성능을 요구하는 쿼리가 필요한 상황에서는 Raw Query를 사용하는 것이 더욱 좋을 수 있답니다.


Raw Query  장점 : 복잡한 쿼리를 작성할 경우, 극한의 성능을 요구하는 쿼리가 필요한 상황에서 유리함


이제 Prisma ORM을 살펴보고, 왜 Raw Query가 아닌 ORM을 사용해야 하는지 경험해볼까요? 

 

03. Prisma 시작하기

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

이번에는 Prisma 프로젝트를 진행하기 위한 폴더를 생성, 라이브러리 설치, 프로젝트 구성.
risma 라이브러리 설치하기

# 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은 개발 코드가 변경되었을 때 자동으로 서버를 재시작 하는 패키지.

 

# prisma가 정상동작하는지 확인하는 코드. 터미널에서 사용하세요
npx prisma --help


주의!! 여기서 생성된 폴더나 파일들은 절대로 임의로 옮기거나 이름을 수정하지 마세요
Prisma는 정해진 경로에 있는 파일을 사용하고 저장하기 때문에 임의로 옮기면 오동작 할 가능성이 높습니다.

 

[Directory Structure]: 

npx prisma init으로 prisma 폴더 안에 prisma.schema파일이 생성됩니다.

   이 파일은 Prisma가 사용할 데이터베이스를 설정하기 위해 사용하는 파일이니 절대 지워선 안됩니다.


root 폴더(최상단 폴더, 프로젝트 폴더)에 .env 파일이 생성됨
   이 파일은 외부에 공유되어선 안되는 비밀 정보들이 저장되어 있는 파일입니다. 숨겨야하는 정보들을 담아두세요


root 폴더에 .gitignore 파일이 생성됨
   입력한 자료들을 git에 업로드되지 않도록 설정할 수 있습니다.

# .gitignore파일 내용. node_modules폴더와 .env파일이 깃허브에 올라가는 것을 막습니다.
node_modules
# Keep environment variables out of version control
.env


nodemon 라이브러리

nodemon은 파일을 저장할 때마다 변경 사항을 감지하고, 자동으로 서버를 재시작해주는 라이브러리입니다. 개발 중 변경사항을 즉시 반영하여 개발 효율성을 향상시킬 수 있습니다.


nodemon 명령어 - 서버 실행

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

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

 

???맥 말하는 걸까?

프로젝트 내에 nodemon이 설정되어 있으면 npx하고 nodemon을 작송해야 실행됩니다.

 


단순히 터미널에 명령어를 사용하는 것 뿐만아니라, package.json에 nodemon을 이용하여 서버를 실행하는 스크립트(scripts)를 등록한다면, 매번 명령어를 입력하지 않아도 간편하게 서버를 시작할 수 있습니다.
아래와 같이 package.json을 수정해주세요
package.json nodemon 스크립트 등록하기

// package.json
...
"scripts": {
"dev": "nodemon app.js"
},

dev 명령어를 사용했을 때 nodemon을 사용할 수 있도록 합니다.

 

package.json 코드

더보기
{
  "name": "prisma111",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "scripts": {
	"dev": "nodemon app.js"
},
  "dependencies": {
    "@prisma/client": "^5.22.0",
    "express": "^4.21.1",
    "prisma": "^5.22.0"
  },
  "devDependencies": {
    "nodemon": "^3.1.7"
  }
}

app.js를 구현하고 터미널에서 yarn run dev 명령어를 실행하면, nodemon을 이용하여 서버를 시작할 수 있습니다.

yarn run dev

dev같은 명령어는 run을 빼도 정상적으로 사용할 수 있는 장점이 있습니다.

 

2) schema.prisma

prisma.schema 파일은 Prisma가 사용할 데이터베이스의 설정 정보를 정의하기 위해 사용하는 파일입니다.
Prisma를 가장 처음 초기화 하였을 때, prisma.schema 파일을 확인한다면, 아래의 2가지 구문이 작성되어 있는 것을 확인할 수 있습니다.
◆ datasource
   데이터베이스에 대한 정의를 하기 위해 사용됩니다.
   Prisma가 어떤 데이터베이스 엔진을 사용할 것인지, 데이터베이스의 위치(URL)는 어디인지  등의 정보를 정의하는데 사용됩니다.
◆ generator
   Prisma 클라이언트를 생성하는 방식을 설정하는 구문입니다.
   → 저희는 generator를 수정하지 않을 것이기 때문에, 참고만 해주세요


3) Prisma datasource

datasourcePrisma가 데이터베이스를 연결할 수 있도록 설정하고, 관리하는 데 필요한 정보를 설정하는 구문입니다.
우선 Prisma는 연결하려는 데이터베이스의 속성을 schema.prisma 파일에서 관리하고 있습니다.  여기서 datasource 프로퍼티에 정의된 속성들을 수정하여 사용자 아이디, 비밀번호, 엔드 포인트 등 다양한 설정값을 입력해주어야합니다.

 

datasource 설정하기

// schema.prisma

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

provider : DB엔진에 대한 유형을 정의하는 부

url : DB와 연결하기 위한 url

env("DATABASE_URL")  : prisma 자체의 문법. DB의 URL정보를 .evn 파일에 있는 DATABASE_URL 로부터 읽오는 문법.


url 부분에서 env("DATABASE_URL") 방식으로, 데이터베이스의 주소가 노출되지 않게 작성하는 dotenv 의 문법을 사용하고 있는데요. env() 문법은 프로젝트의 root 폴더에 있는 .env 파일에 정의되어 있는 정보를 해당 schema.prisma 파일로 불러오는 것입니다.
여기서, dotenv는 어플리케이션의 환경 변수(Environment Variables)를 관리하는 모듈입니다. 실제 코드에서는 민감한 정보를 노출시키지 않도록 보호해주고, 개발 환경에 따라 다르게 설정해야 하는 값을 별도의 파일에서 관리할 수 있게 해줍니다.


그렇다면, schema.prisma 파일이 참조하는 .env 파일은 어떻게 구현되어 있는지 확인해볼까요?
.env 파일 확인하기

# .env

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

Prisma를 초기화 하고, .env 파일을 확인한다면, 위와 같은 내용이 작성되어 있을 것입니다.
.env 파일은 key-value의 형태로 구성되어 있고, DATABASE_URL이라는 하나의 변수가 선언되어있는 것을 확인할 수 있습니다.
만약, 여기서 데이터베이스 URL을 변경하게 된다면, 저희가 대여한 RDS의 데이터베이스와도 연결이 가능하합니다.

 

// schema.prisma

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

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

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

4) 데이터베이스 URL

💡
데이터베이스 URL은 Prisma가 어떤 데이터베이스와 어떻게 연결할지를 알려주는 중요한 정보입니다. URL 내부에는 데이터베이스 엔진 유형사용자 아이디패스워드와 같은 정보가 포함됩니다.
.env 파일의 DATABASE_URL에서 확인한 것처럼, 데이터베이스와 연결하기 위해선 URL을 생성해야합니다. 그렇다면, 이번에는 DATABASE_URL은 어떻게 구성되어 있는지, 그리고 어떻게 구성하는지를 확인해보도록 하겠습니다.
저희는 AWS RDS를 대여하여, RDS의 엔드 포인트, 사용자 아이디, 비밀번호, Port 번호를 전달받았습니다. 이 정보를 바탕으로 Prisma와 연결하기 위한 URL을 작성해보도록 하겠습니다.


Database URL
데이터베이스 URL은 크게 4가지로 나눠집니다.
◆ Protocol
Prisma가 사용할 데이터베이스 엔진을 나타냅니다.
postgresql, sqllite, mysql과 같은 데이터베이스 엔진을 정의합니다.

 

 

  Base URL
데이터베이스의 엔드 포인트와 아이디, 패스워드, 포트 번호를 나타냅니다.
<Id>:<Password>@<RDS Endpoint>:<Port>의 형태로 구성됩니다.

 

  Path
MySQL에서 사용할 데이터베이스 이름을 설정하는 구성 요소입니다.

 

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

 

Prisma의 데이터베이스 URL 구현하기!

대여받은 RDS의 속성값을 바탕으로 데이터베이스 URL을 구성하기
▶ 데이터베이스 엔진: mysql
  마스터 사용자 이름: root
  마스터 암호: aaaa4321
  RDS 엔드포인트: express-database.clx5rpjtu59t.ap-northeast-2.rds.amazonaws.com
  Port 번호: 3306
사용할 DB 이름: prisma_crud


데이터베이스 URL 작성이 완료되었으면, .env 파일의 DATABASE_URL도 해당하는 URL로 변경하기

# .env 파일
DATABASE_URL="mysql://root:aaaa4321@엔드포인트트:3306/DB이름"


5) Prisma model

Prisma의 model 구문특정 Table과 Column의 속성값을 입력하여, 데이터베이스와 Express 프로젝트를 연결 (Mapping)시켜줍니다.


model 구문은 Prisma를 사용할 때 가장 많이 작성하게 될 구문이며, Prisma가 사용할 데이터베이스의 테이블 구조를 정의하기 위해 사용됩니다.
schema.prisma 파일에서는 model에 작성된 정보를 바탕으로 Prisma Client를 통해 JavaScript에서 MySQL의 테이블을 조작할 수 있게 됩니다. 
model 구문은 Javascript에서 MySQL의 테이블을 사용하기 위한 다리 역할을 수행하며, 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(날짜) 등의 데이터 유형이 사용되었습니다.

 

@id @default(autoincrement()) : id로 사용. 기본값(자동생성)


데이터 유형 뒤에 ?가 붙게 된다면, NULL을 허용하는 컬럼이 됩니다.
   → 이 문법은 TypeScript에서 Optional Parateters 라고 불니다.

 

SQL 에서 사용하는 것과 동일하게, UNIQUE 제약 조건과 AUTO_INCREMENT 제약조건을 사용할 수 있습니다.

 

@unique : 컬럼내에 같은 값이 올 수 없음(ex) 이미 있는 닉네임입니다)

 

String 은 varchar 타입.

... String ... @db.Text : 긴 문자열을 적을 때 사용. Text타입

 

@default( 기본값 ) :  값이 입력되지 않을 때 기본값이 적용됩니다.

 

DataTime @default(now()) : 현재 시간이 들어감

 

@updatedAt : 수정할 때 자동으로 현재시간이 들어감

 

@@map("Products")는 Products 테이블을 MySQL에서도 Products란 이름으로 사용하겠다는 뜻입니다.
  → @@map() 을 작성하지 않으면, 테이블명의 대문자는 전부 소문자로 치환된답니다. 

 

 Products 테이블의 요구사항

 아래의 요구 사항을 바탕으로 schema.prisma에 model 구문은 작성하기

Name Type NULL 제약조건 default
productId INTEGGER NOT NULL PRIMARY KEY AUTO_INCREMENT
productName STRING NOT NULL UNIQUE  
price INTEGGER NOT NULL   1000
info TEXT NULL    
creartedAt DATETIME NOT NULL   현재 시간
updateAt DATETIME NOT NULL   현재 시간

 

 Products 테이블의 생성 .sql 파일 

-- CreateTable
CREATE TABLE `Products` (
    `productId` INTEGER NOT NULL AUTO_INCREMENT,
    `productName` VARCHAR(191) NOT NULL,
    `price` INTEGER NOT NULL DEFAULT 1000,
    `info` TEXT NULL,
    `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    `updatedAt` DATETIME(3) NOT NULL,

    UNIQUE INDEX `Products_productName_key`(`productName`),
    PRIMARY KEY (`productId`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;


String 타입은 VARCHAR(191)의 형식을 가집니다.
DateTime 타입은 DATETIME(3) 의 형식을 가집니다.
Text? 타입은 Text 타입과 함께, NULL 제약 조건을 가집니다.


6) Prisma DB, Table 생성하기

아래의 코드스니펫에 있는를 사용하면, schema.prisma 파일에 정의된 내용을 바탕으로 새로운 DB와 테이블이 생성된 것을 확인할 수 있을 것입니다. 😎​
 Prisma CLI 명령어 prisma db pusb   : DB, Table 생성하기

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

 

DB 우클릭 New Query  ->  테이블 컬럼 조회하

DESC 테이블명;

 

 Prisma CLI 더 알아보기

◆ prisma db push

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

prisma init

▶    Prisma를 사용하기 위한 초기 설정을 생성합니다.
▶    이 명령어를 실행하면 schema.prisma 파일과 같은 필요한 설정 파일들이 생성됩니다.

◆ prisma generate

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

◆ prisma db pull

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


→ Prisma CLI 명령어 문서 : https://www.prisma.io/docs/orm/reference/prisma-cli-reference#synopsis

 

7) Prisma Client

Prisma Flow

Prisma는 model을 generate하면, 해당 모델에 대한 정보가 node_modules폴더 내에 있는 Prisma Client에 전달됩니다. 
   →  prisma db push도 내부적으로 generate가 실행됩니다.
Prisma Client는 Prisma Schema에 정의한 데이터베이스 모델(model)을 TypeScript 코드로 변환하여, 개발자가 데이터베이스와 상호작용할 수 있게 해주는데요. 


이러한 과정을 통해, 데이터베이스를 JavaScript에서 손쉽게 다룰 수 있게 되고, Prisma Schema와 동기화된 Prisma Client를 이용해 데이터베이스를 사용할 수 있게 되는 것입니다.

Prisma Client 확인하기

Prisma Schema에 정의 → index.d.ts에 저장 → 변환해서 export

// node_modules/.prisma/client/index.d.ts

export type ProductsPayload<ExtArgs extends $Extensions.Args = $Extensions.DefaultArgs> = {
  name: "Products"
  objects: {}
  scalars: $Extensions.GetResult<{
    productId: number
    productName: string
    price: number
    info: string | null
    createdAt: Date
    updatedAt: Date
  }, ExtArgs["result"]["products"]>
  composites: {}
}

/**
 * Model Products
 * 
 */
export type Products = runtime.Types.DefaultSelection<ProductsPayload>

▶ schema.prisma 파일에 정의한 내용과 같이, Products 테이블에 대한 내용이 위와 같이 작성되어 있습니다.
▶ 저희가 Prisma를 이용하여 데이터베이스를 조작할 때 이러한 Prisma Client로 사용하게 되는 것입니다.

 

1) Prisma의 Method 살펴보기

Prisma는 mongoose와 동일하게, findMany(), findFirst(), findUnique() 등 다양한 메서드를 지원합니다. mongoose를 사용했을 때는 Schema를 이용해 DB를 사용하였다면, Prisma에서는 Prisma Client를 이용해 MySQL의 데이터를 조작할 것입니다.
그러면 이번에 생성한 Posts 테이블의 구조를 간단하게 살펴볼까요?

Posts 테이블은 게시글 제목(title), 내용(content), 비밀번호(password)총 3개의 컬럼을 가지고 있고 postId, createdAt, updatedAt 컬럼은 아무런 데이터를 입력하지 않더라도 기본값을 가질 수 있도록 구성되어 있습니다.
그러면 게시글을 생성 및 수정할 때 필수 인자값 3개를 이용해 권한 검증 및 데이터 생성을 구현하도록 하겠습니다. 😊​
API를 구현하기에 앞서 routes/posts.router.js 파일을 생성하고 express 프로젝트를 초기화 하겠습니다.

 

Prisma 게시글 routes/posts.router.js

// routes/posts.router.js

import express, { query } from "express";

//@prisma/client 라입브러리에서 PrismaClient 가져옴
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 인스턴스를 생성하여 prisma변수에 할당 합니다.

export default router;

PrismaClient

PrismaClient 인스턴스가 할당된 prisma를 통해 
게시글 테이블에 해당하는 post 테이블에 대한 내용을 작성하거나 삭제, 수정할 수 있다.

PrismaClient를 이용해서 DB에 접근했을 때 추가적인 기능을 이 객체의 내부에 지정할 수 있음.
PrismaClient를 생성했을 때 config(설정값 들을 받을 수 있는 객체)를 정의할 수 있음
위에서 config 내부에 log라고 하는 옵션과 errorFormat 속성을지정함.

log 사용의 장점

log를 사용하면 query 실행 중 발생하는 정보성 데이터, 문제 상황에 대한 warning 데이터, 그리고 query 실행 중 DB에서 발생한 에러터미널에 출력할 수 있어 개발 및 디버깅 과정에서 실시간으로 문제를 파악하고 처리할 수 있는 장점을 제공합니다.

errorFormat

Prisma Client에서 반환되는 오류의 수준과 형식을 결정합니다.
   undefined : 정의되지 않으면 기본값은 무색입니다.
    pretty  : 보기좋은 오류 서식을 제공합니다.
    colorless(기본) : 무색 오류 서식을 활성화합니다.
    minimal : 오류 발생을 최소화한 서식 지정이 가능합니다.

 

 

Prisma 게시글 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, '포트로 서버가 열렸어요!');
});


​ Prisma 게시글 API 명세서

기능 Method API URL Request Reponse
게시글 목록 조회 GET /api/posts { } {
  "data":
    [
       {
          "postId": 1, "title": "타이틀입니다.",
          "createdAt": "2024-01-14T05:59:13.747Z",
          "updatedAt": "2024-01-14T05:59:13.747Z"
        },
        {
          "postId": 2,
          "title": "타이틀입니다.",
          "createdAt": "2024-01-14T05:59:13.983Z",
          "updatedAt": "2024-01-14T05:59:13.983Z"
        }
    ]
}
게시글 상세 조회 GET /api/posts/:postid { } {
 "data":
  {
       "postId": 2, "title": "수정된 타이틀입니다.",
       "content": "수정된 콘텐츠 입니다.",
       "createdAt": "2024-01-14T05:59:13.983Z",
       "updatedAt": "2024-01-14T06:00:15.650Z"
 }
}
게시글 생성 POST /api/posts {
  "title": "타이틀입니다.",
  "content": "콘텐츠 입니다.",
  "password": "aaaa1111"
}
{
  "data":
    {
      "postId": 3, "title": "타이틀입니다.",
       "content": "콘텐츠 입니다.",
       "password": "aaaa1111",
       "createdAt": "2024-01-14T06:01:04.135Z",
       "updatedAt": "2024-01-14T06:01:04.135Z"
  }
}
게시글 수정 PUT /api/posts /:postid {
  "title": "수정된 타이틀입니다.",
  "content": "수정된 콘텐츠 입니다.",
  "password": "aaaa1111"
}
{
"data": "게시글이 수정되었습니다."
}
게시글 삭제 DELETE /api/posts /:postid {
  "password": "aaaa1111"
}
{
"data": "게시글이 삭제되었습니다."
}

 


2) Prisma 게시글 생성(Create) API

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

 

Prisma 게시글 생성 API

// 게시글 생성

// app.js의 'app.use("/api", [PostsRouter]);'가 있으니 '/api'를 생략하여 적으세요.
router.post("/posts", async (req, res, next) => {
    // 1. title, content, password를 body로 전달받는다.
    const { title, content, password } = req.body;
    // 2. title, content, password를 이용해 Posts 테이블에 데이터를 삽입 한다.
    // 변수선언 = awaot(대기해라) prisma.테이블이름.creat(데이터 생성)({
    /*  data : {
            body에 입력받은거 쓰기
            title : title,
            content : content,
            password : pasword,
        },
        });*/
    const post = await prisma.posts.create({
        data: {
            title,
            content,
            password,
        },
    });

    // 3. 생성된 게시글을 반환한다.
    return res.status(201).json({ data: post });
});



create 메서드를 이용해 데이터를 생성

서버 실행 방법 

 

터미널에 입력

# 1번
yarn dev
yarn run dev
# 2번
nodemon app.js

app.js의 경로가 ./src/app.js 로 바뀌면 package.json의 경로를 변경해서 프로젝트파일 위치에서 실행 되도록 만들자.

 

 

서버 실행하고 insomnia로 테스트 하기.

경로는 localhost부터 적기

 

위에 log: ["query", "info", "warn", "error"],의 결과

터미널

터미널 내용

insert    데이터 생성

select    생성된 데이터를 바로 조회함

 

3) Prisma 게시글 조회(Read) API

게시글 조회 API는 게시글 목록 조회, 게시글 상세 조회 2개의 API로 구현할 수 있습니다.
1. 게시글 목록 조회 API의 경우 게시글의 내용(content)을 제외하고, 
2. 게시글 상세 조회 API일 경우에만 게시글의 전체 내용이 출력되도록 구현할 예정입니다.


Prisma 게시글 목록 조회 API

/** 게시글 전체 조회 API **/
// 1. 게시글 목록 조회 API의 경우 게시글의 내용(content)을 제외하고,
router.get("/posts", async (req, res, next) => {
    // 받아야 하는 정보가 없어 body 없음
    const posts = await prisma.posts.findMany({
        select: {
            // content는 조회X
            postId: true,
            title: true,
            createdAt: true,
            updatedAt: true,
        },
    });

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

find 메소드 종류

findFirst    처음 일치하는 데이 하나만 조회
findFirstOrThrow     하나만 조회, 데이터가 조회되지 않으면 에러 발생시킴
findMany     일치하는 모든 데이터 조회
findUnique     특정한 데이터 하나를 Unique 키로 조회
findUniqueOrThrow     findUnique 로 조회. 데이터가 조회되지 않으면 에러 발생시킴

https://www.prisma.io/docs/orm/reference/prisma-client-reference#model-queries

 

select

query의 select처럼 사용함.

'조회하려는 컬럼:true'형태로 적어주세요. false는 에러가 발생합니다.

select?는 select*처럼 모든 데이터 선택

 

수정한 posts/router.js파일을 저장 하면 nodemon app.js를 실행중인 상태이기에 자동으로 수정한 코드가 적용된 서버가 재실행 됩니다.



Prisma 게시글 상세 조회 API

/** 게시글 상세 조회 API **/
// 2. 게시글 상세 조회 API일 경우에만 게시글의 전체 내용이 출력되도록 구현할 예정입니다.
router.get("/posts/:postId", async (req, res, next) => {
    // 클라이언트로부터 전달받은 데이터 (:postId)를 객체분해 파라미터 req.params;로 전달받습니다.
    // 이 경우 API URL은 'http://localhost:3017/api/posts/postId값'이 됩니다.
    const { postId } = req.params;
    // 게시글 조회
    const post = await prisma.posts.findFirst({
        // 컬럼 postId의 타입이 int니 '+'를 사용해 입력받은 postId를 int로 타입변환해줍니다.
        where: { postId: +postId },
        select: {
            postId: true,
            title: true,
            content: true,
            createdAt: true,
            updatedAt: true,
        },
    });

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


4) Prisma 게시글 수정(Update) API

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


Prisma 게시글 수정 API

// routes/posts.router.js

/** 게시글 수정 API **/
router.put("/posts/:postId", async (req, res, next) => {
    // 1.  Path Parameters로 어떤 게시글을 수정할 지 postId를 전달받습니다.
    const { postId } = req.params;
    // 2 . 변경할 title, content와 권한 검증을 위한 password를 body로 전달받습니다.
    const { title, content, password } = req.body;

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

    // 3 . postId를 기준으로 게시글을 검색하고, 게시글이 존재하는지 확인합니다.
    if (!post) return res.status(404).json({ message: "게시글이 존재하지 않습니다." });
    // 4 . 게시글이 조회되었다면 해당하는 게시글의 password가 일치하는지 확인합니다.
    else if (post.password !== password) return res.status(401).json({ message: "비밀번호가 일치하지 않습니다." });

    // 5 . 모든 조건을 통과하였다면 게시글을 수정합니다.
    await prisma.posts.update({
        data: { title, content },
        where: {
            postId: +postId,
            password,
        },
    });

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


401 Unauthorized 응답 상태 코드는 요청된 리소스에 대한 유효한 인증 자격 증명이 없기 때문에 클라이언트 요청이 완료되지 않았음을 나타냅니다.

HTTP 상태 코드 : https://developer.mozilla.org/ko/docs/Web/HTTP/Status

 

HTTP 상태 코드 - HTTP | MDN

HTTP 응답 상태 코드는 특정 HTTP 요청이 성공적으로 완료되었는지 알려줍니다. 응답은 5개의 그룹으로 나누어집니다: 정보를 제공하는 응답, 성공적인 응답, 리다이렉트, 클라이언트 에러, 그리고

developer.mozilla.org


 

 


 where 속성에

SQL에서는 특정한 데이터를 검출하기 위해 where절에서 OR(||), AND(&&), LIKE, 정규표현식 등 다양한 연산자를 사용할 수 있습니다.
Prisma의 where절은 여러개의 조건이 들어올 경우 AND(&&) 연산자를 사용한 것과 동일한 결과를 출력해줍니다. 하지만, 이외의 연산자를 사용하고 싶다면, 아래와 같은 문법으로도 사용할 수 있니다.  

await prisma.users.findMany({
  where: {
    OR: [
      {
        email: {
          endsWith: 'prisma.io',
        },
      },
      { email: { endsWith: 'gmail.com' } },
    ],
    NOT: {
      email: {
        endsWith: 'hotmail.com',
      },
    },
  },
})


​Prisma의 논리 연산 : https://www.prisma.io/docs/orm/prisma-client/queries/filtering-and-sorting#combining-operators

 

Filtering and Sorting (Concepts) | Prisma Documentation

Use Prisma Client API to filter records by any combination of fields or related record fields, and/or sort query results.

www.prisma.io

 

5) Prisma 게시글 삭제(Delete) API

게시글 삭제 API는 게시글 수정 API와 동일한 로직을 수행하지만 Body에서 password만 전달받습습니다.

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


Prisma 게시글 삭제 API

// routes/posts.router.js

/** 게시글 삭제 API **/
router.delete("/posts/:postId", async (req, res, next) => {
    // 1.  Path Parameters로 어떤 게시글을 수정할 지 postId를 전달받습니다.
    const { postId } = req.params;
    // 2 . 권한 검증을 위한 password를 body로 전달받습니다.
    const { password } = req.body;

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

    // 3 . postId를 기준으로 게시글을 검색하고, 게시글이 존재하는지 확인합니다.
    if (!post) return res.status(404).json({ message: "게시글이 존재하지 않습니다." });
    // 4 . 게시글이 조회되었다면 해당하는 게시글의 password가 일치하는지 확인합니다.
    else if (post.password !== password) return res.status(401).json({ message: "비밀번호가 일치하지 않습니다." });

    // 5 . 모든 조건을 통과하였다면 게시글을 삭제합니다.
    await prisma.posts.delete({ where: { postId: +postId } });

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

 

// routes/posts.router.js 전체코드

더보기

 

// routes/posts.router.js

import express, { query } from "express";

//@prisma/client 라입브러리에서 PrismaClient 가져옴
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 인스턴스를 생성하여 prisma변수에 할당 합니다.

// 게시글 생성

// app.js의 'app.use("/api", [PostsRouter]);'가 있으니 '/api'를 생략하여 적으세요.
router.post("/posts", async (req, res, next) => {
    // 1. title, content, password를 body로 전달받는다.
    const { title, content, password } = req.body;
    // 2. title, content, password를 이용해 Posts 테이블에 데이터를 삽입 한다.
    // 변수선언 = awaot(대기해라) prisma.테이블이름.creat(데이터 생성)({
    /*  data : {
            body에 입력받은거 쓰기
            title : title,
            content : content,
            password : pasword,
        },
        });*/
    const post = await prisma.posts.create({
        data: {
            title,
            content,
            password,
        },
    });

    // 3. 생성된 게시글을 반환한다.
    return res.status(201).json({ data: post });
});

/** 게시글 전체 조회 API **/
// 1. 게시글 목록 조회 API의 경우 게시글의 내용(content)을 제외하고,
router.get("/posts", async (req, res, next) => {
    // 받아야 하는 정보가 없어 body 없음
    const posts = await prisma.posts.findMany({
        select: {
            // content는 조회X
            postId: true,
            title: true,
            createdAt: true,
            updatedAt: true,
        },
    });

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

/** 게시글 상세 조회 API **/
// 2. 게시글 상세 조회 API일 경우에만 게시글의 전체 내용이 출력되도록 구현할 예정입니다.
router.get("/posts/:postId", async (req, res, next) => {
    // 클라이언트로부터 전달받은 데이터 (:postId)를 객체분해 파라미터 req.params;로 전달받습니다.
    // 이 경우 API URL은 'http://localhost:3017/api/posts/postId값'이 됩니다.
    const { postId } = req.params;
    // 게시글 조회
    const post = await prisma.posts.findFirst({
        // 컬럼 postId의 타입이 int니 '+'를 사용해 입력받은 postId를 int로 타입변환해줍니다.
        where: { postId: +postId },
        select: {
            postId: true,
            title: true,
            content: true,
            createdAt: true,
            updatedAt: true,
        },
    });

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

/** 게시글 수정 API **/
router.put("/posts/:postId", async (req, res, next) => {
    // 1.  Path Parameters로 어떤 게시글을 수정할 지 postId를 전달받습니다.
    const { postId } = req.params;
    // 2 . 변경할 title, content와 권한 검증을 위한 password를 body로 전달받습니다.
    const { title, content, password } = req.body;

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

    // 3 . postId를 기준으로 게시글을 검색하고, 게시글이 존재하는지 확인합니다.
    if (!post) return res.status(404).json({ message: "게시글이 존재하지 않습니다." });
    // 4 . 게시글이 조회되었다면 해당하는 게시글의 password가 일치하는지 확인합니다.
    else if (post.password !== password) return res.status(401).json({ message: "비밀번호가 일치하지 않습니다." });

    // 5 . 모든 조건을 통과하였다면 게시글을 수정합니다.
    await prisma.posts.update({
        data: { title, content },
        where: {
            postId: +postId,
            password,
        },
    });

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

/** 게시글 삭제 API **/
router.delete("/posts/:postId", async (req, res, next) => {
    // 1.  Path Parameters로 어떤 게시글을 수정할 지 postId를 전달받습니다.
    const { postId } = req.params;
    // 2 . 권한 검증을 위한 password를 body로 전달받습니다.
    const { password } = req.body;

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

    // 3 . postId를 기준으로 게시글을 검색하고, 게시글이 존재하는지 확인합니다.
    if (!post) return res.status(404).json({ message: "게시글이 존재하지 않습니다." });
    // 4 . 게시글이 조회되었다면 해당하는 게시글의 password가 일치하는지 확인합니다.
    else if (post.password !== password) return res.status(401).json({ message: "비밀번호가 일치하지 않습니다." });

    // 5 . 모든 조건을 통과하였다면 게시글을 삭제합니다.
    await prisma.posts.delete({ where: { postId: +postId } });

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

export default router;


6) Prisma 리팩토링

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

const prisma = new PrismaClient();

 

Prisma Client 인스턴스 생성

현재는 저희가 게시글(Posts) 라우터만 구현하고 있지만, 이후에 사용자(Users), 사용자 정보(UserInfos), 해시 태그(HashTags)와 같은 여러 라우터들이 추가된다면, 각각의 라우터 갯수마다 데이터베이스와 연결하게 되는 문제가 발생하게 됩니다.
여러 번 데이터베이스의 연결을 생성한다면, 리소스가 과도하게 사용되며, 그로인해 어플리케이션의 성능이 저하될 수 있습니다. 따라서, 최대한 데이터베이스의 연결을 줄이는 것이 효율적인 방법 일 것입니다.
이런 문제를 해결하기 위해 저희는 /utils/prisma/index.js 파일을 구현하여, 하나의 파일에서 데이터베이스 커넥션을 관리하여 최초로 1번만 MySQL과 커넥션을 생성하도록 코드를 구현하면 된답니다.

 

utils/prisma/index.js Prisma 리팩토링

// 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 Prisma 리팩토링
꼭! 상단의const prisma = new PrismaClient() 구문은 삭제해주세요!

// routes/posts.router.js

import { prisma } from '../utils/prisma/index.js';
utils/prisma/index.js

 

 

수정한 router 상단 코드

// routes/posts.router.js

import express, { query } from "express";
// 데이터베이스 커넥션을 관리
import { prisma } from "../utils/prisma/index.js";
const router = express.Router(); // express.Router()를 이용해 라우터를 생성합니다.

// 아래 log, error 설정을 index.js로 옯깁니다.

 

Prisma의 대표적인 API 

prisma.테이블명.findMany(): User 테이블에서 모든 데이터를 조회
prisma. 테이블명 .findUnique(): User 테이블에서 하나의 데이터 조회 (예전에는 findOne 이었다.)
prisma. 테이블명 .create(): User 테이블에 새로운 데이터를 생성
prisma 테이블명 .update(): User 테이블에서 데이터를 수정
prisma.테이블명.delete(): User 테이블에서 데이터를 삭제

 

https://www.prisma.io/docs/orm/prisma-client/queries

 

Prisma Client Queries | Prisma Documentation

Learn about the database queries you can send with Prisma Client.

www.prisma.io