9장. 스스로 만들기

My_Code·2024년 2월 23일

Node.js 교과서

목록 보기
10/11
post-thumbnail

다음 내용은 인프런에서 공부한 내용을 복습하는 차원에서 기록한 것입니다.
출처 : https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-js-%EA%B5%90%EA%B3%BC%EC%84%9C


💻 스스로 만들기

📌 팔로우 취소 구현하기

✏️ 팔로우 취소 버튼 만들기

  • 넌적스 조건문을 이용해서 followingIdList 안에 있지만 사용자와 작성자의 ID가 다르면 팔로우 취소하기 버튼이 나타나게 만듦
  • /views/main.html
...

{% for twit in twits %}
<div class="twit">
  <input type="hidden" value="{{twit.User.id}}" class="twit-user-id">
  <input type="hidden" value="{{twit.id}}" class="twit-id">
  <div class="twit-author">{{twit.User.nick}}</div>
  {% if not followingIdList.includes(twit.User.id) and twit.User.id !== user.id %}
  	<button class="twit-follow">팔로우하기</button>
  {% elif twit.User.id !== user.id %}
  	<button class="twit-unfollow">팔로우 취소하기</button>
  {% endif %}
  <div class="twit-content">{{twit.content}}</div>
  {% if twit.img %}
  	<div class="twit-img"><img src="{{twit.img}}" alt="섬네일"></div>
  {% endif %}
</div>
{% endfor %}

...

✏️ 시퀄라이즈를 통해 데이터베이스 상에서 팔로우 삭제하기

  • removeFollowing 메서드를 이용
  • remove + 모델명 은 뒤에 조건에 맞는 데이터를 지우는 메서드
  • Following은 User와 User간에 생성된 관계 테이블을 의미
  • /controllers/user.js
...

exports.unfollow = async (req, res, next) => {
    try {
        // 현재 로그인한 사용자의 id를 통해 User 모델에서 user 객체를 가져옴
        const user = await User.findOne({ where: { id: req.user.id } });
        if (user) {
            // req.params.id(게시글 작성자 id) 조건에 맞는 데이터를 remove 함
            // req.params.id는 기본적으로 문자열을 반환함
            await user.removeFollowing(parseInt(req.params.id, 10));
            res.redirect('/');
        } else {
            res.status(404).send('no user');
        }
    } catch (error) {
        console.error(error);
        next(error);
    }
};


📌 프로필 정보 수정하기

✏️ 프로필 수정 버튼 만들기

  • 버튼 클릭 시 프로필 수정 페이지인 /profile/update 주소로 이동하게끔 구현
  • /views/layout.html
...

<div class="user-name">{{'안녕하세요! ' + user.nick + '님'}}</div>
<div class="half">
  <div>팔로잉</div>
  <div class="count following-count">{{followingCount}}</div>
</div>
<div class="half">
  <div>팔로워</div>
  <div class="count follower-count">{{followerCount}}</div>
</div>
<input id="my-id" type="hidden" value="{{user.id}}">
<a id="my-profile" href="/profile" class="btn">내 프로필</a>
<a id="my-profile-update" href="/profile/update" class="btn">프로필 수정</a>
<a id="logout" href="/auth/logout" class="btn">로그아웃</a>

...

✏️ 회원가입 페이지를 재활용해서 프로필 수정페이지 만들기

  • 현재 유저의 정보를 가져와서 미리 input태그에 넣어둠
  • 버튼 클릭 시 데이터베이스를 실제로 수정할 수 있는 주소로 이동
  • /views/profile_update.html
{% extends 'layout.html' %}

{% block content %}
  <div class="timeline">
    <form id="join-form" action="/profile/update_process" method="post">
      <div class="input-group">
        <label for="join-email">이메일</label>
        <input id="join-email" type="email" name="email" value="{{user.email}}"></div>
      <div class="input-group">
        <label for="join-nick">닉네임</label>
        <input id="join-nick" type="text" name="nick" value="{{user.nick}}"></div>
      <div class="input-group">
        <label for="join-password">비밀번호</label>
        <input id="join-password" type="password" name="password">
      </div>
      <div class="input-group">
        <label for="join-password_check">비밀번호 확인</label>
        <input id="join-password_check" type="password" name="password_check">
      </div>
      <button id="join-btn" type="submit" class="btn">수정하기</button>
    </form>
  </div>
{% endblock %}

{% block script %}
  <script>
    window.onload = () => {
      if (new URL(location.href).searchParams.get('error')) {
        alert('이미 존재하는 이메일입니다.');
      }
    };
  </script>
{% endblock %}

✏️ 프로필 수정을 위한 라우터

  • 라우터에서 POST 메서드의 주소를 설정
  • /routes/page.js
const express = require('express');
const router = express.Router();
const { renderJoin, renderMain, renderProfile, renderHashtag, renderProfileUpdate, updateProfile } = require('../controllers/page');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');

// 공통적으로 사용하기 원하는 변수들을 res.locals로 설정
// 로그인 과정이 다 끝나면 여기서 라우터들이 동작함
router.use((req, res, next) => {
    res.locals.user = req.user;  // 로그인되어 있지 않으면 req.user는 null값을 가짐
    res.locals.followerCount = req.user?.Followers?.length || 0;
    res.locals.followingCount = req.user?.Followings?.length || 0;
    res.locals.followingIdList = req.user?.Followings?.map(f => f.id) || [];
    next();
});

router.get('/profile', isLoggedIn, renderProfile);
// 프로필 수정 페이지 라우터 설정
router.get('/profile/update', isLoggedIn, renderProfileUpdate);
// 프로필 수정하는 컨트롤러의 메서드와 연결하는 라우터 설정
router.post('/profile/update_process', isLoggedIn, updateProfile);

router.get('/join', isNotLoggedIn, renderJoin);
router.get('/', renderMain);
router.get('/hashtag', renderHashtag);  // hashtag?hashtag=node

module.exports = router;

✏️ 시퀄라이즈를 통해 데이터베이스의 사용자 정보 수정하기

  • 프로필 수정 페이지로 render
  • 회원가입에서 사용했던 코드를 재활용
  • User.create 를 User.update로 변경해서 사용자 정보 변경
  • /controllers/page.js
// 라우터 -> 컨트롤러(요청, 응답을 알고 있음, req랑 res) -> 서비스(요청, 응답을 모름)
const Post = require('../models/post');
const User = require('../models/user');
const Hashtag = require('../models/hashtag');
const bcrypt = require('bcrypt');

...

exports.renderProfileUpdate = (req, res, next) => {
    res.render('profile_update', { title: '프로필 수정 - NodeBird' })
};

exports.updateProfile = async (req, res, next) => {
    const { nick, email, password } = req.body;  // 구조분해 할당에 의해 req.body에서 자동으로 할당됨
    try {
        // 사용자가 email도 변경했을 경우 DB에서 중복체크
        if (req.user.email != email) {
            const exUser = await User.findOne({ where: { email } });
            if (exUser) {
                return res.redirect('/profile/update?error=exist');
            }
        }
        const hash = await bcrypt.hash(password, 12); // 비밀번호 암호화
        await User.update({
            email: email,
            nick: nick,
            password: hash,
        }, {
            where: { id: req.user.id }
        });
        return res.redirect('/');
    } catch(err) {
        console.log(err);
        next(err);
    }
};

...


📌 게시물 내용 수정, 삭제하기

✏️ 게시물 수정, 삭제 버튼 만들고 axios 설정하기

  • /views/main.html
  • 로그인한 상태이고 게시물 유저와 로그인한 유저가 같을 때 버튼 활성화
  • 수정 버튼을 클릭 시 스크립트가 동작해서 그 게시물의 content와 이미지가 있다면 이미지 url을 원래 트윗을 작성하던 input 칸에 넣음
  • 그리고는 내용을 수정 후 다시 짹짹 버튼을 클릭하면 axios로 설정한 주소로 PUT 요청을 보냄
  • 삭제 버튼을 클릭하면 똑같이 스크립트가 동작해서 현재 게시물의 ID를 찾아서 DELETE 요청을 보냄
...
		{% for twit in twits %}
          <div class="twit">
            <input type="hidden" value="{{twit.User.id}}" class="twit-user-id">
            <input type="hidden" value="{{twit.id}}" class="twit-id">
            <div class="twit-author">{{twit.User.nick}}</div>
            {% if user and twit.User.id == user.id %}
              <button style="float:right;" class="twit-delete">삭제</button>
              <button style="float:right; margin-right: 10px;" class="twit-update">수정</button>
            {% endif %}
            {% if not followingIdList.includes(twit.User.id) and twit.User.id !== user.id %}
              <button class="twit-follow">팔로우하기</button>
            {% elif twit.User.id !== user.id %}
              <button class="twit-unfollow">팔로우 취소하기</button>
            {% endif %}
            <div class="twit-content">{{twit.content}}</div>
            {% if twit.img %}
              <div class="twit-img"><img src="{{twit.img}}" alt="섬네일"></div>
            {% endif %}
          </div>
        {% endfor %}
...

<script>
...
  document.querySelectorAll('.twit-update').forEach(function(tag) {
      tag.addEventListener('click', function(event) {
        const myId = document.querySelector('#my-id');  // 로그인된 id
        const twitContent = tag.parentNode.querySelector('.twit-content').textContent;
        const twitImg = tag.parentNode.querySelector('.twit-img img');
        const twitImgSrc= twitImg ? twitImg.src : '';

        if (myId) {
          const userId = tag.parentNode.querySelector('.twit-user-id').value;  // 작성자 id
          if (userId === myId.value) {
            document.getElementById('twit').value = twitContent;
            document.getElementById('img-preview').src = twitImgSrc;
            document.getElementById('img-url').value = twitImgSrc;

            if (twitImgSrc) {
              document.getElementById('img-preview').style.display = 'inline';
            }
          }
        }
        isEditMode = true; // 수정 모드로 변경
        editingTwitId = tag.parentNode.querySelector('.twit-id').value;
      });
    });

    document.getElementById('twit-btn').addEventListener('click', function(event) {
      event.preventDefault();
      const twitContent = document.getElementById('twit').value;
      const twitImgSrc = document.getElementById('img-url').value;

      if (isEditMode) {
        // 수정 API 호출
        axios.put(`/post/${editingTwitId}/update`, { content: twitContent, url: twitImgSrc, isEdit: isEditMode, postId: editingTwitId })
          .then(() => {
            location.reload();
          })
          .catch((error) => {
            console.error(error);
          });
      } else {
        // 새 트윗 생성 API 호출
        axios.post('/post', { content: twitContent, url: twitImgSrc })
          .then(() => {
            location.reload();
          })
          .catch((err) => {
            console.error(err);
          });
      }
    });
    document.querySelectorAll('.twit-delete').forEach(function(tag) {
      tag.addEventListener('click', function(event) {
        const myId = document.querySelector('#my-id');  // 로그인된 id
        const twitId =  tag.parentNode.querySelector('.twit-id').value;

        if (myId) {
          const userId = tag.parentNode.querySelector('.twit-user-id').value;  // 작성자 id
          if (userId === myId.value) {
            if (confirm('정말로 게시물을 삭제하겠습니까?')) {
              axios.delete(`/post/${twitId}/delete`)
                .then(() => {
                  location.reload();
                })
                .catch((err) => {
                  console.error(err);
                });
            }
          }
        }
      });
    });
</script>

✏️ 게시물 수정, 삭제 라우터

  • 클라이언트에서 설정한 주소를 기반으로 똑같이 PUT, DELETE 요청으로 맞춰줌
  • 게시물 수정 라우터는 기존에 게시물을 업로드하는 함수를 활용
  • 게시물 삭제 라우터는 컨트롤러 단에 새로운 함수를 작성
  • /routes/post.js
const express = require('express');
const router = express.Router();
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const fs = require('fs');
const multer = require('multer');
const path = require('path');
const { afterUploadImage, uploadPost, deletePost } = require('../controllers/post');

try {
    fs.readdirSync('uploads');
} catch (error) {
    fs.mkdirSync('uploads');
}

// 이미지를 올리기 위한 multer
const upload = multer({
    storage: multer.diskStorage({
        destination(req, file, cb) {
            console.log(file);
            cb(null, 'uploads/');
        },
        filename(req, file, cb) {
            console.log(file);
            const ext = path.extname(file.originalname);  // 확장자를 추출
            cb(null, path.basename(file.originalname, ext) + Date.now() + ext);  // 이미지.png -> 이미지12039123.png와 같이 변경(중복 제거를 위해)
        }
    }),
    limits: { fileSize: 20 * 1024 * 1024 },
});

...

router.put('/:id/update/img', isLoggedIn, upload.single('img'), afterUploadImage);
router.put('/:id/update', isLoggedIn, upload2.none(), uploadPost);

router.delete('/:id/delete', isLoggedIn, deletePost);

module.exports = router;

✏️ 컨트롤러에서 데이터베이스 접근하기 (에러 해결 완료)

  • 기존에 새로운 게시물을 생성하는 함수에서 수정 여부를 통해 수정할지 생성할지의 코드로 변경
  • /controllters/post.js
...

exports.uploadPost = async (req, res, next) => {
    console.log('uploadPost', req.body);  // req.body.content와 req.body.url를 가져올 수 있음
    try {
        let post = null;
        if (req.body.isEdit) {
            await Post.update({
                content: req.body.content,
                img: req.body.url,
                UserId: req.user.id,
            }, {
                where: {
                id: req.body.postId
                }
            });
            post = await Post.findOne({ where: { id: req.body.postId } });
        } else {
            post = await Post.create({
                content: req.body.content,
                img: req.body.url,
                UserId: req.user.id,
            });
        }
        // 해시태그 정규표현식 사용
        const hashtags = req.body.content.match(/#[^\s#]*/g);
        if (hashtags) {
            // Post와 Hashtag 사이의 다대다 관계 생성
            const result = await Promise.all(hashtags.map((tag) => {
                return Hashtag.findOrCreate({
                    where: { title: tag.slice(1).toLowerCase() }
                });
            }));
            // Hashtag.findOrCreate()의 반환값으로 [instance, created] 형태의 배열을 반환
            // instance에는 값이, created에는 새로운 해시태그가 생성되었는지 여부가 들어 있음
            await post.addHashtags(result.map(r => r[0]));
        }
        res.send('success');  // 원래는 res.redirect('/');

    } catch (error) {
        console.error(error);
        next(error);
    }
};
  • 이상한 곳에서 오래 막혔음
  • 원래는 res.redirect('/'); 였던 부분에서 404에러가 발생함
  • 기존에 새로운 게시물을 생성하는 코드에서는 이상이 없었음
  • 원인은 수정을 하기 위해서 PUT 요청을 했기 때문임
  • 기존에 POST 요청에 의해 redirect 할 때는 router.post('/') 라우터가 존재했기 때문에 가능했음
  • 하지만 PUT 요청에 대한 라우터가 별도로 없었기에 똑같이 router.put('/') 을 만들어야 하는 줄 알았으나,
	document.getElementById('twit-btn').addEventListener('click', function(event) {
      event.preventDefault();
      const twitContent = document.getElementById('twit').value;
      const twitImgSrc = document.getElementById('img-url').value;

      if (isEditMode) {
        // 수정 API 호출
        axios.put(`/post/${editingTwitId}/update`, { content: twitContent, url: twitImgSrc, isEdit: isEditMode, postId: editingTwitId })
          .then(() => {
            location.reload();
          })
          .catch((error) => {
            console.error(error);
          });
      } else {
        // 새 트윗 생성 API 호출
        axios.post('/post', { content: twitContent, url: twitImgSrc })
          .then(() => {
            location.reload();
          })
          .catch((err) => {
            console.error(err);
          });
      }
    });
  • 위에서 작성한 것처럼 then() 처리에 의해서 어짜피 새로고침 되기에 PUT 요청일 때 서버단에서 처리하지 말고 res.send('success'); 처럼 처리하면 성공 시 then() 안에 있는 location.reload(); 가 동작하게 됨
exports.deletePost = async (req, res, next) => {
    try {
        const post = await Post.findOne({ where: { id: req.params.id } });
        if (post) {
            await post.destroy();
        }
    } catch (error) {
        console.error(error);
        next(error);
    }
};
  • 게시물을 삭제하는 컨트롤러는 비교적 간단하게 구현이 가능
  • Post 모델을 통해서 req.params 값으로 가져온 id로 게시물을 탐색
  • 찾은 게시물이 있다면 시퀄라이즈의 destroy 메서드로 해당 게시물을 삭제
  • 여기서 특이한 점은 모델 생성 시 deleteAt을 설정했기에 destroy 메서드를 사용해도 실제 데이터베이스에서 삭제되는 것이 아니라 deleteAt에 시간이 기록됨


📌 게시물 좋아요, 좋아요 취소 누르기

✏️ 게시물 좋아요 버튼 만들기

  • /views/main.html
  • 로그인한 상태이면서 해당 게시물의 좋아요 유저 목록 likes 배열을 확인
  • 배열에 로그인한 유저가 없으면 "좋아요" 버튼이, 아니면 "좋아요 취소" 버튼이 활성화
  • 좋아요 버튼 클릭 시 /post/${twitId}/like 로 Post 요청
  • 좋아요 취소 버튼 클릭 시 /post/${twitId}/unlike 로 Post 요청
  • RestAPI 에 맞도록 할려면 unlike 시에는 Delete 요청으로 추후 변경
...

{% if twit.img %}
	<div class="twit-img"><img src="{{twit.img}}" alt="섬네일"></div>
{% endif %}

{% if user %}
  {% if not likes[twits.indexOf(twit)].includes(user.id) %}
  	<button class="twit-like">좋아요</button>
  {% else %}
  	<button class="twit-unlike">좋아요 취소</button>
  {% endif %}
{% endif %}

...

<script>
  ...
  
	document.querySelectorAll('.twit-like').forEach(function(tag) {
      tag.addEventListener('click', function() {
        const myId = document.querySelector('#my-id');
        if (myId) {
          const userId = tag.parentNode.querySelector('.twit-user-id').value;
          const twitId = tag.parentNode.querySelector('.twit-id').value;
          if (userId !== myId.value) {
            axios.post(`/post/${twitId}/like`)
              .then(() => {
                location.reload();
              })
              .catch((err) => {
                console.error(err);
              });
          }
        }
      });
    });

    document.querySelectorAll('.twit-unlike').forEach(function(tag) {
      tag.addEventListener('click', function() {
        const myId = document.querySelector('#my-id');
        if (myId) {
          const userId = tag.parentNode.querySelector('.twit-user-id').value;
          const twitId = tag.parentNode.querySelector('.twit-id').value;
          if (userId !== myId.value) {
            axios.post(`/post/${twitId}/unlike`)
              .then(() => {
                location.reload();
              })
              .catch((err) => {
                console.error(err);
              });
          }
        }
      });
    });
  
  ...
</script>
  • /controllers/pages.js
  • 좋아요 or 좋아요 취소 버튼인지 구분하기 위한 likes 배열 생성
  • 클라이언트 코드에서 사용가능하도록 만듦
  • 모든 게시물들을 가져올 때 User 모델의 데이터들을 Likers로써 include 함
  • posts라는 변수에는 게시물의 내용뿐만 아니라 게시물을 작성한 작성자의 정보와 좋아요한 유저의 정보도 같이 가져옴
  • map() 함수를 통해서 좋아요한 유저의 id만 따로 빼서 배열로 만듦
...

exports.renderMain = async (req, res, next) => {
    try {
        const posts = await Post.findAll({
            include: [
                {
                    model: User,
                    attributes: ['id', 'nick'],
                },
                {
                    model: User,
                    attributes: ['id', 'nick'],
                    as: 'Likers',
                }],
            order: [['createdAt', 'DESC']]
        });
        res.render('main', { 
            title: 'NodeBird',
            twits: posts,
            likes: posts.map((v) => v.Likers.map((v) => v.id)),
        });
    } catch (error) {
        console.error(error);
        next(error);
    }
};

...

✏️ 게시물과 유저간 N:M 관계 추가

  • /models/post.js
  • Post는 많은 User에 속함(belongsToMany)
  • User는 Liker 라는 별명(as)을 사용
...

	// 테이블 관계
    static associate(db) {
        db.Post.belongsTo(db.User);
        db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' });
        db.Post.belongsToMany(db.User, {  
            through: 'Like',
            as: 'Likers',
        });
    }

...
  • /models/user.js
  • User는 많은 Post에 속함(belongsToMany)
  • Post는 LikedPosts 라는 별명(as)을 사용
...

	// 테이블 관계
    static associate(db) {
        db.User.hasMany(db.Post);
        // 연예인의 팔로워를 찾으려면 연예인의 id로 찾아야 함
        db.User.belongsToMany(db.User, {  // 팔로워(유명 연예인의 팬)
            foreignKey: 'followingId',
            as: 'Followers',
            through: 'Follow'
        });

        // 내가 팔로잉을 하는 사람을 찾으려면 나의 id로 찾아야 함
        db.User.belongsToMany(db.User, {  // 팔로잉 (유명 연예인)
            foreignKey: 'followerId', 
            as: 'Followings',
            through: 'Follow'
        });

        db.User.belongsToMany(db.Post, {  
            through: 'Like',  
            as: 'LikedPosts',
        });
    }

...

✏️ 좋아요 기능 라우터

  • /routes/post.js
  • twitId를 req.params로 받아서 사용 (:id)
const fs = require('fs');
const multer = require('multer');
const path = require('path');
const { afterUploadImage, uploadPost, deletePost, likePost, unlikePost } = require('../controllers/post');

...

router.delete('/:id/delete', isLoggedIn, deletePost);

router.post('/:id/like', isLoggedIn, likePost);

router.post('/:id/unlike', isLoggedIn, unlikePost);

module.exports = router;

...

✏️ 좋아요 기능 컨트롤러 작성

  • /controllers/post.js
  • req.params 로 넘어온 twitId 값을 받아옴
  • twitId로 해당 게시물을 찾고 addLiker로 Like 테이블에 추가
  • 반대로 removeLiker로 좋아요 취소 기능 구현
const Post = require('../models/post');
const User = require('../models/user');
const Hashtag = require('../models/hashtag');

...

exports.likePost = async (req, res, next) => {
    try {
        const post = await Post.findOne({ where: { id: req.params.id } });
        if (post) {
            await post.addLiker(parseInt(req.user.id, 10));
            res.send('success');
        } else {
            res.status(404).send('no user');
        }
    } catch (error) {
        console.error(error);
        next(error);
    }
};

exports.unlikePost = async (req, res, next) => {
    try {
        const post = await Post.findOne({ where: { id: req.params.id } });
        if (post) {
            await post.removeLiker(parseInt(req.user.id, 10));
            res.send('success');
        }
    } catch (error) {
        console.error(error);
        next(error);
    }
};

✏️ 내 프로필에서 좋아요 목록 구현 (추가사항)

  • /views/profile.html
  • 좋아요 목록에는 내가 좋아요한 게시물의 내용이 나열됨
{% extends 'layout.html' %}

{% block content %}
  <div class="timeline">
    <div class="followings half">
      <h2>팔로잉 목록</h2>
      {% if user.Followings %}
        {% for following in user.Followings %}
          <div>{{following.nick}}</div>
        {% endfor %}
      {% endif %}
    </div>
    <div class="followers half">
      <h2>팔로워 목록</h2>
      {% if user.Followers %}
        {% for follower in user.Followers %}
          <div>{{follower.nick}}</div>
        {% endfor %}
      {% endif %}
    </div>
    &nbsp;
    <div class="like-list">
      <h2>좋아요 목록</h2>
      {% if user.LikedPosts %}
        {% for content in LikedPostContentList %}
          <div>{{content}}</div>
        {% endfor %}
      {% endif %}
    </div>
  </div>
{% endblock %}
  • /passport/index.js
  • 로그인 시 로그인한 유저가 좋아요한 게시물들의 정보를 가져옴
  • passport.deserializeUser()을 통해 Post 모델에서 게시물 정보를 include 함
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
const Post = require('../models/post');


module.exports = () => {
    
  	...	
  
    // 결과적으로 req.user를 만드는 곳임
    passport.deserializeUser((id, done) => {  
        User.findOne({ 
            where: { id },
            // as: 'Followers'와 as: 'Followings'는 User 모델이 Follow 테이블을 통해 
            // 자기 자신과 맺고 있는 팔로우 관계를 나타내며, 이 관계를 통해 팔로워와 팔로잉 사용자의 정보를 함께 조회할 수 있음
            include: [
                {
                    model: User,
                    attributes: ['id', 'nick'],
                    as: 'Followers',
                },
                {
                    model: User,
                    attributes: ['id', 'nick'],
                    as: 'Followings',
                },
                {
                    model: Post,
                    attributes: ['id', 'content'],
                    as: 'LikedPosts',
                },
            ]
        })
            .then((user) => done(null, user))  // 그 복원된(조회된) 정보가 req.user가 됨
            .catch(err => done(err));
    });
    local();
    kakao();
};
  • /routes/page.js
  • 좋아요한 게시물 내용을 클라이언트에서도 사용할 로컬 변수로 설정
const express = require('express');
const router = express.Router();
const { renderJoin, renderMain, renderProfile, renderHashtag, renderProfileUpdate, updateProfile } = require('../controllers/page');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
// 공통적으로 사용하기 원하는 변수들을 res.locals로 설정
// 로그인 과정이 다 끝나면 여기서 라우터들이 동작함
router.use((req, res, next) => {
    res.locals.user = req.user;  // 로그인되어 있지 않으면 req.user는 null값을 가짐
    res.locals.followerCount = req.user?.Followers?.length || 0;
    res.locals.followingCount = req.user?.Followings?.length || 0;
    res.locals.followingIdList = req.user?.Followings?.map(f => f.id) || [];
    res.locals.LikedPostContentList = req.user?.LikedPosts?.map(f => f.content) || [];
    next();
});

...

✏️ 좋아요 목록 구현 모습



📌 작성자명 클릭 시 작성자의 전체 게시물 출력

✏️ 작성자명 클릭 가능하게 변경

  • /views/main.html
  • <a> 태그를 이용해 클릭 시 주소 이동하게끔 변경
...

<input type="hidden" value="{{twit.User.id}}" class="twit-user-id">
<input type="hidden" value="{{twit.id}}" class="twit-id">
<a href="/user/{{twit.User.id}}"><button class="twit-author">{{twit.User.nick}}</button></a>
{% if user and twit.User.id == user.id %}
	<button style="float:right;" class="twit-delete">삭제</button>
	<button style="float:right; margin-right: 10px;" class="twit-update">수정</button>
{% endif %}

...
  • /public/main.css
  • 작성자명 css 추가
...

.twit-author { 
  font-weight: bold; 
  font-size: 16px;
  margin-right: 10px; 
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
}
.twit-author:hover {
  opacity: 0.5;
}
.twit-author:active {
  opacity: 0.5;
}

...

✏️ 작성자명 게시물 검색 라우터

  • /routes/page.js
  • 게시물 작성자의 id를 req.params 데이터로 받아옴
  • 해당 컨트롤러와 연결
const express = require('express');
const router = express.Router();
const { renderJoin, renderMain, renderProfile, renderHashtag, renderProfileUpdate, updateProfile, searchUserPost } = require('../controllers/page');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');

...

router.get('/join', isNotLoggedIn, renderJoin);
router.get('/', renderMain);
router.get('/hashtag', renderHashtag);  // hashtag?hashtag=node

router.get('/user/:id', searchUserPost);

module.exports = router;
  • /controllers/page.js
  • req.params.id로 넘어온 게시물의 작성자ID를 통해 데이터베이스에서 검색
  • 이때 좋아요 기능은 유지하기 위해서 User 모델을 Likers 별명으로 include 함
...

exports.searchUserPost = async (req, res, next) => {
    try {
        console.log(req.params.id);
        const user = await User.findOne({ where: { id: req.params.id } });
        const posts = await Post.findAll({ 
            where: { UserId: req.params.id }, 
            include: [
                {
                    model: User,
                    attributes: ['id', 'nick'],
                },
                {
                    model: User,
                    attributes: ['id', 'nick'],
                    as: 'Likers',
                }],
            order: [['createdAt', 'DESC']] });
        res.render('main', {
            title: `${user.nick} | NodeBird`,
            twits: posts,
            likes: posts.map((v) => v.Likers.map((v) => v.id)) || [],
        });
    } catch (error) {
        console.error(error);
        next(error);
    }
};


📌 매번 데이터베이스를 조회하지 않도록 deserializeUser 캐싱하기 (△)

✏️ 캐싱처리하기 위한 미들웨어 작성

  • /middlewares/index.js
  • userCache 라는 객체 변수로 캐싱
  • cacheTTL 는 유효시간을 설정
  • 다른 파일에서 사용자 데이터를 사용할 때 매번 User.findOne()로 찾는게 아니라 userCache 객체를 getUserCache() 함수로 가져옴
  • 사용자의 데이터가 변할 시 setUserCache() 함수로 캐시 객체 업데이트
const User = require('../models/user');
const Post = require('../models/post');

...

let userCache = {};
let cacheTTL = 0;

exports.createUserCache  = (req, res, next) => {
    return {
        getUserCache: () => userCache,
        getCacheTTL: () => cacheTTL,
        setUserCache: async (id) => {
            if (id === -1) {
                userCache = {};
            } else {
                userCache = await User.findOne({ 
                    where: { id },
                    include: [
                        {
                            model: User,
                            attributes: ['id', 'nick'],
                            as: 'Followers',
                        },
                        {
                            model: User,
                            attributes: ['id', 'nick'],
                            as: 'Followings',
                        },
                        {
                            model: Post,
                            attributes: ['id', 'content'],
                            as: 'LikedPosts',
                        },
                    ]
                })
            }
        },
        setCacheTTL: (newCacheTTL) => {
            if (newCacheTTL === 0) {
                cacheTTL = 0;
            } else {
                const TTL = 30 * 1000;
                cacheTTL = newCacheTTL + TTL;
            }
        }
    }
};

✏️ deserializeUser 에서 Cache 객체 사용

  • /passport/index.js
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
const Post = require('../models/post');
const { createUserCache } = require('../middlewares/index');
const userCache = createUserCache();


module.exports = () => {
  
	...
  	
    // 결과적으로 req.user를 만드는 곳임
    passport.deserializeUser((id, done) => {  // 세션쿠키 값을 통해 얻은 유저 아이디를 가지고 User 정보를 복원시킴
        if (Object.keys(userCache.getUserCache()).length !== 0 && userCache.getCacheTTL() > Date.now()){
            console.log('Cache: ', userCache.getCacheTTL, Date.now());
            return done(null, userCache.getUserCache());
        }

        User.findOne({ 
            where: { id },
            // as: 'Followers'와 as: 'Followings'는 User 모델이 Follow 테이블을 통해 
@@ -40,8 +47,12 @@ module.exports = () => {
                    as: 'LikedPosts',
                },
            ]
        })
            .then((user) => done(null, user))  // 그 복원된(조회된) 정보가 req.user가 됨
        })  // 그 복원된(조회된) 정보가 req.user가 됨
            .then((user) => {
                done(null, user);
                userCache.setUserCache(user.id);
                userCache.setCacheTTL(Date.now());
            })  
            .catch(err => done(err));
    });

    local();
    kakao();
};


📌 deserializeUser 캐싱에 관해서...

  • 사실 제대로 구현이 되지 않았음
  • 어떻게 구현해야 할지 도통 감이 오질 않음
  • 위에서는 캐싱처리라고 구현했지만 실제 성능은 비슷하거나 더 좋지 않게 나오는 것 같음...ㅠㅠ
  • 추후 공부하다가 더 알게 된다면 수정할 예정
  • redis 같은 서버를 이용해서 서버 사용량을 줄일 수도 있다고 함
profile
조금씩 정리하자!!!

0개의 댓글