1) 해시함수란?
해시함수란, 알고리즘의 한 종류로서 임의의 데이터를 입력 받아 항상 고정된 길이의 임의의 값으로 변환해주는 함수를 말합니다.
JWT란?
JSON Web Token의 줄임말로, JSON 객체를 사용해 정보를 안정성 있게 전달하는 웹표준이에요!
플라스크 서버에서 로그인 기능 구현하기
로그인/회원가입 토글 기능 만들기
이제 로그인 중인지, 회원가입 중인지 상황에 맞게 각 요소들을 숨겼다, 드러냈다 하는 기능을 만들어봅시다.
Bulma에서는 is-hidden이라는 클래스를 이용해서 요소를 숨길 수 있습니다. CSS로는 아래와 같이 정의되어있어요.
.is-hidden {
display: none!important;
}
이 클래스를 로그인 화면에서 숨겨야하는 요소들에 붙여주세요.
이제 숨겨져 있으면 드러내고, 드러나 있으면 숨겨주는 함수를 만들어야겠죠? 우선 sign-up-box div에 적용시켜보겠습니다.
function toggle_sign_up() {
if ($("#sign-up-box").hasClass("is-hidden")) {
$("#sign-up-box").removeClass("is-hidden")
} else {
$("#sign-up-box").addClass("is-hidden")
}
}
jQuery에는 이것을 더 간단하게 도와주는 함수가 있는데요, 바로 toggleClass()입니다.
function toggle_sign_up() {
$("#sign-up-box").toggleClass("is-hidden")
}
이렇게 한 번에 토글할 수 있는 함수를 만들어 회원가입하기 버튼과 취소 버튼에 연결해주면 끝!
회원가입 기능 만들기
회원가입할 때는 입력 받은 값들이 형식에 맞는지 우선 확인해야겠죠?
이렇게 복잡한 조건을 확인할 때는 '정규표현식(Regular Expressions)'을 이용하여 비교하는 것이 좋습니다. 형식을 확인하여 결과를 참/거짓으로 반환하는 함수를 정의하면 편리하겠죠?
[코드스니펫] - 아이디, 비밀번호 정규표현식
function is_nickname(asValue) {
var regExp = /^(?=.*[a-zA-Z])[-a-zA-Z0-9_.]{2,10}$/;
return regExp.test(asValue);
}
function is_password(asValue) {
var regExp = /^(?=.*\d)(?=.*[a-zA-Z])[0-9a-zA-Z!@#$%^&*]{8,20}$/;
return regExp.test(asValue);
}
그리고 아이디는 다른 사람과 겹치면 안되기 때문에 중복확인을 해주어야겠죠? 서버로 POST 요청을 보내 아이디가 존재하는지 확인해주세요.
이제 이 조건들을 만족할 때만 회원가입 POST 요청을 보내도록 함수를 짜면 끝!
포스팅 기능 만들기
글과 현재 시각을 문자열로 받아 POST 요청을 보내고, 저장에 성공하면 모달을 닫고 새로고침해줍니다.
[코드스니펫] - 포스팅 함수
function post() {
let comment = $("#textarea-post").val()
let today = new Date().toISOString()
$.ajax({
type: "POST",
url: "/posting",
data: {
comment_give: comment,
date_give: today
},
success: function (response) {
$("#modal-post").removeClass("is-active")
window.location.reload()
}
})
}
서버에서는 글과 현재 시각을 받아 로그인한 사용자의 정보로부터 아이디, 이름, 프로필 사진을 같이 저장합니다.
[코드스니펫] - 포스팅 API
user_info = db.users.find_one({"username": payload["id"]})
comment_receive = request.form["comment_give"]
date_receive = request.form["date_give"]
doc = {
"username": user_info["username"],
"profile_name": user_info["profile_name"],
"profile_pic_real": user_info["profile_pic_real"],
"comment": comment_receive,
"date": date_receive
}
db.posts.insert_one(doc)
포스팅 카드 띄우는 기능 만들기
포스트를 저장했으니 이번에는 받아와봅시다.
서버에서는 DB에서 최근 20개의 포스트를 받아와 리스트로 넘겨줍니다. 나중에 좋아요 기능을 쓸 때 각 포스트를 구분하기 위해서 MongoDB가 자동으로 만들어주는 _id 값을 이용할 것인데요, ObjectID라는 자료형이라 문자열로 변환해주어야합니다.
[코드스니펫] - 문자열로 변환하기
posts = list(db.posts.find({}).sort("date", -1).limit(20))
for post in posts:
post["_id"] = str(post["_id"])
클라이언트에서는 각 포스트를 카드로 만들어줍니다. 기존에 있던 카드는 다 지우고 새로 만들어서 담벼락에 붙여줍니다.
[코드스니펫] - 포스팅 카드 만들기
function get_posts() {
$("#post-box").empty()
$.ajax({
type: "GET",
url: "/get_posts",
data: {},
success: function (response) {
if (response["result"] == "success") {
let posts = response["posts"]
for (let i = 0; i < posts.length; i++) {
let post = posts[i]
let time_post = new Date(post["date"])
let html_temp = `<div class="box" id="${post["_id"]}">
<article class="media">
<div class="media-left">
<a class="image is-64x64" href="/user/${post['username']}">
<img class="is-rounded" src="/static/${post['profile_pic_real']}"
alt="Image">
</a>
</div>
<div class="media-content">
<div class="content">
<p>
<strong>${post['profile_name']}</strong> <small>@${post['username']}</small> <small>${time_post}</small>
<br>
${post['comment']}
</p>
</div>
<nav class="level is-mobile">
<div class="level-left">
<a class="level-item is-sparta" aria-label="heart"token interpolation">${post['_id']}', 'heart')">
<span class="icon is-small"><i class="fa fa-heart"
aria-hidden="true"></i></span> <span class="like-num">2.7k</span>
</a>
</div>
</nav>
</div>
</article>
</div>`
$("#post-box").append(html_temp)
}
}
}
})
}
이 get_posts() 함수가 페이지가 로딩되었을 때, 실행되게 하면 되겠죠?
[코드스니펫] - get_posts 실행하기
$(document).ready(function () {
get_posts()
})
포스팅 시간 나타내기
이번에는 포스팅한 지 얼마나 되었는지 보여주는 기능을 만들어봅시다.
자바스크립트의 Date 오브젝트 간의 빼기의 결과는 밀리초로 주어집니다.
function time2str(date) {
let today = new Date()
let time = (today - date) / 1000 / 60 // 분
return parseInt(time) + "분 전"
}
60분이 넘어가는 경우에는 시간으로 나타내봅시다.
function time2str(date) {
let today = new Date()
let time = (today - date) / 1000 / 60 // 분
if (time < 60) {
return parseInt(time) + "분 전"
}
time = time / 60 // 시간
return parseInt(time) + "시간 전"
}
24시간이 넘어가는 경우에는 일수로 나타내볼까요?
function time2str(date) {
let today = new Date()
let time = (today - date) / 1000 / 60 // 분
if (time < 60) {
return parseInt(time) + "분 전"
}
time = time / 60 // 시간
if (time < 24) {
return parseInt(time) + "시간 전"
}
time = time / 24
return parseInt(time) + "일 전"
}
7일 이상일 때에는 날짜로 보여주도록 하겠습니다.
[코드스니펫] - 포스팅 시간 나타내기
function time2str(date) {
let today = new Date()
let time = (today - date) / 1000 / 60 // 분
if (time < 60) {
return parseInt(time) + "분 전"
}
time = time / 60 // 시간
if (time < 24) {
return parseInt(time) + "시간 전"
}
time = time / 24
if (time < 7) {
return parseInt(time) + "일 전"
}
return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`
}
이것을 get_posts() 함수 안에 각 포스팅 카드에 포스팅 시각 대신 넣어주면 되겠죠?
[코드스니펫] - time2str(time_post)
let time_before = time2str(time_post)
좋아요/좋아요 취소 기능 만들기
서버
우선 서버 쪽 기능을 먼저 생각해봅시다. 하트를 누르면 1) 어떤 포스트를 2) 누가 눌렀고 3) 좋아요인지 좋아요 취소인지를 알아야겠죠? 숙제로 만들 다른 반응들(⭐, 👍)을 생각하면 어느 아이콘을 눌렀는지도 알아야겠네요.
DB에 저장할 때는 1) 누가 2) 어떤 포스트에 3) 어떤 반응을 남겼는지 세 정보만 넣으면 되고, 좋아요인지, 취소인지에 따라 해당 도큐먼트를 insert_one()을 할지 delete_one()을 할지 결정해주어야합니다.
if action_receive =="like":
db.likes.insert_one(doc)
else:
db.likes.delete_one(doc)
좋아요 컬렉션을 업데이트한 이후에는 해당 포스트에 해당 타입의 반응이 몇 개인지를 세서 보내주어야합니다.
count = db.likes.count_documents({"post_id": post_id_receive, "type": type_receive})
[코드스니펫] - 좋아요 업데이트 API
user_info = db.users.find_one({"username": payload["id"]})
post_id_receive = request.form["post_id_give"]
type_receive = request.form["type_give"]
action_receive = request.form["action_give"]
doc = {
"post_id": post_id_receive,
"username": user_info["username"],
"type": type_receive
}
if action_receive =="like":
db.likes.insert_one(doc)
else:
db.likes.delete_one(doc)
count = db.likes.count_documents({"post_id": post_id_receive, "type": type_receive})
return jsonify({"result": "success", 'msg': 'updated', "count": count})
클라이언트
API에서 요구하는 데이터가 사용자 정보, 포스트 아이디, 좋아요/좋아요 취소, 아이콘 종류입니다.
여기에서 하트를 누른 사람의 정보는 로그인 정보에서 받아왔으므로 나머지 3개만 데이터로 보내주면 됩니다.
좋아요인지, 좋아요 취소인지는 아이콘의 클래스가 fa-heart인지 fa-heart-o인지로 알 수 있습니다.
업데이트에 성공하면 아이콘의 클래스를 바꾸고 좋아요 숫자도 업데이트해줍니다.
[코드스니펫] - 좋아요 업데이트 함수 클라이언트
function toggle_like(post_id, type) {
console.log(post_id, type)
let $a_like = $(`#${post_id} a[aria-label='heart']`)
let $i_like = $a_like.find("i")
if ($i_like.hasClass("fa-heart")) {
$.ajax({
type: "POST",
url: "/update_like",
data: {
post_id_give: post_id,
type_give: type,
action_give: "unlike"
},
success: function (response) {
console.log("unlike")
$i_like.addClass("fa-heart-o").removeClass("fa-heart")
$a_like.find("span.like-num").text(response["count"])
}
})
} else {
$.ajax({
type: "POST",
url: "/update_like",
data: {
post_id_give: post_id,
type_give: type,
action_give: "like"
},
success: function (response) {
console.log("like")
$i_like.addClass("fa-heart").removeClass("fa-heart-o")
$a_like.find("span.like-num").text(response["count"])
}
})
}
}
좋아요 숫자 표시하기
이제 좋아요 기능이 생겼으니 포스팅 카드를 만들 때도 좋아요 개수를 제대로 입력해주도록 합시다.
우선 서버에서 포스트 목록을 보내줄 때 그 포스트에 달린 하트가 몇 개인지, 내가 단 하트도 있는지 같이 세어 보내줍니다.
for post in posts:
post["_id"] = str(post["_id"])
post["count_heart"] = db.likes.count_documents({"post_id": post["_id"], "type": "heart"})
post["heart_by_me"] = bool(db.likes.find_one({"post_id": post["_id"], "type": "heart", "username": payload['id']}))
클라이언트에서는 이 정보를 받아 찬 하트("fa-heart")를 보여줄 것인지, 빈 하트("fa-heart-o")를 보여줄 것인지 결정합니다.
let class_heart = ""
if (post["heart_by_me"]) {
class_heart = "fa-heart"
} else {
class_heart = "fa-heart-o"
}
이것을 '조건부 삼항 연산자(ternary operator)'를 쓰면 한 줄로 나타낼 수 있어요!
let class_heart = post['heart_by_me'] ? "fa-heart": "fa-heart-o"
조건부 삼항 연산자는 다음과 같은 구조로 써주면 됩니다.
변수 = 조건 ? 참일 때 값 : 거짓일 때 값
이 정보를 html_temp를 만들 때 하트 개수와 함께 넣어주면 끝!
<span class="icon is-small"><i class="fa ${class_heart}" aria-hidden="true"></i></span>
<span class="like-num">${post["count_heart"]}</span>
좋아요 숫자도 형식을 조금 바꿔볼까요?
우선 10,000개가 넘으면 '12k'처럼 정수+k 형식으로 만들어줍니다.
500개가 넘으면 '0.5k'처럼 소숫점 아래 한 자리 수에서 반올림해줍니다.
좋아요 수가 0개일 때는 숫자를 적지 않습니다.
작은 숫자는 그대로 적습니다.
[코드스니펫] 좋아요 숫자 형식
function num2str(count) {
if (count > 10000) {
return parseInt(count / 1000) + "k"
}
if (count > 500) {
return parseInt(count / 100) / 10 + "k"
}
if (count == 0) {
return ""
}
return count
}
get_posts()와 toggle_like() 안에 넣어줍니다.
// get_posts()
<span class="icon is-small"><i class="fa ${class_heart}" aria-hidden="true"></i></span>
<span class="like-num">${num2str(post["count_heart"])}</span>
// toggle_like()
$a_like.find("span.like-num").text(num2str(response["count"]))
27) 틀 만들기
프로필 페이지의 모습은 메인 페이지와 아주 비슷하기 때문에 우선 복사해오겠습니다. mystyle.css와 myjs.js 파일 임포트하는 것을 잊지 마세요!
[코드스니펫] - 프로필 페이지 템플릿
<body class="has-navbar-fixed-top">
<nav class="navbar is-fixed-top is-white" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<img src="{{ url_for('static', filename='logo.png') }}">
<strong class="is-sparta"
style="font-family: 'Stylish', sans-serif;font-size: larger;">SWEETER</strong>
</a>
</div>
</nav>
<section class="section">
<article class="media">
<figure class="media-left" style="align-self: center">
<a class="image is-32x32" href="/user/{{ user_info.username }}">
<img class="is-rounded" src="{{ url_for('static', filename=user_info.profile_pic_real) }}">
</a>
</figure>
<div class="media-content">
<div class="field">
<p class="control">
<input id="input-post" class="input is-rounded" placeholder="무슨 생각을 하고 계신가요?"
onclick='$("#modal-post").addClass("is-active")'>
</p>
</div>
</div>
</article>
<div class="modal" id="modal-post">
<div class="modal-background" onclick='$("#modal-post").removeClass("is-active")'></div>
<div class="modal-content">
<div class="box">
<article class="media">
<div class="media-content">
<div class="field">
<p class="control">
<textarea id="textarea-post" class="textarea"
placeholder="무슨 생각을 하고 계신가요?"></textarea>
</p>
</div>
<nav class="level is-mobile">
<div class="level-left">
</div>
<div class="level-right">
<div class="level-item">
<a class="button is-sparta" onclick="post()">포스팅하기</a>
</div>
<div class="level-item">
<a class="button is-sparta is-outlined"
onclick='$("#modal-post").removeClass("is-active")'>취소</a>
</div>
</div>
</nav>
</div>
</article>
</div>
</div>
<button class="modal-close is-large" aria-label="close"
onclick='$("#modal-post").removeClass("is-active")'></button>
</div>
</section>
<section class="section">
<div id="post-box" class="container">
<div class="box">
<article class="media">
<div class="media-left">
<a class="image is-64x64" href="#">
<img class="is-rounded"
src={{ url_for("static", filename="profile_pics/profile_placeholder.png") }} alt="Image">
</a>
</div>
<div class="media-content">
<div class="content">
<p>
<strong>홍길동</strong> <small>@username</small> <small>10분 전</small>
<br>
글을 적는 칸
</p>
</div>
<nav class="level is-mobile">
<div class="level-left">
<a class="level-item is-sparta" aria-label="heart"
onclick="toggle_like('', 'heart')">
<span class="icon is-small"><i class="fa fa-heart"
aria-hidden="true"></i></span> <span
class="like-num">2.7k</span>
</a>
</div>
</nav>
</div>
</article>
</div>
</div>
</section>
</body>
페이지가 로딩되고 나면 포스팅 카드들을 띄워줍니다.
$(document).ready(function () {
get_posts()
})
28) 프로필 영역 만들기
프로필 페이지에서는 각 사용자의 프로필이 보여야겠죠! hero 클래스와 media 클래스를 이용해 만들어보겠습니다.
[코드스니펫] - 프로필 영역
<section class="hero is-white">
<div class="hero-body" style="padding-bottom:1rem;margin:auto;min-width: 400px">
<article class="media">
<figure class="media-left" style="align-self: center">
<a class="image is-96x96" href="#">
<img class="is-rounded" src="{{ url_for('static', filename=user_info.profile_pic_real) }}">
</a>
</figure>
<div class="media-content">
<div class="content">
<p>
<strong>{{ user_info.profile_name }}</strong> <small>@{{ user_info.username }}</small>
<br>
{{ user_info.profile_info }}
</p>
</div>
</div>
</article>
</div>
</section>