
mkdir nodebird
cd nodebird
npm init -ynpm i express cookie-parser express-session morgan multer dotenv nunjucksnpm i sequelize mysql2 sequelize-clinpm i passport passport-local passport-kakao bcryptnpm i -D nodemon
다음과 같이 폴더들을 미리 만들어둡니다.
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');
const passport = require('passport');
dotenv.config();
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');
const app = express();
passportConfig(); // 패스포트 설정
app.set('port', process.env.PORT || 8001);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
// DB 연결 (sync)
sequelize.sync({ force: false })
.then(() => {
console.log('데이터베이스 연결 성공');
})
.catch((err) => {
console.error(err);
});
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(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(passport.initialize());
app.use(passport.session());
// 라우터 연결
app.use('/', pageRouter);
app.use('/auth', authRouter);
app.use('/post', postRouter);
app.use('/user', userRouter);
// 404 처리 미들웨어
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'), '번 포트에서 대기 중');
});
그 외로 기본적인 라우터와 템플릿 엔진을 만들어보면..
=> 다 설정 후 서버 실행
npm start
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,
paranoid: true,
modelName: 'User',
tableName: 'users',
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,
paranoid: false,
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;
```
### 3-3. Hashtag 모델 (models/hashtag.js)
태그 검색을 위해 별도로 저장합니다.
> **models/hashtag.js**
```javascript
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,
paranoid: false,
modelName: 'Hashtag',
tableName: 'hashtags',
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {
db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' });
}
};
module.exports = Hashtag;
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,
paranoid: false,
modelName: 'Hashtag',
tableName: 'hashtags',
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {}
};
module.exports = Hashtag;
index.js
로그인 인증을 담당할 Passport 설정을 진행합니다.
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); // 세션에 user.id만 저장
});
passport.deserializeUser((id, done) => {
User.findOne({where: { id })
.then(user => done(null, user)) // req.user에 저장
.catch(err => done(err));
});
local();
kakao();
};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);
}
}));
};
카카오 로그인
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 && profile._json.kakao_account && profile._json.kakao_account.email,
nick: profile.displayName,
snsId: profile.id,
provider: 'kakao',
});
done(null, newUser);
}
} catch (error) {
console.error(error);
done(error);
}
}));
};
npm i multerconst 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 폴더가 없어 생성합니다.');
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 },
});
// 이미지 업로드 라우터
router.post('/img', isLoggedIn, upload.single('img'), afterUploadImage);
// 게시글 업로드 라우터
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), uploadPost);
module.exports = router;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 express = require('express');
const { isLoggedIn } = require('../middlewares');
const { follow } = require('../controllers/user');
const router = express.Router();
router.post('/:id/follow', isLoggedIn, follow);
module.exports = router;const User = require('../models/user');
exports.follow = async (req, res, next) => {
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);
}
};
1. 이 메서드는 현재 로그인한 사용자가 다른 사용자를 팔로우하도록 관계를 설정하는 역할을 한다. 빈칸에 들어갈 시퀄라이즈 메서드는?
// controllers/user.js
const user = await User.findOne({ where: { id: req.user.id } });
if (user) {
// 시퀄라이즈가 N:M 관계 설정을 위해 자동으로 생성해준 메서드 사용
await user.( 1 )(parseInt(req.params.id, 10));
res.send('success');
}
2. 다음은 passport/index.js의 deserializeUser 부분입니다. 매 요청마다 사용자 정보를 DB에서 불러올 때, 팔로워와 팔로잉 목록까지 함께 불러오기 위해 ( 1 )에 들어갈 옵션 코드를 작성하세요. (힌트: User 모델에서 정의한 as 속성을 사용해야 합니다.)
// passport/index.js
passport.deserializeUser((id, done) => {
User.findOne({
where: { id },
// 비밀번호 조회 방지(attributes) 및 관계 모델 연결
( 1 )
})
.then(user => done(null, user))
.catch(err => done(err));
});
<빈칸QUIZ 답>
1. dotenv
2. bcyrpt
3. serializeUser
4. deserializeUser
5. multer
<코드작성QUIZ 답>
1. addFollowing
2.
include: [{
model: User,
attributes: ['id', 'nick'],
as: 'Followers',
}, {
model: User,
attributes: ['id', 'nick'],
as: 'Followings',
}],
출처 : 조현영, 『 Node.js 교과서 개정 3판』, 길벗(2022)
실습 코드: 덕성여대 교보문고 Node.js교과서
Corner Node.js 2
Editor Yeonyeon
| [Node.js 2팀] 10장 웹 API 서버 만들기 (1) | 2025.12.19 |
|---|---|
| [Node.js 2팀] 8장. 몽고디비 (0) | 2025.11.21 |
| [Node.js 2팀] 7장. MySQL (1) | 2025.11.14 |
| [Node.js 2팀] 5장. 패키지 매니저~ 6장. 익스프레스 웹 서버 만들기 (0) | 2025.11.07 |
| [Node.js 2팀] 3장. 노드 기능 알아보기 ~ 4장. http 모듈로 서버 만들기 (0) | 2025.10.31 |