Raw Query는 데이터베이스에 SQL을 이용하여 직접 쿼리를 요청하는 것
# yarn으로 프로젝트를 초기화합니다.
yarn init -y
# express와 mysql 드라이버를 설치합니다.
yarn add express mysql2
mysql2
라이브러리// 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
password
database
이외에도 timezone
으로 시간대를 설정하거나, ssl
로 ssl 인증서를 설정하는 것과 같은 옵션 설정을 할 수 있다.
// 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()
형식으로 사용한다.
// 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
변수에 할당하게 된다.
// 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
을 삽입한다
// 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
# 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 <실행할 JavaScript 파일명>
# nodemon을 이용해 app.js 파일 실행하기
nodemon app.js
터미널 명령어로 사용하는 것뿐만 아니라, package.json
에 nodemon
을 이용하여 서버를 실행하는 스크립트를 등록하여 간편하게 서버를 시작할 수 있다.
// package.json
...
"scripts": {
"dev": "nodemon app.js"
},
이제 터미널에서 yarn run dev
명령어를 실행하면, nodemon을 이용하여 서버를 시작할 수 있다.
Prisma를 처음 초기화했을 때, prisma.schema
파일에 아래 두 가지 구문이 작성된다.
datasource
generator
datasource
프로퍼티에 정의된 속성들을 수정하여 사용자 아이디, 비밀번호, 엔드 포인트 등 다양한 설정값을 입력해주어야 한다.
// schema.prisma
datasource db {
// MySQL 데이터베이스 엔진을 사용합니다.
provider = "mysql"
// 데이터베이스 연결 정보를 .env 파일의 DATABASE_URL 로부터 읽어옵니다.
url = env("DATABASE_URL")
}
provider
: Prisma가 사용할 데이터베이스 엔진url
: 데이터베이스를 연결하기 위한 URLurl
부분에서 데이터베이스의 주소가 노출되지 않게 작성하는 dotenv
문법을 사용하고 있다. 이는 .env
파일에 정의된 정보를 schema.prisma
파일로 불러온다.
# .env 파일
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
데이터베이스 URL은 크게 네 가지로 나뉜다.
Protocal
postgresql
, sqllite
, mysql
등Base URL
<Id>:<Password>@<RDS Endpoint>:<Port>
의 형태Path
Argument
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")
}
Int
, String
, DateTime
등이 쓰였다.?
가 붙으면 NULL
을 허용하는 컬럼이 된다.@@map("Product")
는 Product
테이블을 MySQL에서도 Product
란 이름으로 사용하겠단 뜻이다. 작성하지 않으면 테이블명이 소문자로 치환된다.# schema.prisma 파일에 설정된 모델을 바탕으로 MySQL에 정보를 업로드합니다.
npx prisma db push
prisma db push
schema.prisma
파일에 정의된 설정값을 실제 데이터베이스에 반영prisma generate
가 실행된다.prisma init
schema.prisma
파일과 같은 필요한 설정 파일들이 생성된다.prisma generate
schema.prisma
파일에 변경 사항이 생겼거나, 데이터베이스 구조가 변경되었을 때, 이명령어를 사용해 Prisma Client를 최신 상태로 유지할 수 있다.prisma db pull
prisma.schema
파일로 가져온다.prisma generate
명령어를 사용해 변경 사항을 Prisma Client에 반영할 수 있다.model
을 generate
하면 해당 모델에 대한 정보가 node_modules
폴더 내에 있는 Prisma Client에 전달된다.
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, '포트로 서버가 열렸어요!');
});
게시글 생성 API의 비즈니스 로직
1.title
,content
,password
를 body로 전달받는다.
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 });
});
게시글 목록 조회 API의 경우 게시글의 내용(content)을 제외하고,
게시글 상세 조회 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 });
});
// 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
는 변수명 앞에 +
연산자가 붙은 경우, 문자열 타입을 숫자형 타입으로 변환해준다.
게시글 수정 API의 비즈니스 로직
1. Path Parameters로 어떤 게시글을 수정할 지postId
를 전달받습니다.
2. 변경할title
,content
와 권한 검증을 위한password
를 body로 전달받습니다.
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: '게시글이 수정되었습니다.' });
});
게시글 삭제 API의 비즈니스 로직
1. Path Parameters로 어떤 게시글을 수정할 지postId
를 전달받습니다.
2. 권한 검증을 위한password
를 body로 전달받습니다.
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: '게시글이 삭제되었습니다.' });
});
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';