๐Ÿ”Ž์ต์Šคํ”„๋ ˆ์Šค๋กœ SNS ์„œ๋น„์Šค ๋งŒ๋“ค๊ธฐ

์„œ๊ฐ€ํฌยท2021๋…„ 11์›” 15์ผ
0

Node.js

๋ชฉ๋ก ๋ณด๊ธฐ
8/15
post-thumbnail

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๊ฐ–์ถ”๊ธฐ

1. NodeBird SNS ์„œ๋น„์Šค

๊ธฐ๋Šฅ: ๋กœ๊ทธ์ธ, ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ, ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ, ํ•ด์‹œํƒœ๊ทธ ๊ฒ€์ƒ‰, ํŒ”๋กœ์ž‰

  • express-generator ๋Œ€์‹  ์ง์ ‘ ๊ตฌ์กฐ๋ฅผ ๊ฐ–์ถค
  • ํ”„๋ŸฐํŠธ์—”๋“œ ์ฝ”๋“œ๋ณด๋‹ค ๋…ธ๋“œ ๋ผ์šฐํ„ฐ ์ค‘์‹ฌ์œผ๋กœ ๋ณผ ๊ฒƒ
  • ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค MySQL ์„ ํƒ

2. ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ํ•˜๊ธฐ

nodebird ํด๋”๋ฅผ ๋งŒ๋“ค๊ณ  package.json ํŒŒ์ผ ์ƒ์„ฑ

  • ๋…ธ๋“œ ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ๋ณธ

๐Ÿ”ปpackage.json

{
  "name": "nodebird",
  "version": "0.0.1",
  "description": "์ต์Šคํ”„๋ ˆ์Šค๋กœ ๋งŒ๋“œ๋Š” SNS ์„œ๋น„์Šค",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "seokahi",
  "license": "MIT",
  "dependencies": {
    "cookie-parser": "^1.4.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "express-session": "^1.17.1",
    "morgan": "^1.10.0",
    "mysql2": "^2.1.0",
    "nunjucks": "^3.2.1",
    "sequelize": "^5.21.6",
    "sequelize-cli": "^5.5.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.3"
  }
}

์‹œํ€„๋ผ์ด์ฆˆ ํด๋” ๊ตฌ์กฐ ์ƒ์„ฑ

npm i sequelize mysql2 sequelize-cli
npx sequelize init

3. ํด๋” ๊ตฌ์กฐ ์„ค์ •

views(ํ…œํ”Œ๋ฆฟ ์—”์ง„), routes(๋ผ์šฐํ„ฐ), public(์ •์  ํŒŒ์ผ), passport(ํŒจ์ŠคํฌํŠธ) ํด๋” ์ƒ์„ฑ

  • app.js์™€ .env ํŒŒ์ผ๋„ ์ƒ์„ฑ

4. ํŒจํ‚ค์ง€ ์„ค์น˜์™€ nodemon

npm ํŒจํ‚ค์ง€ ์„ค์น˜ ํ›„ nodemon๋„ ์„ค์น˜

  • nodemon์€ ์„œ๋ฒ„ ์ฝ”๋“œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ์ž๋™์œผ๋กœ ์„œ๋ฒ„๋ฅผ ์žฌ์‹œ์ž‘ํ•ด์คŒ
  • nodemon์€ ์ฝ˜์†” ๋ช…๋ น์–ด์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ธ€๋กœ๋ฒŒ๋กœ ์„ค์น˜

npm i express cookie-parser express-session morgan multer dotenv nunjucks
npm i -D nodemeon

5. app.js

๋…ธ๋“œ ์„œ๋ฒ„์˜ ํ•ต์‹ฌ์ธ app.js ํŒŒ์ผ ์ž‘์„ฑ

๐Ÿ”ป.env

COOKIE_SECRET=nodebirdsecret

๐Ÿ”ป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);
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;
  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'), '๋ฒˆ ํฌํŠธ์—์„œ ๋Œ€๊ธฐ์ค‘');
});

6. ๋ผ์šฐํ„ฐ ์ƒ์„ฑ

์†Œ์Šค์ฝ”๋“œ ์ฐธ๊ณ 

  • routes/page.js: ํ…œํ”Œ๋ฆฟ ์—”์ง„์„ ๋ Œ๋”๋งํ•˜๋Š” ๋ผ์šฐํ„ฐ
  • views/layout.html: ํ”„๋ก ํŠธ ์—”๋“œ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ(๋กœ๊ทธ์ธ/์œ ์ € ์ •๋ณด ํ™”๋ฉด)
  • views/main.html: ๋ฉ”์ธ ํ™”๋ฉด(๊ฒŒ์‹œ๊ธ€๋“ค์ด ๋ณด์ž„)
  • views/profile.html: ํ”„๋กœํ•„ ํ™”๋ฉด(ํŒ”๋กœ์ž‰ ๊ด€๊ณ„๊ฐ€ ๋ณด์ž„)
  • views/error.html: ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์—๋Ÿฌ๊ฐ€ ํ‘œ์‹œ๋  ํ™”๋ฉด
  • public/main.css: ํ™”๋ฉด CSS

npm start๋กœ ์„œ๋ฒ„ ์‹คํ–‰ ํ›„ http://localhost:8001 ์ ‘์†

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ธํŒ…ํ•˜๊ธฐ

1. ๋ชจ๋ธ ์ƒ์„ฑ

์†Œ์Šค์ฝ”๋“œ ์ฐธ๊ณ 

  • models/user.js: ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ”๊ณผ ์—ฐ๊ฒฐ๋จ
    provider: ์นด์นด์˜ค ๋กœ๊ทธ์ธ์ธ ๊ฒฝ์šฐ kakao, ๋กœ์ปฌ ๋กœ๊ทธ์ธ(์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ)์ธ ๊ฒฝ์šฐ local
    snsId: ์นด์นด์˜ค ๋กœ๊ทธ์ธ์ธ ๊ฒฝ์šฐ ์ฃผ์–ด์ง€๋Š” id
  • models/post.js: ๊ฒŒ์‹œ๊ธ€ ๋‚ด์šฉ๊ณผ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋ฅผ ์ €์žฅ(์ด๋ฏธ์ง€๋Š” ํŒŒ์ผ๋กœ ์ €์žฅ)
  • models/hashtag.js: ํ•ด์‹œํƒœ๊ทธ ์ด๋ฆ„์„ ์ €์žฅ(๋‚˜์ค‘์— ํƒœ๊ทธ๋กœ ๊ฒ€์ƒ‰ํ•˜๊ธฐ ์œ„ํ•ด์„œ)

2. models/index.js

models/index.js
์‹œํ€„๋ผ์ด์ฆˆ๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์ฃผ๋Š” ์ฝ”๋“œ ๋Œ€์‹  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝ

  • ๋ชจ๋ธ๋“ค์„ ๋ถˆ๋Ÿฌ์˜ด(require)
  • ๋ชจ๋ธ ๊ฐ„ ๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๊ด€๊ณ„ ์„ค์ •
  • User(1):Post(๋‹ค)
  • Post(๋‹ค):Hashtag(๋‹ค)
  • User(๋‹ค):User(๋‹ค)

๐Ÿ”ปmodels/index.js

const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];
const User = require('./user');
const Post = require('./post');
const Hashtag = require('./hashtag');

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.init(sequelize);
Post.init(sequelize);
Hashtag.init(sequelize);

User.associate(db);
Post.associate(db);
Hashtag.associate(db);

module.exports = db;

๐Ÿ”ปmodels/user.js

const Sequelize = require('sequelize');

module.exports = class User extends Sequelize.Model {
  static init(sequelize) {
    return super.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.STRING(10),
        allowNull: false,
        defaultValue: 'local',
      },
      snsId: {
        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) {
    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',
    });
  }
};

3. associate ์ž‘์„ฑํ•˜๊ธฐ

๋ชจ๋ธ๊ฐ„์˜ ๊ด€๊ณ„๋“ค associate์— ์ž‘์„ฑ

  • 1๋Œ€๋‹ค: hasMany์™€ belongsTo
  • ๋‹ค๋Œ€๋‹ค: belongsToMany
    foreignKey: ์™ธ๋ž˜ํ‚ค
    as: ์ปฌ๋Ÿผ์— ๋Œ€ํ•œ ๋ณ„๋ช…
    through: ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”๋ช…

๐Ÿ”ปmodels/hastag.js

const Sequelize = require('sequelize');

module.exports = class Hashtag extends Sequelize.Model {
  static init(sequelize) {
    return super.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' });
  }
};

๐Ÿ”ป models/post.js

  const Sequelize = require('sequelize');

module.exports = class Post extends Sequelize.Model {
  static init(sequelize) {
    return super.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' });
  }
};

๐Ÿ”ป models/user.js

const Sequelize = require('sequelize');

module.exports = class User extends Sequelize.Model {
  static init(sequelize) {
    return super.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.STRING(10),
        allowNull: false,
        defaultValue: 'local',
      },
      snsId: {
        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) {
    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',
    });
  }
};  

4. ํŒ”๋กœ์ž‰-ํŒ”๋กœ์›Œ ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„

User(๋‹ค):User(๋‹ค)

  • ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„์ด๋ฏ€๋กœ ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”(Follow) ์ƒ์„ฑ๋จ
  • ๋ชจ๋ธ ์ด๋ฆ„์ด ๊ฐ™์œผ๋ฏ€๋กœ ๊ตฌ๋ถ„ ํ•„์š”ํ•จ(as๊ฐ€ ๊ตฌ๋ถ„์ž ์—ญํ• , foreignKey๋Š” ๋ฐ˜๋Œ€ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์˜ ํ”„๋ผ์ด๋จธ๋ฆฌ ํ‚ค ์ปฌ๋Ÿผ)
  • ์‹œํ€„๋ผ์ด์ฆˆ๋Š” as ์ด๋ฆ„์„ ๋ฐ”ํƒ•์œผ๋กœ ์ž๋™์œผ๋กœ addFollower, getFollowers, addFollowing, getFollowings ๋ฉ”์„œ๋“œ ์ƒ์„ฑ

5. ์‹œํ€„๋ผ์ด์ฆˆ ์„ค์ •ํ•˜๊ธฐ

์‹œํ€„๋ผ์ด์ฆˆ ์„ค์ •์€ config/config.json์—์„œ

  • ๊ฐœ๋ฐœํ™˜๊ฒฝ์šฉ ์„ค์ •์€ development ์•„๋ž˜์—

๐Ÿ”ปconfig/config.json

{
  "development": {
    "username": "root",
    "password": "nodejsbook",
    "database": "nodebird",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "operatorAliases": false
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

์„ค์ • ํŒŒ์ผ ์ž‘์„ฑ ํ›„ nodebird ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ

6. ๋ชจ๋ธ๊ณผ ์„œ๋ฒ„ ์—ฐ๊ฒฐํ•˜๊ธฐ

sequelize.sync()๊ฐ€ ํ…Œ์ด๋ธ” ์ƒ์„ฑ

  • IF NOT EXIST(SQL๋ฌธ)์œผ๋กœ ํ…Œ์ด๋ธ”์ด ์—†์„ ๋•Œ๋งŒ ์ƒ์„ฑํ•ด์คŒ

๐Ÿ”ป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 { 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'));
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;
  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'), '๋ฒˆ ํฌํŠธ์—์„œ ๋Œ€๊ธฐ์ค‘');
});

6. ๋ชจ๋ธ๊ณผ ์„œ๋ฒ„ ์—ฐ๊ฒฐํ•˜๊ธฐ

npm start๋กœ ์„œ๋ฒ„ ์‹คํ–‰ ์‹œ ์ฝ˜์†”์— SQL๋ฌธ์ด ํ‘œ์‹œ๋จ

Passport ๋ชจ๋“ˆ๋กœ ๋กœ๊ทธ์ธ

1. ํŒจ์ŠคํฌํŠธ ์„ค์น˜ํ•˜๊ธฐ

๋กœ๊ทธ์ธ ๊ณผ์ •์„ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ๋Š” Passport ์„ค์น˜ํ•˜๊ธฐ

  • ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”๋ฅผ ์œ„ํ•œ bcrypt๋„ ๊ฐ™์ด ์„ค์น˜
  • ์„ค์น˜ ํ›„ app.js์™€๋„ ์—ฐ๊ฒฐ
  • passport.initialize(): ์š”์ฒญ ๊ฐ์ฒด์— passport ์„ค์ •์„ ์‹ฌ์Œ
  • passport.session(): req.session ๊ฐ์ฒด์— passport ์ •๋ณด๋ฅผ ์ €์žฅ
    +* express-session ๋ฏธ๋“ค์›จ์–ด์— ์˜์กดํ•˜๋ฏ€๋กœ ์ด๋ณด๋‹ค ๋” ๋’ค์— ์œ„์น˜ํ•ด์•ผ ํ•จ

๐Ÿ”ป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 authRouter = require('./routes/auth');
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);
app.use('/auth', authRouter);

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'), '๋ฒˆ ํฌํŠธ์—์„œ ๋Œ€๊ธฐ์ค‘');
});

2. ํŒจ์ŠคํฌํŠธ ๋ชจ๋“ˆ ์ž‘์„ฑ

passport/index.js ์ž‘์„ฑ

  • passport.serializeUser: req.session ๊ฐ์ฒด์— ์–ด๋–ค ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ์ง€ ์„ ํƒ, ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋‹ค ๋“ค๊ณ  ์žˆ์œผ๋ฉด ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋งŽ์ด ์ฐจ์ง€ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉ์ž์˜ ์•„์ด๋””๋งŒ ์ €์žฅ
  • passport.deserializeUser: req.session์— ์ €์žฅ๋œ ์‚ฌ์šฉ์ž ์•„์ด๋””๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ DB ์กฐํšŒ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์–ป์–ด๋‚ธ ํ›„ req.user์— ์ €์žฅ

๐Ÿ”ป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();
};

3. ํŒจ์ŠคํฌํŠธ ์ฒ˜๋ฆฌ ๊ณผ์ •

๋กœ๊ทธ์ธ ๊ณผ์ •

    1. ๋กœ๊ทธ์ธ ์š”์ฒญ์ด ๋“ค์–ด์˜ด
    1. passport.authenticate ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ
    1. ๋กœ๊ทธ์ธ ์ „๋žต ์ˆ˜ํ–‰(์ „๋žต์€ ๋’ค์— ์•Œ์•„๋ด„)
    1. ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ์ฒด์™€ ํ•จ๊ป˜ req.login ํ˜ธ์ถœ
    1. req.login ๋ฉ”์„œ๋“œ๊ฐ€ passport.serializeUser ํ˜ธ์ถœ
    1. req.session์— ์‚ฌ์šฉ์ž ์•„์ด๋””๋งŒ ์ €์žฅ
    1. ๋กœ๊ทธ์ธ ์™„๋ฃŒ

๋กœ๊ทธ์ธ ์ดํ›„ ๊ณผ์ •

    1. ๋ชจ๋“  ์š”์ฒญ์— passport.session() ๋ฏธ๋“ค์›จ์–ด๊ฐ€ passport.deserializeUser ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ
    1. req.session์— ์ €์žฅ๋œ ์•„์ด๋””๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์‚ฌ์šฉ์ž ์กฐํšŒ
    1. ์กฐํšŒ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ req.user์— ์ €์žฅ
    1. ๋ผ์šฐํ„ฐ์—์„œ req.user ๊ฐ์ฒด ์‚ฌ์šฉ ๊ฐ€๋Šฅ

4. ๋กœ์ปฌ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ

passport-local ํŒจํ‚ค์ง€ ํ•„์š”

  • ๋กœ์ปฌ ๋กœ๊ทธ์ธ ์ „๋žต ์ˆ˜๋ฆฝ
  • ๋กœ๊ทธ์ธ์—๋งŒ ํ•ด๋‹นํ•˜๋Š” ์ „๋žต์ด๋ฏ€๋กœ ํšŒ์›๊ฐ€์ž…์€ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•จ
  • ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ–ˆ๋Š”์ง€, ํ•˜์ง€ ์•Š์•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ์ฒดํฌํ•˜๋Š” ๋ฏธ๋“ค์›จ์–ด๋„ ๋งŒ๋“ฆ

๐Ÿ”ปroutes/middlewares.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}`);
  }
};

๐Ÿ”ปroutes/page.js

const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');

const router = express.Router();

router.use((req, res, next) => {
  res.locals.user = req.user;
  res.locals.followerCount = 0;
  res.locals.followingCount = 0;
  res.locals.followerIdList = [];
  next();
});

router.get('/profile', isLoggedIn, (req, res) => {
  res.render('profile', { title: '๋‚ด ์ •๋ณด - NodeBird' });
});

router.get('/join', isNotLoggedIn, (req, res) => {
  res.render('join', { title: 'ํšŒ์›๊ฐ€์ž… - NodeBird' });
});

router.get('/', (req, res, next) => {
  const twits = [];
  res.render('main', {
    title: 'NodeBird',
    twits,
  });
});

module.exports = router;

5. ํšŒ์›๊ฐ€์ž… ๋ผ์šฐํ„ฐ

routes/auth.js ์ž‘์„ฑ

  • bcrypt.hash๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”
  • hash์˜ ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋Š” ์•”ํ˜ธํ™” ๋ผ์šด๋“œ
  • ๋ผ์šด๋“œ๊ฐ€ ๋†’์„์ˆ˜๋ก ์•ˆ์ „ํ•˜์ง€๋งŒ ์˜ค๋ž˜ ๊ฑธ๋ฆผ
  • ์ ๋‹นํ•œ ๋ผ์šด๋“œ๋ฅผ ์ฐพ๋Š” ๊ฒŒ ์ข‹์Œ
  • error ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์œผ๋กœ 1ํšŒ์„ฑ ๋ฉ”์‹œ์ง€

6. ๋กœ๊ทธ์ธ ๋ผ์šฐํ„ฐ

routes/auth.js ์ž‘์„ฑ

  • passport.authenticate(โ€˜localโ€™): ๋กœ์ปฌ ์ „๋žต
    *์ „๋žต์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๋‚˜๋ฉด authenticate์˜ ์ฝœ๋ฐฑ ํ•จ์ˆ˜ ํ˜ธ์ถœ๋จ
  • authError: ์ธ์ฆ ๊ณผ์ • ์ค‘ ์—๋Ÿฌ,
  • user: ์ธ์ฆ ์„ฑ๊ณต ์‹œ ์œ ์ € ์ •๋ณด
  • info: ์ธ์ฆ ์˜ค๋ฅ˜์— ๋Œ€ํ•œ ๋ฉ”์‹œ์ง€
  • ์ธ์ฆ์ด ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด req.login์œผ๋กœ ์„ธ์…˜์— ์œ ์ € ์ •๋ณด ์ €์žฅ

๐Ÿ”ปroutes/auth.js

const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const User = require('../models/user');

const router = express.Router();

router.post('/join', isNotLoggedIn, 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);
  }
});

router.post('/login', isNotLoggedIn, (req, res, next) => {
  passport.authenticate('local', (authError, user, info) => {
    if (authError) {
      console.error(authError);
      return next(authError);
    }
    if (!user) {
      return res.redirect(`/?loginError=${info.message}`);
    }
    return req.login(user, (loginError) => {
      if (loginError) {
        console.error(loginError);
        return next(loginError);
      }
      return res.redirect('/');
    });
  })(req, res, next); // ๋ฏธ๋“ค์›จ์–ด ๋‚ด์˜ ๋ฏธ๋“ค์›จ์–ด์—๋Š” (req, res, next)๋ฅผ ๋ถ™์ž…๋‹ˆ๋‹ค.
});

router.get('/logout', isLoggedIn, (req, res) => {
  req.logout();
  req.session.destroy();
  res.redirect('/');
});

router.get('/kakao', passport.authenticate('kakao'));

router.get('/kakao/callback', passport.authenticate('kakao', {
  failureRedirect: '/',
}), (req, res) => {
  res.redirect('/');
});

module.exports = router;

7. ๋กœ์ปฌ ์ „๋žต ์ž‘์„ฑ

passport/localStrategy.js ์ž‘์„ฑ

  • usernameField์™€ passwordField๊ฐ€ input ํƒœ๊ทธ์˜ name(body-parser์˜ req.body)
  • ์‚ฌ์šฉ์ž๊ฐ€ DB์— ์ €์žฅ๋˜์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•œ ํ›„ ์žˆ๋‹ค๋ฉด ๋น„๋ฐ€๋ฒˆํ˜ธ ๋น„๊ต(bcrypt.compare)
  • ๋น„๋ฐ€๋ฒˆํ˜ธ๊นŒ์ง€ ์ผ์น˜ํ•œ๋‹ค๋ฉด ๋กœ๊ทธ์ธ

๐Ÿ”ป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',
  }, 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);
    }
  }));
};

8. ์นด์นด์˜ค ๋กœ๊ทธ์ธ ๊ตฌํ˜„

passport/kakaoStrategy.js ์ž‘์„ฑ

  • clientID์— ์นด์นด์˜ค ์•ฑ ์•„์ด๋”” ์ถ”๊ฐ€
  • callbackURL: ์นด์นด์˜ค ๋กœ๊ทธ์ธ ํ›„ ์นด์นด์˜ค๊ฐ€ ๊ฒฐ๊ณผ๋ฅผ ์ „์†กํ•ด์ค„ URL
  • accessToken, refreshToken: ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ์นด์นด์˜ค๊ฐ€ ๋ณด๋‚ด์ค€ ํ† ํฐ(์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ)
  • profile: ์นด์นด์˜ค๊ฐ€ ๋ณด๋‚ด์ค€ ์œ ์ € ์ •๋ณด
  • profile์˜ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํšŒ์›๊ฐ€์ž…

๐Ÿ”ปpassport/kakaoStrategy.js

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_email,
          nick: profile.displayName,
          snsId: profile.id,
          provider: 'kakao',
        });
        done(null, newUser);
      }
    } catch (error) {
      console.error(error);
      done(error);
    }
  }));
};

9. ์นด์นด์˜ค ๋กœ๊ทธ์ธ์šฉ ๋ผ์šฐํ„ฐ ๋งŒ๋“ค๊ธฐ

ํšŒ์›๊ฐ€์ž…๊ณผ ๋กœ๊ทธ์ธ์ด ์ „๋žต์—์„œ ๋™์‹œ์— ์ˆ˜ํ–‰๋จ

  • passport.authenticate(โ€˜kakaoโ€™)๋งŒ ํ•˜๋ฉด ๋จ
  • /kakao/callback ๋ผ์šฐํ„ฐ์—์„œ๋Š” ์ธ์ฆ ์„ฑ๊ณต ์‹œ(res.redirect)์™€ ์‹คํŒจ ์‹œ(failureRedirect) ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•  ๊ฒฝ๋กœ ์ง€์ •

๐Ÿ”ปroutes/auth.js

const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const User = require('../models/user');

const router = express.Router();

router.post('/join', isNotLoggedIn, 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);
  }
});

router.post('/login', isNotLoggedIn, (req, res, next) => {
  passport.authenticate('local', (authError, user, info) => {
    if (authError) {
      console.error(authError);
      return next(authError);
    }
    if (!user) {
      return res.redirect(`/?loginError=${info.message}`);
    }
    return req.login(user, (loginError) => {
      if (loginError) {
        console.error(loginError);
        return next(loginError);
      }
      return res.redirect('/');
    });
  })(req, res, next); // ๋ฏธ๋“ค์›จ์–ด ๋‚ด์˜ ๋ฏธ๋“ค์›จ์–ด์—๋Š” (req, res, next)๋ฅผ ๋ถ™์ž…๋‹ˆ๋‹ค.
});

router.get('/logout', isLoggedIn, (req, res) => {
  req.logout();
  req.session.destroy();
  res.redirect('/');
});

router.get('/kakao', passport.authenticate('kakao'));

router.get('/kakao/callback', passport.authenticate('kakao', {
  failureRedirect: '/',
}), (req, res) => {
  res.redirect('/');
});

module.exports = router;

๐Ÿ”ป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 authRouter = require('./routes/auth');
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);
app.use('/auth', authRouter);

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'), '๋ฒˆ ํฌํŠธ์—์„œ ๋Œ€๊ธฐ์ค‘');
});

10. ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์•ฑ ๋งŒ๋“ค๊ธฐ

https://developers.kakao.com์— ์ ‘์†ํ•˜์—ฌ ํšŒ์›๊ฐ€์ž…

  • NodeBird ์•ฑ ๋งŒ๋“ค๊ธฐ

10. ์นด์นด์˜ค ์•ฑ ํ‚ค ์ €์žฅํ•˜๊ธฐ

REST API ํ‚ค๋ฅผ ์ €์žฅํ•ด์„œ .env์— ์ €์žฅ

๐Ÿ”ป.env

COOKIE_SECRET=nodebirdsecret
KAKAO_ID=5d4daf57becfd72fd9c919882552c4a6

11. ์นด์นด์˜ค ์›น ํ”Œ๋žซํผ ์ถ”๊ฐ€

์›น ํ”Œ๋žซํผ์„ ์ถ”๊ฐ€ํ•ด์•ผ callbackURL ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์Œ

12. ์นด์นด์˜ค ๋™์˜ํ•ญ๋ชฉ ์„ค์ •

์ด๋ฉ”์ผ, ์ƒ์ผ ๋“ฑ์˜ ์ •๋ณด๋ฅผ ์–ป๊ธฐ ์œ„ํ•ด ๋™์˜ํ•ญ๋ชฉ ์„ค์ •

13. ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์‹œ๋„

์นด์นด์˜คํ†ก ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์ฐฝ์œผ๋กœ ์ „ํ™˜

  • ๊ณ„์ • ๋™์˜ ํ›„ ๋‹ค์‹œ NodeBird ์„œ๋น„์Šค๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ

Multer ๋ชจ๋“ˆ๋กœ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ตฌํ˜„ํ•˜๊ธฐ

1. ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ตฌํ˜„

form ํƒœ๊ทธ์˜ enctype์ด multipart/form-data

  • body-parser๋กœ๋Š” ์š”์ฒญ ๋ณธ๋ฌธ์„ ํ•ด์„ํ•  ์ˆ˜ ์—†์Œ
  • multer ํŒจํ‚ค์ง€ ํ•„์š”

    npm i multer

  • ์ด๋ฏธ์ง€๋ฅผ ๋จผ์ € ์—…๋กœ๋“œํ•˜๊ณ , ์ด๋ฏธ์ง€๊ฐ€ ์ €์žฅ๋œ ๊ฒฝ๋กœ๋ฅผ ๋ฐ˜ํ™˜ํ•  ๊ฒƒ์ž„
  • ๊ฒŒ์‹œ๊ธ€ form์„ submitํ•  ๋•Œ๋Š” ์ด๋ฏธ์ง€ ์ž์ฒด ๋Œ€์‹  ๊ฒฝ๋กœ๋ฅผ ์ „์†ก

2. ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ผ์šฐํ„ฐ ๊ตฌํ˜„

fs.readdir, fs.mkdirSync๋กœ upload ํด๋”๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ
multer() ํ•จ์ˆ˜๋กœ ์—…๋กœ๋“œ ๋ฏธ๋“ค์›จ์–ด ์ƒ์„ฑ

  • storage: diskStorage๋Š” ์ด๋ฏธ์ง€๋ฅผ ์„œ๋ฒ„ ๋””์Šคํฌ์— ์ €์žฅ(destination์€ ์ €์žฅ ๊ฒฝ๋กœ, filename์€ ์ €์žฅ ํŒŒ์ผ๋ช…)
  • limits๋Š” ํŒŒ์ผ ์ตœ๋Œ€ ์šฉ๋Ÿ‰(5MB)
  • upload.single(โ€˜imgโ€™): ์š”์ฒญ ๋ณธ๋ฌธ์˜ img์— ๋‹ด๊ธด ์ด๋ฏธ์ง€ ํ•˜๋‚˜๋ฅผ ์ฝ์–ด ์„ค์ •๋Œ€๋กœ ์ €์žฅํ•˜๋Š” ๋ฏธ๋“ค์›จ์–ด
  • ์ €์žฅ๋œ ํŒŒ์ผ์— ๋Œ€ํ•œ ์ •๋ณด๋Š” req.file ๊ฐ์ฒด์— ๋‹ด๊น€

3. ๊ฒŒ์‹œ๊ธ€ ๋“ฑ๋ก

upload2.none()์€ multipart/formdata ํƒ€์ž…์˜ ์š”์ฒญ์ด์ง€๋งŒ ์ด๋ฏธ์ง€๋Š” ์—†์„ ๋•Œ ์‚ฌ์šฉ

  • ๊ฒŒ์‹œ๊ธ€ ๋“ฑ๋ก ์‹œ ์•„๊นŒ ๋ฐ›์€ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ ์ €์žฅ
  • ๊ฒŒ์‹œ๊ธ€์—์„œ ํ•ด์‹œํƒœ๊ทธ๋ฅผ ์ฐพ์•„์„œ ๊ฒŒ์‹œ๊ธ€๊ณผ ์—ฐ๊ฒฐ(post.addHashtags)
  • findOrCreate๋Š” ๊ธฐ์กด์— ํ•ด์‹œํƒœ๊ทธ๊ฐ€ ์กด์žฌํ•˜๋ฉด ๊ทธ๊ฑธ ์‚ฌ์šฉํ•˜๊ณ , ์—†๋‹ค๋ฉด ์ƒ์„ฑํ•˜๋Š” ์‹œํ€„๋ผ์ด์ฆˆ ๋ฉ”์„œ๋“œ

๐Ÿ”ปroutes/post.js

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const { Post, Hashtag } = require('../models');
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 },
});

router.post('/img', isLoggedIn, upload.single('img'), (req, res) => {
  console.log(req.file);
  res.json({ url: `/img/${req.file.filename}` });
});

const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), 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);
  }
});

module.exports = router;

4. ๋ฉ”์ธ ํŽ˜์ด์ง€์— ๊ฒŒ์‹œ๊ธ€ ๋ณด์—ฌ์ฃผ๊ธฐ

๋ฉ”์ธ ํŽ˜์ด์ง€(/) ์š”์ฒญ ์‹œ ๊ฒŒ์‹œ๊ธ€์„ ๋จผ์ € ์กฐํšŒํ•œ ํ›„ ํ…œํ”Œ๋ฆฟ ์—”์ง„ ๋ Œ๋”๋ง

  • include๋กœ ๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ๋ชจ๋ธ์„ ํ•ฉ์ณ์„œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Œ
  • Post์™€ User๋Š” ๊ด€๊ณ„๊ฐ€ ์žˆ์Œ (1๋Œ€๋‹ค)
  • ๊ฒŒ์‹œ๊ธ€์„ ๊ฐ€์ ธ์˜ฌ ๋•Œ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž๊นŒ์ง€ ๊ฐ™์ด ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ)

๐Ÿ”ปroutes/page.js

const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const { Post, User } = require('../models');

const router = express.Router();

router.use((req, res, next) => {
  res.locals.user = req.user;
  res.locals.followerCount = 0;
  res.locals.followingCount = 0;
  res.locals.followerIdList = [];
  next();
});

router.get('/profile', isLoggedIn, (req, res) => {
  res.render('profile', { title: '๋‚ด ์ •๋ณด - NodeBird' });
});

router.get('/join', isNotLoggedIn, (req, res) => {
  res.render('join', { title: 'ํšŒ์›๊ฐ€์ž… - NodeBird' });
});

router.get('/', 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);
  }
});

module.exports = router;

ํ”„๋กœ์ ํŠธ ๋งˆ๋ฌด๋ฆฌํ•˜๊ธฐ

1. ํŒ”๋กœ์ž‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„

POST /:id/follow ๋ผ์šฐํ„ฐ ์ถ”๊ฐ€

  • /์‚ฌ์šฉ์ž์•„์ด๋””/follow
  • ์‚ฌ์šฉ์ž ์•„์ด๋””๋Š” req.params.id๋กœ ์ ‘๊ทผ
  • user.addFollowing(์‚ฌ์šฉ์ž์•„์ด๋””)๋กœ ํŒ”๋กœ์ž‰ํ•˜๋Š” ์‚ฌ๋žŒ ์ถ”๊ฐ€

๐Ÿ”ปroutes/user.js

const express = require('express');

const { isLoggedIn } = require('./middlewares');
const User = require('../models/user');

const router = express.Router();

router.post('/:id/follow', isLoggedIn, 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);
  }
});

module.exports = router;

2. ํŒ”๋กœ์ž‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„

deserializeUser ์ˆ˜์ •

  • req.user.Followers๋กœ ํŒ”๋กœ์›Œ ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • req.user.Followings๋กœ ํŒ”๋กœ์ž‰ ์ ‘๊ทผ
  • ๋‹จ, ๋ชฉ๋ก์ด ์œ ์ถœ๋˜๋ฉด ์•ˆ ๋˜๋ฏ€๋กœ ํŒ”๋กœ์›Œ/ํŒ”๋กœ์ž‰ ์ˆซ์ž๋งŒ ํ”„๋ŸฐํŠธ๋กœ ์ „๋‹ฌ

๐Ÿ”ป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 },
      include: [{
        model: User,
        attributes: ['id', 'nick'],
        as: 'Followers',
      }, {
        model: User,
        attributes: ['id', 'nick'],
        as: 'Followings',
      }],
    })
      .then(user => done(null, user))
      .catch(err => done(err));
  });

  local();
  kakao();
};

๐Ÿ”ปroutes/page.js

const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const { Post, User, Hashtag } = require('../models');

const router = express.Router();

router.use((req, res, next) => {
  res.locals.user = req.user;
  res.locals.followerCount = req.user ? req.user.Followers.length : 0;
  res.locals.followingCount = req.user ? req.user.Followings.length : 0;
  res.locals.followerIdList = req.user ? req.user.Followings.map(f => f.id) : [];
  next();
});

router.get('/profile', isLoggedIn, (req, res) => {
  res.render('profile', { title: '๋‚ด ์ •๋ณด - NodeBird' });
});

router.get('/join', isNotLoggedIn, (req, res) => {
  res.render('join', { title: 'ํšŒ์›๊ฐ€์ž… - NodeBird' });
});

router.get('/', 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);
  }
});

router.get('/hashtag', 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);
  }
});

module.exports = router;

3. ํ•ด์‹œํƒœ๊ทธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

GET /hashtag ๋ผ์šฐํ„ฐ ์ถ”๊ฐ€

  • ํ•ด์‹œํƒœ๊ทธ๋ฅผ ๋จผ์ € ์ฐพ๊ณ (hashtag)
  • hashtag.getPosts๋กœ ํ•ด์‹œํƒœ๊ทธ์™€ ๊ด€๋ จ๋œ ๊ฒŒ์‹œ๊ธ€์„ ๋ชจ๋‘ ์ฐพ์Œ
  • ์ฐพ์œผ๋ฉด์„œ include๋กœ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž ๋ชจ๋ธ๋„ ๊ฐ™์ด ๊ฐ€์ ธ์˜ด

๐Ÿ”ปroutes/page.js

router.get('/hashtag', 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);
  }
});

module.exports = router;

4. ์—…๋กœ๋“œํ•œ ์ด๋ฏธ์ง€ ์ œ๊ณตํ•˜๊ธฐ

express.static ๋ฏธ๋“ค์›จ์–ด๋กœ uploads ํด๋”์— ์ €์žฅ๋œ ์ด๋ฏธ์ง€ ์ œ๊ณต

  • ํ”„๋ŸฐํŠธ์—”๋“œ์—์„œ๋Š” /img/์ด๋ฏธ์ง€๋ช… ์ฃผ์†Œ๋กœ ์ด๋ฏธ์ง€ ์ ‘๊ทผ ๊ฐ€๋Šฅ

๐Ÿ”ป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 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,
});
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);

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'), '๋ฒˆ ํฌํŠธ์—์„œ ๋Œ€๊ธฐ์ค‘');
});

5. ํ”„๋กœ์ ํŠธ ํ™”๋ฉด

์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๊ณ  http://localhost:8001 ์ ‘์†

๐Ÿ˜ƒ์ถœ์ฒ˜๐Ÿ˜ƒ
Node.js ๊ต๊ณผ์„œ - ๊ธฐ๋ณธ๋ถ€ํ„ฐ ํ”„๋กœ์ ํŠธ ์‹ค์Šต๊นŒ์ง€
https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-%EA%B5%90%EA%B3%BC%EC%84%9C/dashboard

0๊ฐœ์˜ ๋Œ“๊ธ€