์ƒ์„ธ ์ปจํ…์ธ 

๋ณธ๋ฌธ ์ œ๋ชฉ

[๋…ธ๋“œ 2] 9์žฅ. ์ต์Šคํ”„๋ ˆ์Šค๋กœ SNS ์„œ๋น„์Šค ๋งŒ๋“ค๊ธฐ

23-24/Node.js 2

by _๋„๋‹ด 2023. 12. 1. 10:00

๋ณธ๋ฌธ

728x90

 

๐ŸŒŸ9์žฅ ํ‚ค์›Œ๋“œ๐ŸŒŸ

์ปจํŠธ๋กค๋Ÿฌ

๋ผ์šฐํ„ฐ์™€ ๋ฏธ๋“ค์›จ์–ด

Passport

์นด์นด์˜ค ๋กœ๊ทธ์ธ

multer

 

 

 

 

๐Ÿ“ MySQL๊ณผ Passport๋ฅผ ์ด์šฉํ•ด ๋กœ๊ทธ์ธ, ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ, ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ, ํ•ด์‹œํƒœ๊ทธ ๊ฒ€์ƒ‰, ํŒ”๋กœ์ž‰ ๊ธฐ๋Šฅ์ด ์žˆ๋Š” SNS ์„œ๋น„์Šค๋ฅผ ์ œ์ž‘

 

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

$ npm init
$ npm i sequelize mysql2 sequelize-cli
$ npx sequelize init

package.json ์ƒ์„ฑ ํ›„  ์‹œํ€„๋ผ์ด์ฆˆ๋ฅผ ์„ค์น˜

 

๐Ÿ“Œ ์ปจํŠธ๋กค๋Ÿฌ

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,
  });
};

 

  • ๋ผ์šฐํ„ฐ์˜ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์—ญํ• 
  • ์‹ค๋ฌด์—์„œ ์ฝ”๋“œ๋ฅผ ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์—ญํ• ์„ ๋ถ„๋ฆฌํ•œ ๊ฒƒ
  • ํŽ˜์ด์ง€๋ฅผ ํ™”๋ฉด์— ๋ Œ๋”๋งํ•˜๋Š” ์—ญํ• 

๐Ÿ“Œ ์ปจํŠธ๋กค๋Ÿฌ์™€ ์„œ๋น„์Šค

์„œ๋น„์Šค๋Š” ํ•ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ์˜ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋‹ด๋‹น,
์ปจํŠธ๋กค๋Ÿฌ๋Š” ์š”์ฒญ(req)๊ณผ ์‘๋‹ต(res)์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋‹ด๋‹น

 

 

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

๐Ÿ“ MySQL๊ณผ ์‹œํ€„๋ผ์ด์ฆˆ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •

 

foreach๋ฅผ ์‚ฌ์šฉํ•ด ๊ฐ๊ฐ์˜ ๋ชจ๋ธ๋“ค์„ ์‹œํ€„๋ผ์ด์ฆˆ ๊ฐ์ฒด์— ์—ฐ๊ฒฐํ•˜๋Š” ์ฝ”๋“œ

๋”๋ณด๊ธฐ
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;

 

๐Ÿ“Œ ๊ฐ™์€ ํ…Œ์ด๋ธ” ๊ฐ„ N:M ๊ด€๊ณ„์™€ as

  • ๊ฐ™์€ ํ…Œ์ด๋ธ” ๊ฐ„ N:M ๊ด€๊ณ„์—์„œ๋Š” ๋ชจ๋ธ ์ด๋ฆ„๊ณผ ์ปฌ๋Ÿผ ์ด๋ฆ„์„ ๋”ฐ๋กœ ์„ค์ •
  • foreignKey๊ฐ€ followerId์ด๋ฉด as๋Š” Followings
  • foreignKey๊ฐ€ followingId์ด๋ฉด as๋Š” Followers

 

๐Ÿ“Œ ์ฝ˜์†” : ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค ์ƒ์„ฑ

$ npx sequelize db:create

 

๐Ÿ“Œ app.js : ๋ชจ๋ธ ์„œ๋ฒ„์™€ ์—ฐ๊ฒฐ

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'));

 

 

9.3 Passport ๋ชจ๋“ˆ๋กœ ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ

๐Ÿ“ passportConfig ๊ด€๋ จ ๋‚ด์šฉ ์ž‘์„ฑํ•˜๊ธฐ

โ–ถ 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();
};
  • serializeUser์—์„œ ์„ธ์…˜์— ์ €์žฅํ–ˆ๋˜ ์•„์ด๋””๋ฅผ ๋ฐ›์•„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์กฐํšŒ
  • ์กฐํšŒํ•œ ์ •๋ณด๋ฅผ req.user์— ์ €์žฅ, ๊ฐ€์ ธ์˜ค๊ธฐ

๐Ÿ“Œ serializeUser : ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ์ฒด์—์„œ ์•„์ด๋””๋ฅผ ์ถ”๋ ค ์„ธ์…˜์— ์ €์žฅ

๐Ÿ“Œ deserializeUser : ์„ธ์…˜์— ์ €์žฅํ•œ ์•„์ด๋””๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ์ฒด ๋ถˆ๋Ÿฌ์˜ค๊ธฐ

⇒ ์„ธ์…˜์— ๋„ˆ๋ฌด ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์ง€ ์•Š๊ธฐ ์œ„ํ•จ

๐Ÿ’ก ์ตœ์ข…๊ณผ์ • : ์ฒซ ๋กœ๊ทธ์ธ, ์„ธ์…˜ ์ƒ์„ฑ

1. /auth/login ๋ผ์šฐํ„ฐ๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ์ธ ์š”์ฒญ์ด ๋“ค์–ด์˜ด
2. ๋ผ์šฐํ„ฐ์—์„œ passport.authenticate ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ
3. ๋กœ๊ทธ์ธ ๋กœ์ง ์ˆ˜ํ–‰
4. ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ์ฒด์™€ ํ•จ๊ป˜ req.login ํ˜ธ์ถœ
5. req.login ๋ฉ”์„œ๋“œ๊ฐ€ passport/serializeUser ํ˜ธ์ถœ
6. req.session์— ์‚ฌ์šฉ์ž ์•„์ด๋””๋งŒ ์ €์žฅํ•ด์„œ ์„ธ์…˜ ์ƒ์„ฑ
7. express-session์— ์„ค์ •ํ•œ ๋Œ€๋กœ ๋ธŒ๋ผ์šฐ์ €์— connect.sid ์„ธ์…˜ ์ฟ ํ‚ค ์ „์†ก
8. ๋กœ๊ทธ์ธ ์™„๋ฃŒ
๐Ÿ’ก ๋กœ๊ทธ์ธ ์ดํ›„

1. ์–ด๋– ํ•œ ์š”์ฒญ์ด ๋“ค์–ด์˜ด
2. ๋ผ์šฐํ„ฐ์— ์š”์ฒญ์ด ๋„๋‹ฌํ•˜๊ธฐ ์ „์— passport.session ๋ฏธ๋“ค์›จ์–ด๊ฐ€ passport.deserializeUser ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ
3. connect.sid ์‹œ์…˜ ์ฟ ํ‚ค๋ฅผ ์ฝ๊ณ  ์„ธ์…˜ ๊ฐ์ฒด๋ฅผ ์ฐพ์•„์„œ req.session์œผ๋กœ ๋งŒ๋“ฆ
4. req.session์— ์ €์žฅ๋œ ์•„์ด๋””๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์‚ฌ์šฉ์ž ์กฐํšŒ
5. ์กฐํšŒ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ req.user์— ์ €์žฅ(deserializeUser)
6. ๋ผ์šฐํ„ฐ์—์„œ req.user ๊ฐ์ฒด ์‚ฌ์šฉ ๊ฐ€๋Šฅ

 

 

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

๐Ÿ“Œ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž : ํšŒ์› ๊ฐ€์ž…, ๋กœ๊ทธ์ธ ๋ผ์šฐํ„ฐ ์ ‘๊ทผ X
๐Ÿ“Œ ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž : ๋กœ๊ทธ์•„์›ƒ ๋ผ์šฐํ„ฐ ์ ‘๊ทผ X

⇒ Passport์˜ req.isAuthenticated ์‚ฌ์šฉํ•ด ๋กœ๊ทธ์ธ ์—ฌ๋ถ€๋ฅผ ๊ฒ€์‚ฌํ•˜๋Š” ๋ฏธ๋“ค์›จ์–ด ์ž‘์„ฑ

โ–ถ 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
  • isLoggedIn๊ณผ iNotLoggedIn ๋ฏธ๋“ค์›จ์–ด๋กœ ํ™œ์šฉ

 

๐Ÿ“Œ ๋ผ์šฐํ„ฐ ์ž‘์„ฑ

โ–ถ routes/page.js, routes/auth.js

๋”๋ณด๊ธฐ

routes/page.js

const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const { renderProfile, renderJoin, renderMain } = require('../controllers/page');

const router = express.Router();

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

router.get('/profile', isLoggedIn, renderProfile);

router.get('/join', isNotLoggedIn, renderJoin);

router.get('/', renderMain);

module.exports = router;

๐Ÿ”‘ res.locals.user ์†์„ฑ์— req.user๋ฅผ ๋„ฃ์–ด ๋„Œ์ ์Šค์—์„œ user ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด์— ์ ‘๊ทผ

  • isLoggedIn : renderProfile
  • isNotLoggedIn : renderJoin

routes/auth.js

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;

 

๐Ÿ“Œ ์ปจํŠธ๋กค๋Ÿฌ ์ž‘์„ฑ

โ–ถ controllers/auth.js

๋”๋ณด๊ธฐ
const bcrypt = require('bcrypt');
const passport = require('passport');
const User = require('../models/user');

exports.join = 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);
  }
}

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

exports.logout = (req, res) => {
  req.logout(() => {
    res.redirect('/');
  });
};

๐Ÿ”‘ exports.login

  • ๋กœ๊ทธ์ธ ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด passport.authenticate('local') ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ๋กœ์ปฌ ๋กœ๊ทธ์ธ ์ „๋žต์„ ์ˆ˜ํ–‰
  • ๋ผ์šฐํ„ฐ ๋ฏธ๋“ค์›จ์–ด ์•ˆ์— ๋“ค์–ด ์žˆ๋Š” ๋ฏธ๋“ค์›จ์–ด๋Š” ๋‚ด๋ถ€ ๋ฏธ๋“ค์›จ์–ด์— (req, res, next)๋ฅผ ์ธ์ˆ˜๋กœ ์ œ๊ณต
  • ๋ฏธ๋“ค์›จ์–ด์— ์‚ฌ์šฉ์ž ์ •์˜ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€
  • user์— ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์žˆ๋‹ค๋ฉด req.login ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœ, req.login์€ passport.serializeUser๋ฅผ ํ˜ธ์ถœ

 

๐Ÿ“Œ ๋กœ๊ทธ์ธ ์ „๋žต ์ž‘์„ฑ

โ–ถ 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);
    }
  }));
};

1. ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ผ์น˜ํ•˜๋Š” ์ด๋ฏธ์ผ์ด ์žˆ๋Š”์ง€ ๊ฒ€์ƒ‰

2. ์žˆ๋‹ค๋ฉด bcrypt์˜ compare ํ•จ์ˆ˜๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋น„๊ต

3. ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•œ๋‹ค๋ฉด done ํ•จ์ˆ˜์˜ ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋„ฃ์–ด ๋ณด๋‚ด๊ธฐ

4. done์ด ํ˜ธ์ถœ๋œ ํ›„์—๋Š” ๋‹ค์‹œ passport.authenticate์˜ ์ฝœ๋ฐฑ ํ•จ์ˆ˜์—์„œ ๋‚˜๋จธ์ง€ ๋กœ์ง์ด ์‹คํ–‰

5. ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋˜๋ฉด์„œ ๋กœ๊ทธ์ธ ํผ ๋Œ€์‹  ํšŒ์› ์ •๋ณด๋ฅผ ๋ณด์—ฌ์คŒ

 

 

9.3.2 ์นด์นด์˜ค ๋กœ๊ทธ์ธ ๊ตฌํ˜„ํ•˜๊ธฐ

๐Ÿ“Œ ์นด์นด์˜คํ†ก ๋กœ๊ทธ์ธ ์ „๋žต ์ž‘์„ฑ

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

๐Ÿ”‘ ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์„ค์ •

  • clientID : ์นด์นด์˜ค์—์„œ ๋ฐœ๊ธ‰ํ•ด์ฃผ๋Š” ์•„์ด๋””, process.env.KAKAO_ID๋กœ ์„ค์ •ํ•˜์—ฌ ๋…ธ์ถœ ๋ง‰์Œ
  • callbackURL : ์นด์นด์˜ค๋กœ๋ถ€ํ„ฐ ์ธ์ฆ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์„ ๋ผ์šฐํ„ฐ ์ฃผ์†Œ

๐Ÿ”‘ ํšŒ์› ๊ฐ€์ž…

1. ์นด์นด์˜ค์—์„œ ์ธ์ฆ ํ›„ callbackURL์— ์ ํžŒ ์ฃผ์†Œ๋กœ accessToken, refreshToken, profile ์ „์†ก

2. profile ๊ฐ์ฒด์—์„œ ์›ํ•˜๋Š” ์ •๋ณด๋ฅผ ๊บผ๋‚ด์™€ ํšŒ์› ๊ฐ€์ž…

3. ์‚ฌ์šฉ์ž๋ฅผ ์ƒ์„ฑ ํ›„ done ํ•จ์ˆ˜ ํ˜ธ์ถœ

 

๐Ÿ“Œ ์นด์นด์˜คํ†ก ๋ผ์šฐํ„ฐ ์ž‘์„ฑ

โ–ถ routes/auth.js

๋”๋ณด๊ธฐ
...
router.get('/logout', isLoggedIn, logout);

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

// GET /auth/kakao/callback
router.get('/kakao/callback', passport.authenticate('kakao', {
  failureRedirect: '/?error=์นด์นด์˜ค๋กœ๊ทธ์ธ ์‹คํŒจ',
}), (req, res) => {
  res.redirect('/'); // ์„ฑ๊ณต ์‹œ์—๋Š” /๋กœ ์ด๋™
});

module.exports = router;
  • GET /auth/kakao์—์„œ ๋กœ๊ทธ์ธ ์ „๋žต(kakaoStrategy)์„ ์ˆ˜ํ–‰, ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์ฐฝ์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
  • ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์ฐฝ์—์„œ ๋กœ๊ทธ์ธ ํ›„ ์„ฑ๊ณต ์—ฌ๋ถ€ ๊ฒฐ๊ณผ๋ฅผ GET /auth/kakao/callback์œผ๋กœ ๋ฐ›๊ธฐ
  • passport.authenticate ๋ฉ”์„œ๋“œ์— ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์Œ, ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ๋‚ด๋ถ€์ ์œผ๋กœ req.login์„ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ

์นด์นด์˜ค clientID ๋ฐœ๊ธ‰์€ p456 ์ฐธ๊ณ 

 

9.4 multer ํŒจํ‚ค์ง€๋กœ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๊ตฌํ˜„ํ•˜๊ธฐ

  • ์‚ฌ์ง„์ด ์—…๋กœ๋“œ ๋œ ๊ฒŒ์‹œ๊ธ€์„ ์ €์žฅํ•  ๋•Œ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๊ฐ€ ์ €์žฅ๋จ
  • ์ด๋ฏธ์ง€๋Š” ์„œ๋ฒ„ ๋””์Šคํฌ(uploads ํด๋”)์— ์ €์žฅ๋จ

๐Ÿ“Œ ์ฝ˜์†” : ํŒจํ‚ค์ง€ ์„ค์น˜

$ npm i multer

 

๐Ÿ“Œ ๋ผ์šฐํ„ฐ ์ž‘์„ฑ

โ–ถ routes/post.js

๋”๋ณด๊ธฐ
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 /post/img ๋ผ์šฐํ„ฐ : ์ด๋ฏธ์ง€ ํ•˜๋‚˜๋ฅผ ์—…๋กœ๋“œ๋ฐ›์€ ๋’ค ์ด๋ฏธ์ง€์˜ ์ €์žฅ ๊ฒฝ๋กœ๋ฅผ ํด๋ผ์ด์–ธํŠธ๋กœ ์‘๋‹ต
  • POST /post : ๊ฒŒ์‹œ๊ธ€ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ, ์ด๋ฏธ์ง€ ์ฃผ์†Œ๊ฐ€ req.body.url๋กœ ์ „์†ก

 

๐Ÿ“Œ ์ปจํŠธ๋กค๋Ÿฌ ์ž‘์„ฑ

โ–ถ controllers/post.js

๋”๋ณด๊ธฐ
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);
  }
};
  • ๊ฒŒ์‹œ๊ธ€ ๋‚ด์šฉ์—์„œ ํ•ด์‹œํƒœ๊ทธ๋ฅผ ์ •๊ทœํ‘œํ˜„์‹(/#[^\s#]+/g)์œผ๋กœ ์ถ”์ถœ
  • findOrCreate ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ํ•ด์‹œํƒœ๊ทธ๊ฐ€ ์กด์žฌํ•˜๋ฉด ๊ฐ€์ ธ์˜ค๊ณ , ์•„๋‹ˆ๋ผ๋ฉด ์ƒ์„ฑ ํ›„ ๊ฐ€์ ธ์˜ด
  • ๊ฒฐ๊ด๊ฐ’์œผ๋กœ [๋ชจ๋ธ, ์ƒ์„ฑ ์—ฌ๋ถ€] ๋ฐ˜ํ™˜
  • ๋ชจ๋ธ์„ ์ถ”์ถœํ•ด์„œ post.addHashtags ๋ฉ”์„œ๋“œ๋กœ ๊ฒŒ์‹œ๊ธ€๊ณผ ์—ฐ๊ฒฐ

 

๐Ÿ“Œ ๋ฉ”์ธ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์‹œ ๊ฒŒ์‹œ๊ธ€ ๋ณด์—ฌ์ฃผ๋„๋ก ์ปจํŠธ๋กค๋Ÿฌ ์ˆ˜์ •

โ–ถ controllers/page.js

๋”๋ณด๊ธฐ
const { User, Post } = 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);
  }
}
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ ํ›„ ๊ฒฐ๊ณผ๋ฅผ twits์— ๋„ฃ์–ด ๋ Œ๋”๋ง
  • ์กฐํšŒํ•  ๋•Œ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž์˜ ์•„์ด๋””์™€ ๋‹‰๋„ค์ž„์„ JOINํ•ด์„œ ์ œ๊ณต
  • ์ตœ์‹ ์ˆœ์œผ๋กœ ๊ฒŒ์‹œ๊ธ€ ์ •๋ ฌ

 

 

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

๐Ÿ“ ํŒ”๋กœ์ž‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

 

๐Ÿ“Œ ์ปจํŠธ๋กค๋Ÿฌ์™€ ๋ผ์šฐํ„ฐ ์ž‘์„ฑ

โ–ถ controllers/user.js , routes/user.js

๋”๋ณด๊ธฐ

controllers/user.js

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);
  }
};

 

routes/user.js

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;

๐Ÿ”‘ POST /user/:id/folllow : ํŒ”๋กœ์šฐํ•  ์‚ฌ์šฉ์ž๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์กฐํšŒํ•œ ํ›„, ์‹œํ€„๋ผ์ด์ฆˆ์—์„œ ์ถ”๊ฐ€ํ•œ addFollowing ๋ฉ”์„œ๋“œ๋กœ ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์™€์˜ ๊ด€๊ณ„ ์ง€์ •

 

โญ ํŒ”๋กœ์ž‰ ๊ด€๊ณ„๊ฐ€ ์ƒ๊ฒผ์œผ๋ฏ€๋กœ 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) => {
    console.log('serialize');
    done(null, user.id);
  });

  passport.deserializeUser((id, done) => {
    console.log('deserialize');
    User.findOne({
      where: { id },
      include: [{
        model: User,
        attributes: ['id', 'nick'],
        as: 'Followers',
      }, {
        model: User,
        attributes: ['id', 'nick'],
        as: 'Followings',
      }],
    })
      .then(user => {
        console.log('user', user);
        done(null, user);
       })
      .catch(err => done(err));
  });

  local();
  kakao();
};

 

 

๐Ÿ“ ํ•ด์‹œํƒœ๊ทธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

 

๐Ÿ“Œ ์ปจํŠธ๋กค๋Ÿฌ์™€ ๋ผ์šฐํ„ฐ ์ž‘์„ฑ

โ–ถ controllers/page.js , routes/page.js

๋”๋ณด๊ธฐ

controllers/page.js

const {
  renderProfile, renderJoin, renderMain, renderHashtag,
} = require('../controllers/page');
...
router.get('/hashtag', renderHashtag);

 

routes/page.js

...
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);
  }
};
  • GET /hashtag : ํ•ด์‹œํƒœ๊ทธ๋กœ ์กฐํšŒ, ์—†๋Š” ๊ฒฝ์šฐ ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ, ์žˆ๋Š” ๊ฒฝ์šฐ getPosts ๋ฉ”์„œ๋“œ๋กœ ๋ชจ๋“  ๊ฒŒ์‹œ๊ธ€์„ ๊ฐ€์ ธ์˜ด
  • ์กฐํšŒ ํ›„ ๋ฉ”์ธ ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋งํ•˜๋ฉด์„œ ์กฐํšŒ๋œ ๊ฒŒ์‹œ๊ธ€๋งŒ twits์— ๋„ฃ์–ด ๋ Œ๋”๋ง

 

 

[์ถœ์ฒ˜] ์กฐํ˜„์˜, ใ€ŽNode.js ๊ต๊ณผ์„œใ€, ๊ธธ๋ฒ—(2020), p417-472


๋นˆ์นธ ์ฑ„์šฐ๊ธฐ ๋ฌธ์ œ (๋นˆ์นธ์„ ๋“œ๋ž˜๊ทธํ•ด์„œ ์ •๋‹ต์„ ๋งžํ˜€ ๋ณด์„ธ์š”!)

  1. ์ธ์ฆ์„ ํŽธ๋ฆฌํ•˜๊ฒŒ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“ˆ๋กœ, ์œ„ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋กœ๊ทธ์ธ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ ๋ชจ๋“ˆ์€? (passport)
  2. ๋ผ์šฐํ„ฐ์˜ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋ฉฐ, ํŽ˜์ด์ง€๋ฅผ ํ™”๋ฉด์— ๋ Œ๋”๋งํ•˜๋Š” ์—ญํ• ์„ ๋งก์€ ๊ฒƒ์€? (controller)
  3. (์„œ๋น„์Šค)๋Š” ํ•ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ์˜ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„, (์ปจํŠธ๋กค๋Ÿฌ)๋Š” ์š”์ฒญ(req)๊ณผ ์‘๋‹ต(res)์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋‹ด๋‹น์ด๋‹ค.
  4.  ๊ฐ™์€ ํ…Œ์ด๋ธ” ๊ฐ„ N:M ๊ด€๊ณ„์—์„œ foreignKey๊ฐ€ followerId์ด๋ฉด as๋Š” (Followings)์ด๋‹ค.
  5. ๋‹ค์Œ์€ passport/index.js์˜ ์ฝ”๋“œ ์ผ๋ถ€์ด๋‹ค.
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));
  });

๐Ÿ“Œ (serializeUser) : ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ์ฒด์—์„œ ์•„์ด๋””๋ฅผ ์ถ”๋ ค (์„ธ์…˜)์— ์ €์žฅ

๐Ÿ“Œ (deserializeUser) : ์„ธ์…˜์— ์ €์žฅํ•œ ์•„์ด๋””๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ์ฒด ๋ถˆ๋Ÿฌ์˜ค๊ธฐ

⇒ (์„ธ์…˜)์— ๋„ˆ๋ฌด ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์ง€ ์•Š๊ธฐ ์œ„ํ•จ

6. ์‚ฌ์ง„์ด ์—…๋กœ๋“œ ๋œ ๊ฒŒ์‹œ๊ธ€์„ ์ €์žฅํ•  ๋•Œ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ด๋ฏธ์ง€ (๊ฒฝ๋กœ)๊ฐ€ ์ €์žฅ๋˜๊ณ , ์ด๋ฏธ์ง€๋Š” (์„œ๋ฒ„ ๋””์Šคํฌ(uploads ํด๋”))์— ์ €์žฅ๋จ

 

์ฝ”๋“œ ๋ฌธ์ œ

 

1. 

๋‹ค์Œ์€ routes/page.js์˜ ์ฝ”๋“œ ์ผ๋ถ€์ž…๋‹ˆ๋‹ค.

const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const {
  renderProfile, renderJoin, renderMain, renderHashtag,
} = require('../controllers/page');

const router = express.Router();

router.use((๋นˆ์นธ1) => {
  ๋นˆ์นธ2
  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;

tabase, username, password

์ •๋‹ต

๋นˆ์นธ1 : req, res, next

๋นˆ์นธ 2 : res.locals.user = req.user; 

 

2.

๋‹ค์Œ์€ routes/auth.js์˜ ์ฝ”๋“œ ์ผ๋ถ€์ž…๋‹ˆ๋‹ค.

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', ๋นˆ์นธ1, join); 

// POST /auth/login
router.post('/login', ๋นˆ์นธ2, login);

// GET /auth/logout
router.get('/logout', ๋นˆ์นธ3, logout);

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

// GET /auth/kakao/callback
router.get('/kakao/callback', passport.authenticate('kakao', {
  failureRedirect: '/?error=์นด์นด์˜ค๋กœ๊ทธ์ธ ์‹คํŒจ',
}), (req, res) => {
  res.redirect('/'); // ์„ฑ๊ณต ์‹œ์—๋Š” /๋กœ ์ด๋™
});

module.exports = router;

 

์ •๋‹ต :

๋นˆ์นธ 1 : isNotLoggedIn

๋นˆ์นธ 2 : isNotLoggedIn

๋นˆ์นธ 3 : isLoggedIn


Node.js #2 

Editor : ํŒŒ์˜ค๋ฆฌ

728x90

๊ด€๋ จ๊ธ€ ๋”๋ณด๊ธฐ