로그인, 이미지 업로드, 게시글 작성, 해시태그 검색, 팔로잉 등의 기능이 있는 SNS 서비스 만들기
9.1. 프로젝트 구조 갖추기
1. nodebird라는 폴더를 만들고 package.json 생성
* 항상 package.json을 제일 먼저 생성한다.
* npm init 명령 또는 직접 생성
{
"name" : "nodebird",
"version" : "0.0.1",
"description" : "익스프레스로 만드는 SNS 서비스",
"main" : "app.js",
"scripts" : {
"start" : "nodemon app"
},
"author" : "Inyeong",
"license" : "MIT"
}
2. 시퀄라이즈 설치
사용자와 게시물 간, 게시물과 해시태그 간의 관계가 중요하므로 관계형 데이터베이스인 MySQL을 사용한다.
npm i sequelize mysql2 sequelize-cli : node_modules 폴더와 package-lock.json 생성
npx sequelize init : config, migrations, models, seeders 폴더 생성
npx 명령어는 전역 설치를 피하기 위해서
3. 폴더 생성
템플릿 파일을 넣을 views 폴더
라우터를 넣을 routes 폴더
정적 파일을 넣을 public 폴더
passport 패키지를 위한 passport 폴더 생성
익스프레스 서버 코드가 담길 app.js와 설정값들을 담을 .env 파일을 nodebird 폴더 안에 생성
4. 필요한 npm 패키지 설치하고 app.js 작성
템플릿 엔진은 넌적스 사용
npm i express cookie-parser express-session morgan multer dotenv nunjucks
npm i -D nodemon
5. app.js와 .env 파일 작성
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);
// 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');
});
// 앱을 8001번 포트에 연결
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
COOKIE_SECRET = cookiesecret
6. 라우터와 템플릿 엔진 만들기
routes/page.js
const express = require('express');
const {renderProfile, renderJoin, renderMain} = require('../controllers/page');
const router = express.Router();
// 라우터용 미들웨어, 모든 템플릿 엔진에서 공통으로 사용하기 때문에 변수를 res.lacals로 설정
router.use((req, res, next) => {
res.locals.user = null;
res.locals.followerCount = 0;
res.locals.followingCount = 0;
res.locals.followingIdList = [];
next();
});
// 페이지 3개로 구성
router.get('/profile', renderProfile);
router.get('/join', renderJoin);
router.get('/', renderMain);
module.exports = router;
controllers/page.js
renderProfile, renderJoin, renderMain과 괕이 라우터 마지막에 위치해 클라이언트에 응답을 보내는 미들웨어를 컨트롤러라고 한다. controllers 폴더를 만들고 그 안에 page.js 생성
컨트롤러는 res.send, res.json, res.redirect, res.render 등이 존재하는 미들웨어
코드를 편하게 관리하기 위해 컨트롤러를 따로 분리한다.
// 내 정보 페이지를 화면에 렌더링
exports.renderProfile = (req, res) => {
res.render('profile', {title: '내 정보 - NodeBird'});
};
// 회원 가입 페이지를 화면에 렌더링
exports.renderJoin = (req, res) => {
res.render('join', {title: '회원 가입 - NodeBird'});
};
// 메인 페이지를 렌더링하면서 넌적스에 twits를 전달
exports.renderMain = (req, res, next) => {
const twits = [];
res.render('main', {
title: 'NodeBird',
twits,
});
};
클라이언트 코드 작성
npm start로 서버 실행하고 localhost:8001에 접속
9.2. 데이터베이스 세팅하기
MySQL과 시퀄라이즈로 데이터베이스 설정
로그인 기능이 있으므로 사용자 테이블, 게시글을 저장할 테이블, 해시태그를 사용하므로 해시태그 테이블도 필요하다.
models/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,
},
// SNS 로그인을 했을 경우 provider와 snsId를 저장
provider: {
// ENUM : 넣을 수 있는 값을 제한하는 데이터 형식
type: Sequelize.ENUM('local', 'kakao'), // 이메일, 비밀번호 로그인이나 카카오 로그인 둘 중 하나만 선택 가능
allowNull: false,
defaultVaule: 'local', // 기본 이메일, 비밀번호 로그인
},
snsId: {
type: Sequelize.STRING(30),
allowNull: true,
},
}, {
// 테이블 옵션 timestamps, paranoid -> createdAt, updatedAt, deletedAt
sequelize,
timestapms: true,
unserscored: false,
modelName: 'User',
tableName: 'users',
paranoid: true,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {}
};
module.exports = User;
models/post.js
/* 게시글 내용과 이미지 경로를 저장하는 게시글 모델 */
// 게시글 등록자의 아이디를 담은 컬럼은 관계 설정 시 시퀄라이즈가 알아서 생성
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,
timestapms: true,
unserscored: false,
modelName: 'Post',
tableName: 'posts',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {}
};
module.exports = Post;
models/hashtag.js
/* 태그 이름을 저장하는 해시태그 모델 */
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){}
};
module.exports = Hashtag;
생성한 모델들을 시퀄라이즈에 등록
models/index.js
/* 생성한 모델들을 시퀄라이즈에 등록 */
// 1. 각각의 모델들을 시퀄라이즈 객체에 연결하는 방법
const Sequelize = require('sequelize');
const User = require('./user');
const Post = require('./post');
const Hashtag = require('./hashtag');
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;
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);
module.exports = db;
/* 생성한 모델들을 시퀄라이즈에 등록 */
// 2. 모델의 개수가 많은 경우 테이블 자동으로 생성되는 방식 이용
// 미완성인 모델도 읽어들이므로, 미완성 테이블이 생길 수 있음에 주의
// models 폴더에 모델이 아닌 다른 파일 넣지 않도록 주의
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;
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,
},
// SNS 로그인을 했을 경우 provider와 snsId를 저장
provider: {
// ENUM : 넣을 수 있는 값을 제한하는 데이터 형식
type: Sequelize.ENUM('local', 'kakao'), // 이메일, 비밀번호 로그인이나 카카오 로그인 둘 중 하나만 선택 가능
allowNull: false,
defaultVaule: 'local', // 기본 이메일, 비밀번호 로그인
},
snsId: {
type: Sequelize.STRING(30),
allowNull: true,
},
}, {
// 테이블 옵션 timestamps, paranoid -> createdAt, updatedAt, deletedAt
sequelize,
timestapms: true,
unserscored: false,
modelName: 'User',
tableName: 'users',
paranoid: true,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) { // 각 모델 간의 관계 정의
db.User.hasMany(db.Post); // User 모델과 Post 모델은 1:N 관계
db.User.belongsToMany(db.User, { // User 모델 간 N:M 관계 - 팔로잉 기능
foreignKey: 'followingId', // 컬럼 이름 설정하여 두 사용자 아이디 구별
as: 'Followers', // as는 foreignKey와 반대되는 모델
through: 'Follow', // 모델 이름 지정
});
db.User.belongsToMany(db.User, {
foreignKey: 'followerId',
as: 'Followings',
through: 'Follow',
});
}
};
module.exports = User;
post.js
/* 게시글 내용과 이미지 경로를 저장하는 게시글 모델 */
// 게시글 등록자의 아이디를 담은 컬럼은 관계 설정 시 시퀄라이즈가 알아서 생성
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,
timestapms: true,
unserscored: false,
modelName: 'Post',
tableName: 'posts',
paranoid: false,
charset: 'utf8mb4',
collate: 'utf8mb4_general_ci',
});
}
static associate(db) {
db.Post.belongsTo(db.User); // User 모델과 Post 모델은 1:N 관계
db.Post.belongsToMany(db.Hashtag, {through: 'PostHashtag'}); // Post 모델과 Hahtag 모델은 N:M 관계
}
};
module.exports = Post;
hashtag.js
/* 태그 이름을 저장하는 해시태그 모델 */
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'}); // Hashtag 모델과 Post 모델은 N:M 관계
}
};
module.exports = Hashtag;
NodeBird 모델은 User, Hashtag, Post와 시퀄라이즈가 관계를 파악해 생성한 PostHashtag, Follow 5개
자동으로 생성된 모델도 db.sequelize.models.PostHashtag, db.sequelize.models.Follow로 접근 가능
생성한 모델을 데이터베이스 및 서버와 연결
데이터베이스 nodebird 생성
시퀄라이즈는 config.json을 읽어 데이터베이스 생성해주는 기능 있다.
{
"development": {
"username": "root",
"password": "[root 비밀번호]",
"database": "nodebird",
"host": "127.0.0.1",
"dialect": "mysql"
}
}
config.json의 password와 데이터베이스 이름을 수정하고 콘솔에 npx sequelize db:create 명령어 입력
데이터베이스 생성 후 모델을 서버와 연결
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');
const { sequelize } = require('./models');
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,
});
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(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);
// 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');
});
// 앱을 8001번 포트에 연결
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
npm start로 서버 실행, 시퀄라이즈는 테이블 생성 쿼리문에 IF NOT EXISTS를 넣어주므로 테이블이 없을 때 테이블을 자동으로 생성. 데이터베이스 세팅이 완료되어 사용자 정보 저장할 수 있다.
9.3. Passport 모듈로 로그인 구현하기
회원 가입과 로그인 기능을 구현할 수 있게 하는 Passport 모듈
기존 SNS 서비스 계정으로 로그인하는 것도 구현 가능하다.
Passport 관련 패키지 설치
npm i passport passport-local passport-kakao bcrypt
Passport 모듈을 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');
const passport = require('passport'); // 추가
dotenv.config();
const pageRouter = require('./routes/page');
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,
});
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(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);
// 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');
});
// 앱을 8001번 포트에 연결
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
passport/index.js
const passport = require('passport');
const local = require('/localStrategy');
const kakao = rquire('/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();
};
9.4. multer 패키지로 이미지 업로드 구현하기
9.5. 프로젝트 마무리하기
'Programming > Node.js' 카테고리의 다른 글
| [Node.js] 11. 노드 서비스 테스트하기 (0) | 2025.01.06 |
|---|---|
| [Node.js] 10. 웹 API 서버 만들기 (0) | 2025.01.01 |
| [Node.js] 6. 익스프레스 웹 서버 만들기 (0) | 2024.11.19 |
| [Node.js] 3. 노드 기능 알아보기 (1) (1) | 2024.10.09 |
| [Node.js] 2. JavaScript 정리 (0) | 2024.10.09 |