SNS 만들기 동작별 흐름 이해하기 -3(with Node, MySQL, Nunjucks) ★

백지연·2022년 2월 11일
1

NodeJS

목록 보기
21/26
post-thumbnail

이번에 다룰 내용은 나머지 이미지 업로드, 팔로우-팔로잉 기능, 해시태그 검색 기능을 이해해보겠다. 따라서 이번 포스팅에서는 로그인만을 다루며 코드를 이해해보겠다!
사실 이 부분도 이전 포스팅에서도 한 번 다뤘었는데, 어떤 흐름으로 흘러가는지 정확히 하기 위해 이번 포스팅을 작성했다.

Github: https://github.com/delay-100/study-node/tree/main/ch9/sns5

이전 포스팅에서 파헤쳐 본 것

  1. 기본 module 세팅
  2. 전체 app.js 세팅
  3. 메인 페이지 이해하기 +layout.html 설명
  4. 회원가입 기능 이해하기
  1. 로컬 로그인 기능 이해하기
  2. kakao 로그인 기능 이해하기

이번 포스팅에서 파헤쳐 볼 것

  1. 글쓰기/이미지 업로드 이해하기
  2. 팔로우-팔로잉 기능 이해하기
  3. 해시태그 검색 기능 이해하기

Model은 SNS 만들기 -2에서 다뤘으므로 흐름 정리 포스팅에서는 다루지 않겠다. (model git 주소)


7. 글쓰기/이미지/해시태그 업로드 이해하기

  • 글쓰기/이미지 업로드 폼 입력 화면 (이미지 미리보기 상태)

  • 글쓰기/이미지 업로드 완료 화면 (글 작성 완료 상태)

해당 위치

메인 페이지 이해하기 中 post 작성 폼 내용

1. post 작성 폼
-> 짹짹 버튼 클릭 시 type="submit"에 의해 form 태그의 action="/post" 실행 + method="post"
-> routes/post.js의 post("/") 실행
-> http://127.0.0.1:8001/post

Git [routes/post.js] 中 /

const express = require('express');
const multer = require('multer');
const { Post, Hashtag } = require('../models');
const { isLoggedIn } = require('./middlewares');

const router = express.Router();

const upload2 = multer();

// POST /post 라우터, 게시글 업로드 처리
router.post('/', isLoggedIn, upload2.none(), async (req, res, next) => {// upload2.none(): 데이터 형식이 multipart지만 이미지 데이터가 들어있지 않으므로 none 메서드 사용(이미지 주소가 온 것이고 데이터는 이미 POST /post/img 라우터에 저장됨)
    try {
        const post = await Post.create({
            content: req.body.content,
            img: req.body.url, // req.body.url: 이미지 주소가 저장되어있는 곳
            UserId: req.user.id,
        });
        const hashtags = req.body.content.match(/#[^\s#]+/g); // /#[%\s#]: 해시태그 정규표현식

        if (hashtags) { // 위의 식으로 추출된 해시태그가 존재하면
            const result = await Promise.all(  // 아래에서 map으로 여러 개의 해시태그가 나오기 때문에 Promise.all 사용
                hashtags.map((tag ,idx)=> {
                    return Hashtag.findOrCreate({ // sequelize 메소드(데이터베이스에 해시태그가 존재하면 가져오고, 존재하지 않으면 생성 후 가져옴), Hashtag 생성 
                                                 // 결과값으로 [모델, 생성 여부] 반환
                        where: { title: tag.slice(1).toLowerCase()}, // 해시태그에서 #을 떼고 소문자로 바꿈 
                    })
                }),
            );
            console.log(result);
            await post.addHashtags(result.map(r => r[0])); // result.map(r => r[0]): 모델만 추출함, post.addHashtags(): 해시태그 모델들을 게시글과 연결
        } 
        res.redirect('/');
    } catch (error) {
        console.error(error);
        next(error);
    }
});

=7-1> 1. isLoggedIn을 routes/middlewares에서 불러옴, 2. res.redirect('/')에 인해 routes/page.js/가 실행(3-1 참고) 됨

7-1.1. isNotLoggedIn을 routes/middlewares에서 불러옴
Git [routes/middleware.js] 中 isLoggedIn 미들웨어

// 로그인 확인 관련 미들웨어 생성

// 로그인이 된 상태를 확인하는 미들웨어
exports.isLoggedIn = (req, res, next) => {
    // 로그인이면 허용
    if(req.isAuthenticated()){ // req.isAuthenticated(): 로그인 중이면 true, 아니면 false
        next(); // 다음 미들웨어로 넘겨줌
    } else { // 로그인이 아니면 비허용
        res.status(403).send('로그인 필요');
    }
};

=7-2> 미들웨어를 실행(로그인이 된 상태이면 허용)한 후 다시 routes/post.jsget("/")의 실행 중인 곳으로 돌아감

메인 페이지 이해하기 中 post에 이미지 추가

1.2 post에 이미지 추가
-> id가 imginput 추가
-> scripts의 axios.post('/post/img', formData)가 실행될 때,
-> routes/post.js의 post('/img') 실행

Git [views/main.html] 中 script부분의 img change addEventListener

if(document.getElementById('img')){
            document.getElementById('img').addEventListener('change', function(e){
                const formData = new FormData();
                console.log(this, this.files);
                formData.append('img', this.files[0]);  // 서버에 있는 가장 첫 번째 multer 파일 업로드, 썸네일 추가
                                                        // formData.append(key, value)인데 key에 있는 변수를 서버에서도 사용해야 함, this는 e.target  
                axios.post('/post/img', formData) // 두 번째 인수에 데이터를 넣어서 보냄, routes/`post.js`의 post('/img') 실행
                    .then((res)=>{
                        document.getElementById('img-url').value = res.data.url; // url: `/img/${req.file.filename}`
                        document.getElementById('img-preview').src = res.data.url;
                        document.getElementById('img-preview').style.display = 'inline';
                    })
                    .catch((err)=>{
                        console.error(err);
                    });
            });
        }

=7-3> 1. axios.post('/post/img', formData)가 실행될 때 routes/post.js/post/img가 실행, 2. 7-3.1.이 실행된 후, res.json으로 온 url을 저장

getElementById로 저장한 데이터는 아래의 id에 쓰임
Git [views/main.html]

<img id="img-preview" src="" style="display: none;" width="250" alt="미리보기"> {# 아래의 js로 src 안에 미리보기 주소를 넣음#}
<input id="img-url" type="hidden" name="url"> {# req.body.url #}

Git [routes/post.js] 中 /post/img

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const { isLoggedIn } = require('./middlewares');

const router = express.Router();

try {
    fs.readdirSync('uploads');
} catch (error) {
    console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
    fs.mkdirSync('uploads');
}

const upload = multer({
    storage: multer.diskStorage({
        destination(req, file, cb) {
            cb(null, 'uploads/');
        },
        filename(req, file, cb) {
            const ext = path.extname(file.originalname);
            cb(null, path.basename(file.originalname, ext) + Date.now() + ext );
        },
    }),
    limits: {fileSize: 5*1024*1024 },
});

// POST /post/img 라우터, 이미지 하나를 업로드 받은 후 이미지의 저장 경로를 클라이언트로 응답 받음
router.post('/img', isLoggedIn, upload.single('img'), (req, res) => { // 이미지 하나를 업로드 받음
    console.log(req.file);
    res.json({url: `/img/${req.file.filename}`}); // 이미지의 저장 경로를 클라이언트로 응답
                                                  // static 미들웨어가 /img 경로의 정적 파일을 제공하므로 클라이언트에서 업로드한 이미지에 접근 가능
});

=7-4>7-3.2의 res.data.url로 url 값 넘겨줌


8. 팔로우-팔로잉 기능 이해하기

  • 팔로우하기 클릭시, 팔로잉하시겠습니까? 팝업

  • 팔로우 한 상태 -> 팔로우하기 버튼이 사라짐

  • http://127.0.0.1:8001/profile - 내 프로필

해당 위치

  • url 주소: http://127.0.0.1:8001/profile
  • api 주소: sns5/routes/user.js, sns5/routes/page.js
  • html: (sns5/views/main.html), sns5/views/profile.html

메인 페이지 이해하기 中 팔로우하기 버튼 클릭

3. 팔로우하기 버튼
-> 팔로우하기 버튼 클릭 시 class="twit-follow"에 의해 <script>addEventListener 실행

Git [views/main.html] 中 .twit-follow script

document.querySelectorAll('.twit-follow').forEach(function(tag) { // document의 모든 twit-follow class에 click eventlistener 추가 
            tag.addEventListener('click', function() {
                const myId = document.querySelector('#my-id'); // myId는 <input id="my-id" type="hidden" value="{{user.id}}">
                if(myId){
                    const userId = tag.parentNode.querySelector('.twit-user-id').value;
                    if(userId !== myId.value){
                        if(confirm('팔로잉하시겠습니까?')){
                            axios.post(`user/${userId}/follow`)
                            .then(() =>{
                                location.reload();
                            })
                            .catch((err)=>{
                                console.error(err);
                            });
                        }
                    }
                }
            });
        });

=8-1> myId(현재 로그인한 사용자)와 userId(post 작성자)가 다른 경우에 axios.post(`user/${userId}/follow`)에 의해 routes/user.js/:id/follow 실행

Git [routes/user.js]

const express = require('express');

const { isLoggedIn } = require('./middlewares');
const User = require('../models/user');

const router = express.Router();

// POST /user/:id/follow 라우터
router.post('/:id/follow', isLoggedIn, async (req, res, next) => {
    try {
        const user = await  User.findOne({ where: { id: req.user.id }}); // 팔로우 할 사용자(post 작성자)를 db에서 user id로 조회 

        if(user) {
            await user.addFollowing(parseInt(req.params.id, 10)); // :id 부분: req.params.id임, sequelize에서 추가한 addFollowing 메서드로 현재 로그인한 사용자와의 관계 지정, 10: 10진수
                                                                  // 팔로잉 관계가 생겼으므로 req.user에도 팔로워와 팔로잉 목록을 저장 -> 앞으로 사용자 정보를 불러올 때 팔로워, 팔로잉 목록 같이 불러옴 
                                                                  // req.user를 바꾸려면 passport/index.js의 deserializeUser를 바꿔야 함
            res.send('success');
        } else {
            res.status(404).send('no user');
        }
    } catch (error) {
        console.error(error);
        next(error);
    }
});

module.exports = router;

=8-2> 1. isLoggedIn 실행 후 반환(7-2 참고), 2. userId(post 작성자)의 following에 현재 로그인 되어있는 user 추가, 8-2.3. res.send('success');로 팔로우 성공, 8-2.4 res.status(404).send('no user');로 팔로잉 실패(팔로우 할 유저 없음)
+:id는 params.id임! ( : = params, id = id )

layout.html 설명 中 내 프로필 버튼 클릭

2. 내 프로필 버튼(팔로우, 팔로잉 목록 보여줌)
-> 내 프로필 버튼 클릭 시 href="/profile"에 의해 http://127.0.0.1:8001/profile 에 get 요청
-> routes/page.js의 get("/profile") 실행

Git [routes/page.js] 中 get /profile

// http://127.0.0.1:8001/profile 에 get요청이 왔을 때
router.get("/profile", isLoggedIn, (req, res) => {
  res.render("profile", { title: "내 정보 - sns" });
});

=8-3> res.render("profile",~)에 의해 views/profile.html이 실행(렌더링)됨
+[render 변수]

  • title: "내 정보 - sns"

Git [view/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>
    </div>
{% endblock %}

=8-4> 현재 user의 팔로잉, 팔로워 목록을 보여줌


9. 해시태그 검색 기능 이해하기

해당 위치

메인 페이지 이해하기 中 해시태그 검색 버튼 내용

1. 해시태그 검색 버튼
-> 검색 버튼 클릭 시 class="btn"에 의해 action="/hastag" 실행될 때,
-> routes/page.js의 get('/hashtag') 실행

Git [routes/page.js] 中 get /hashtag

const { Post, User, Hashtag } = require('../models');

// http://127.0.0.1:8001/hashtag 에 get요청이 왔을 때
router.get('/hashtag', async (req, res, next) => {
  const query = req.query.hashtag; // router.get은 req.body를 쓰지 않고 req.query로 값 전달
  if (!query) { // query가 없는 경우(해시태그가 없는 경우)
    return res.redirect('/'); // 메인페이지로 돌려보냄
  }
  // query가 있는 경우(해시태그가 있는 경우)
  try {
    const hashtag = await Hashtag.findOne({
      where: {title: query}
    }); // 해당 query 값이 Hashtag 테이블에 있는지 검색  
    let posts = [];
    if (hashtag){
      posts = await hashtag.getPosts({include: [{model: User}]}); // 있으면 모든 게시글을 가져옴
    }
    return res.render('main', { 
      title: `${query}|sns`,
      twits: posts, // 조회 후 views/main.html 페이지를 렌더링하면서 전체 게시글 대신 조회된 게시글만 twits에 넣어 렌더링 함 
    });
  } catch (error) {
    console.error(error);
    return next(error);
  }
});

=9-1> 1. 쿼리 값이 없는 경우 routes/page.js의 get / 실행 , 2.res.render('main', { title: `${query}|sns`, twits: posts});로 인해 views/main.html 실행
+[render 변수]

  • title: "해시태그로 검색한 값|sns"
  • twits: posts(검색한 해시태그가 들어있는 post들의 배열)

Git [views/main.html 中 twits 보여주기

  {% for twit in twits %} {# 렌더링 시 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>
                {# followerIdList: routes/page.js에서 res.locals에서 넣은 변수 #}
                {% if not followerIdList.includes(twit.User.id) and twit.User.id !== userid %} {# 작성자를 제외하고, 나의 팔로워 아이디 목록에 작성자의 아이디가 없으면 팔로우 버튼을 보여주기 위함 #}
                <button class="twit-follow">팔로우하기</button> {# 아래의 js 코드가 class 명(twit-follow)으로 동작 실행 #}
                {% endif %}
                <div class="twit-content">{{twit.content}}</div>
                {% if twit.img %} 
                    <div class="twit-img"><img src="{{twit.img}}" alt="섬네일"></div>
                {% endif %}
            </div>
{% endfor %}

=9-2> 해시태그로 걸러진 twits들이 twit으로 하나씩 반복되며 게시글이 뿌려짐


전체 파일 구조


이렇게 모든 기능들을 순서대로 파헤쳐보았다! 이전보다 확실히 코드를 이해하기 수월해진 것 같다:3

몇 주동안 9장에서 헤맸는데, 드디어 이 책의 다음 챕터인 api를 공부해보겠다!

잘못된 정보 수정 및 피드백 환영합니다!!

profile
TISTORY로 이사중! https://delay100.tistory.com

0개의 댓글