프로젝트 생성
140자의 단문 메시지를 보내고 공유하는 서비스를 따라 해 보자.
nodebird라는 프로젝트를 만들자.
{
"name": "nodebird",
"version": "1.0.0",
"description": "sns service using express",
"main": "index.js",
"scripts": {
"start": "nodemon app",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "hvvan",
"license": "MIT"
}
npm i sequelize mysql2 sequelize-cli
npx sequelize init
그리고 views, route, public, passport 폴더를 만들자.
그리고 패키지들을 설치하자.
npm i express cookie-parser express-session morgan multer dotenv nunjucks
npm i -D nodemon
app.js를 만들어보자.
const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
dotenv.config();
const pageRouter = require('./routes/page');
const app = express();
app.set('port', process.env.PORT || 8001);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(cookieParser(process.env.COOKE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie:{
httpOnly: true,
secure: false,
},
}));
app.use('/', pageRouter);
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'productiong' ? err : {};
res.status(err.status || 500);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중...');
});
기본적으로 에러가 발생하면 처리해주는 라우터만 존재한다.
router를 더 구현하여 추가할 예정이다.
하나하나 만들어 보자.
const express = require('express');
const router = express.Router();
router.use((req, res, next) => {
res.locals.user = null;
res.locals.followerCount = 0;
res.locals.followingCount = 0;
res.locals.followerIdList = 0;
next();
});
router.get('/profile', (req, res) => {
res.render('profile', {title: '내 정보 - NodeBird'});
})
router.get('/join', (req, res) => {
res.render('join', {title: '회원가입 - NodeBird'});
})
router.get('/', (req, res) => {
const twits = [];
res.render('main', {
title: 'NodeBird',
twits,
});
});
module.exports = router;
routes/page.js
router.use로 라우터용 미들웨어를 만들어 템플릿 엔진에서 사용할 user, followingCount, followerCount, follwerIdList 변수를 res.locals로 설정했다.
다음은 클라이언트 코드이다.
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, user-scalable=no">
<link rel="stylesheet" href="/main.css">
<title>{{ title }}</title>
</head>
<body>
<div class="container">
<div class="profile-wrap">
<div class="profile">
{% if user and user.id %}
<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="logout" href="/auth/logout" class="btn">로그아웃</a>
{% else %}
<form id="login-form" action="/auth/login" method="post">
<div class="input-group">
<label for="email">이메일</label>
<input id="email" type="email" name="email" required autofocus>
</div>
<div class="input-group">
<label for="email">비밀번호</label>
<input id="password" type="password" name="password" required>
</div>
<a id="join" href="/join" class="btn">회원가입</a>
<button id="login" type="submit" class="btn">로그인</button>
<a id="kakao" href="/auth/kakao" class="btn">카카오톡</a>
</form>
{% endif %}
</div>
<footer>
Made by
<a href="https://www.zerocho.com" target="_black">zerocho</a>
</footer>
</div>
{% block content %}
{% endblock %}
</div>
<script src="https://unpkg.com/axios/dist/ axios.min.s"></script>
<script>
window.onload = () => {
if(new URL(location.href).searchParams.get('loginError')){
alert(new URL(location.href).searchParams.get('loginError'));
}
};
</script>
{% block script %}
{% endblock %}
</body>
</html>
views/layout.html
{% extends "layout.html" %}
{% block content %}
<div class="timeline">
{% if user %}
<div>
<form id="twit-form" action="/post" method="post" enctype="multipart/form-data">
<div class="input-group">
<textarea id="twit" name="content" maxlength="140"></textarea>
</div>
<div class="img-preview">
<img id="img-preview" src="" style="display: none;" width="250" alt="미리보기">
<input id="img-url" type="hidden" name="url">
</div>
<div>
<label for="img-label" for="img">사진업로드</label>
<input id="img" type="file" accept="image/*">
<button id="twit-btn" type="submit" class="btn">짹짹</button>
</div>
</form>
</div>
{% endif %}
<div class="twits">
<form id="hashtag-form" action="/hashtag">
<input type="text" name="hashtag" placeholder="태그 검색">
<button class="btn">검색</button>
</form>
{% 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 followerList.includes(twit.User.id) and twit.User.id != user.id %}
<button class="twit-follow">팔로우 하기</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 %}
</div>
</div>
{% endblock %}
{% block script %}
<script>
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]);
axios.post('/post/img', formData)
.then((res) => {
document.getElementById('img-url').value = res.data.url;
document.getElementById('img-preview').src = res.data.url;
document.getElementById('img-preview').style.display = 'inline';
})
.catch((err) => {
console.error(err);
});
});
}
document.querySelectorAll('.twit-follow').forEach(function(tag) {
tag.addEventListener('click', function(){
const myId = document.querySelector('#my-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);
});
}
}
}
});
});
</script>
{% endblock %}
views/main.html
nunjucks를 이용한 블록을 껴 넣는 방법이다.
{% 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 %}
views/profile.html
{% extends "layout.html" %}
{% block content %}
<div class="timeline">
<form id="join-form" action="/auth/join" method="post">
<div class="input-group">
<label for="join-email">이메일</label>
<input id="join-email" type="email" name="email">
</div>
<div class="input-group">
<label for="join-nick">닉네임</label>
<input id="join-nick" type="email" name="nick">
</div>
<div class="input-group">
<label for="join-password">비밀번호</label>
<input id="join-password" type="password" name="password">
</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 %}
views/join.html
{% extends "layout.html" %}
{% block content %}
<h1>{{ message }}</h1>
<h2>{{ error.status }}</h2>
<pre>{{ message.stack }}</pre>
{% endblock %}
view/error.html
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; }
.btn {
display: inline-block;
padding: 0 5px;
text-decoration: none;
cursor: pointer;
border-radius: 4px;
background: white;
border: 1px solid silver;
color: crimson;
height: 37px;
line-height: 37px;
vertical-align: top;
font-size: 12px;
}
input[type='text'], input[type='email'], input[type='password'], textarea {
border-radius: 4px;
height: 37px;
padding: 10px;
border: 1px solid silver;
}
.container { width: 100%; height: 100%; }
@media screen and (min-width: 800px) {
.container { width: 800px; margin: 0 auto; }
}
.input-group { margin-bottom: 15px; }
.input-group label { width: 25%; display: inline-block; }
.input-group input { width: 70%; }
.half { float: left; width: 50%; margin: 10px 0; }
#join { float: right; }
.profile-wrap {
width: 100%;
display: inline-block;
vertical-align: top;
margin: 10px 0;
}
@media screen and (min-width: 800px) {
.profile-wrap { width: 290px; margin-bottom: 0; }
}
.profile {
text-align: left;
padding: 10px;
margin-right: 10px;
border-radius: 4px;
border: 1px solid silver;
background: lightcoral;
}
.user-name { font-weight: bold; font-size: 18px; }
.count { font-weight: bold; color: crimson; font-size: 18px; }
.timeline {
margin-top: 10px;
width: 100%;
display: inline-block;
border-radius: 4px;
vertical-align: top;
}
@media screen and (min-width: 800px) { .timeline { width: 500px; } }
#twit-form {
border-bottom: 1px solid silver;
padding: 10px;
background: lightcoral;
overflow: hidden;
}
#img-preview { max-width: 100%; }
#img-label {
float: left;
cursor: pointer;
border-radius: 4px;
border: 1px solid crimson;
padding: 0 10px;
color: white;
font-size: 12px;
height: 37px;
line-height: 37px;
}
#img { display: none; }
#twit { width: 100%; min-height: 72px; }
#twit-btn {
float: right;
color: white;
background: crimson;
border: none;
}
.twit {
border: 1px solid silver;
border-radius: 4px;
padding: 10px;
position: relative;
margin-bottom: 10px;
}
.twit-author { display: inline-block; font-weight: bold; margin-right: 10px; }
.twit-follow {
padding: 1px 5px;
background: #fff;
border: 1px solid silver;
border-radius: 5px;
color: crimson;
font-size: 12px;
cursor: pointer;
}
.twit-img { text-align: center; }
.twit-img img { max-width: 75%; }
.error-message { color: red; font-weight: bold; }
#search-form { text-align: right; }
#join-form { padding: 10px; text-align: center; }
#hashtag-form { text-align: right; }
footer { text-align: center; }
public/main.css
이제 npm satrt로 서버를 실행해보자.