다음 내용은 인프런에서 공부한 내용을 복습하는 차원에서 기록한 것입니다.
출처 : https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-js-%EA%B5%90%EA%B3%BC%EC%84%9C
const Sequelize = require('sequelize');
// const User = require('./user');
// const Post = require('./post');
// const Hashtag = require('./hashtag');
const fs = require('fs');
const path = require('path');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config.json')[env];
const db = {};
const sequelize = new Sequelize( // config에 있는 설정값들을 통해 시퀄라이즈 연결방법
config.database, config.username, config.password, config,
);
// DB에 config에서 가져온 설정값을 기반으로 시퀄라이즈 적용
db.sequelize = sequelize;
// 아래와 같이 직접 하나씩 구현도 가능
// db.User = User;
// db.Post = Post;
// db.Hashtag = Hashtag;
// User.initiate(sequelize);
// Post.initiate(sequelize);
// Hashtag.initiate(sequelize);
// User.associate(db);
// Post.associate(db);
// Hashtag.associate(db);
// 아래는 위와 동일한 실행을 하나 반복적인 내용을 자동화하기 위한 코드
const basename = path.basename(__filename);
fs.readdirSync(__dirname)
.filter(file => {
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
})
.forEach((file) => {
const model = require(path.join(__dirname, file));
db[model.name] = model;
model.initiate(sequelize);
});
Object.keys(db).forEach(modelName => {
console.log(modelName);
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
module.exports = db;
1대다: hasMany와 belongsTo
다대다: belongsToMany
/model/user.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: {
type: Sequelize.ENUM('local', 'kakao'),
allowNull: false,
defaultValue: 'local',
},
snsId: {
type: Sequelize.STRING(30),
allowNull: true
}
}, {
sequelize,
timestamps: true, //createdAt, updatedAt
underscored: false,
modelName: 'User', // 자바스크립트에서 사용하는 이름
tableName: 'users', // 데이터베이스에서 사용하는 테이블 이름
paranoid: true, // deletedAt 유저 삭제일 (soft delete)
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
})
}
// 테이블 관계
static associate(db) {
db.User.hasMany(db.Post);
// 연예인의 팔로워를 찾으려면 연예인의 id로 찾아야 함
db.User.belongsToMany(db.User, { // 팔로워(유명 연예인의 팬)
foreignKey: 'followingId',
as: 'Followers',
through: 'Follow'
});
// 내가 팔로잉을 하는 사람을 찾으려면 나의 id로 찾아야 함
db.User.belongsToMany(db.User, { // 팔로잉 (유명 연예인)
foreignKey: 'followerId',
as: 'Followings',
through: 'Follow'
});
}
}
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,
paranoid: true,
modelName: 'Post',
tableName: 'posts',
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,
paranoid: true,
modelName: 'Hashtag',
tableName: 'hashtags',
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci'
})
}
// 테이블 관계
static associate(db) {
db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' });
}
}
module.exports = Hashtag;
...
const nunjucks = require('nunjucks');
const { sequelize } = require('./models');
...
// 넌적스 템플릿엔진을 사용할 때 view폴더를 사용
// 즉, render를 사용할 때 views폴더에 있는 html 파일을 사용한다는 의미
nunjucks.configure('views', {
express: app,
watch: true,
});
// 실제로 sequelize를 데이터베이스에 연결하는 부분
sequelize.sync()
.then(() => {
console.log('데이터베이스 연결 성공');
})
.catch((err) => {
console.error(err);
})
...
...
const path = require('path');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const passport = require('passport');
const { sequelize } = require('./models');
dotenv.config(); // process.env
const pageRouter = require('./routes/page');
const authRouter = require('./routes/auth');
const passportConfig = require('./passport');
const app = express();
passportConfig();
app.set('port', process.env.PORT|| 8001);
app.set('view engine', 'html'); //템플릿엔진으로 읽을 때 html 파일을 사용
...
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true, // 자바스크립트 접근 못하게 만들때
secure: false, // https 사용할 때 true
}
}));
//passport 미들웨어의 위치는 반드시 express-session 밑에 선언해야 함
// passport연결시 req.user, req.login, req.logout가 생성됨
// (즉, passport가 로그인시 필요한 것들을 만들어 줌)
app.use(passport.initialize());
app.use(passport.session());
// connect.sid라는 이름으로 세션 쿠키가 브라우저로 전송 (쿠키 로그인을 도와주는 함수)
// 브라우저에 connect.sid=123876128942와 같은 쿠키가 저장됨
// 이후에는 쿠키와 함께 서버로 보내짐 (이 때 cookie-parser가 분석해서 보냄)
// passport가 cookie-parser에 의해 만들어진 객체를 통해 /passport/index.js에서 세션쿠키를 통해 유저아이디를 찾음
// passport.session 미들웨어가 passport.deserializeUser 메서드를 호출
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = () => {
// controller/auth.js에 있는 req.login에 의해서 실행됨
passport.serializeUser((user, done) => { // user === exUser
done(null, user.id); //user의 id만 추출 -> req.session에 사용자 아이디를 세션에 저장
});
// 세션에 저장할 때 { 세션쿠키: 유저아이디 } -> 메모리에 저장
// user정보를 통째로 사용하면 메모리가 너무 많이 사용되기에 유저아이디만 추출해서 사용
// 하지만 메모리에 로그인정보를 저장한다는 것 자체가 문제가 되지만
// 이에 대한 해결법은 추후에 공부할 예정
// cookie-parser에 의해 객체가 된 세션 쿠키 값(123876128942)를 기준으로 유저아이디를 찾음
// passport.session 미들웨어가 passport.deserializeUser 메서드를 호출
// 결과적으로 req.user를 만드는 곳임
passport.deserializeUser((id, done) => { // 세션쿠키 값을 통해 얻은 유저 아이디를 가지고 User 정보를 복원시킴
User.findOne({ where: { id } })
.then((user) => done(null, user)) // 그 복원된(조회된) 정보가 req.user가 됨
.catch(err => done(err));
});
local();
};
exports.isLoggedIn = (req, res, next) => {
if (req.isAuthenticated()) { // 패스포트 통해서 로그인 했는지
next();
} else {
res.status(403).send('로그인 필요');
}
};
exports.isNotLoggedIn = (req, res, next) => {
if (!req.isAuthenticated()) {
next();
} else {
const message = encodeURIComponent('로그인한 상태입니다.');
res.redirect(`/?error=${message}`);
}
};
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);
module.exports = router;
const User = require('../models/user');
const bcrypt = require('bcrypt');
const passport = require('passport');
exports.join = async (req, res, next) => {
const { nick, email, password } = req.body; // 구조분해 할당에 의해 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(err) {
console.log(err);
next(err);
}
};
// POST /auth/login
exports.login = (req, res, next) => {
// login 호출 시 passport.authenticate가 실행되는데
// 이 때 'local' 에 의해서 localStrategy가 실행됨
// localStrategy에 의해 반환된 done을 가지고 콜백함수 실행
// done(서버실패, 성공유저, 로직실패)이 (authError, user, info) => {}을 호출시킴
// done에 의해서 user === exUser
passport.authenticate('local', (authError, user, info) => {
if (authError) { // 서버 실패한 경우
console.error(authError);
return next(authError);
}
if (!user) { // 로직 실패한 경우
return res.redirect(`/?loginError=${info.message}`);
}
// req.login에 의해 /passport/inde.js 가 실행됨
return req.login(user, (loginError) => { // 로그인 성공한 경우
if (loginError) {
console.error(loginError);
return next(loginError);
}
return res.redirect('/');
})
})(req, res, next);
};
const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
const bcrypt = require('bcrypt');
const User = require('../models/user');
module.exports = () => {
passport.use(new LocalStrategy({
usernameField: 'email', // req.body.email을 읽어서 사용
passwordField: 'password', // req.body.password을 읽어서 사용
passReqToCallback: false
}, async (email, password, done) => { // done(서버실패, 성공유저정보, 로직실패)
// 로그인 시켜야 하는지 말아야 하는지 판단하는 로직
try {
// email을 통해 User 모델에서 사용자가 있는지 확인
const exUser = await User.findOne({ where: { email } });
if (exUser) {
const result = await bcrypt.compare(password, exUser.password) // 사용자가 입력한 비번과 DB의 비번을 비교
if (result) {
done(null, exUser);
} else {
done(null, false, { message: '비밀번호가 일치하기 않습니다.' });
}
} else {
done(null, false, { message: '가입되지 않은 회원입니다.' });
}
} catch(error) {
console.error(error);
done(error);
}
}));
};
const passport = require("passport");
const { Strategy: KakaoStrategy } = require('passport-kakao');
const User = require("../models/user");
module.exports = () => {
passport.use(new KakaoStrategy({
clientID: process.env.KAKAO_ID,
callbackURL: '/auth/kakao/callback',
// accessToken, refreshToken를 받아오긴 하지만 여기서는 사용하지 않음
// accessToken, refreshToken는 카카오API를 사용할 경우에 사용함
}, async (accessToken, refreshToken, profile, done) => {
console.log('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',
});
console.log(exUser);
done(null, newUser);
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
const express = require('express');
const passport = require('passport');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const { join, login, logout } = require('../controllers/auth');
const router = express.Router();
...
// GET /auth/kakao
router.get('/kakao', passport.authenticate('kakao')); // 카카오톡 로그인 화면으로 redirect
// /auth/kakao -> 카카오톡로그인화면 -> /auth/kakao/callback
// GET /auth/kakao/callback
router.get('/kakao/callback', passport.authenticate('kakao', {
failureRedirect: '/?loginError=카카오로그인 실패',
}), (req, res) => {
res.redirect('/');
});
module.exports = router;
const express = require('express');
const router = express.Router();
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const fs = require('fs');
const multer = require('multer');
const path = require('path');
const { afterUploadImage, uploadPost } = require('../controllers/post');
try {
fs.readdirSync('uploads');
} catch (error) {
fs.mkdirSync('uploads');
}
// 이미지를 올리기 위한 multer
const upload = multer({
storage: multer.diskStorage({
destination(req, file, cb) {
console.log(file);
cb(null, 'uploads/');
},
filename(req, file, cb) {
console.log(file);
const ext = path.extname(file.originalname); // 확장자를 추출
cb(null, path.basename(file.originalname, ext) + Date.now() + ext); // 이미지.png -> 이미지12039123.png와 같이 변경(중복 제거를 위해)
}
}),
limits: { fileSize: 20 * 1024 * 1024 },
});
// multer 사용 시 FormData객체에서 사용하는 formData.append('img', this.files[0])의 'img'변수와 동일하게 사용해야 함
router.post('/img', isLoggedIn,upload.single('img'), afterUploadImage);
const upload2 = multer(); // 게시글을 올리기 위한 multer
router.post('/', isLoggedIn, upload2.none(), uploadPost);
module.exports = router;
const Post = require('../models/post');
const Hashtag = require('../models/hashtag');
// 업로드한 이미지의 url를 프론트로 보내는 곳 (미리보기 때문에)
exports.afterUploadImage = (req, res) => {
console.log(req.file);
res.json({ url: `/img/${req.file.filename}` });
};
// 실제 게시글이 업로드 되는 곳
exports.uploadPost = async (req, res, next) => {
console.log(req.body); // req.body.content와 req.body.url를 가져올 수 있음
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) {
// Post와 Hashtag 사이의 다대다 관계 생성
const result = await Promise.all(hashtags.map((tag) => {
return Hashtag.findOrCreate({
where: { title: tag.slice(1).toLowerCase() }
});
}));
console.log('result: ', result);
// Hashtag.findOrCreate()의 반환값으로 [instance, created] 형태의 배열을 반환
// instance에는 값이, created에는 새로운 해시태그가 생성되었는지 여부가 들어 있음
await post.addHashtags(result.map(r => r[0]));
}
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
};
// 라우터 -> 컨트롤러(요청, 응답을 알고 있음, req랑 res) -> 서비스(요청, 응답을 모름)
const Post = require('../models/post');
const User = require('../models/user');
const Hashtag = require('../models/hashtag');
...
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 (error) {
console.error(error);
next(error);
}
};
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, attributes: ['id', 'nick'] }],
order: [['createdAt', 'DESC']]
});
}
res.render('main', {
title: `${query} | NodeBird`,
twits: posts,
});
} catch (error) {
console.error(error);
next(error);
}
};
/사용자아이디/follow
사용자 아이디는 req.params.id로 접근
user.addFollowing(사용자아이디)로 팔로잉하는 사람 추가
/routes/user.js
const express = require('express');
const router = express.Router();
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const { follow, unfollow } = require('../controllers/user');
router.post('/:id/follow', isLoggedIn, follow);
module.exports = router;
const User = require('../models/user');
exports.follow = async (req, res, next) => {
// req.user.id와 req.params.id가 필요
// 현재 유저의 id와 팔로우하는 사람의 id가 필요
try {
const user = await User.findOne({ where: { id: req.user.id } });
if (user) {
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를 만드는 곳이 deserializeUser이기 때문에 수정 필요
기존에는 그냥 유저 id를 통해서 유저 정보만 가져옴
req.user.Followers로 팔로워 접근 가능
req.user.Followings로 팔로잉 접근
단, 목록이 유출되면 안 되므로 팔로워/팔로잉 숫자만 프런트로 전달
요약하면, as: 'Followers'와 as: 'Followings'는 User 모델이 Follow 테이블을 통해 자기 자신과 맺고 있는 팔로우 관계를 나타내며, 이 관계를 통해 팔로워와 팔로잉 사용자의 정보를 함께 조회할 수 있음
/passport/index.js
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');
module.exports = () => {
...
passport.deserializeUser((id, done) => { // 세션쿠키 값을 통해 얻은 유저 아이디를 가지고 User 정보를 복원시킴
User.findOne({
where: { id },
// as: 'Followers'와 as: 'Followings'는 User 모델이 Follow 테이블을 통해
// 자기 자신과 맺고 있는 팔로우 관계를 나타내며, 이 관계를 통해 팔로워와 팔로잉 사용자의 정보를 함께 조회할 수 있음
include: [
{
model: User,
attributes: ['id', 'nick'],
as: 'Followers',
},
{
model: User,
attributes: ['id', 'nick'],
as: 'Followings',
},
]
})
.then((user) => done(null, user)) // 그 복원된(조회된) 정보가 req.user가 됨
.catch(err => done(err));
});
local();
kakao();
};
const express = require('express');
const router = express.Router();
const { renderJoin, renderMain, renderProfile, renderHashtag } = require('../controllers/page');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
// 공통적으로 사용하기 원하는 변수들을 res.locals로 설정
// 로그인 과정이 다 끝나면 여기서 라우터들이 동작함
router.use((req, res, next) => {
res.locals.user = req.user; // 로그인되어 있지 않으면 req.user는 null값을 가짐
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); // hashtag?hashtag=node
module.exports = router;
해시태그를 먼저 찾고(hashtag)
hashtag.getPosts로 해시태그와 관련된 게시글을 모두 찾음
찾으면서 include로 게시글 작성자 모델도 같이 가져옴
/routes/page.js
const express = require('express');
const router = express.Router();
const { renderJoin, renderMain, renderProfile, renderHashtag } = require('../controllers/page');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
...
router.get('/hashtag', renderHashtag); // hashtag?hashtag=node
module.exports = router;
// 라우터 -> 컨트롤러(요청, 응답을 알고 있음, req랑 res) -> 서비스(요청, 응답을 모름)
const Post = require('../models/post');
const User = require('../models/user');
const Hashtag = require('../models/hashtag');
...
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({
// posts에게 User의 id와 nick값을 사용할 수 있게 해주는 옵션
include: [{ model: User, attributes: ['id', 'nick'] }],
order: [['createdAt', 'DESC']]
});
}
res.render('main', {
title: `${query} | NodeBird`,
twits: posts,
});
} catch (error) {
console.error(error);
next(error);
}
};
...
app.use(express.static(path.join(__dirname, 'public'))); // 프론트에서 public 폴더를 자유롭게 접근가능하게 허용
app.use('/img', express.static(path.join(__dirname, 'uploads'))); // img경로를 통해서 프론트가 uploads 폴더에 접근가능하게 허용
...
app.use('/', pageRouter);
app.use('/auth', authRouter);
app.use('/post', postRouter);
app.use('/user', userRouter);
...
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중...');
});