하나의 게시글 ( Posts )은 여러개의 댓글 ( Comments )이 작성될 수 있다.
Posts 테이블과 Comments 테이블은 1:N 관계를 가지고 있다.
[게시판 프로젝트] 라이브러리 설치
# 프로젝트를 초기화합니다.
yarn init -y
# 라이브러리를 설치합니다.
yarn add express prisma @prisma/client cookie-parser jsonwebtoken
# nodemon 라이브러리를 DevDependency로 설치합니다.
yarn add -D nodemon
# 설치한 Prisma를 초기화 하여, Prisma를 사용할 수 있는 구조를 생성합니다.
npx prisma init
package.json에 "type":"module"도 추가 ( ES6 문법 사용하기 위함 )
프로젝트 구조 ( 게시판 프로젝트 )
내 프로젝트 폴더 이름
├── .env
├── .gitignore
├── package.json
├── yarn.lock
├── prisma
│ └── schema.prisma
└── src
└── app.js
SQL로 미리 확인해보기
CREATE TABLE Users
(
userId INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(191) UNIQUE NOT NULL,
password VARCHAR(191) NOT NULL,
createdAt DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updatedAt DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
);
CREATE TABLE UserInfos
(
userInfoId INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
userId INTEGER UNIQUE NOT NULL, -- 1:1 관계 이므로 UNIQUE 조건을 삽입합니다.
name VARCHAR(191) NOT NULL,
age INTEGER NOT NULL,
gender VARCHAR(191) NOT NULL,
profileImage VARCHAR(191) NULL,
createdAt DATETIME(3)
NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updatedAt DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
);
ALTER TABLE UserInfos
ADD CONSTRAINT FK_UserInfos_Users
FOREIGN KEY (userId) REFERENCES Users (userId) ON DELETE CASCADE;
CREATE TABLE Posts
(
postId INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
userId INTEGER NOT NULL,
title VARCHAR(191) NOT NULL,
content VARCHAR(191) NOT NULL,
createdAt DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updatedAt DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
);
ALTER TABLE Posts
ADD CONSTRAINT FK_Posts_Users
FOREIGN KEY (userId) REFERENCES Users (userId) ON DELETE CASCADE;
CREATE TABLE Comments
(
commentId INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
userId INTEGER NOT NULL,
postId INTEGER NOT NULL,
content VARCHAR(191) NOT NULL,
createdAt DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updatedAt DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
);
ALTER TABLE Comments
ADD CONSTRAINT FK_Comments_Posts
FOREIGN KEY (postId) REFERENCES Posts (postId) ON DELETE CASCADE;
ALTER TABLE Comments
ADD CONSTRAINT FK_Comments_Users
FOREIGN KEY (userId) REFERENCES Users (userId) ON DELETE CASCADE;
[게시판 프로젝트] Prisma 설계하기
Prisma model 구현하기
먼저, 구현될 요구사항을 바탕으로 Prisma의 모델을 작성해보자.
각 테이블의 요구사항을 바탕으로 schema.prisma 파일의 모델을 작성해보자.
사용자 ( Users ) 테이블
Name
타입 ( Type )
NULL
default
userId ( PK )
INTEGER
NOT NULL
AUTO_INCREMENT
email
STRING
NOT NULL
password
STRING
NOT NULL
createdAt
DATETIME
NOT NULL
현재 시간
updatedAt
DATETIME
NOT NULL
현재 시간
게시글 ( Posts ) 테이블
Name
타입 ( Type )
NULL
default
postId ( PK )
INTEGER
NOT NULL
AUTO_INCREMENT
title
STRING
NOT NULL
content
TEXT
NOT NULL
ceratedAt
DATETIME
NOT NULL
현재 시간
updatedAt
DATETIME
NOT NULL
현재 시간
사용자 정보 ( UsersInfos ) 테이블
Name
타입 ( Type )
NULL
default
userInfoId ( PK )
INTEGER
NOT NULL
AUTO_INCREMENT
name
STRING
NOT NULL
age
INTEGER
NULL
gender
STRING
NOT NULL
profileImage
STRING
NULL
createdAt
DATETIME
NOT NULL
현재 시간
updatedAt
DATETIME
NOT NULL
현재 시간
댓글 ( Comments ) 테이블
Name
타입 ( Type )
NULL
default
commentId ( PK )
INTEGER
NOT NULL
AUTO_INCREMENT
content
STRING
NOT NULL
createdAt
DATETIME
NOT NULL
현재 시간
updatedAt
DATETIME
NOT NULL
현재 시간
위 작성한 테이블을 기준으로 Prisma model을 구현해보자.
schema.prisma에 Prisma mode을 입력한다.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Users{
userId Int @id @default(autoincrement()) @map("userId")
email String @unique @map("email")
password String @map("password")
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
@@map("Users")
}
model Posts{
postId Int @id @default(autoincrement()) @map("postId")
title String @map("title")
content String @map("content") @db.Text
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
@@map("Posts")
}
model UserInfos{
userInfoId Int @id @default(autoincrement()) @map("userInfoId")
name String @map("name")
age Int? @map("age")
gender String @map("gender")
profileImage String? @map("profileImage")
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
@@map("UserInfos")
}
model Comments{
commentId Int @id @default(autoincrement()) @map("commentId")
content String @map("content")
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
@@map("Comments")
}
Prisma 1:1 관계
요구사항 중 "사용자 ( Users )는 1개의 사용자 정보 ( UserInfo )를 가지고 있다." 에서 사용자와 사용자 정보 모델의 경우 1:1 관계를 가지고 있는 것을 확인할 수 있다. 해당 모델을 비교해 Prisma model은 어떤 방법으로 관계를 설정하는지 확인해보자.
// schema.prisma
model Users {
userId Int @id @default(autoincrement()) @map("userId")
email String @unique @map("email")
password String @map("password")
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
userInfos UserInfos? // 사용자(Users) 테이블과 사용자 정보(UserInfos) 테이블이 1:1 관계를 맺습니다.
@@map("Users")
}
model UserInfos {
userInfoId Int @id @default(autoincrement()) @map("userInfoId")
userId Int @unique @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
name String @map("name")
age Int? @map("age")
gender String @map("gender")
profileImage String? @map("profileImage")
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
// Users 테이블과 관계를 설정합니다.
user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)
@@map("UserInfos")
}
사용자 ( Users ) 모델은 사용자 정보 ( UserInfos ) 모델과 1:1관계를 가지고 있다.
여기서, 1:1 관계란 한 사용자가 하나의 사용자 정보만 가질 수 있고, 한 사용자 정보는 한 사용자에게만 속할 수 있다는 것을 의미한다.
이런 1:1 관계를 설정할 때는 다음과 같은 내용을 포함해야 한다.
관계를 설정하려는 모델 ( UserInfos )에서 어떤 모델과 관계를 맺을지 ( Users ) 설정해야 한다.
관계를 맺게되는 모델 ( Users )에서 어떤 모델이 관계를 맺는지 ( UserInfos ) 설정해야한다.
관계를 맺게되는 모델 ( Users )에서 타입을 지정할 때, Optional Parameter( ? )를 지정해 줘야한다. ( 사용자는 사용자 정보가 존재하지 않을 수 있기 때문 )
사용자 정보 ( UserInfos ) 모델에서 설정한 부분을 자세히 살펴보자.
● Users
일반적인 Int, String과 같은 타입이 아닌, 참조할 다른 모델을 지정한다.
사용자 ( Users ) 모델을 참조하므로 Users로 작성되어있다
● fields
● 사용자 정보 ( UserInfos ) 모델에서 사용할 외래키 ( Forien Key ) 컬럼을 지정한다.
● 여기서는 userId 컬럼으로 외래키를 지정했다.
● references
● key : 참조하는 다른 모델의 Column를 지정한다.
● 여기서는 사용자 ( Users ) 모델의 userId 컬럼을 참조한다.
● onDelete | onUpdate
● 참조하는 모델이 삭제 or 수정될 경우 어떤 행위를 할 지 설정한다.
● Cascade 옵션을 선택해 사용자가 삭제될 경우 그에 연결된 사용자 정보도 함께 삭제되도록 설정했다.
Prisma 1:N 연관 관계
요구사항 중 "사용자 ( Users )는 여러개의 게시글 ( Posts )을 등록할 수 있다." 에서 사용자와 게시글 모델의 경우 1:N 관계를 가지고 있는 것을 확인할 수 있다. 이번에도 2가지의 모델의 Prisma model에서 어떻게 관계를 설정하는지 확인해보자.
먼저 게시글 ( Posts ) model에서 관계를 설정하는 부분을 살펴보자.
// schema.prisma
model Users {
userId Int @id @default(autoincrement()) @map("userId")
email String @unique @map("email")
password String @map("password")
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
userInfos UserInfos? // 사용자(Users) 테이블과 사용자 정보(UserInfos) 테이블이 1:1 관계를 맺습니다.
posts Posts[] // 사용자(Users) 테이블과 게시글(Posts) 테이블이 1:N 관계를 맺습니다.
@@map("Users")
}
model Posts {
postId Int @id @default(autoincrement()) @map("postId")
userId Int @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
title String @map("title")
content String @map("content") @db.Text
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
// Users 테이블과 관계를 설정합니다.
user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)
@@map("Posts")
}
사용자 ( Users ) 모델과 게시글 ( Posts ) 모델은 1:N 관계를 가지고 있다.
여기서 1:N 관계랑 한 사용자는 여러개의 게시글을 작성할 수 있다는 것을 의미한다.
이런 1:N 관계를 설정할 때는 다음과 같은 내용을 포함해야 한다.
관계를 설정하려는 모델 ( Posts )에서 어떤 모델과 관계를 맺을지 ( Users ) 설정해야한다.
관계를 맺게되는 모델 ( Users )에서 어떤 모델이 관계를 맺는지 ( Posts ) 설정해야한다.
관계를 맺게되는 모델 ( Users )에서 타입을 지정할 때, 배열 연산자 ( [] )를 작성해줘야한다. ( 사용자는 여러개의 게시글을 가질 수 있기 때문 )
현재 게시글 모델의 경우 작성한 사용자가 회원 탈퇴 ( onDelete )하게 될 경우 작성한 모든 게시글이 삭제되도록 구현되어 있다. 이런 설정은 @relation 어노테이션을 사용해 지정한다.
// Users 테이블과 관계를 설정합니다.
user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)
여기서 User는 게시글 ( Posts )이 참조하는 다른 모델을 지정하고, fields는 게시글 ( Posts ) 모델에서 사용할 외래키 컬럼을 지정한다. references는 참조하는 다른 모델의 컬럼을 지정하고, onDelete는 참조하는 모델이 삭제될 경우 어떤 행위를 할 지 설정한다.
onDelete의 경우, Cascade 옵션으로 사용자가 삭제될 경우 연관된 게시글 또한 삭제되도록 설정했다.
댓글(Comments) 또한, 게시글 ( Posts )과 마찬가지로 사용자 ( Users ) 모델과 1:N 관계를 가지고 있다.
댓글 ( Comments ) model에서 관계를 설정하는 부분을 살펴보자.
model Users {
userId Int @id @default(autoincrement()) @map("userId")
email String @unique @map("email")
password String @map("password")
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
userInfos UserInfos? // 사용자(Users) 테이블과 사용자 정보(UserInfos) 테이블이 1:1 관계를 맺습니다.
posts Posts[] // 사용자(Users) 테이블과 게시글(Posts) 테이블이 1:N 관계를 맺습니다.
comments Comments[] // 사용자(Users) 테이블과 댓글(Comments) 테이블이 1:N 관계를 맺습니다.
@@map("Users")
}
model Posts {
postId Int @id @default(autoincrement()) @map("postId")
userId Int @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
title String @map("title")
content String @map("content") @db.Text
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
// Users 테이블과 관계를 설정합니다.
user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)
comments Comments[] // 게시글(Posts) 테이블과 댓글(Comments) 테이블이 1:N 관계를 맺습니다.
@@map("Posts")
}
model Comments {
commentId Int @id @default(autoincrement()) @map("commentId")
postId Int @map("postId") // 게시글(Posts) 테이블을 참조하는 외래키
userId Int @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
content String @map("content")
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
// Posts 테이블과 관계를 설정합니다.
post Posts @relation(fields: [postId], references: [postId], onDelete: Cascade)
// Users 테이블과 관계를 설정합니다.
user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)
@@map("Comments")
}
게시판 프로젝트 최종 Prisma model
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Users {
userId Int @id @default(autoincrement()) @map("userId")
email String @unique @map("email")
password String @map("password")
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
userInfos UserInfos? // 사용자(Users) 테이블과 사용자 정보(UserInfos) 테이블이 1:1 관계를 맺습니다.
posts Posts[] // 사용자(Users) 테이블과 게시글(Posts) 테이블이 1:N 관계를 맺습니다.
comments Comments[] // 사용자(Users) 테이블과 댓글(Comments) 테이블이 1:N 관계를 맺습니다.
@@map("Users")
}
model Posts {
postId Int @id @default(autoincrement()) @map("postId")
userId Int @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
title String @map("title")
content String @map("content") @db.Text
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
// Users 테이블과 관계를 설정합니다.
user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)
comments Comments[] // 게시글(Posts) 테이블과 댓글(Comments) 테이블이 1:N 관계를 맺습니다.
@@map("Posts")
}
model UserInfos {
userInfoId Int @id @default(autoincrement()) @map("userInfoId")
userId Int @unique @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
name String @map("name")
age Int? @map("age")
gender String @map("gender")
profileImage String? @map("profileImage")
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
// Users 테이블과 관계를 설정합니다.
user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)
@@map("UserInfos")
}
model Comments {
commentId Int @id @default(autoincrement()) @map("commentId")
postId Int @map("postId") // 게시글(Posts) 테이블을 참조하는 외래키
userId Int @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
content String @map("content")
createdAt DateTime @default(now()) @map("createdAt")
updatedAt DateTime @updatedAt @map("updatedAt")
// Posts 테이블과 관계를 설정합니다.
post Posts @relation(fields: [postId], references: [postId], onDelete: Cascade)
// Users 테이블과 관계를 설정합니다.
user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)
@@map("Comments")
}
mysecretkey를 secretkey로 변경해서 실행해보자 ( 그러면 아래와 같은 에러가 발생하게되어, 검증에 실패했다는 것을 확인할 수 있다. )
JWT를 써야 하는 이유
JWT는 두 가지 중요한 특징을 가지고 있다.
JWT가 인증 서버에서 발급되었는지 위변조 여부를 확인할 수 있다.
누구든지 JWT 내부에 들어있는 정보를 확인할 수 있다. ( 복호화 )
만약 JWT를 사용하지 않은 상태에서 사용자 로그인을 구현하려고 하면 어떻게 될까?
JWT를 적용하지 않은 로그인 API를 만들어보자
import express from 'express';
const app = express();
app.post('/login', function (req, res, next) {
const user = { // 사용자 정보
userId: 203, // 사용자의 고유 아이디 (Primary key)
email: "archepro84@gmail.com", // 사용자의 이메일
name: "이용우", // 사용자의 이름
}
res.cookie('sparta', user); // sparta 라는 이름을 가진 쿠키에 user 객체를 할당합니다.
return res.status(200).end();
});
app.listen(5002, () => {
console.log(5002, "번호로 서버가 켜졌어요!");
});
사용자의 정보가 sparta 이름을 가진 쿠키에 할당된다.
쿠키의 속성값이나 만료 시간을 클라이언트가 언제든지 수정할 수 있다
쿠키의 위변조 여부를 확인 할 수 없다.
JWT를 적용한 로그인 API를 만들어보자
import express from 'express';
import JWT from 'jsonwebtoken';
const app = express();
app.post('/login', (req, res) => {
// 사용자 정보
const user = {
userId: 203,
email: 'archepro84@gmail.com',
name: '이용우',
};
// 사용자 정보를 JWT로 생성
const userJWT = JWT.sign(
user, // user 변수의 데이터를 payload에 할당
'secretOrPrivateKey', // JWT의 비밀키를 secretOrPrivateKey라는 문자열로 할당
{ expiresIn: '1h' }, // JWT의 인증 만료시간을 1시간으로 설정
);
// userJWT 변수를 sparta 라는 이름을 가진 쿠키에 Bearer 토큰 형식으로 할당
res.cookie('sparta', `Bearer ${userJWT}`);
return res.status(200).end();
});
app.listen(5002, () => {
console.log(5002, '번호로 서버가 켜졌어요!');
});
사용자의 정보를 Payload에 저장한 JWT를 sparta 이름을 가진 쿠키에 할당된다.
JWT를 생성할 때 위변조 여부를 확인할 수 있는 비밀키를 사용했다.
쿠키의 만료시간과 별개로 JWT의 만료시간을 설정했다.
암호화 된 데이터의 사용 방법
보통 암호화 된 데이터는 클라이언트 ( 브라우저 ) 가 전달받아 다양한 수단 ( 쿠키, 로컬스토리지 등 )을 통해 저장해 API 서버에 요청을 할 때 서버가 요구하는 HTTP 인증 양식에 맞게 보내주어 인증을 시도한다.
브라우저가 서버로부터 응답으로 Set-Cookie 헤더를 받은 경우 해당 데이터를 저장한 뒤 모든 요청에 포함하여 보낸다.
쿠키는 사용자가 naver.com과 같은 웹 사이트를 방문할 때마다 이전에 방문했던 정보를 기억하는 데이터 파일이다.
데이터를 여러 사이트에 공유할 수 있기 때문에 보안에 취약할 수 있다.
쿠키는 userId=user-1321;userName=sparta 와 같이 문자열 형식으로 존재하며 쿠키간에는 세미콜론 (;) 으로 구분한다.
세션
쿠키를 기반으로 구성된 기술이다. 단, 클라이언트가 마음대로 데이터를 확인 할 수 있던 쿠키와는 다르게 세션은 데이터를 서버에만 저장한다.
세션은 일반적으로 세션 Id를 쿠키를 이용해 클라이언트에게 전달해, 서버는 이 세션 Id를 사용해 저장된 세션 데이터를 조회한다.
세션을 통해 사용자의 상태 정보를 서버에 저장하면, 서버는 사용자의 상태를 추적 할 수 있게 된다.
보안성은 좋으나, 반대로 사용자가 많은 경우 서버에 저장해야 할 데이터가 많아져서 서버 컴퓨터가 감당하지 못하는 문제가 생기기 쉽다.
쿠키와 마찬가지로 세션 역시 만료 기간이 있다.
쿠키 ( Cookie ) 만들어보기
서버가 클라이언트의 HTTP 요청 ( Request )을 수신할 때, 서버는 응답 ( Response )과 함께 Set-Cookie 라는 헤더를 함께 전송할 수 있다. 그 후 쿠키는 해당 서버에 의해 만들어진 응답 ( Response )과 함께 Cookie HTTP 헤더안에 포함되어 전달받는다.
// 'Set-Cookie'를 이용하여 쿠키를 할당하는 API
app.get("/set-cookie", (req, res) => {
let expire = new Date();
expire.setMinutes(expire.getMinutes() + 60); // 만료 시간을 60분으로 설정합니다.
res.writeHead(200, {
'Set-Cookie': `name=sparta; Expires=${expire.toGMTString()}; HttpOnly; Path=/`,
});
return res.end();
});
res.cookie() 를 이용해 쿠키 할당하기
// 'res.cookie()'를 이용하여 쿠키를 할당하는 API
app.get("/set-cookie", (req, res) => {
let expires = new Date();
expires.setMinutes(expires.getMinutes() + 60); // 만료 시간을 60분으로 설정합니다.
res.cookie('name', 'sparta', {
expires: expires
});
return res.end();
});
req를 이용해 쿠키 접근하기
클라이언트는 서버에 요청 ( Request )을 보낼 때 자신이 보하고 있는 쿠키를 자동으로 서버에 전달하게 된다.
여기서 클라이언가 전달하는 쿠키 정보는 Request header에 포함되어 서버에 전달되게 된다.
서버에서 어떤 방식으로 쿠키를 사용할까?
일반적으로 쿠키는 req.headers.cookie에 들어있다. req.headers는 클라이언트가 요청한 Request의 헤더 ( header )를 의미한다.
Request의 헤더
헤더는 요청을 보낸 클라이언트에 관한 상세한 정보를 담고 있다.
이 정보에는 사용자의 브라우저 유형, 운영 체제, 데스크탑이나 모바일의 사용 유무 등이 포함된다.
깨알 같이, 쿠키 정보 또한 이 헤더 안에 포함되어 있는것을 확인할 수 있다.
/get-cookie에 접근했을 때, 클라이언트가 전달한 모든 쿠키를 출력하는 API를 만들어보자
// 'req.headers.cookie'를 이용하여 클라이언트의 모든 쿠키를 조회하는 API
app.get('/get-cookie', (req, res) => {
const cookie = req.headers.cookie;
console.log(cookie); // name=sparta
return res.status(200).json({ cookie });
});
cookie-parser 미들웨어 적용하기
요청에 추가된 쿠키를 req.cookies 객체로 만들어 더이상 req.headers.cookie와 같이 번거롭게 사용할 필요 없어진다.
이전에는 req.headers.cookie 와 같이 여러 프로퍼티를 넘어서야 쿠키를 사용할 수 있었으나 cookie-parser 미들웨어를 이용하면 더욱 간편하게 쿠키를 관리할 수 있다.
cookie-parser 설치
# yarn을 이용해 cookie-parser를 설치합니다.
yarn add cookie-parser
cookie-parser 미들웨어를 전역으로 사용하려면 다음과 같이 사용한다.
app.use(cookieParser());
cookie-parser 등록하기
import cookieParser from 'cookie-parser';
app.use(cookieParser());
// 'req.cookies'를 이용하여 클라이언트의 모든 쿠키를 조회하는 API
app.get('/get-cookie', (req, res) => {
const cookies = req.cookies;
console.log(cookies);
return res.status(200).json({ cookie: cookies });
});
쿠키 조회 부분을 req.cookies로 변경했고, 쿠키의 형태가 name=sparta에서 { name: 'sparta' } 형태의 객체로 변환된 것을 확인 할 수 있다.
쿠키의 경우 서버를 재시작하거나 새로고침을 하더라도 로그인이 유지된다. 이는 사용자에게는 편리하나, 서버의 입장에서는 보안 문제가 발생할 수 있다. 쿠키가 조작되거나 노출된다면 해당 권한을 탈튀당해, 악의적인 공격을 받을 수 있게 되는 것이다.
그렇다면, 쿠키에는 어떤 정보를 담아야 할까?
사용자가 누구인지 확실하게 구분할 수 있는 정보를 넣어줘야 한다.
먼저 /set-session API를 호출했을 때 name=sparta 의 정보를 서버에 저장하고, 저장한 시간 정보를 쿠키로 반환 받는 API와 /get-session API를 호출했을 때 쿠키의 시간 정보를 이용해 서버에 저장된 name 정보를 출력하는 API를 만들어보자.
/set-session API 만들기
let session = {};
app.get('/set-session', function (req, res, next) {
// 현재는 sparta라는 이름으로 저장하지만, 나중에는 복잡한 사용자의 정보로 변경될 수 있습니다.
const name = 'sparta';
const uniqueInt = Date.now();
// 세션에 사용자의 시간 정보 저장
session[uniqueInt] = { name };
res.cookie('sessionKey', uniqueInt);
return res.status(200).end();
});
서버에서 해당 사용자의 정보를 저장하기 위해 session 객체를 생성한다.
/set-session API가 호출되면 name=sparta의 정보를 세션에 삽입하고, 해당하는 데이터를 검색하기 위한 시간 정보를 쿠키로 반환한다.
/get-session API 만들기
app.get('/get-session', function (req, res, next) {
const { sessionKey } = req.cookies;
// 클라이언트의 쿠키에 저장된 세션키로 서버의 세션 정보를 조회합니다.
const name = session[sessionKey];
return res.status(200).json({ name });
});
쿠키에 저장된 sessionKey를 이용해 session에 저장된 데이터를 불러온다.
세션 API 전체 코드, app.js
import express from 'express';
import cookieParser from 'cookie-parser';
const app = express();
const PORT = 5001;
app.use(express.json());
app.use(cookieParser());
// 'req.cookies'를 이용하여 클라이언트의 모든 쿠키를 조회하는 API
app.get('/get-cookie', (req, res) => {
const cookies = req.cookies;
console.log(cookies);
return res.status(200).json({ cookie: cookies });
});
let session = {};
app.get('/set-session', function (req, res, next) {
// 현재는 sparta라는 이름으로 저장하지만, 나중에는 복잡한 사용자의 정보로 변경될 수 있습니다.
const name = 'sparta';
const uniqueInt = Date.now();
// 세션에 사용자의 시간 정보 저장
session[uniqueInt] = { name };
res.cookie('sessionKey', uniqueInt);
return res.status(200).end();
});
app.get('/get-session', function (req, res, next) {
const { sessionKey } = req.cookies;
// 클라이언트의 쿠키에 저장된 세션키로 서버의 세션 정보를 조회합니다.
const name = session[sessionKey];
return res.status(200).json({ name });
});
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
터미널에서 yarn run dev 명령어를 실행하면, nodemon을 이용해 서버를 시작할 수 있게 된다.
schema.prisma
Prisma가 사용할 데이터베이스의 설정 정보를 정의하기 위해 사용하는 파일이다.
Prisma를 가장 처음 초기화 하였을 때, prisma.schema 파일을 확인한다면, 아래의 2가지 구문이 작성되어 있는 것을 확인할 수 있다.
● datasource
데이터베이스에 대한 정의를 하기 위해 사용된다.
Prisma가 어떤 데이터베이스 엔진을 사용할 것인지, 데이터베이스의 위치 ( URL )는 어디인지 등의 정보를 정의하는데 사용된다.
● generator
Prisma 클라이언트를 생성하는 방식을 설정하는 구문이다.
Prisma datasource
Prisma가 데이터베이스를 연결할 수 있도록 설정하고, 관리하는 데 필요한 정보를 설정하는 구문이다.
우선 Prisma는 연결하려는 데이터베이스의 속성을 schema.prisma 파일에서 관리하고 있다.
여기서 datasource 프로퍼티에 정의한 속성들을 수정해 사용자 아이디, 비밀번호, 엔드 포인트 등 다양한 설정값을 입력해주어야 한다.
datasource 설정
// schema.prisma
datasource db {
// MySQL 데이터베이스 엔진을 사용합니다.
provider = "mysql"
// 데이터베이스 연결 정보를 .env 파일의 DATABASE_URL 로부터 읽어옵니다.
url = env("DATABASE_URL")
}
처음 생성된 datasource 구문은 위와 같이 작성되어 있다. 각 프로퍼티들은 아래와 같은 속성을 가지고 있다.
1. provider : Prisma 가 사용할 데이터베이스 엔진의 유형
2. url : 데이터베이스를 연결하기 위한 URL
url 부분에서 env("DATABASE_URL") 방식으로, 데이터베이스의 주소가 노출되지 않게 작성하는 dotenv 의 문법을 사용하고 있다. env() 문법은 프로젝트의 root 폴더에 있는 .env 파일에 정의되어 있는 정보를 해당 schema.prisma 파일로 불러오는 것이다.
여기서, dotenv 는 어플리케이션의 환경 변수를 관리하는 모듈이다. 실제 코드에서 민감한 정보를 노출시키지 않도록 보호해주고, 개발 환경에 따라 다르게 설정해야 하는 값을 별도의 파일에서 관리할 수 있게 해준다.
Prisma는 model을 generate 하면, 해당 모델에 대한 정보가 node_modules 폴더 내에 있는 Prisma Client에 전달된다.
( prisma db push도 내부적으로 generate 가 실행된다. )
Prisma Client는 Prisma Schema에 정의한 데이터베이스 모델 ( model )을 TypeScript 코드로 변환해, 개발자가 데이터베이스와 상호작용할 수 있게 해준다. 이러한 과정을 통해, 데이터베이스를 JavaScript에서 손쉽게 다룰 수 있게 되고, Prisma Schema와 동기화된 Prisma Client를 이용해 데이터베이스를 사용할 수 있게 된다.
Prisma Client 확인해보기
// 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 테이블에 대한 내용이 위와 같이 작성되어 있다.
schema.prisma 정의한 내용을 prisma generate를 이용해 index.d.ts에 추가하고 최종적으로는 index.d.ts에 있는 내용을 바탕으로 prisma를 구성한다.
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 프로젝트를 초기화 하자.
// 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
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 게시글 생성 ( Create ) API
게시글 생성 API의 비즈니스 로직
title, content, password 를 body로 전달받는다.
title, content, password 를 이용해 Posts 테이블에 데이터를 삽입 한다.
PrismaClient 는 Prisma를 사용해 실제 데이터베이스와의 연결을 관리하는 객체다.
new PrismaClient() 를 이용해 Javascript에서 Prisma를 사용할 수 있도록 인스턴스를 생성하게 된다.
const prisma = new PrismaClient();
앞서 작성한 게시글 ( 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 인스턴스를 생성합니다.
Node.js 에서 Raw Query를 사용하기 위해서 AWS RDS에서 대여받은 MySQL에 연결을 도와주는 데이터베이스 드라이버가 필요하다. 데이터베이스에 직접 SQL을 요청하고, 테이블을 생성하거나, 데이터를 삽입하는 API를 mysql2 라이브러리를 이용해 구현해보자.
Raw Query 라이브러리 설치
# yarn으로 프로젝트를 초기화합니다.
yarn init -y
# express와 mysql 드라이버를 설치합니다.
yarn add express mysql2
mysql2는 MySQL 데이터베이스를 Node.js에서 사용할 수 있게 도와주는 라이브러리다.
데이터베이스와 개발 언어 사이를 연결해주는 역할을 담당하기 때문에, 데이터베이스 드라이버라는 이름으로도 불린다.
데이터베이스 연결하기
우선, app.js 파일을 만들고, mysql2 라이브러리를 사용해 AWS RDS의 MySQL과 연결을 설정해야 한다.
// app.js
import express from 'express';
import mysql from 'mysql2';
const connect = mysql.createConnection({
host: 'AWS RDS 엔드포인트', // 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, '포트로 서버 열림!');
});
mysql2 라이브러리를 읽어오고, createConnection() 함수를 이용해 MySQL DB와 연결한다.
mysql2 데이터베이스 연결 속성 알아보기
host ( mysql2 데이터베이스 드라이버가 접속할 데이터베이스의 주소를 나타낸다. )
user ( 데이터베이스의 계정 명을 나타낸다. )
password ( 데이터베이스의 비밀번호를 나타낸다. )
database ( 데이터베이스의 이름을 나타낸다. )
이 외에도, timezone 으로 시간대를 설정하거나, ssl로 SSL 인증서를 설정하는 등 다양한 옵션을 설정할 수 있다.
테이블 생성 API
CREATE TABLE sql로 테이블을 생성할 수 있다.
클라이언트로부터 생성할 테이블 이름을 tableName으로 전달받아서 새로운 테이블을 생성해보자.
테이블 생성 API 테이블 구조
Name
타입
NULL
제약 조건
default
id ( PK )
INTEGER
NOT NULL
PRIMARY KEY
AUTO_INCREMENT
name
STRING
NOT NULL
createdAt
DATETIME
NOT NULL
현재 시간
/** 테이블 생성 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: '테이블 생성에 성공하였습니다.' });
});
Raw Query의 사용법
mysql2 라이브러리에서 Raw Query는 connect.promise().query() 형식으로 사용한다.