기본적으로 Node.js는 단일 스레드에서 실행되고, 이벤트 루프는 한번에 하나의 프로세스만 발생한다.
따라서 CPU 집약적인 작업 ( 대규모 데이터 처리, 이미지 변환 등 )이 메인 스레드를 차단 할 수 있다.
Worker Thread
CPU 집약적인 작업을 실행할 때 메인 스레드의 성능과 응답성을 유지하기 위해 사용하는 백그라운드 스레드다.
Wokrer는 비동기적으로 실행되고, JavaScript의 싱글 스레드 모델을 보완하여 멀티스레드 환경을 제공한다.
1. 독립된 스레드
Woker는 메인 스레드와 별도의 실행 컨텍스트에서 동작하며, 독립적인 global scope를 가진다.
2. 메세지 기반 통신
메인 스레드와 Worker는 postMessage()와 onMessage를 통해 데이터를 교환한다.
woker.js
// Worker 내부 코드
onmessage = function(event) {
const data = event.data; // 메인 스레드에서 받은 데이터
const result = data * 2; // 작업 처리
postMessage(result); // 결과 반환
};
main.js
// 메인 스레드 코드
const worker = new Worker('worker.js');
// 메시지 보내기
worker.postMessage(10);
// 메시지 받기
worker.onmessage = function(event) {
console.log('Worker로부터 받은 결과:', event.data); // 20
};
// Worker 종료
worker.terminate();
// app.js
import express from 'express';
import expressSession from 'express-session';
const app = express();
const PORT = 3019;
app.use(express.json());
app.use(
expressSession({
secret: 'express-session-secret-key.', // 세션을 암호화하는 비밀 키를 설정
resave: false, // 클라이언트의 요청이 올 때마다 세션을 새롭게 저장할 지 설정, 변경사항이 없어도 다시 저장
saveUninitialized: false, // 세션이 초기화되지 않았을 때 세션을 저장할 지 설정
cookie: {
// 세션 쿠키 설정
maxAge: 1000 * 60 * 60 * 24, // 쿠키의 만료 기간을 1일로 설정합니다.
},
}),
);
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
express-session은 아래와 같이 전역 미들웨어로 등록된다.
app.use(
expressSession({
secret: 'express-session-secret-key.', // 세션을 암호화하는 비밀 키를 설정
resave: false, // 클라이언트의 요청이 올 때마다 세션을 새롭게 저장할 지 설정, 변경사항이 없어도 다시 저장
saveUninitialized: false, // 세션이 초기화되지 않았을 때 세션을 저장할 지 설정
cookie: {
// 세션 쿠키 설정
maxAge: 1000 * 60 * 60 * 24, // 쿠키의 만료 기간을 1일로 설정합니다.
},
}),
);
각각의 구성요소는 아래와 같은 내용을 담는다.
secret
세션 ID를 암호화하기 위한 비밀 키
클라이언트에게 발급할 세션 ID를 암호화하기 위한 비밀 키 정보
resave
클라이언트의 요청( Request )이 들어올 때마다 세션 정보를 다시 저장할 지 설정한다.
변경사항이 없더라도 true로 설정하면, 매번 새로운 세션에 저장된다.
saveUninitialized
req.session에 아무런 정보가 저장이 되지 않아도 사용자에게 세션 ID를 발급할지 설정한다.
# yarn을 이용해 프로젝트를 초기화합니다.
yarn init -y
# express, jsonwebtoken, cookie-parser 패키지를 설치합니다.
yarn add express jsonwebtoken cookie-parser
package.json에 "type":"module" 추가
app.js ( 기본적인 틀 )
// app.js
import express from 'express';
import jwt from 'jsonwebtoken';
import cookieParser from 'cookie-parser';
const app = express();
const PORT = 3019;
// 비밀 키는 외부에 노출되면 안되겠죠? 그렇기 때문에, .env 파일을 이용해 비밀 키를 관리해야함
const ACCESS_TOKEN_SECRET_KEY = `HangHae99`; // Access Token의 비밀 키를 정의한다.
const REFRESH_TOKEN_SECRET_KEY = `Sparta`; // Refresh Token의 비밀 키를 정의한다.
app.use(express.json());
app.use(cookieParser());
app.get('/', (req, res) => {
return res.status(200).send('Hello Token!');
});
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
Refresh Token과 Access Token을 발급하는 API 만들어보기
POST /tokens 의 주소를 갖는 API 생성
// app.js
import express from 'express';
import jwt from 'jsonwebtoken';
import cookieParser from 'cookie-parser';
const app = express();
const PORT = 3019;
// 비밀 키는 외부에 노출되면 안되겠죠? 그렇기 때문에, .env 파일을 이용해 비밀 키를 관리해야함
const ACCESS_TOKEN_SECRET_KEY = `HangHae99`; // Access Token의 비밀 키를 정의한다.
const REFRESH_TOKEN_SECRET_KEY = `Sparta`; // Refresh Token의 비밀 키를 정의한다.
app.use(express.json());
app.use(cookieParser());
app.get('/', (req, res) => {
return res.status(200).send('Hello Token!');
});
let tokenStorage = {}; // Refresh Token을 저장할 객체
/** Access Token, Refresh Token 발급 API **/
app.post('/tokens', (req, res) => {
// id 전달
const { id } = req.body;
// accessToken 발급
const accessToken = createAccessToken(id);
// refreshToken 발급
const refreshToken = createRefreshToken(id);
// Refresh Token을 가지고 해당 유저의 정보를 서버에 저장합니다.
tokenStorage[refreshToken] = {
id: id, // 사용자에게 전달받은 ID를 저장합니다.
ip: req.ip, // 사용자의 IP 정보를 저장합니다.
// 사용자의 User Agent 정보를 저장합니다.
// 특정 클라이언트가 어떤 방식으로 서버에 요청했는지를 알수 있는 정보
// 예를 들어 firefox를 이용했는지, chrome을 이용했는지, 모바일로 chrome을 실행했는지 등
userAgent: req.headers['user-agent'],
};
//console.log(tokenStorage);
res.cookie('accessToken', accessToken); // Access Token을 Cookie에 전달한다.
res.cookie('refreshToken', refreshToken); // Refresh Token을 Cookie에 전달한다.
return res
.status(200)
.json({ message: 'Token이 정상적으로 발급되었습니다.' });
});
// Access Token을 생성하는 함수
function createAccessToken(id) {
const accessToken = jwt.sign(
{ id: id }, // JWT 데이터
ACCESS_TOKEN_SECRET_KEY, // Access Token의 비밀 키
{ expiresIn: '10s' }, // Access Token이 10초 뒤에 만료되도록 설정한다.
);
return accessToken;
}
// Refresh Token을 생성하는 함수
function createRefreshToken(id) {
const refreshToken = jwt.sign(
{ id: id }, // JWT 데이터
REFRESH_TOKEN_SECRET_KEY, // Refresh Token의 비밀 키
{ expiresIn: '7d' }, // Refresh Token이 7일 뒤에 만료되도록 설정한다.
);
return refreshToken;
}
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
Refresh Token의 정보를 어디서 관리해야할까?
위 예시에서는 tokenStorage 라는 변수에서 관리했지만, 이는 인 메모리 방식을 사용하기 때문에 서버가 재시작 또는 종료될 경우 모든 정보가 사라지게 된다.
따라서 별도의 테이블에서 Refresh Token을 저장하고 관리해야한다.
이렇게 하면, Refresh Token 검증 작업을 MySQL과 같은 데이터베이스를 조회함과 동시에 함께 처리할 수 있다.
Refresh Tokens Table
Name
타입 ( Type )
NULL
default
비고
tokenId ( PK )
INTEGER
NOT NULL
AUTO_INCREMENT
토큰의 기본 키
userId ( FK )
INTEGER
NOT NULL
사용자의 기본 키
token
STRING
NOT NULL
리프레시 토큰
expiresAt
DATETIME
NOT NULL
만료 날짜
createdAt
DATETIME
NOT NULL
현재 시간
생성 날짜
이 외에도 ip 또는 user-Agent와 같은 정보를 추가할 수 있다.
Access Token 검증 API
서버에서 발급받은 Access Token을 검증하는 GET /tokens/validate API를 만들어보자.
// app. js
...
/** 엑세스 토큰 검증 API **/
app.get('/tokens/validate', (req, res) => {
const accessToken = req.cookies.accessToken;
if (!accessToken) {
return res
.status(400)
.json({ errorMessage: 'Access Token이 존재하지 않습니다.' });
}
const payload = validateToken(accessToken, ACCESS_TOKEN_SECRET_KEY);
if (!payload) {
return res
.status(401)
.json({ errorMessage: 'Access Token이 유효하지 않습니다.' });
}
const { id } = payload;
return res.json({
message: `${id}의 Payload를 가진 Token이 성공적으로 인증되었습니다.`,
});
});
// Token을 검증하고 Payload를 반환합니다.
function validateToken(token, secretKey) {
try {
const payload = jwt.verify(token, secretKey);
return payload;
} catch (error) {
return null;
}
}
ValidateToken 함수 추가 설명
ValidaeToken 함수는 제공된 토큰이 유효한지 여부를 검증하는 역할을 담당한다.
secretkey를 전달받아, 서버에서 검증할 비밀 키를 설정한다.
Access Token 이나 Refresh Token이 우리가 발급한 것인지 검증한다.
Access Token 이나 Refresh Token의 만료 여부를 검증한다.
사용자가 Cookie를 전달할 때, Access Token이 없다면 에러가 발생한다.
{ "message": "Access Token이 존재하지 않습니다." }
사용자가 전달한 Access Token이 유효하지 않을 경우 에러가 발생한다.
{ "message": "Access Token이 유효하지 않습니다." }
사용자가 Access Token을 이용해 API를 호출하면, 해당 토큰의 상태에 따라 적절한 응답 ( Response )이 반환되는 것을 확인 할 수 있다.
Access Token이 정상적으로 인증되면, 응답에서 페이로드 값을 확인할 수 있다.
Refresh Token으로 Access Token을 재발급하는 API
서버에서 발급받은 Access Token을 검증하는 POST /tokens/refresh API를 만들어보자.
// app. js
...
/** 리프레시 토큰 검증 API **/
app.post('/tokens/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken)
return res
.status(400)
.json({ errorMessage: 'Refresh Token이 존재하지 않습니다.' });
const payload = validateToken(refreshToken, REFRESH_TOKEN_SECRET_KEY);
if (!payload) {
return res
.status(401)
.json({ errorMessage: 'Refresh Token이 유효하지 않습니다.' });
}
const userInfo = tokenStorage[refreshToken];
if (!userInfo)
return res.status(419).json({
errorMessage: 'Refresh Token의 정보가 서버에 존재하지 않습니다.',
});
const newAccessToken = createAccessToken(userInfo.id);
res.cookie('accessToken', newAccessToken);
return res.json({ message: 'Access Token을 새롭게 발급하였습니다.' });
});
// Token을 검증하고 Payload를 반환합니다.
function validateToken(token, secretKey) {
try {
const payload = jwt.verify(token, secretKey);
return payload;
} catch (error) {
return null;
}
}
사용자가 Cookie를 전달할 때, Refresh Token이 없다면 에러가 발생한다.
{ "message": "Refresh Token이 존재하지 않습니다." }
사용자가 전달한 Refresh Token이 유효하지 않을 경우 에러가 발생한다.
{ "message": "Refresh Token이 유효하지 않습니다." }
Refresh Token이 유효하지만, 서버에 해당 토큰 정보가 없을 경우 에러가 발생한다.
{ "message": "Refresh Token의 정보가 서버에 존재하지 않습니다." }
사용자가 Refresh Token을 이용해 API를 호출하면, 해당 토큰의 상태에 따라 적절한 응답 ( Response )이 반환되는 것을 확인할 수 있다.
만약, Refresh Token이 정상적으로 인증되면, 사용자에게 Access Token을 담은 Cookie를 전달하는 것을 확인할 수 있다.
Express.js를 이용한 게시판 프로젝트를 만들기 전에, 우선 app.js에 기본적인 토대를 만들고 간단한 API를 구현해보자.
모든 Router에서 Prisma를 사용할 수 있도록 utils/prisma/index.js 파일을 생성한다.
app.js 초기화
// src/app.js
import express from 'express';
import cookieParser from 'cookie-parser';
const app = express();
const PORT = 3018;
app.use(express.json());
app.use(cookieParser());
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
Primsa 초기화
// src/utils/prisma/index.js
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient({
// Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해줍니다.
log: ['query', 'info', 'warn', 'error'],
// 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해줍니다.
errorFormat: 'pretty',
}); // PrismaClient 인스턴스를 생성합니다.
회원가입 API 리팩토링이 완료되었으면, API Client를 이용해 해당 API를 호출해보자.
Users 테이블의 password 항목이 이전과 다르게 암호화된 값으로 작성되어 있을 것이다.
로그인 API
로그인 API는 bcrypt로 암호화된 사용자 비밀번호를 인증하는 방식이다.
클라이언트가 제공한 비밀번호와 데이터베이스에 저장된 암호화된 비밀번호를 검증해 구현해보자.
로그인 API 비즈니스 로직
email, password를 body로 전달받는다.
전달 받은 email에 해당하는 사용자가 있는지 확인한다.
전달 받은 password와 데이터베이스의 저장된 password를 bcrypt를 이용해 검증한다.
로그인에 성공하면, 사용자에게 JWT를 발급한다.
로그인 API는 클라이언트가 전달한 정보를 바탕으로 사용자를 확인한다. 클라이언트로부터 받은 email과 password를 사용해 데이터베이스에 저장된 사용자를 검증하고, 검증에 성공하면 JWT를 담고있는 쿠키를 생성해 반환한다.
클라이언트는 로그인 이후 요청부터 쿠키를 함께 보내면, 이를 통해 서버에서는 해당 사용자를 식별하고, 인증 ( Authentication ), 인가 ( Authorization ) 과정을 거친다.
로그인 API
// src/routes/users.route.js
import jwt from 'jsonwebtoken';
/** 로그인 API **/
router.post('/sign-in', async (req, res, next) => {
const { email, password } = req.body;
const user = await prisma.users.findFirst({ where: { email } });
if (!user)
return res.status(401).json({ message: '존재하지 않는 이메일입니다.' });
// 입력받은 사용자의 비밀번호와 데이터베이스에 저장된 비밀번호를 비교합니다.
else if (!(await bcrypt.compare(password, user.password)))
return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });
// 로그인에 성공하면, 사용자의 userId를 바탕으로 토큰을 생성합니다.
const token = jwt.sign(
{
userId: user.userId,
},
'custom-secret-key',
);
// authotization 쿠키에 Berer 토큰 형식으로 JWT를 저장합니다.
res.cookie('authorization', `Bearer ${token}`);
return res.status(200).json({ message: '로그인 성공' });
});
사용자 인증 미들웨어
사용자 인증 미들웨어는 클라이언트로 부터 전달받은 쿠키를 검증하는 작업을 수행한다.
클라이언트가 제공한 쿠키에 담겨있는 JWT를 이용해 사용자를 조회하도록 구현해보자.
사용자 인증 미들웨어 비즈니스 로직
클라이언트로 부터 쿠키 ( Cookie )를 전달받는다.
쿠키 ( Cookie )가 Bearer 토큰 형식인지 확인한다.
서버에서 발급한 JWT가 맞는지 검증한다.
JWT의 userId를 이용해 사용자를 조회한다.
req.user에 조회된 사용자 정보를 할당한다.
다음 미들웨어를 실행한다.
사용자 인증 미들웨어는 클라이언트가 전달한 쿠키를 바탕으로 사용자를 검증한다. 이 과정에서 토큰이 만료되진 않았는지, 토큰의 형식은 일치하는지, 서버가 발급한 토큰이 맞는지 등 다양한 검증을 수행해 사용자의 권한을 확인한다.
이렇게, JWT를 통해 사용자를 인증하는 것을 인증 ( Authentication )이라고 부른다.
클라이언트는 인증 과정을 전부 통과하였을 때에 로그인된 사용자만 사용할 수 있는 게시글 작성 API, 사용자 정보 조회 API와 같은 권한이 필요한 API를 사용할 수 있게 된다. 이 사용자의 권한을 확인하는 과정을 인가 ( Authorization ) 과정이라고 부른다. ( 사용자 인증 미들웨어는 인증 기능만 제공한다. 이후 관리자 권한, 등급별 권한이 필요하면, 별도의 인가 과정을 추가해야한다 )
사용자 인증 미들웨어
// src/middlewares/auth.middleware.js
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js';
export default async function (req, res, next) {
try {
const { authorization } = req.cookies;
if (!authorization) throw new Error('토큰이 존재하지 않습니다.');
// Bearer이 tokenType에 들어가고, %20을 건너 뛴 후에 이후 내용이 token으로 들어간다.
const [tokenType, token] = authorization.split(' ');
if (tokenType !== 'Bearer')
throw new Error('토큰 타입이 일치하지 않습니다.');
const decodedToken = jwt.verify(token, 'custom-secret-key');
const userId = decodedToken.userId;
const user = await prisma.users.findFirst({
where: { userId: +userId },
});
if (!user) {
res.clearCookie('authorization');
throw new Error('토큰 사용자가 존재하지 않습니다.');
}
// req.user에 사용자 정보를 저장합니다.
req.user = user;
next();
} catch (error) {
res.clearCookie('authorization');
// 토큰이 만료되었거나, 조작되었을 때, 에러 메시지를 다르게 출력합니다.
switch (error.name) {
case 'TokenExpiredError':
return res.status(401).json({ message: '토큰이 만료되었습니다.' });
case 'JsonWebTokenError':
return res.status(401).json({ message: '토큰이 조작되었습니다.' });
default:
return res
.status(401)
.json({ message: error.message ?? '비정상적인 요청입니다.' });
}
}
}
사용자 정보 조회 API
사용자 정보 조회 API는 일반적으로 다른 사용자의 정보를 조회하기 위해 사용한다.
사용자 정보 조회 API 비즈니스 로직
클라이언트가 로그인된 사용자인지 검증한다.
사용자를 조회할 때, 1:1 관계를 맺고 있는 Users와 UserInfos 테이블을 조회한다.
조회한 사용자의 상세한 정보를 클라이언트에게 반환한다.
사용자 정보 조회 API는 단순히 Users 테이블 하나만 조회를 하는 것이 아니라, UserInfos 테이블을 함께 조회한다.
그렇기 때문에, 각각의 테이블을 1번씩 조회하게 되어 총 2번의 조회를 하게되는 문제가 발생한다.
하나의 게시글 ( 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")
}