반응형
프로젝트 구조 갖추기
npm init
npm i sequelize mysql2 sequelize-cli
npx sequelize init
템플릿을 넣을 views
라우터를 넣을 routes
정적파일을 넣은 public
위 코드와 파일들을 생성하며 폴더 구조를 형성한다
routes/page.js
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;
get /profile, get /join, get / 으로 페이지 세 개로 구성했고
router.use로 템플릿 엔진에서 사용할 user, followingCount, followerCount, followingidList 변수를 res.locals로 설정했다.
res.locals로 설정하는 이유는 user와 followingcount, followercount, followingidlist 변수는 모든 템플릿 엔진에서 공통으로 사용되기 때문이다.
controllers/page.js
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,
});
};
이전과 다르게 controllers에서 따로 미들웨어를 다른 곳에서 불러와 사용한다.
일단은 코드를 관리하기 편하기 위해서라고 알고 있자
- 기본적인 프론트 구성을 다 하면 아래와 같이 잘 작동하는 것을 볼 수 있다.
데이터베이스 세팅하기
- MySQL과 시퀄라이즈로 데이터 베이스를 설정한다
- 로그인 기능이 있으므로 사용자 테이블이 필요하고, 게시글을 저장할 게시글 테이블도 필요하다. 해시태그를 사용하므로 해시태크 테이블도 필요하다
- models 폴더
- user.js: 사용자 정보를 저장하고, 로컬과 카카오 로그인 에 따른 설정이 있다
- post.js: 게시글 내용과 이미지 경로를 저장한다
- hashtsg.js: 태그 이름을 저장한다.
- index.js: 객체를 만들지만, 많아지는 모델을 고려해 새로운 구조를 통해 디렉토리에 있는 모든 파일들을 조회하도록 수정했다.
- associate 함수를 수정해 모델간의 관계를 정했다
- 같은 테이블 간에서도 관계를 정할 수 있다.
user.js
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',
});
}
post.js
static associate(db) {
db.Post.belongsTo(db.User);
db.Post.belongsToMany(db.Hashtag, {through: 'PostHashtag'});
}
hashtag.js
static associate(db) {
db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' });
}
};
"development": {
"username": "root",
"password": "비번",
"database": "nodebird",
"host": "127.0.0.1",
"dialect": "mysql"
}
으로 config.json을 수정하고 원래는 데이터 베이스를 따로 만들어야하지만
시퀄라이즈는 config.json을 읽어 데이터베이스를 생성해주는 기능이 있다.
npx sequelize db:create 를 쳐서 데이터베이스를 생성해보자
- 마지막으로 app.js를 수정해 모델과 서버를 연결해 데이터베이스 세팅을 완료했다.
Passprot 모듈로 로그인 구현하기
npm i passport passport-local passport-kakao bcrypt
를 입력해 passport 관련 패키지들을 설치하자
const passport = require('passport');
const passportConfig = require('./passport');
passportConfig(); // 패스포트 설정
app.use(passport.initialize());
app.use(passport.session());
app.js를 수정해 조금뒤 만들 passport 모듈과 연결해주자
passport/index.js
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();
};
- 로그인의 전체적인 과정
- /auto/login 라우터를 통해 로그인 요청이 들어옴
- 라우터에서 passport.authenticate 메서드 호출
- 로그인 전략(LocalStrategy) 수행
- 로그인 성공 시 사용자 정보 객체와 함께 req.login 호출
- req.login 메서드가 passport.seializeUser 호출
- req.session에 사용자 아이디만 저장해서 세션 생성
- express-session에 설정한 대로 브라우저에 connect.sid 세션 쿠키 전송
- 로그인 완료
로컬 로그인 구현
- 로컬 로그인이랑 다른 SNS 서비스를 통해 로그인하지 않고 자체적으로 회원가입 후 로그인하는 것
- passport 모듈은 이미 설치했으므로 로컬 로그인 전략만 세우면 된다. 회원가입, 로그인, 로그아웃 라우터를 먼저 만들어보자.
middlewares/index.js
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}`);
}
};
req 객체에 isAuthenticated 메서드를 추가하고, 로그인 중이면 true, 아니면 false이다.
- routes, controllers 디렉토리에 둘다 auth.js를 만들어 회원 가입, 로그인, 로그아웃을 할 수 있도록 작성했다.
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/kakaoStrategy
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);
}
}));
};
카카오 로그인 구현을 위해 카카오 로그인에 대한 전략을 구성한다
- 카카오 개발자 사이트에 들어가 API 키를 발급받아 .env 파일에 넣는다
- 여기에서 닉네임과 이메일 정보를 받기 위해 동의를 선택해야한다
- 하지만 이메일은 권한이 없어 동의를 얻지 못했다…
- 그래도 일단 뭐 로그인은 어찌저찌 구현이 됐다
multer 패키지로 이미지 업로드 구현하기
- 이미지 업로드를 위해 multer 모듈을 사용해 멀티 파트 형식의 이미지를 업로드 해보자
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);
}
};
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;
라우트와 컨트롤러에 post.js를 만들어 게시글을 업로드하는 방식을 구현했다.
또한 page.js를 수정해 조회할 때 게시글 작성자의 아이디와 닉네임을 join해서 제공하고, 게시글 순서를 정렬했다.
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);
}
}
프로젝트 마무리
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);
}
};
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;
팔로우 기능을 위해 라우트와 컨트롤러에 user.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);
}
};
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;
그리고 이제 모듈들을 다 만들었다면 제일 중요한 연결 차례이다
app.js를 수정해 라우트의 post.js와 user.js를 연결했다
const postRouter = require('./routes/post');
const userRouter = require('./routes/user');
app.use('/post', postRouter);
app.use('/user', userRouter);
- 터미널에서도 잘 작동하는 것을 볼 수 있다!!!
반응형
'WINK-(Web & App) > Express.js (Node.js) 스터디' 카테고리의 다른 글
[2024-2 Node.js 스터디] 류상우 #6주차 (0) | 2024.11.25 |
---|---|
[2024-2 Node.js 스터디] 김민재 #6주차 (0) | 2024.11.25 |
[2024-2 Node.js 스터디] 류상우 #5주차 (2) | 2024.11.18 |
[2024-2 Node.js 스터디] 류상우 #4주차 (1) | 2024.11.11 |
[2024-2 Node.js 스터디] 김민재 #4주차 (0) | 2024.11.11 |