
이번에는 DDL이 아니라 직접 GUI에서 테이블을 만들어봤어요.

GUI 상에서 직접 row를 수정할 수도 있고, 생성할 수도 있어요.

이렇게 sub_num과 video_count는 null 시 default로 0을 지정할 수 있어요.
참조 무결성이란 부모 테이블에 존재하지 않는 값을 자식 테이블이 가질 수 없도록 강제하는 것을 말해요.
예를 들어, users 테이블에 없는 유저 ID로는 channels를 생성할 수 없게 막는 것이 있어요.

이렇게 user_id가 users 테이블에 없는 id (여기서는 10)을 했더니 다음과 같은 참조 무결성 제약조건이 터졌어요.

기존 1,2,3 id 중에 2을 지우고, 새로 row를 만들면 2이 아니라 4로 만들어져요!
가장 마지막 값을 기준으로 1을 더하는 것을 알 수 있어요.
ORM 없이 mysql2 라이브러리로 연결해보기로 했어요.
이렇게 Array<Object> 형식으로 응답이 오는 걸 알 수 있었어요.

즉슨, result에서 키 값으로 조회할 수 있다는 말이에요

그리고 created_at 칼럼도 추가했어요.


created_at 도 이렇게 나온 것을 볼 수 있었어요.
db 조회시 서울시간으로 나오지 않는 것이 어색해서 조정하려고 해요.

먼저 db 연결시 타임존을 아시아/서울로 설정하면 우리가 예상한대로 되지 않아요.
// 기존 시간 세팅 확인하기
SELECT @@global.time_zone, @@session.time_zone;
// 기존 세팅은 둘의 값이 모두 SYSTEM임.
SET GLOBAL time_zone = 'Asia/Seoul';
SET time_zone = 'Asia/Seoul';
설정을 변경하면 조회한 테이블에서도 자연스럽게 값이 바뀐 것을 확인할 수 있어요

하지만 노드에서 보이는 시간은 그대로 일 텐데 이를 바꾸기 위한 코드가 createConnection() 내부의 'dateStrings : true' 와 timezone: '+09:00 이에요.
기존에는 시간이 '2024-05-09T08:47:47.000Z' 으로 나오는 불편함이 있었지만,
설정을 해주게 되면 '2024-05-09 17:47:47' 로 원하는 문자열로 잘 나오는 것을 확인할 수 있어요.
const connection = db.createConnection({
host: 'localhost',
port: 3307,
user: 'user',
password: 'root',
database: 'mydb',
dateStrings: true,
timezone: '+09:00'
})
모듈화는 지난 시간에 연동했던 connection을 export default 를 활용해 내보내고, import 를 통해서 불러서 사용해 보겠습니다.
// db.js
export default connection;
// librarians.js, books.js
import mariadb from '../db''
이제는 전역 변수로 db를 쓰는 게 아니라 실제로 db를 조회하는 방식으로 리팩토링해야해요.
router.get('/users/:email', (req,res) => {
const email = req.params.email;
conn.query(
`SELECT * FROM users where email= ${email}`,
(err, results, fields) => {
if(results.length === 0) return res.status(404).json(
{ "message": "No User Found" }
)
res.json(results);
}
)
});
이 코드에서 socket hang up 에러가 발생했어요.
이 에러는 error 발생시 아무런 로직이 없으면 생기는 에러라고 해요.
또한 ${email} 처럼 받으려면 요청을 받을 때 '' 를 붙여서 파라미터에 줘야해요.

해결결과 잘 요청이 온 것을 볼 수 있어요.
const email = req.params.email;
conn.query(
`SELECT * FROM users where email= ?`, email,
(err, results, fields) => {
if (err) return res.status(500).json(err);
res.json(results);
}
또한 정석적으로는 템플릿 문자열 대신 ? 자리에 매개변수를 넣는 방식으로 구현해요.

이렇게 바꾸니 요청을 보낼 때 '' 도 필요없게 됐어요.

// 회원가입
router.post('/register', (req,res) => {
const { email, password, nickname, contact } = req.body;
// validation
if (!email || !password || !nickname || !contact) return res.status(400).send("모든 필드를 입력해주세요.");
conn.query(
`INSERT INTO users (email, password, name, contact) VALUES (?, ?, ?, ?)`, [email, password, nickname, contact],
(err, results, fields) => {
if (err) return res.status(500).json(err);
res.json(results);
}
)
});
매개변수를 여러 개 주려면 []를 사용해서 한 번에 전달할 수 있어요.
router.delete('/users/:email', (req,res) => {
const email = req.params.email;
conn.query(
`DELETE FROM users WHERE email = ?`, email,
(err, results, fields) => {
if (err) return res.status(500).json(err);
res.json(results);
}
)
});
삭제도 query를 통해 db에서 삭제하도록 구현했어요.

응답값을 보면 affectedRows 가 0이에요.
{
"fieldCount": 0,
"affectedRows": 1,
"insertId": 0,
"info": "",
"serverStatus": 2,
"warningStatus": 0,
"changedRows": 0
}
만약 있는 이메일로 삭제를 했다면 영향받은 row가 1로 나와요.
router.post('/login', (req,res) => {
const { email, password } = req.body;
let user = {};
conn.query(
`SELECT * FROM users where email= ? and password = ?`, [email, password],
(err, results, fields) => {
if (err) return res.status(401).json({
"message": "로그인에 실패했습니다."
});
user = results[0];
}
)
res.json(user);
});
이렇게 구현했을 때 응답이 잘 나올 줄 알았는데, 로그인을 성공해도 빈 객체가 나왔어요.
이유는 conn.query 가 비동기이기 때문이었어요.
user 객체에 담겨지기 전에 응답으로 내보내니까 빈 객체로 보내지더라고요.
해결하기 위해 res.json(user) 를 콜백함수 안으로 넣어 해결했어요.
저는 로그인에서는 유저에게 고객 정보를 최소한으로 줘야한다고 생각했어요.
그래서 아이디와 비밀번호 중 어느 것이 틀려도 무조건 로그인에 실패했다고 전달하는 게 맞다고 생각했어요. (아이디만 맞다는 게 힌트가 된다고 생각)
혹은 다음과 같이 async/await으로 바꾸는 방법도 있어요.
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const [results] = await conn.query(
`SELECT * FROM users WHERE email = ? AND password = ?`,
[email, password]
);
if (results.length > 0) {
// 응답으로 보낼 유저 객체
const user = results[0];
res.json(user);
} else {
res.status(400).json({ "message": "아이디 또는 비밀번호가 틀렸습니다." });
}
} catch (err) {
console.error(err);
res.status(500).json({ "message": "서버 오류가 발생했습니다." });
}
});
db를 전역 변수에서 실제 db로 바꾸면서 기존 변수를 사용하던 코드를 지우기로 했어요.
우리 코드에서 가장 위험한 건 sql을 string으로 작성하고 있는 부분이에요.
router.get('/users/:email', (req,res) => {
const email = req.params.email;
let sql =`SELECT * FROM users where email= ?`
conn.query(sql,
email,
(err, results, fields) => {
if (err) return res.status(500).json(err);
res.json(results);
}
)
});
우선 다음과 같이 sql을 따로 변수화해서 가독성 좋게 만들었어요.
또한 같은 맥락으로 /channels 또한 sql 사용하는 법으로 리팩토링했어요.
router
.route('/:id')
.get((req, res) => {
const id = parseInt(req.params.id);
let sql =`SELECT * FROM channels where id= ?`
conn.query(sql,
id,
(err, results, fields) => {
if (err) return res.status(500).json(err);
res.json(results);
}
)
})

router
.route('/')
.get((req, res) => {
let { userId } = req.body;
let sql = 'SELECT * FROM channels WHERE user_id = ?'
// && 연산자로 userId 유효성 검사
userId && conn.query(sql, userId,
(err, results) => {
if(results.length) res.json(results);
else res.status(404).json({"message": "No Channel Found"});
})
})
여기서 강의는 && 연산자를 사용해 userId가 있어야 sql이 실행되도록 만들었어요.
개인적으로는 프론트엔드에서는 이런 방식이 좋다고 생각하는데, 백엔드에서는 예외처리가 꼼꼼해야한다고 생각해서 선호하지 않는 방식이에요.
라고 했는데, 바로 다음 장에서 백엔드에서 short ciruit을 하지 않는 걸 권장한다고 했고, if문으로 다시 돌려놨어요.
router
.route('/')
.get((req, res) => {
let { userId } = req.body;
let sql = 'SELECT * FROM channels WHERE user_id = ?'
// && 연산자로 userId 유효성 검사
userId && conn.query(sql, userId,
(err, results) => {
if(results.length) res.json(results);
else res.status(404).json({"message": "No Channel Found"});
})
})

/channels 를 사용하는 코드들도 모두 리팩토링하고 정상 작동되는 것을 확인했어요.