๐ ์ด๋ฒ ์ฅ์์๋ ๋ก๊ทธ์ธ, ์ด๋ฏธ์ง ์ ๋ก๋, ๊ฒ์๊ธ ์์ฑ, ํด์ํ๊ทธ ๊ฒ์, ํ๋ก์ ๋ฑ์ ๊ธฐ๋ฅ์ด ์๋ SNS ์๋น์ค๋ฅผ ๋ง๋ ๋ค.
๋ค์๊ณผ ๊ฐ์ด ์ฑ๋ช
์ธ 'nodebird'๋ผ๋ ์ด๋ฆ์ผ๋ก ํด๋๋ฅผ ๋ง๋ค๊ณ package.json์ ์์ฑํ๋ค.
์ํ๋ผ์ด์ฆ๋ ์ค์นํ๋ค.
$ npm init
$ npm i sequelize mysql2 sequelize-cli
$ npx sequelize init
๋ค์๊ณผ ๊ฐ์ด ํด๋์ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ถ๋ค.
nodebird
> config
> migrations
> models
> node_modules
> passport
> public
> routes
> seeders
> views
.env
app.js
{ } package-lock.json
{ } package.json
๊ทธ๋ค์, ํ์ํ npm ํจํค์ง๋ฅผ ์ค์นํ ํ
$ 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); // ์ฑ์ 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; // ๋ผ์ฐํฐ๊ฐ ์์ ์ 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'), '๋ฒ ํฌํธ์์ ๋๊ธฐ์ค');
});
์๋๋ ๊ธฐ๋ณธ์ ์ธ ๋ผ์ฐํฐ์ ํ
ํ๋ฆฟ ์์ง์ด๋ค.
const express = require('express');
const { renderProfile, renderJoin, renderMain } = require('../controllers/page');
const router = express.Router();
router.use((req, res, next) => {
res.locals.user = null;
res.locals.followerCount = 0;
res.locals.followingCount = 0;
res.locals.followingIdList = [];
next();
});
router.get('/profile', renderProfile);
router.get('/join', renderJoin);
router.get('/', renderMain);
module.exports = router;
์ ์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด, router.use๋ก ๋ผ์ฐํฐ์ฉ ๋ฏธ๋ค์จ์ด๋ฅผ ๋ง๋ค์ด ํ
ํ๋ฆฟ ์์ง์์ ์ฌ์ฉํ user, followingCount, followerCount, followingIdList ๋ณ์๋ฅผ res.locals๋ก ์ค์ ํ๋ค. ์ฌ๊ธฐ์ ์ด์ ๊ณผ๋ ๋ค๋ฅด๊ฒ ๋ผ์ฐํฐ์ ๋ฏธ๋ค์จ์ด๋ฅผ ๋ค๋ฅธ ๊ณณ์์ ๋ถ๋ฌ์ค๊ณ ์๋ค๋ ์ ์ ์ฃผ๋ชฉํด์ผ ํ๋ค.
: ์๋ renderProfile, renderJoin, renderMain๊ณผ ๊ฐ์ด ๋ผ์ฐํฐ ๋ง์ง๋ง์ ์์นํด ํด๋ผ์ด์ธํธ์ ์๋ต์ ๋ณด๋ด๋ ๋ฏธ๋ค์จ์ด๋ฅผ ๋งํ๋ค.
exports.renderProfile = (req, res) => {
res.render('profile', { title: '๋ด ์ ๋ณด - NodeBird' });
};
exports.renderJoin = (req, res) => {
res.render('join', { title: 'ํ์๊ฐ์
- NodeBird' });
};
exports.renderMain = (req, res, next) => {
const twits = [];
res.render('main', {
title: 'NodeBird',
twits,
});
};
์ปจํธ๋กค๋ฌ๋ ์ค๋ฌด์์ ์ฝ๋๋ฅผ ํธ๋ฆฌํ๊ฒ ๊ด๋ฆฌํ๊ธฐ ์ํด ์ฌ์ฉํ๋ค๊ณ ๋ณด๋ฉด ๋๋ค.
๊ทธ ์ธ์ ํ์ํ html, css ์ฝ๋๋ https://github.com/ZeroCho/nodejs-book/tree/master ์์ ์ฐธ์กฐํ๊ธฐ ๋ฐ๋๋ค.
๐ ์ฌ์ฉ์ ํ ์ด๋ธ, ๊ฒ์๊ธ ํ ์ด๋ธ, ํด์ํ๊ทธ ํ ์ด๋ธ
models
> user.js
> post.js
> hashtag.js
์ฌ์ฉ์ ์ ๋ณด(์ด๋ฉ์ผ, ๋๋ค์, ๋น๋ฐ๋ฒํธ)๋ฅผ ์ ์ฅํ๋ ํ ์ด๋ธ์ด๋ค. ์ด๋ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ์ฌ์ฉ๋๋ค.
const Sequelize = require('sequelize');
class User extends Sequelize.Model {
static initiate(sequelize) {
User.init({
email: {
type: Sequelize.STRING(40),
allowNull: true,
unique: true,
},
nick: {
type: Sequelize.STRING(15),
allowNull: false,
},
password: {
type: Sequelize.STRING(100),
allowNull: true,
},
provider: { // SNS ๋ก๊ทธ์ธ์ ํ์ ๊ฒฝ์ฐ ์ ์ฅ
type: Sequelize.ENUM('local', 'kakao'),
allowNull: false,
defaultValue: 'local',
},
snsId: { // SNS ๋ก๊ทธ์ธ์ ํ์ ๊ฒฝ์ฐ ์ ์ฅ
type: Sequelize.STRING(30),
allowNull: true,
},
}, {
sequelize, // ํ
์ด๋ธ ์ต์
timestamps: true,
underscored: false,
modelName: 'User',
tableName: 'users',
paranoid: true,
charset: 'utf8',
collate: 'utf8_general_ci',
});
}
static associate(db) {}
};
module.exports = User;
๊ฒ์๊ธ ๋ด์ฉ๊ณผ ์ด๋ฏธ์ง ๊ฒฝ๋ก๋ฅผ ์ ์ฅํ๋ค.
const Sequelize = require('sequelize');
class Post extends Sequelize.Model {
static initiate(sequelize) {
Post.init({
content: {
type: Sequelize.STRING(140),
allowNull: false,
},
img: {
type: Sequelize.STRING(200),
allowNull: true,
},
}, {
sequelize,
timestamps: true,
underscored: false,
modelName: 'Post',
tableName: 'posts',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {
db.Post.belongsTo(db.User);
db.Post.belongsToMany(db.Hashtag, {through: 'PostHashtag'});
}
}
module.exports = Post;
ํ๊ทธ ์ด๋ฆ์ ์ ์ฅํ๋ค.
const Sequelize = require('sequelize');
class Hashtag extends Sequelize.Model {
static initiate(sequelize) {
Hashtag.init({
title: {
type: Sequelize.STRING(15),
allowNull: false,
unique: true,
},
}, {
sequelize,
timestamps: true,
underscored: false,
modelName: 'Hashtag',
tableName: 'hashtags',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {
db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' });
}
};
module.exports = Hashtag;
์์์ ์์ฑํ ๋ชจ๋ธ์ ์ํ๋ผ์ด์ฆ์ ๋ฑ๋กํ ํ, ๊ฐ๊ฐ์ ๋ชจ๋ธ์ ์ํ๋ผ์ด์ฆ ๊ฐ์ฒด์ ์ฐ๊ฒฐํ๋ค.
const Sequelize = require('sequelize');
const fs = require('fs');
const path = require('path');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];
const db = {};
const sequelize = new Sequelize(
config.database, config.username, config.password, config,
);
db.sequelize = sequelize;
const basename = path.basename(__filename);
fs
.readdirSync(__dirname) // ํ์ฌ ํด๋์ ๋ชจ๋ ํ์ผ์ ์กฐํ
.filter(file => { // ์จ๊น ํ์ผ, index.js, js ํ์ฅ์๊ฐ ์๋ ํ์ผ ํํฐ๋ง
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => { // ํด๋น ํ์ผ์ ๋ชจ๋ธ ๋ถ๋ฌ์์ init
const model = require(path.join(__dirname, file));
console.log(file, model.name);
db[model.name] = model;
model.initiate(sequelize);
});
Object.keys(db).forEach(modelName => { // associate ํธ์ถ
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
module.exports = db;
๊ฐ ๋ชจ๋ธ ๊ฐ์ ๊ด๊ณ๋ฅผ models/user.js ์ associate ํจ์ ๋ด์ ์ ์ํ๋ค.
static associate(db) {
db.User.hasMany(db.Post);
db.User.belongsToMany(db.User, {
foreignKey: 'followingId',
as: 'Followers',
through: 'Follow',
});
db.User.belongsToMany(db.User, {
foreignKey: 'followerId',
as: 'Followings',
through: 'Follow',
});
}
};
์ ๋ชจ๋ธ๋ค์ N:M ๊ด๊ณ๋ฅผ ๊ฐ์ง๋ค.
ํ๋์ ์ฌ์ฉ์๊ฐ ๋ค๋ฅธ ์ฌ์ฉ์์ ๊ด๊ณ๋ฅผ ๋งบ์ ์ ์๋ ๊ด๊ณ๋ฅผ N:M ๊ด๊ณ๋ผ๊ณ ํ๋๋ฐ, ํ๋ก์ ๊ธฐ๋ฅ์ด ๋ํ์ ์ด๋ค.
๋์ผํ ํ
์ด๋ธ์ ์ฐธ์กฐํ๋ ๊ด๊ณ๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํด์๋ as ์ต์
์ ์ฌ์ฉํ๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋ชจ๋ ์ค์ ํ ํ์๋ ๋ค์๊ณผ ๊ฐ์ด app.js์ ๋ชจ๋ธ์ ์๋ฒ์ ์ฐ๊ฒฐํ๋ค.
...
dotenv.config();
const pageRouter = require('./routes/page');
**const { sequelize } = require('./models');**
const app = express();
app.set('port', process.env.PORT || 8001);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
**sequelize.sync({ force: false })
.then(() => {
console.log('๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ฑ๊ณต');
})
.catch((err) => {
console.error(err);
});**
app.use(morgan('dev'));
...
๐ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ
๋ณต์กํ ์์
์ ์ง์ ๊ตฌํํ๊ธฐ๋ณด๋จ, ์ด๋ฏธ ๊ฒ์ฆ๋ ๋ชจ๋์ธ Passport ๋ชจ๋์ ์ด์ฉํด ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค.
๋จผ์ Passport ๊ด๋ จ ํจํค์ง๋ฅผ ์ค์นํ๋ค.
$ npm i passport passport-local passport-kakao bcrypt
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = () => {
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findOne({ where: { id } })
.then(user => done(null, user))
.catch(err => done(err));
});
local();
kakao();
};
์ ์ฝ๋์์
๋ก๊ทธ์ธ ๊ณผ์ ์ ๋ค์๊ณผ ๊ฐ๋ค.
1. /auth/login ๋ผ์ฐํฐ๋ฅผ ํตํด ๋ก๊ทธ์ธ ์์ฒญ์ด ๋ค์ด์ด
2. ๋ผ์ฐํฐ์์ passport.authenticate ๋ฉ์๋ ํธ์ถ
3. ๋ก๊ทธ์ธ ๋ก์ง ์ํ
4. ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ฒด์ ํจ๊ป req.login ํธ์ถ
5. req.login ๋ฉ์๋๊ฐ passport/serializeUser ํธ์ถ
6. req.session์ ์ฌ์ฉ์ ์์ด๋๋ง ์ ์ฅํด์ ์ธ์ ์์ฑ
7. express-session์ ์ค์ ํ ๋๋ก ๋ธ๋ผ์ฐ์ ์ connect.sid ์ธ์ ์ฟ ํค ์ ์ก
8. ๋ก๊ทธ์ธ ์๋ฃ
๋ก๊ทธ์ธ ์ดํ์ ๊ณผ์ ์ ๋ค์๊ณผ ๊ฐ๋ค.
1. ์์ฒญ์ด ๋ค์ด์ด
2. ๋ผ์ฐํฐ์ ์์ฒญ์ด ๋๋ฌํ๊ธฐ ์ ์ passport.session ๋ฏธ๋ค์จ์ด๊ฐ passport.deserializeUser ๋ฉ์๋ ํธ์ถ
3. connect.sid ์ธ์ ์ฟ ํค๋ฅผ ์ฝ๊ณ ์ธ์ ๊ฐ์ฒด๋ฅผ ์ฐพ์์ req.session์ผ๋ก ๋ง๋ฆ
4. req.session์ ์ ์ฅ๋ ์์ด๋๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฌ์ฉ์ ์กฐํ
5. ์กฐํ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ req.user์ ์ ์ฅ(deserializeUser)
6. ๋ผ์ฐํฐ์์ req.user ๊ฐ์ฒด ์ฌ์ฉ ๊ฐ๋ฅ
๐ ์์ฒด์ ์ผ๋ก ํ์ ๊ฐ์
ํ ๋ก๊ทธ์ธํ๋ ๊ฒ (์์ด๋/๋น๋ฐ๋ฒํธ or ์ด๋ฉ์ผ/๋น๋ฐ๋ฒํธ๋ก ๋ก๊ทธ์ธ)
passport-local ๋ชจ๋์ ์ฌ์ฉํ์ฌ ๊ตฌํํ๋ค.
-> ์ ๊ทผ ๊ถํ์ ์ ํํ๋ ๋ฏธ๋ค์จ์ด๊ฐ ํ์ (middlewares/index.js)
exports.isLoggedIn = (req, res, next) => {
if (req.isAuthenticated()) { // ๋ก๊ทธ์ธ x ์ํ
next();
} else {
res.status(403).send('๋ก๊ทธ์ธ ํ์');
}
};
exports.isNotLoggedIn = (req, res, next) => {
if (!req.isAuthenticated()) { // ๋ก๊ทธ์ธ o ์ํ
next();
} else {
const message = encodeURIComponent('๋ก๊ทธ์ธํ ์ํ์
๋๋ค.');
res.redirect(`/?error=${message}`);
}
};
์ด ๋ฏธ๋ค์จ์ด๊ฐ page ๋ผ์ฐํฐ์ ๋ค์๊ณผ ๊ฐ์ด ์ ์ฉ๋๋ค. (routes/page.js)
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const { renderProfile, renderJoin, renderMain } = require('../controllers/page');
const router = express.Router();
router.use((req, res, next) => {
res.locals.user = req.user;
res.locals.followerCount = 0;
res.locals.followingCount = 0;
res.locals.followingIdList = [];
next();
});
router.get('/profile', isLoggedIn, renderProfile);
router.get('/join', isNotLoggedIn, renderJoin);
router.get('/', renderMain);
module.exports = router;
์์ฒ๋ผ ์ ๊ทผ ์ค์ ์ ์๋ฃํ์์ผ๋ฉด, ํ์ ๊ฐ์
, ๋ก๊ทธ์ธ, ๋ก๊ทธ์์ ๋ผ์ฐํฐ๋ฅผ ์์ฑํ๋ค. (routes/auth.js)
const express = require('express');
const passport = require('passport');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const { join, login, logout } = require('../controllers/auth');
const router = express.Router();
// POST /auth/join
router.post('/join', isNotLoggedIn, join);
// POST /auth/login
router.post('/login', isNotLoggedIn, login);
// GET /auth/logout
router.get('/logout', isLoggedIn, logout);
// GET /auth/kakao
router.get('/kakao', passport.authenticate('kakao'));
// GET /auth/kakao/callback
router.get('/kakao/callback', passport.authenticate('kakao', {
failureRedirect: '/?error=์นด์นด์ค๋ก๊ทธ์ธ ์คํจ',
}), (req, res) => {
res.redirect('/'); // ์ฑ๊ณต ์์๋ /๋ก ์ด๋
});
module.exports = router;
ํ์ ๊ฐ์
, ๋ก๊ทธ์ธ, ๋ก๊ทธ์์ ์ปจํธ๋กค๋ฌ๋ ์์ฑํ๋ค. (controllers/auth.js)
const bcrypt = require('bcrypt');
const passport = require('passport');
const User = require('../models/user');
// ํ์ ๊ฐ์
์ปจํธ๋กค๋ฌ
exports.join = async (req, res, next) => {
const { email, nick, password } = req.body;
try {
const exUser = await User.findOne({ where: { email } });
if (exUser) {
return res.redirect('/join?error=exist');
}
const hash = await bcrypt.hash(password, 12);
await User.create({
email,
nick,
password: hash,
});
return res.redirect('/');
} catch (error) {
console.error(error);
return next(error);
}
}
// ๋ก๊ทธ์ธ ์ปจํธ๋กค๋ฌ
exports.login = (req, res, next) => {
passport.authenticate('local', (authError, user, info) => {
if (authError) {
console.error(authError);
return next(authError);
}
if (!user) {
return res.redirect(`/?error=${info.message}`);
}
return req.login(user, (loginError) => {
if (loginError) {
console.error(loginError);
return next(loginError);
}
return res.redirect('/');
});
})(req, res, next); // ๋ฏธ๋ค์จ์ด ๋ด์ ๋ฏธ๋ค์จ์ด์๋ (req, res, next)๋ฅผ ๋ถ์
๋๋ค.
};
// ๋ก๊ทธ์์ ์ปจํธ๋กค๋ฌ
exports.logout = (req, res) => {
req.logout(() => {
res.redirect('/');
});
};
๋ง์ง๋ง์ผ๋ก ๋ก๊ทธ์ธ ์ ๋ต์ ๊ตฌํํ๋ฉด ๋๋ค. (passport/localStrategy.js)
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/user');
module.exports = () => {
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback: false,
}, async (email, password, done) => {
try {
const exUser = await User.findOne({ where: { email } });
if (exUser) {
const result = await bcrypt.compare(password, exUser.password);
if (result) {
done(null, exUser);
} else {
done(null, false, { message: '๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.' });
}
} else {
done(null, false, { message: '๊ฐ์
๋์ง ์์ ํ์์
๋๋ค.' });
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
์ ๋ต์ ๋ค์๊ณผ ๊ฐ์ ๋ก์ง์ ๊ฐ์ง๋ค.
๐ ๋ก๊ทธ์ธ ์ธ์ฆ ๊ณผ์ ์ ์นด์นด์ค์ ๋งก๊ธฐ๋ ๊ฒ
passport-kakao ๋ชจ๋๋ก๋ถํฐ Strategy ์์ฑ์๋ฅผ ๋ถ๋ฌ์ ์ ๋ต์ ๊ตฌํํ๋ค.
const passport = require('passport');
const KakaoStrategy = require('passport-kakao').Strategy;
const User = require('../models/user');
// ์นด์นด์ค ๋ก๊ทธ์ธ์ ๋ํ ์ค์
module.exports = () => {
passport.use(new KakaoStrategy({
clientID: process.env.KAKAO_ID,
callbackURL: '/auth/kakao/callback',
}, async (accessToken, refreshToken, profile, done) => { // ์นด์นด์ค ํ์๊ฐ์
์ฌ๋ถ ์กฐํ
console.log('kakao profile', profile);
try {
const exUser = await User.findOne({
where: { snsId: profile.id, provider: 'kakao' },
});
if (exUser) {
done(null, exUser);
} else { // ์นด์นด์ค ํ์์ด ์๋๋ผ๋ฉด ํ์๊ฐ์
์งํ
const newUser = await User.create({
email: profile._json?.kakao_account?.email,
nick: profile.displayName,
snsId: profile.id,
provider: 'kakao',
});
done(null, newUser);
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
...
router.get('/logout', isLoggedIn, logout);
// GET /auth/kakao -> ์นด์นด์ค ๋ก๊ทธ์ธ ์ ๋ต ์ํ
router.get('/kakao', passport.authenticate('kakao'));
// GET /auth/kakao/callback -> ๋ก๊ทธ์ธ ํ ์ฑ๊ณต ์ฌ๋ถ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์
router.get('/kakao/callback', passport.authenticate('kakao', {
failureRedirect: '/?error=์นด์นด์ค๋ก๊ทธ์ธ ์คํจ',
}), (req, res) => {
res.redirect('/'); // ์ฑ๊ณต ์์๋ /๋ก ์ด๋
});
module.exports = router;
์ ๋ผ์ฐํฐ ์์ฑ ํ app.js์ ์ฐ๊ฒฐํ๋ฉด ๋๋ค.
๊ทธ ํ https://developers.kakao.com/ ์ ์ ํ clientID๋ฅผ ๋ฐ๊ธ๋ฐ๊ณ ์นด์นด์ค ๊ฐ๋ฐ์ ๊ณ์ ๊ณผ ์นด์นด์ค ๋ก๊ทธ์ธ์ฉ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ฑ๋กํ์ฌ ์๋ํ๋ ๊ฒ์ ํ์ธํ๋ฉด ๋๋ค.
๐ multer ๋ชจ๋์ ์ฌ์ฉํด ๋ฉํฐ ํํธ ํ์์ ์ด๋ฏธ์ง ์
๋ก๋ ๊ธฐ๋ฅ ๊ตฌํ
๋จผ์ ํจํค์ง๋ฅผ ์ค์นํ ํ
$ npm i multer
post ๋ผ์ฐํฐ(routes/post.js)์
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { afterUploadImage, uploadPost } = require('../controllers/post');
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'), afterUploadImage);
// POST /post -> ๊ฒ์๊ธ ์
๋ก๋๋ฅผ ์ฒ๋ฆฌํ๋ ๋ผ์ฐํฐ
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), uploadPost);
module.exports = router;
์ปจํธ๋กค๋ฌ(controllers/post.js)๋ฅผ ์์ฑํ๋ค.
const { Post, Hashtag } = require('../models');
exports.afterUploadImage = (req, res) => {
console.log(req.file);
res.json({ url: `/img/${req.file.filename}` });
};
exports.uploadPost = async (req, res, next) => {
try {
const post = await Post.create({
content: req.body.content,
img: req.body.url,
UserId: req.user.id,
});
const hashtags = req.body.content.match(/#[^\s#]*/g);
if (hashtags) {
const result = await Promise.all(
hashtags.map(tag => {
return Hashtag.findOrCreate({
where: { title: tag.slice(1).toLowerCase() },
})
}),
);
await post.addHashtags(result.map(r => r[0]));
}
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
};
์์ ๊ณผ์ ์ ํตํด ๊ฒ์๊ธ ์์ฑ ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋์์ผ๋ฏ๋ก ๋ฉ์ธ ํ์ด์ง ๋ก๋ฉ ์ ๋ฉ์ธ ํ์ด์ง์ ๊ฒ์๊ธ์ด ํจ๊ป ๋ก๋ฉ๋๋๋ก controllers/page.js๋ฅผ ์ถ๊ฐํ๋ค.
const { User, Post } = require('../models');
exports.renderProfile = (req, res) => {
res.render('profile', { title: '๋ด ์ ๋ณด - NodeBird' });
};
exports.renderJoin = (req, res) => {
res.render('join', { title: 'ํ์๊ฐ์
- NodeBird' });
};
exports.renderMain = async (req, res, next) => {
try {
const posts = await Post.findAll({
include: {
model: User,
attributes: ['id', 'nick'],
},
order: [['createdAt', 'DESC']],
});
res.render('main', {
title: 'NodeBird',
twits: posts,
});
} catch (err) {
console.error(err);
next(err);
}
}
๐ ํ๋ก์ ๊ธฐ๋ฅ๊ณผ ํด์ํ๊ทธ ๊ฒ์ ๊ธฐ๋ฅ
๋ ๊ทธ๋ฌ๋ฏ์ด ๋ผ์ฐํฐ(routes/user.js)๋ฅผ ๋จผ์ ๊ตฌํํ๊ณ
const express = require('express');
const { isLoggedIn } = require('../middlewares');
const { follow } = require('../controllers/user');
const router = express.Router();
// POST /user/:id/follow
router.post('/:id/follow', isLoggedIn, follow);
module.exports = router;
์ปจํธ๋กค๋ฌ(controllers/user.js)๋ฅผ ๊ตฌํํ๋ค.
const User = require('../models/user');
exports.follow = async (req, res, next) => {
try {
const user = await User.findOne({ where: { id: req.user.id } });
if (user) { // req.user.id๊ฐ followerId, req.params.id๊ฐ followingId
await user.addFollowing(parseInt(req.params.id, 10));
res.send('success');
} else {
res.status(404).send('no user');
}
} catch (error) {
console.error(error);
next(error);
}
};
ํ๋ก์ ๊ด๊ณ๊ฐ ์๊ฒผ์ผ๋ฏ๋ก req.user์๋ ํ๋ก์์ ํ๋ก์ ๋ชฉ๋ก์ ์ ์ฅํ๋ค. (passport/index.js)
...
passport.deserializeUser((id, done) => {
console.log('deserialize');
User.findOne({
where: { id },
include: [{
model: User,
attributes: ['id', 'nick'],
as: 'Followers',
}, {
model: User,
attributes: ['id', 'nick'],
as: 'Followings',
}],
})
.then(user => {
console.log('user', user);
done(null, user);
})
.catch(err => done(err));
});
...
routes/page.js๋ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ค.
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const {
renderProfile, renderJoin, renderMain, renderHashtag,
} = require('../controllers/page');
const router = express.Router();
router.use((req, res, next) => {
res.locals.user = req.user;
res.locals.followerCount = req.user?.Followers?.length || 0;
res.locals.followingCount = req.user?.Followings?.length || 0;
res.locals.followingIdList = req.user?.Followings?.map(f => f.id) || [];
next();
});
router.get('/profile', isLoggedIn, renderProfile);
router.get('/join', isNotLoggedIn, renderJoin);
router.get('/', renderMain);
router.get('/hashtag', renderHashtag);
module.exports = router;
๋ง์ง๋ง์ผ๋ก ์ปจํธ๋กค๋ฌ(controllers/page.js)๋ ์์ฑํ ํ
const { User, Post, Hashtag } = require('../models');
exports.renderProfile = (req, res) => {
res.render('profile', { title: '๋ด ์ ๋ณด - NodeBird' });
};
exports.renderJoin = (req, res) => {
res.render('join', { title: 'ํ์๊ฐ์
- NodeBird' });
};
exports.renderMain = async (req, res, next) => {
try {
const posts = await Post.findAll({
include: {
model: User,
attributes: ['id', 'nick'],
},
order: [['createdAt', 'DESC']],
});
res.render('main', {
title: 'NodeBird',
twits: posts,
});
} catch (err) {
console.error(err);
next(err);
}
}
exports.renderHashtag = async (req, res, next) => {
const query = req.query.hashtag;
if (!query) {
return res.redirect('/');
}
try {
const hashtag = await Hashtag.findOne({ where: { title: query } });
let posts = [];
if (hashtag) {
posts = await hashtag.getPosts({ include: [{ model: User }] });
}
return res.render('main', {
title: `${query} | NodeBird`,
twits: posts,
});
} catch (error) {
console.error(error);
return next(error);
}
};
๋ผ์ฐํฐ๋ฅผ app.js์ ์ฐ๊ฒฐํ๋ฉด ๋๋๋ค.
...
const pageRouter = require('./routes/page');
const authRouter = require('./routes/auth');
const postRouter = require('./routes/post');
const userRouter = require('./routes/user');
const { sequelize } = require('./models');
const passportConfig = require('./passport');
...
app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/img', express.static(path.join(__dirname, 'uploads')));
app.use(express.json());
...
app.use('/', pageRouter);
app.use('/auth', authRouter);
app.use('/post', postRouter);
app.use('/user', userRouter);
...
1. ( ์ปจํธ๋กค๋ฌ )๋ ๋ผ์ฐํฐ ๋ง์ง๋ง์ ์์นํด ํด๋ผ์ด์ธํธ์ ์๋ต์ ๋ณด๋ด๋ ๋ฏธ๋ค์จ์ด์ด๋ค.
2. ํ๋์ ์ฌ์ฉ์๊ฐ ๋ค๋ฅธ ์ฌ์ฉ์์ ๊ด๊ณ๋ฅผ ๋งบ์ ์ ์๋ ๊ด๊ณ๋ฅผ ( N:M ๊ด๊ณ )๋ผ๊ณ ๋ถ๋ฅธ๋ค.
3. N:M ๊ด๊ณ์์ ๋์ผํ ํ
์ด๋ธ์ ์ฐธ์กฐํ๋ ๊ด๊ณ๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํด์๋ ( as ) ์ต์
์ ์ฌ์ฉํ๋ค.
4. ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ์ฌ์ฉํ ์ ์๋ ๋ชจ๋์๋ ( Passport )๊ฐ ์๋ค.
5. ๋ก์ปฌ ๋ก๊ทธ์ธ์ด ์๋ ์นด์นด์ค ๋ก๊ทธ์ธ์ ํ๊ธฐ ์ํด์๋ ( passport-kakao ) ๋ชจ๋์ ์ฌ์ฉํ๋ค.
6. ์ด๋ฏธ์ง ์
๋ก๋๋ฅผ ์ํด์๋ ( multer ) ๋ชจ๋์ ์ฌ์ฉํ๋ค.
7. ํ๋ก์ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ํ๋ก์์ ํ๋ก์ ๋ชฉ๋ก์ ( req.user )์ ์ ์ฅํด์ผ ํ๋ค.
8. ๋ผ์ฐํฐ๋ฅผ ๊ตฌํํ ํ์๋ ๋ฐ๋์ ( app.js )์ ํด๋น ๋ผ์ฐํฐ๋ฅผ ์ถ๊ฐํด์ผ ํ๋ค.
1. ๋ก๊ทธ์ธ ๋ผ์ฐํฐ๋ฅผ ๊ตฌํํ๊ธฐ ์ , ์ ๊ทผ ๊ถํ์ ์ ์ดํ๋ ๋ฏธ๋ค์จ์ด๋ฅผ ์์ฑํ๋ ค ํ๋ค. ๋น์นธ์ ์ฑ์ฐ์์ค.
exports.isLoggedIn = (req, res, next) => {
if ( ) { // (1)
next();
} else {
res.status(403).send('๋ก๊ทธ์ธ ํ์');
}
};
exports.isNotLoggedIn = (req, res, next) => {
if ( ) { // (2)
next();
} else {
const message = encodeURIComponent('๋ก๊ทธ์ธํ ์ํ์
๋๋ค.');
res.redirect(`/?error=${message}`);
}
};
2. Passport ๋ชจ๋๋ก ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ณ ์ ํ๋ค. ๋ค์ passport/index.js ์ฝ๋์ ๋น์นธ์ ์ฑ์ฐ์์ค.
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = () => {
// (1)
});
// (2)
});
local();
kakao();
};
1.
(1) req.isAuthenticated()
(2) !req.isAuthenticated()
2.
(1) passport.serializeUser((user, done) => {
done(null, user.id);
(2) passport.deserializeUser((id, done) => {
User.findOne({ where: { id } })
.then(user => done(null, user))
.catch(err => done(err));
์ถ์ฒ : ์กฐํ์, ใ Node.js ๊ต๊ณผ์ ๊ฐ์ 3ํใ, ๊ธธ๋ฒ(2022)
Corner Node.js 1
Editor : ๋น ๋ค์ฝ์ฝ๋
[๋ ธ๋ 1ํ] 8์ฅ. ๋ชฝ๊ณ ๋๋น (0) | 2024.12.27 |
---|---|
[๋ ธ๋ 1ํ] 7์ฅ. MySQL (0) | 2024.11.29 |
[๋ ธ๋ 1ํ] 6์ฅ. ์ต์คํ๋ ์ค ์น ์๋ฒ ๋ง๋ค๊ธฐ (0) | 2024.11.22 |
[๋ ธ๋ 1ํ] 5์ฅ. ํจํค์ง ๋งค๋์ (0) | 2024.11.15 |
[๋ ธ๋ 1ํ] 4์ฅ. http ๋ชจ๋๋ก ์๋ฒ ๋ง๋ค๊ธฐ (0) | 2024.11.15 |