본문 바로가기

BackEnd/Node

[노드교과서] 섹션 9. 노드버드 SNS 만들기

9.1 프로젝트 구조 갖추기

 1. NodeBird SNS 서비스

  ⇒ 기능 : 로그인, 이미지 업로드, 게시글 작성, 해시태그 검색, 팔로잉

   - express-generator 대신 직접 구조를 갖춘다.

   - 프런트엔드 코드보다 노드 라우터 중심으로 학습을 진행한다.

   - 관계형 데이터베이스 MySQL을 사용한다.

 2. 프로젝트 시작하기

  ⇒ nodebird 폴터를 생성 후 npm init을 통해 package.json 파일을 생성한다.

  ⇒ 시퀄라이즈 폴더 구조를 생성한다.

$ npm i sequelize mysql2 sequelize-cli
// 글로벌 설치를 안하고 디펜던시에서 찾아서 설치를 하려면 npx를 붙여 입력해야한다.
$ npx sequelize init

 3. 폴더 구조 설정

  ⇒ views(템플릿 엔진), routes(라우터), public(정적 파일), passwort(패스포트) 폴더를 생성한다.

 4. 패키지 설치와 nodemon

  ⇒ npm 패키지 설치 후 nodemon도 설치한다.

   - nodemon은 서버 코드가 변경 되었을 때 자동으로 서버를 재시작한다.

$ npm i express cookie-parser express-session morgan multer dotenv nunjucks
$ npm i passport passport-local passport-kakao bcrypt
$ npm i nodemon -D

 

   - nodemon은 콘솔 명령어이기 때문에 package.json scripts에 명령 실행문을 설정해야 한다.

{
  "name": "nodebird",
  "version": "0.0.1",
  "description": "익스프레스로 만드는 SNS 서비스",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "JaeikKim",
  "license": "MIT",
  "devDependencies": {
    "nodemon": "^3.0.2"
  },
  "dependencies": {
    "bcrypt": "^5.1.1",
    "cookie-parser": "^1.4.6",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "express-session": "^1.17.3",
    "morgan": "^1.10.0",
    "multer": "^1.4.5-lts.1",
    "mysql2": "^3.7.0",
    "nunjucks": "^3.2.4",
    "passport": "^0.7.0",
    "passport-kakao": "^1.0.1",
    "passport-local": "^1.0.0",
    "sequelize": "^6.35.2",
    "sequelize-cli": "^6.6.2"
  }
}

 5. app.js

  ⇒ 노드 서버의 핵심인 app.js 파일을 작성한다.

   - .env 파일도 함께 추가해야 한다.

// .env
COOKIE_SECRET=cookiesecret

 

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

dotenv.config();  // process.env

// 라우팅을 위한 페이지
const pageRouter = require('./routes/page');
const authRouter = require('./routes/auth');
const postRouter = require('./routes/post');
const userRouter = require('./routes/user');
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,
}); 

// 테이블을 다시 생성하는 조건 force: true
sequelize.sync({ force : false})
    .then(() => {
        console.log('데이터베이스 연결 성공');
    })
    .catch((err) => {
        console.error(err);
    });

app.use(morgan('dev'));
// (개발자가 업로드할 파일들의 경로와 유저 업로드 경로를 구분해서 관리하자)
// public폴더를  static폴더로 만든다.
// 보안상 브라우저에서는 파일접근이 불가하나 public만 허용한다.
// __dirname은 lecture폴더를 가르킨다.
app.use(express.static(path.join(__dirname, 'public')));

// uploads 폴더를 프론트에서 /img 주소 아래에서 접근이 가능하도록 설정한다.
app.use('/img', express.static(path.join(__dirname,'uploads')));

// body-parser
app.use(express.json());  // json요청을 받을 수 있게 하고(req.body를 ajax json 요청으로부터 받아온다)
app.use(express.urlencoded({extended : false})); // form요청을 받을 수 있게 함(req.body를 폼으로부터 데이터를 받아온다)

// cookie-parser
app.use(cookieParser(process.env.COOKIE_SECRET));  // {connect.sid : 12312312312312} 쿠키파서는 객체로 만들어준다.

app.use(session({
    resave : false,
    saveUninitialized : false,
    secret : process.env.COOKIE_SECRET,
    cookie: {
        httpOnly : true,  // 자바스크립트에서 접근을 못하게 
        secure : false,   // https를 적용할 때 true로 변경처리
    }
}));
app.use(passport.initialize()); // pasport연결 시 req.user, req.login, req.isAuthenticate, req.logout 생성한다.
app.use(passport.session());    // 7. connect.sid라는 이름으로 세션 쿠키가 브라우저로 전송된다.
// 브라우저 connect.sid = 12312312312312이 저장된다.


app.use('/', pageRouter);
app.use('/auth', authRouter);
app.use('/post', postRouter);
app.use('/user', userRouter);
app.use((req,res,next) => { // 404 NOT FOUND
    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. 라우터 생성

   - middlewares/index.js : 사용자가 로그인 했는지 로그인하지 않았는지 체크하는 미들웨어이다.

// 현재 로그인 여부를 확인하는 미들웨어
exports.isLoggedIn = (req, res, next) => {
    if(req.isAuthenticated()) { // 패스포트를 통한 로그인 여부
        next();
    } else {
        res.status(403).send('로그인 필요');
    }
}

exports.isNotLoggendIn = (req, res, next) => {
    if(!req.isAuthenticated()) {
        next();
    } else {
        const message = encodeURIComponent('로그인한 상태입니다.');
        res.redirect(`/?error=${message}`);  // localhost:8001?error=메시지
    }
}

 

   - routes/page.js :템플릿 엔진을 렌더링하는 라우터이다. 

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


// res.loccals는 미들웨어 간의 공유되는 데이터이다.
// 미들웨어는 항상 next를 호출해야 다음으로 넘어간다.
router.use((req,res, next) => {
    res.locals.user = req.user;

    // req.user가 Followers, Followings의 정보를 가짐으로서 req가 커지는 부작용이 있다.
    // 해당 경우에는 deserializeUser에서 하는 방법도 있지만..
    // 현재 소스 위치에서 await User.find를 사용해 값을 얻어 처리할 수도 있는 점을 인지해야 한다.
    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', isNotLoggendIn, renderJoin);
router.get('/', renderMain);
router.get('/hashtag', renderHashtag);

module.exports = router;

 

 - routes/auth.js : 로그인을 담당하는 라우터이다.

const express = require('express');
const passport = require('passport');
const { isLoggedIn, isNotLoggendIn } = require('../middlewares');
const { join, login, logout } = require('../controllers/auth');

const router = express.Router();

// POST /auto/join
router.post('/join', isNotLoggendIn, join);

// POST /auth/login
router.post('/login', isNotLoggendIn, login);

// get /auth/logout
router.get('/logout', isLoggedIn, logout);

// get /auth/kakao
router.get('/kakao', passport.authenticate('kakao'));  // 카카오톡 로그인 화면으로 redirect

// GET /auth/kakao/callback
router.get('/kakao/callback', passport.authenticate('kakao', {
    failureRedirect: '/?error=카카오로그인 실패',
}), (req, res) => {
    res.redirect('/'); // 성공 시에는 /로 이동
});

module.exports = router;

 

 - routes/post.js : 글작성을 담당하는 라우터이다.

const express = require('express');
const router = express.Router();

const { isLoggedIn, isNotLoggendIn } = require('../middlewares');
const fs = require('fs');
const multer = require('multer');
const path = require('path');
const { afterUploadImage, uploadPost} = require('../controllers/post')

try {
    fs.readdirSync('uploads');
} catch(err) {
    fs.mkdirSync('uploads');
}

const upload = multer({
    storage : multer.diskStorage({

        destination(req, file, cd) {
            cd(null, 'uploads/');
        },
        filename(req, file, cd) {
            // 이미지 파일의 중복을 막기 위해 년월일시간을 붙여 중복을 제거할 수 있다.
            // 이미지.png -> 이미지240101120000.png
            const ext = path.extname(file.originalname);    // 확장자
            cd(null, path.basename(file.originalname, ext) + Date.now() + ext);
            console.log('ext',ext);
            console.log('path.basename(file.originalname, ext)',path.basename(file.originalname, ext));
        }
    }),
    limits : { fileSize : 5 * 1024 * 1024},
});


router.post('/img', isLoggedIn, upload.single('img'), afterUploadImage);

// multer의 설정을 별도로 가기 때문에 추가적으로 선언한다.
const upload2 = multer();
router.post('/', isLoggedIn, upload.none(),  uploadPost);

module.exports = router;

 

 - routes/user.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;

 

 7. 컨트롤러와 서비스

  ⇒ 컨트롤러는 라우터에 마지막에 위치하는 미들웨어로 응답을 보내는 역할을 수행한다.

   - 컨트롤러에서 비지니스 로직을 서비스(service)라는 개념으로 분리하는 경우도 많다.

    서비스는 해당 컨트롤러의 핵심 비즈니스 로직을 담당하지만 요청(req)와 응답(res)에 대해서는 모른다.

 

   - controllers/page.js

const Post = require('../models/post');
const User = require('../models/user');
const Hashtag = require('../models/hashtag');
exports.renderProfile = (req, res, next) => {
    // 두번째 인수는 프론트에 넘기는 값이다.
    res.render('profile', {title : '내 정보 - NodeBird'});
};
exports.renderJoin = (req, res, next) => {
    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, attributes: ['id','nick']}],
              order : [['createdAt','DESC']],
          });
      }

      res.render('main', {
          title : `${query} | NodeBird`,
          twits : posts,
      })
  } catch (err) {
      console.error(err);
      next(err);
  }
};

// 라우터는 컨트롤러를 호출(요청, 응답을 안다)
// 컨트롤러는 서비스(요청, 응답을 모른다)를 호출한다.
// 라우터 -> 컨트롤러 -> 서비스

 

   - controllers/auth.js

const User = require('../models/user');
const bcrypt = require ('bcrypt');
const passport = require('passport');
exports.join = async (req, res, next) => {
    // ES2015 구조분해 할당을 통한 파라미터 설정
    const {nick, email, password} = req.body;
    try {
        const exUser = await User.findOne({where: {email}});
        if (exUser) {
            return res.redirect('/join?error=exist');
        } else {
            const hash = await bcrypt.hash(password, 12);
            await User.create({
                email,
                nick,
                password : hash,
            });
            return res.redirect('/'); // status 302
        }
    } catch (err) {
        console.error(err);
        next(err);
    }

}

// POST /auto/login(로그인 요청을 하면 여기로 와 localstrategy를 탄다.
exports.login = (req, res, next) => {
    console.log('test222');
    // 해당 패턴은 미들웨어 확정 패턴이다.
    passport.authenticate('local', (authError, user, info) => {
        console.log('test333');
        // passport의 done 호출시 이부분으로 온다.
        if(authError) { // 서버실패
            console.error(authError);
            next(authError);
        }
        if(!user) {     // 로직실패
            return res.redirect(`/?error=${info.message}`);
        }
        return req.login(user, (loginError) => {  // 로그인 성공
           if(loginError) {
               console.error(loginError);
               next(loginError);
           }
           return res.redirect('/');
        })
    })(req,res,next);
};

// 세션쿠키를 제거하는 행위이다.
exports.logout = (req, res) => {
    console.log('logout');
    req.logout(() => {
        res.redirect('/');
    })
};

 

   - controllers/post.js

const Post = require('../models/post');
const Hashtag = require('../models/hashtag');
exports.afterUploadImage = (req, res) => {
    console.log("req.file : " + req.file);
    console.log(`/img/${req.file.filename}`)
    res.json({ url : `/img/${req.file.filename}`});
};

exports.uploadPost = async (req, res ,next) => {
    //        req.body.content
    //        req.body.url
    try {

        const post = await Post.create({
            content : req.body.content,
            img : req.body.url,
            UserId : req.user.id,
        });
        // /#[^\s#]*/g  : 해시태그 찾는 정규식
        const hashtags = req.body.content.match(/#[^\s#]*/g);
        if(hashtags) {
            const result = await Promise.all(
                // promise 배열
                hashtags.map((tag) => {
                    // findOrCreate : 해당 해시태그가 있으면 찾아오고 없으면 만들어서 가져온다.
                    return Hashtag.findOrCreate({
                        where : { title : tag.slice(1).toLowerCase()}
                    });
                })
            );
            console.log('result', result);
            // post와 hashtag관의 다대다 관계를 설정한다.
            await post.addHashtags(result.map(r => r[0]));
        }
        res.redirect('/');
    } catch(err) {
        console.error(err);
    }
};

 

   - controllers/user.js

const User = require('../models/user');
exports.follow = async (req, res) => {
    // req.user.id, req.params.id
    try {
        const user = await User.findOne( {where : { id: req.user.id}});
        if(user) {
            console.log('진입');
            await user.addFollowing(parseInt(req.params.id, 10));
            res.send('success');
        } else {
            res.status(404).send('no user');
        }


    } catch (err) {
        console.error(err);
        next(err);
    }

};

 

9.2 데이터베이스 세팅하기

 1. 모델 생성

   - 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,
            },
            provider: {
                type : Sequelize.ENUM('local', 'kakao'),
                allowNull : false,
                defaultValue : 'local',
            },
            snsId : {
                type : Sequelize.STRING(30),
                allowNull : true,
                unique : true,
            }
        }, {
            sequelize,
            timestamps : true, // createAt, updatedAt
            underscored : false,
            modelName : 'User',
            tableName : 'users',
            paranoid : true,     // deletedAt 유재 삭제일(논리삭제)
            chatset : 'utf8',             // 어떤 방식의 문자로 저장할지
            collate : 'utf8_general_ci',  // 저장된 문자를 어떤 방식으로 정렬할지
        });

        // Sequelize에서는 email, snsId 둘중 하나가 입력이 되어있는지 체크해주는 기능이 존재한다.

    }

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

        // Follow 접근하는 방법 : db.sequelize.model.Follow

    }
}
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 : false,
            },

        }, {
            sequelize,
            timestamps : true, // createAt, updatedAt
            paranoid : false,     // deletedAt 유재 삭제일(논리삭제)
            underscored : false,
            modelName : 'Post',
            tableName : 'posts',
            chatset : 'utf8mb4',             // 어떤 방식의 문자로 저장할지
            collate : 'utf8mb4_general_ci',  // 저장된 문자를 어떤 방식으로 정렬할지
        })
    }

    static associate(db) {
        db.Post.belongsTo(db.User);
        db.Post.belongsToMany(db.Hashtag, {through : 'PostHashtag'});
        // PostHashtag에 접근하는 방법 : db.sequelize.model.PostHashtag
    }
}

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, // createAt, updatedAt
            paranoid : false,     // deletedAt 유재 삭제일(논리삭제)
            underscored : false,
            modelName : 'Hashtag',
            tableName : 'hashtags',
            chatset : 'utf8mb4',             // 어떤 방식의 문자로 저장할지
            collate : 'utf8mb4_general_ci',  // 저장된 문자를 어떤 방식으로 정렬할지
        })
    }

    static associate(db) {
        db.Hashtag.belongsToMany(db.Post, {through : 'PostHashtag'});
    }
}

module.exports = Hashtag;

 

   - models/index.js

const Sequelize = require('sequelize');
// const User = require('./user');
// const Post = require('./post');
// const Hashtag = require ('./hashtag');
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); // index.js
fs.readdirSync(__dirname)
  .filter(file => {
      return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
    })
    .forEach(file => {
      const model = require(path.join(__dirname,file));
      console.log(file, model.name);
      db[model.name] = model;
      model.initiate(sequelize);
    });

Object.keys(db).forEach(modelName => {
  console.log(modelName);
  if(db[modelName].associate) {
    db[modelName].associate(db);
  }
});


// 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);
// npx sequelize db:create



module.exports = db;

 3. associate

  ⇒ 모델 간의 관계들 associate에 작성한다.

   - 1:N : hasMany와 belongTo

   - N:M : belonsToMany

 4. 팔로잉 - 팔로워 다대다 관계

  ⇒ User(다) : User(다)

   - 다대다 관계이므로 중간 테이블(Follow)가 sequelize associate를 통해 생성된다.

   - 모델 이름이 동일하므로 구분이 필요해 as(별칭)과 foreignKey(외래키)를 통해 관계를 맺는다.

   - 시퀄라이즈는 as 이름을 바탕으로 자동으로 addFollower, getFollowers, addFollowing, getFollwings 메서드를 생성

노드교과서 교안 참고

 5. 시퀄라이즈 설정

  ⇒ 시퀄라이즈 설정은 config/config.json에서 한다. (개발은 'development')

{
  "development": {
    "username": "root",
    "password": "0000",
    "database": "nodebird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  ...
}

 

  ⇒ 설정 파일 작성 후 nodebird 데이터베이스를 콘솔 명령어 npx sequelize db/create를 입력해 생성한다.

$ npx sequelize db:create

 

9.3 passport 모듈로 로그인

 2. 패스포트 모듈 작성

  ⇒ passport/index.js

   - passport.serializeuser : req.session 객체에 어떤 데이터를 저장할 지 선택한다.

    사용자 정보를 다 들고 있으면 메모리를 많이 차지하기 때문에 사용자의 아이디만 저장한다.

   - passport.deserializeUser

    : req.session에 저장된 사용자 아이디를 바탕으로 DB 조회로 사용자 정보를 얻어 req.user에 저장한다.

const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');

module.exports = () => {

    // 5. req.login을 하면 이부분이 실행된다.
    passport.serializeUser((user, done) => {  // user === exUser
        // 6. req.session에 사용자 아이디만 저장되어 세션이 생성된다.
        done(null, user.id);  // user id만 추출
    });
    // 세션 { 12312312312312 : 1 }  { 세션쿠키 : 유저아이디 }
    // -> 사용자 정보가 세션쿠키로 저장되므로 메모리에 올라간다.
    //    메모리에 많은 사용자 정보들이 들어가게 될 경우 너무 크기 때문에 유저id만 저장한다.

    // 사용자 id를 찾아 이부분을 실행시킨다.
    passport.deserializeUser((id, done) => {  // id : 1
        User.findOne( {
            where : { id },
            include : [
                {
                    model : User,
                    attributes : ['id', 'nick'],
                    as : 'Followers',
                },  //  팔로잉
                {
                    model: User,
                    attributes : ['id', 'nick'],
                    as : 'Followings'
                },  // 팔로워
            ],
        })
            .then((user) => done(null, user))    // req.user 생성
            .catch(err => done(err));

        // req.session은 content.sid 쿠키로 세션에서 찾을 때 생성된다.
    });
    local();
    kakao();
}

 

   - passport/localStrategy.js

const passport = require('passport');
const { Strategy : LocalStrategy } = require('passport-local');
const bcrypt = require('bcrypt');
const User = require('../models/user');

// 로그인을 시켜도 되는지 안되는지 판단을 하는 부분이다.
module.exports = () => {
    // passport에 등록해둔 LocalStrategy
    passport.use(new LocalStrategy({
        usernameField : 'email',    // req.body.email
        passwordField : 'password', // req.body.password
        passReqToCallback : false  // 로직 상 request가 필요할 경우 true로 변경한다.
    // passReqToCallback : true일 경우 req 사용가능
    // }, async (req, email, password, done)));
    }, async (email, password, done) => {   // 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 (err) {
            console.error(err);
            // 서버실패
            done(err);
        }
    }));
};

 

   - passport/kakaoStategy.js

const passport = require('passport');
const { Strategy : KakaoStrategy } = require('passport-kakao');
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(err) {
            console.error(err);
            done(err);
        }

    }));
};

 3. 패스포트 처리과정

  ⇒ 로그인 과정

   1. /auth/login 라우터를 통해 로그인 요청이 들어온다.
   2. 라우터에서 passport.authenticate 메서드를 호출한다.
   3. 로그인 전략(LocalStrategy)를 수행한다.
   4. 로그인 성공 시 사용자 정보 객체와 함께 req.login을 호출한다.
   5. req.login을 하면 passport/index.js에 잇는 passport.serializeUser가 실행된다.
   6. req.session에 사용자 아이디만 저장해서 세션을 생성한다.(메모리문제해소)
   7. express-session에 설정한 대로 브라우저에 connnect.sid 세션 쿠키를 전송한다.
   8. 로그인 완료

  ⇒ 로그인 이후의 과정
   1. 요청이 들어옴(어떤 요청이든 상관없다.)
   2. 라우터에 요청이 도달하기 전에 passport.session() 미들웨어가 passport.deserializeUser 메서드를 호출한다.
   3. connect.sid 세션 쿠키를 읽고 세션 객체를 찾아 req.session을 만든다.
   4. req.session에 저장된 아이디로 데이터베이스에서 사용자를 조회한다.
   5. 조회된 사용자 정보를 req.user에 저장한다.
   6. 라우터에서 req.user 객체를 사용한다.

 

9.5.1
- 스스로 해보기
팔로잉 끊기(시퀄라이즈의 destroy 메서드와 라우터를 활용해라)
프로필 정보 변경하기(시퀄라이즈의  upload 메서드와 라우터를 활용해라)
게시글 좋아요 누르기 및 좋아요 취소하기(사용자-게시글 모델간 N:M관계 정립 후 라우터 활용)
게시글 삭제하기(등록자와 현재 로그인한 사용자가 같을 때, 시퀄라이즈의 destroy 메서드와 라우터를 활용)
사용자 이름을 누르면 그 사용자의 게시글만 보여주기
매번 데이터베이스를 조회하지 않도록 deserializeUser 캐싱하기(객체 선언 후 객체에 사용자 정보 저장, 객체 안에 캐싱된 값이 잇으면 조회)

9.5.2 핵심 정리
 - 서버는 요청이 응답하는 것이 핵심 임무이므로 요청을 수락하든 거절하든 상관없이 반드시 응답해야 합다.
   이때 한 번만 응답해야 에러가 발생하지 않는다.
 - 개발 시 서버를 매번 수동으로 재시작하지 않으려면 nodemon을 사용하는 것이 좋다.
 - dotoenv 패키지와 .env파일로 유출되면 안되는 비밀키를 관리한다.
 - 라우터는 routes 폴더에, 데이터베이스는 models폴더에, html 파일은 views폴더에 각각 구분해서 저장하면 프로젝트 규모가 커져도 관리하기 쉽다.
 - 라우터에서 응답을 보내는 미들웨어를 컨트롤러라고 한다. 컨트롤러도 따로 분리하면(controllers 폴더)코드를 관리할 때 편하다.
 - 데이터베이스를 구성하기 전에 데이터 간 1:1, 1:N, N:M 관계를 잘 파악해야 한다.
 - middlewares/index.js 처럼 라우터 내에 미들웨어를 사용할 수 있다는 것을 기억하자.
 - Passport의 인증 과정을 기억해 둬야한다.  특히 serializeUser와 deserializeuser가 언제 호출되는지 파악해야 한다.
 - 프런트엔드 form 태그의 인코딩 방식이 multipart일 때는 multer와 같은 multipart 처리용 패키지를 사용하는 것이 좋다. 
 
9.6 함께 보면 좋은 자료
 - Passport 공식 문서 : http://www.passportjs.org
 - passport-local 공식 문서 : https://www.npmjs.com/package/passport-local
 - passport-kakao 공식 문서 : https://www.npmjs.com/package/passport-kakao
 - bcrypt 공식 문서 : https://www.npmjs.com/package/bcrypt
 - 카카오 로그인 : https://developers.kakao.com/docs/latest/ko/kakaologin/common