이번에 다룰 내용은 나머지 이미지 업로드, 팔로우-팔로잉 기능, 해시태그 검색 기능을 이해해보겠다. 따라서 이번 포스팅에서는 로그인만을 다루며 코드를 이해해보겠다!
사실 이 부분도 이전 포스팅에서도 한 번 다뤘었는데, 어떤 흐름으로 흘러가는지 정확히 하기 위해 이번 포스팅을 작성했다.
Github: https://github.com/delay-100/study-node/tree/main/ch9/sns5
- 기본 module 세팅
- 전체 app.js 세팅
- 메인 페이지 이해하기 +layout.html 설명
- 회원가입 기능 이해하기
- 로컬 로그인 기능 이해하기
- kakao 로그인 기능 이해하기
- 글쓰기/이미지 업로드 이해하기
- 팔로우-팔로잉 기능 이해하기
- 해시태그 검색 기능 이해하기
Model은 SNS 만들기 -2에서 다뤘으므로 흐름 정리 포스팅에서는 다루지 않겠다. (model git 주소)
글쓰기/이미지 업로드 폼 입력 화면 (이미지 미리보기 상태)
글쓰기/이미지 업로드 완료 화면 (글 작성 완료 상태)
해당 위치
- url 주소: http://localhost:8001/, (http://127.0.0.1:8001/post)
- api 주소: sns5/routes/
post.js
- html: sns5/views/
main.html
메인 페이지 이해하기 中 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.js
의 get("/")
의 실행 중인 곳으로 돌아감
메인 페이지 이해하기 中 post에 이미지 추가
1.2 post에 이미지 추가
-> id가img
인input
추가
-> 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 값 넘겨줌
팔로우하기 클릭시, 팔로잉하시겠습니까? 팝업
팔로우 한 상태 -> 팔로우하기 버튼이 사라짐
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 변수]
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의 팔로잉, 팔로워 목록을 보여줌
해당 위치
- url 주소: http://127.0.0.1:8001/
- api 주소: sns5/routes/
page.js
- html: sns5/views/
main.html
메인 페이지 이해하기 中 해시태그 검색 버튼 내용
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 변수]
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를 공부해보겠다!
잘못된 정보 수정 및 피드백 환영합니다!!