* 위 내용을 정리하였음
1. package.json 만들기
npm init
{
"name": "sns",
"version": "0.0.1",
"description": "simple sns serveice with express",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "jsdysw",
"license": "MIT"
}
- npm start로 nodmon을 실행할 수 있는 스크립트도 꼭 적어주자.
2. package 설치
- NoSQL 데이터 베이스를 사용할 것 이므로 relation을 자바스크립트 객체와 매핑해주는 시퀄라이즈를 설치한다.
npm i sequelize mysql2 sequelize-cli
- config, models, migrations, seeders 폴더 생성을 위해 다음 명령어를 실행 한다.
npx sequelize init
- 시퀄라이즈를 사용하려면 -g(전역) 설치를 해야 한다. 하지만 전역설치는 package.json에 기록되지 않기 때문에 npm i로 설치한 다음 npx로 실행해서 전역 설치한 효과를 얻도록 한다.
- npx는 패키지를 실행하는데 단 한번 실행을 위해 package.json에 있는 모듈을 실행한다. 만약 모듈이 없다면 설치하고 실행한 다음 바로 fallback한다.
npm i express cookie-parser express-session morgan multer dotenv nunjucks
npm i -D nodemon
- express : npm에서 제공하는 웹 서버 프레임워크
- cookie-parser : 요청에 동봉된 쿠키를 해석해 req.cookies 객체로 만들어준다.
- express-session : 세션관리
- morgan : 요청,응답 정보를 콘솔에 기록
- multer : 이미지, 동영상 등의 파일들을 멀티파트 형식으로 업로드할 때 사용하는 미들웨어
- dotenv : .env 파일 관리
- nunjucks : 템플릿 엔진
- nodemon : 소스가 바뀌었을때 자동으로 서버를 재실행, 개발용으로 설치
- public : 정적 파일들 위치
- routes : 라우터 위치
- views : 템플릿 파일들 위치
3. 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.COOKIE_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 !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중');
});
- pageRouter, 404 응답 미들웨어, 에러처리 미들웨어가 있다.
4. .env
COOKIE_SECRET=cookiesecret
5. routes/page.js
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 = [];
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, next) => {
const twits = [];
res.render('main', {
title: 'NodeBird',
twits,
});
});
module.exports = router;
- 시작하자마자 router.use로 라우터용 미들웨어를 하나 만들었다.
- 템플릿 엔진에서 사용할 user, followingCount 등 변수를 res.locals에 저장한다. res.locals는 어디서든 사용할 수 있다.
- 위 값들을 모든 템플릿 엔진들이 공통으로 사용할 것이라 여기에 한번에 저장한것이다.
6. views
// view/layout.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" href="/main.css">
</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="password">비밀번호</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="_blank">ZeroCho</a>
</footer>
</div>
{% block content %}
{% endblock %}
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></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>
- 렌더링할때 user가 존재하면 사용자 정보, 팔로워 수를 보여주고 그렇지 않다면 로그인 메뉴를 보여준다.
// views/main.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 id="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 followerIdList.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 %}
- user가 존재하면 for문으로 twits 배열 안에 게시물 정보들을 화면에 그린다.
// 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>
</div>
{% endblock %}
// views/join.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="text" 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/error.html
{% extends 'layout.html' %}
{% block content %}
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
{% endblock %}
- 서버에서 에러 발생시 에러 내용을 보여주는 화면이다.
7. public
- 디자인을 위한 css파일이 담겨있다.
// public/main.css
* { 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; }
8. 실행
- npm start로 서버 실행 후 http://localhost:8001에 접속하여 확인해보자.
'ComputerScience > NodeJs' 카테고리의 다른 글
node - 9.3 익스프레스로 SNS 서비스 만들기(로그인 구현) (0) | 2022.02.06 |
---|---|
node - 9.2 익스프레스로 SNS 서비스 만들기(SQL DB) (0) | 2022.02.05 |
node - 8 몽고디비 (0) | 2022.02.03 |
node - 7.6 시퀄라이즈 사용하기 (0) | 2022.01.31 |
node - 7 MySQL (0) | 2022.01.29 |