우리 프로젝트를 보면 좋아요 기능이 존재한다.
로그인을 한 후 jwt토큰을 가지고 회원인증 후 좋아요 기능을 사용할 수 있다.
단순히 좋아요를 눌러 db에 +1를 하면 전혀 어려운 기능이 아니다.
그런데 우리는 redis라는 메모리 캐싱을 사용하고 많은 사용자가 이용할 것을 고려하여 개발 중이기에, 좋아요 갯수를 보여줄 것과 좋아요 갯수를 이용하여 보여지는 랭킹 구역이나 고민하여 구현을 진행하였다.
좋아요 보여주기
웹 사이트의 메인 페이지에 들어가면 자동으로 likes : {webtoonID} 라는 redis의 key, value 값이 저장이 됨 (루프문을 통해 처음 바로 저장)-> 웹툰 페이지에 들어가면 자동으로 webtoonID를 받아오는데 이 ID값을 조회하여 likes:{id} 값의 value를 보여줌좋아요 수정하기
유저 id와 웹툰 id를 파라미터로 받아온 후 usp_put_get_likes 프로시저를 호출함.(이는 db의 좋아요 값을 수정하는 파라미터) -> 그리고 바로 redis에 저장되어 있던 likes:{ID}의 값을 INCRBY 또는 DECRBY를 함.
위는 LikeTable임.
유저 id와 웹툰 id를 가지고 좋아요를 수정함.
해당하는 likes가 null일땐 true로 바꿈.
null이 아닐 땐 기존 값의 반대로 바꿈.
CREATE DEFINER=`admin`@`%` PROCEDURE `usp_put_likes`(
IN pi_vch_userID INT,
IN pi_vch_webtoonEnName VARCHAR(100)
)
BEGIN
DECLARE v_webtoonID INT;
DECLARE v_likes BOOLEAN;
SELECT webtoonID INTO v_webtoonID
FROM WebtoonTable
WHERE webtoonEnName = pi_vch_webtoonEnName;
SELECT likes INTO v_likes
FROM LikeTable
WHERE userID = pi_vch_userID AND webtoonID = v_webtoonID;
-- 좋아요가 null이면 우선적으로 true로 필드 추가
IF v_likes IS NULL THEN
INSERT INTO LikeTable (userID, webtoonID, likes)
VALUES (pi_vch_userID, v_webtoonID, TRUE);
ELSE
-- 아니면 반대로
UPDATE LikeTable
SET likes = NOT v_likes
WHERE userID = pi_vch_userID AND webtoonID = v_webtoonID;
END IF;
END
//likeRoute
const LikeController = require('../controllers/likeController');
module.exports = (server) => {
// 좋아요 조회
server.get('/api/show_like', LikeController.viewLike);
// 좋아요 수정(추가, 삭제)
server.put('/api/update_like', LikeController.insertLike);
};
api 경로를 지정하는 코드이다.
//likeController
const LikeService = require('../service/likeService');
const axios = require('axios');
const LikeController = {
// 좋아요 보기
async viewLike(req, res) {
try {
const { id } = req.query; // 웹툰 아이디
const like = await LikeService.viewLike(id);
res.send(like); // 좋아요 갯수
} catch (error) {
console.error(error);
res.status(500).json({ message: '서버 오류' });
}
},
// 좋아요 수정
async insertLike(req, res) {
try {
const userID = req.body.userID;
const { EnName } = req.body;
const tokenResponse = await axios.get('http://3.39.187.19:4000/api/Token', {
headers: {
Cookie: req.headers.cookie,
},
});
if (tokenResponse.data === '토큰 인증 성공') {
if(!EnName || !userID){
res.send(400, { message: '좋아요 에러' });
throw new Error('좋아요 에러');
}else{
const resultMessage = await LikeService.insertLike(userID, EnName);
res.send(resultMessage); // '좋아요 수정 성공'
}
} else {
res.json({ message: '로그인 하세요' });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: '서버 오류' });
}
}
};
module.exports = LikeController;
파라미터 값을 가지고 서비스 메서드를 호출한 후 반환값을 클라이언트에 응답 값으로 보내는 코드이다.
여기서 유의깊게 봐야할 코드는 회원 인증 코드이다
const tokenResponse = await axios.get('http://3.39.187.19:4000/api/Token', { headers: { Cookie: req.headers.cookie, }, }); if (tokenResponse.data === '토큰 인증 성공') { if(!EnName || !userID){ res.send(400, { message: '좋아요 에러' }); throw new Error('좋아요 에러'); }else{ const resultMessage = await LikeService.insertLike(userID, EnName); res.send(resultMessage); // '좋아요 수정 성공' } }
기존에 만들어 놓은 회원인증을 호출한 후 응답값이 '토큰 인증 성공'일 경우에만 좋아요 수정 메서드를 호출한 후 '좋아요 수정 성공' 이라는 값을 클라로 보낸다.
//likeService
const redisClient = require('../redis'); // redis.js 모듈을 가져옴
const { getConn } = require('../database');
const LikeService = {
// 좋아요 보기
async viewLike(id) {
const conn = await getConn();
try {
const key = `likes:${id}`;
const value = await redisClient.get(key);
if (value !== null) {
return value;
} else {
return { message: "좋아요 오류"};
}
} catch (error) {
throw error;
} finally {
conn.release();
}
},
// 좋아요 수정 및 삽입
async insertLike(userID, EnName) {
const conn = await getConn();
try {
const values = [userID, EnName];
const LikeQuery = 'Call usp_put_get_likes(?, ?);';
const [result] = await conn.query(LikeQuery, values);
const likeKey = `likes:${result[0][0].webtoonID}`;
// 이미 눌러 likes가 true이면 1 빼고 null이거나 false이면 1 증가
const redisOperation = result[0][0].likes ? 'DECRBY' : 'INCRBY';
await redisClient[redisOperation](likeKey, 1);
//await redisClient.del(`webtoon : ${result[0][0].webtoonWeek}`);
return '좋아요 수정 성공';
} catch (error) {
throw error;
} finally {
conn.release();
}
}
};
module.exports = LikeService;
좋아요 서비스 로직이다.
const redisOperation = result[0][0].likes ? 'DECRBY' : 'INCRBY';
await redisClient[redisOperation](likeKey, 1);
이 부분을 보면 redis의 값을 수정한다. 이는 많은 사용자가 동시에 눌렀을 경우를 대비하여 redis의 값을 수정하는 것이다. 많은 사용자가 db 값을 수정하면 네트워크 비용이 많이 나올 수 있기 때문에 비교적 속도가 빠른 redis의 연산을 수행하도록 했다.
로그인 후 좋아요 버튼을 누르면 +1 or -1이 되고 응답이 "좋아요 수정 성공"으로 나온다.
db의 값도 수정이 된다.
그러나 수정 후 webtoon:All 키 값인 value를 본다면 좋아요가 수정이 안 되어 있을 것이다.
이유는 redis의 키인 likes:{id}를 수정했기 때문에 이미 저장된 값인 webtoon:All의 키값은 수정되지 않는다. (그렇기에 webtoon:All의 키값은 1시간이 지나면 삭제되고 다시 재생성 하도록 만들었다.)
이번엔 로그인 하지 않은 상태에서 좋아요를 수정해보겠다.
클라이언트에서 로그인을 하지 않으면 쿠키에 token 값이 없기 때문에 api 호출되지 않도록 해놓았다.
redis를 이용하니 좋아요 갯수를 보여주고 수정할 때 어떻게 해야할지 고민을 많이 했는데 내 생각대로 기능만 작동되게 구현은 해놓았지만 실무에서는 어떻게 사용할지 궁금하고, 좋아요 갯수가 업데이트 될 때마다 likes:{id} 키 값을 삭제하려고 했지만 사용자가 많을 땐 한 번에 여러번 api를 호출하면 키를 삭제하는 과정에서 redis를 사용하는 의미가 없을 것 같기에 INCRBY, DECRBY를 사용했다.