Node.JS ) expressServer - ORM, SNS서비스 만들기, sequelize, passport 활용(localStrategy, kakaoStrategy), bcrypt
Programming/JS

Node.JS ) expressServer - ORM, SNS서비스 만들기, sequelize, passport 활용(localStrategy, kakaoStrategy), bcrypt

반응형

목차

     


     

    2022.04.29 - [Programming/BACKEND] - Node.JS)04.26( ExpressServer - ORM( sequelize ), 게시판 )

     

    1. 사전 설정

    1.1. npm 모듈 설치 및 환경 설정

    • npm init (npm 환경 구성)
    • npm i express (express 설치)
    • npm i -D nodemon (nodemon 설치)
    • package.json 
      • "start": "nodemon app"로 수정
    • npm i sequelize sequelize-cli mysql2
      • sequelize, mysql2 모듈 설치
    • npx sequelize init (sequelize 환경 구성)
    • /config/config.json 파일 수정 (pw와 db 수정)
      • "password":"adminuser", "database":"nodegram" 으로 변경
    • npm i cookie-parser express-session nunjucks dotenv 
    • npm i passport passport-local passport-kakao bcryp

     

    • passport, public, routers, uploads, views 폴더 생성

     

    • .env 환경변수파일 생성하여 암호화 코드를 작성한다.

    COOKIE_SECRET=nodejssns

     


     

    1.2. 모델 생성

    1.2.1. index.js

    const Sequelize = require('sequelize');
    const env = process.env.NODE_ENV || 'development';
    const config = require(__dirname + '/../config/config.json')[env];
    const db = {};
    
    const User = require('./user');
    const Post = require('./post');
    const Hashtag = require('./hashtag');
    
    let sequelize = new Sequelize( config.database, config.username, config.password, config );
    
    db.sequelize = sequelize;
    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;

     

    1.2.2. user.js

    user모델로 users 테이블과 follow테이블이 생성되는데,
    follow테이블이 생성되는 부분이 복잡할 수 있으니 유심히 봐야한다.

    const Sequelize = require('sequelize');
    
    // id는 자동생성
    // email(문자 50)null 고유값, nick(문자30)notnull, password(문자100)notnull, provider(문자20)notnull default='local', snsid(문자30)null
    // 모델명:User, 테이블명 users, 나머지 associate 설정은 posts테이블과 같다
    
    module.exports = class User extends Sequelize.Model{
        static init( sequelize ){
            return super.init({
                email:{ 
                    type:Sequelize.STRING(50),
                    allowNull:true,
                    unique:true,    // null값은 고유값 적용이 되지 않는다. (null이 여러개 있어도 괜춘)
                 }, 
                nick:{
                    type:Sequelize.STRING(30),
                    allowNull:false,
                }, 
                password:{
                    type:Sequelize.STRING(100),
                    allowNull:true,	// kakao로 로그인 시, 비번이 저장되지 않기 때문
                }, 
                provider:{
                    type:Sequelize.STRING(20),
                    allowNull:false,
                    defaultValue:'local',
                }, 
                snsid:{
                    type:Sequelize.STRING(50),
                    allowNull:true,
                },
            },{
                sequelize,
                timestamps:true,// 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
                underscored:false,
                paranoid:false, 
                modelName:'User', 
                tableName:'users',
                charset:'utf8mb4',
                collate:'utf8mb4_general_ci',
            })
        }
        static associate(db){
            //db.User.hasMany(db.Post, {foriegnKey:'', sourceKey:''});
            db.User.hasMany(db.Post);
    
            //follower를 위한 것    
            db.User.belongsToMany(db.User, {foreignKey:'followingId', as:'Followers',through:'Follow'}); 
    
            //following를 위한 것
            db.User.belongsToMany(db.User, {foreignKey:'followerId', as:'Followings', through:'Follow'}); 
        }
    }
    
    // 유저1이 유저2를 팔로잉한다 (유저1 -> 유저2)
    // 유저1(followers), 유저2(followings)로 레코드 생성
    
    // 반대로 맞팔 하면 (유저1 <- 유저2)
    // 유저2(followers), 유저1(followings)로 레코드 생성
    
    // 유저1(followers), 유저3(followings)
    // 유저3(followers), 유저1(followings)
    // 유저3(followers), 유저2(followings)
    // 유저2(followers), 유저3(followings)
    
    // 유저1의 팔로워(followers)를 조회하려면?
    // -> following에서 유저1을 조회
    
    // 유저1이 팔로잉(following)하는 유저를 조회하려면?
    // -> follower에서 유저1을 조회

    위 모델로 생성된 users(좌) / follow(우) 테이블

     

    1.2.3. post.js

    users테이블과 posts테이블이 1:N 관계가 성립되어,
    users의 id가 posts 테이블에 외래키로 userId컬럼이 생성된다.

    // 모델명 : Post, 테이블명:posts
    // 필드 : content(140)notnull, img(200)null, user와 1:n 관계표시 - user모델 생성후 설정
    // timestamp true, underscored false, paranoid false 나머지 기존설정
    const Sequelize = require('sequelize');
    
    module.exports = class Post extends Sequelize.Model{
        static init( sequelize ){
            return super.init({
                content:{
                    type:Sequelize.STRING(200),
                    allowNull:false,
                },
                img:{
                    type:Sequelize.STRING(200),
                    allowNull:true,
                },
            },{
                sequelize,
                timestamp:true,   // 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
                underscored:false,
                paranoid:false,     // 이 멤버가 true면, deleteAt(삭제시간) 필드가 생성된다
                modelName:'Post',   // sequelize가 사용할 모델(테이블) 이름
                tableName:'posts',  // 데이터베이스의 자체 테이블의 이름
                charset:'utf8mb4',
                collate:'utf8mb4_general_ci',
            });
        }
        static associate(db){
            //db.Post.belongTo(db.User, {foreignKey:'', targetKey:'' })
            db.Post.belongsTo(db.User);
            db.Post.belongsToMany(db.Hashtag,{through:'PostHashtag'});
        }
    };

    posts 테이블

     

    1.2.4. hashtag.js

    • post의 id는 posthashtag 테이블의 외래키로 postId가 되고,
    • hashtags의 id는 posthashtag테이블의 외래키로 hashtagId컬럼이 된다.
    // hashtag.js
    
    const Sequelize = require('sequelize');
    module.exports = class Hashtag extends Sequelize.Model{
        static init(sequelize){
            return super.init({
                title:{
                    type:Sequelize.STRING(20),
                    allowNull:false,
                    unique:true,
                },
            },{
                sequelize,
                timestamps:false,// 이 속성이 true면, createAt(생성시간), updateAt(수정시간) 필드가 자동생성된다.
                underscored:false,
                paranoid:false, 
                modelName:'Hashtag', 
                tableName:'hashtags',
                charset:'utf8mb4',
                collate:'utf8mb4_general_ci',
            });
        }
        static associate(db){
            db.Hashtag.belongsToMany(db.Post, {through:'PostHashtag'});
        }
    };
    
    // hashtags 테이블의 필드는 id와 title 둘뿐이다.
    // posts 테이블과 M:N관계가 성립
    
    // posts 테이블
    // 1번 게시물 : #사과 #배
    // 2번 게시물 : #배 #오렌지
    // 3번 게시물 : #오렌지 #사과
    
    // 중간에 다리역할을 하는 테이블이 필요하다.
    // 위 코드로 자동 생성할 수 있다.
    // 1번 게시물 - 1번 해시태그
    // 1번 게시물 - 2번 해시태그
    // 2번 게시물 - 2번 해시태그
    // 2번 게시물 - 3번 해시태그
    // 3번 게시물 - 1번 해시태그
    // 3번 게시물 - 3번 해시태그
    
    // hashtags 테이블
    // 1번 해시태그 - 사과
    // 1번 해시태그 - 배
    // 1번 해시태그 - 오렌지

    hashtags(좌), posthashtag(우) 테이블

     


     

    1.3. routers / 라우터 작성

     

    1.3.1. auth.js

    주로 로그인에 관련한 라우터

    // auth.js 주로 로그인에 관련한 내용
    
    const express = require('express');
    const User = require('../models/user');
    const bcrypt = require('bcrypt');
    const passport = require('passport');
    
    const router = express.Router();
    
    
    module.exports = router;

     

    1.3.2. page.js

    const express = require('express');
    const {Post, User, Hashtag } = require('../models');
    const router = express.Router();
    
    
    module.exports = router;

     

    1.3.3. user.js

    const express = require('express');
    const User = require('../models/user');
    
    const router = express.Router();
    
    
    module.exports = router;

     

    1.3.4. post.js

    해당 스크립트에서 multer가 사용되므로, npm i multer 명령어로 multer 모듈을 설치한다.

    const express = require('express');
    const multer = requires('multer');
    const path = require('path');
    const fs = require('fs');
    const { Post, User, Hashtag} = require('../models');
    
    const router = express.Router();
    
    
    module.exports = router;

     


     

    1.4. app.js

    const express = require('express');
    const cookieParser = require('cookie-parser');
    const session = require('express-session');
    const path = require('path');
    const nunjucks = require('nunjucks');
    const dotenv = require('dotenv');
    const passport = require('passport');
    
    const app = express();
    app.set('port', process.env.PORT || 3000);
    
    // dotenv 설정은 가장 위에 쓰는것이 좋다.
    dotenv.config();
    
    app.set('view engine', 'html');
    nunjucks.configure('views', {express:app, watch:true,});
    
    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,
        },
    }));
    
    
    const {sequelize} = require('./models');
    const { prependListener } = require('process');
    sequelize.sync({force:false})
    .then(()=>{
        console.log('db연결 성공');
    })
    .catch((err)=>{
        console.error(err);
    });
    
    
    // 라우터 require
    const pageRouter = require('./routers/page');
    const postRouter = require('./routers/post');
    const authRouter = require('./routers/auth');
    const userRouter = require('./routers/user');
    
    app.use('/', pageRouter);
    app.use('/post', postRouter);
    app.use('/auth', authRouter);
    app.use('/user', userRouter);
    
    // app.get('/',(req,res)=>{
    //     res.send('<h1>nodegram</h1>');
    // });
    
    
    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);
        console.log(err);
        res.render('error');
    
    });
    
    app.listen(app.get('port'), ()=>{
        console.log(app.get('port'),'번 포트에서 대기중');
    });

     


     

    2. 기능 구현

    2.1. 로그인 페이지로 이동

    2.1.1. views/layout.html 작성

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <!-- <title>layout.html</title> -->
        <title>{{title}}</title>
        <link rel="stylesheet" href="/main.css">
    </head>
    <body>
        <div class="container">
            <div class="profile-wrap">
                <div class="profile">
                    {% if user %}   
                    <!-- 로그인 유저가 null이 아니라면 (로그인 한 사람이 있다면 )
                        로그인 한 사람의 정보가 세션에 저장이 되고 
                        그 세션값이 현재 파일에 render에 의해 담겨져 왔다는 뜻-->
                        <div class="user-name">
                            {{'안녕하세요' + user.nick + '님'}}
                        </div>
                        <div class="harf"><div>팔로워</div>
                            <div class="count follower-count">{{followerCount}}</div>
                        </div>
                        <input type="hidden" id="my-id" value="{{user.id}}">
                        <a id="my-profile" href="/profile" class="btn">내 프로필</a>
                        <a id="logout" href="/auth/logout" class="btn">로그아웃</a>
                    {% else %}
                        <form id="login-form" action="/auth/login" method="post">
                            <div class="input-group">
                                <label id="email">이메일</label>
                                <input type="text" id="email" name="email">
                            </div>
                            <div class="input-group">
                                <label id="password">비밀번호</label>
                                <input type="password" id="password" name="password">
                            </div>
                            <a id="join" href="/join" class="btn">회원가입</a>
                            <button id="login" type="submit" class="btn">로그인</button>
                            <a id="kakao" href="/auth/kakao" class="btn">카카오톡</a>
                        </form>
                    {% endif %}
                </div>
            </div>
            {% block content %}
            {% endblock %}
        </div>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    {% block script %}
    {% endblock %}
    </body>
    </html>

     

    2.1.2. page.js - router.get( '/' )

    const express = require('express');
    const { Post, User, Hashtag } = require('../models');
    const router = express.Router();
    
    ...
    
    // 로그인 페이지로 이동 ('/')
    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:'Nodegram',     // 타이틀
                    user:req.user,      // 로그인유저 객체
                    followerCount:0,    // 로그인 유저의 팔로워 수
                    followingCount:0,   // 로그인 유저의 팔로잉 수
                    followerIdList:[],  // 팔로워 리스트 (배열)
                    posts,           // 전체 포스팅 객체
                }
            );
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    ...
    
    module.exports = router;

     

    2.1.3. main.html

    {% extends 'layout.html' %}
    <!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
    <!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
    
    
    <!-- 블록 컨텐츠 작성-->
    {% block content %}
    <div class="timeline">
        {% if user %}
        <div>
            <form id="post-form" action="/post" method="post" enctype="multipart/form-data">
                <div class="input-group">
                    <textarea id="twit" name="content" maxlength="140"></textarea>
                </div>
                <div class="img-preview">
                    <img id="img-preview" src="" style="display:none;" width="250" alt="미리보기">
                    <input id="img-url" type="hidden" name="url">
                </div>
                <div>
                    <label id="img-label" for="img">사진 업로드</label>
                    <input id="img" type="file" accept="image/*">
                    <button id="post-btn" type="submit" class="btn">포스팅</button>
                </div>
            </form>
        </div>
        {% endif %}
        <div class="twits">
            <form id="hashtag-form" action="/hashtag">
                <input type="text" name="hashtag" placeholder="태그검색">
                <button class="btn">검색</button>
            </form>
            <br>
    
            {% for post in posts %}
            <div class="twit">
                <!-- 아이디 --><!-- 닉네임 -->
                <input type="hidden" value="{{post.id}}" class="twit-id">
                <input type="hidden" value="{{post.UserId}}" class="twit-user-id">
                <div class="twit-author" style="font-weight:bold; font-family:Verdana;">
                    {{post.id}} &nbsp; -&nbsp; {{post.User.nick}}
                </div>
                <!-- 이미지 -->
                {% if post.img %}
                    <!-- 현재 게시물의 이미지가 있다면 이미지태그 표시-->
                    <div class="twit-img"><img src="{{post.img}}"></div><br>
                {% endif %}
                <!-- content -->
                <div class="twit-content" style="font-weight:bold; font-family:Verdana;">
                    <pre>{{post.content}}</pre>
                </div>
            </div>
            {% endfor %}
        </div>
    </div>
    {% endblock %}
    
    
    <!-- 블록 스크립트 작성 -->
    {% block script %}
    <script type="text/javascript">
        document.getElementById('img').addEventListener('change', (e)=>{
            const formData = new FormData();
            formData.append('img', e.target.files[0]);
            axios.post('/post/img', formData)
            .then((res)=>{
                docu
                document.getElementById('img-url').value = res.data.url;
                document.getElementById('img-preview').src = res.data.url;
                document.getElementById('img-preview').style.display = 'inline';
            })
            .catch((err)=>{ console.error(err); });
        });
    </script>
    {% endblock %}

     


     

    2.2. 회원가입 폼 블럭으로 이동

    2.2.1. layout.html

    <!DOCTYPE html>
    	...
        <title>{{title}}</title>
        <link rel="stylesheet" href="/main.css">
    </head>
    <body>
        <div class="container">
            <div class="profile-wrap">
                <div class="profile">
                    {% if user %}   
                    
                    ...
                    
                    {% else %}
                        <form id="login-form" action="/auth/login" method="post">
                            
                            ...
                            
                            <a id="join" href="/join" class="btn">회원가입</a>
                            <button id="login" type="submit" class="btn">로그인</button>
                            <a id="kakao" href="/auth/kakao" class="btn">카카오톡</a>
                        </form>
                    {% endif %}
                </div>
            </div>
            {% block content %}
            {% endblock %}
        </div>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    {% block script %}
    {% endblock %}
    </body>
    </html>

     

    2.2.2. page.js - router.get( '/join' )

    const express = require('express');
    const { Post, User, Hashtag } = require('../models');
    const router = express.Router();
    
    ...
    
    // 회원가입 폼 블럭으로 이동
    router.get('/join', (req,res,next)=>{
        res.render('join', 
            {
                title:'회원가입-Nodegram',
            }
        );
    });
    
    ...
    
    module.exports = router;

     

    2.2.3. join.html

    {% extends 'layout.html' %}
    
    {% block content %}
    <div class="timeline">
        <form id="join-form" action="/auth/join" method="post">
            <div class="input-group">
                <label for="join-email">이메일</label>
                <input id="join-email" type="email" name="email">
            </div>
            <div class="input-group">
                <label for="join-nick">닉네임</label>
                <input id="join-nick" type="nick" name="nick">
            </div>
            <div class="input-group">
                <label for="join-password">비밀번호</label>
                <input id="join-password" type="password" name="password">
            </div>
            <button id="join-btn" type="submit" class="btn">회원가입</button>
        </form>
    </div>
    {% endblock %}
    
    
    <!-- 블록 스크립트 작성 -->
    {% block script %}
    <script type="text/javascript">
        window.onload=()=>{
            // localhost:3000/join.html?error=exists
            // 폼 실행 후 되돌아와서 페이지 전환을 하고자하는 주소에 error라는 파라미터가 있다면
            if(new URL(location.href).searchParams.get('error')){
                alert('이미 존재하는 이메일입니다.');
                location.href='/';
            } 
        };
    </script>
    {% endblock %}

     


     

    2.3. 회원가입 동작

    2.3.1. join.html

    {% extends 'layout.html' %}
    
    {% block content %}
    <div class="timeline">
        <form id="join-form" action="/auth/join" method="post">
            <div class="input-group">
                <label for="join-email">이메일</label>
                <input id="join-email" type="email" name="email">
            </div>
            <div class="input-group">
                <label for="join-nick">닉네임</label>
                <input id="join-nick" type="nick" name="nick">
            </div>
            <div class="input-group">
                <label for="join-password">비밀번호</label>
                <input id="join-password" type="password" name="password">
            </div>
            <button id="join-btn" type="submit" class="btn">회원가입</button>
        </form>
    </div>
    {% endblock %}
    
    
    <!-- 블록 스크립트 작성 -->
    {% block script %}
    <script type="text/javascript">
        window.onload=()=>{
            // localhost:3000/join.html?error=exists
            // 폼 실행 후 되돌아와서 페이지 전환을 하고자하는 주소에 error라는 파라미터가 있다면
            if(new URL(location.href).searchParams.get('error')){
                alert('이미 존재하는 이메일입니다.');
                location.href='/';
            } 
        };
    </script>
    {% endblock %}

     

    2.3.2. auth.js - router.post( '/join' ) 

    회원가입에 실패하면
    join.html의 스크립트 블럭의 searchPrams가 error 파라미터를 찾아내어 가 동작 alert()을 띄운다.

    // auth.js 주로 로그인에 관련한 내용
    const express = require('express');
    const User = require('../models/user');
    const bcrypt = require('bcrypt');
    const passport = require('passport');
    
    const router = express.Router();
    
    ...
    
    //일반 회원가입 동작
    router.post('/join',async(req,res,next)=>{
        // const email = req.body.email;
        // const nick = req.body.nick;
        // const password = req.body.password;
        // req.body객체 -> {email:'abc@abc.com', nick:'hong' password:'1234'}
        const { email, nick, password } = req.body;
    
        try{
            const exUser = await User.findOne({
                where:{email}
            }); // 전송된 email이 이미 가입된 이메일인지 조회
            if(exUser){
                //exUser가 null이 아니라면 (이미 회원 가입이 되어있다면)
                return res.redirect('/join?error=exist');
            }
    
            const hash = await bcrypt.hash(password, 12);
            // bcrypt로 비밀번호를 암호화한다.
            // 해시연산의 뜻 : 암호화와 비슷한 연산의 결과로 같은 원본 데이터라도 연산 결과가 절대 같은 결과가 나오지 않게 하는 연산.
            // 12 : 해시화를 하기 위한 복잡도 인수. 숫자가 클수록 해시화 암호화가 복잡해지고, 복구 연산도 오래걸린다. 12는 약 1초 정도의 시간이 걸린다.
            await User.create({
                email,
                nick,
                password:hash,
            }); //이메일, 닉네임, 패스워드로 회원 추가
            return res.redirect('/');   // main페이지로 이동
        }catch(err){
            console.error(err);
            next(err);
        }
    });
    
    ...
    
    module.exports = router;

    bcrypt.hash( )로 해시화된 비밀번호를 테이블에서 확인할 수 있다.

     


     

    2.4. 로그인 동작

    참고 자료

    https://www.zerocho.com/category/NodeJS/post/57b7101ecfbef617003bf457

     

    https://www.zerocho.com/category/NodeJS/post/57b7101ecfbef617003bf457

     

    www.zerocho.com

     

    2.4.1. layout.html

    로그인 버튼을 클릭하면 form태그의 action속성의 url로 post전송된다.

    <!DOCTYPE html>
    	...
        <title>{{title}}</title>
        <link rel="stylesheet" href="/main.css">
    </head>
    <body>
        <div class="container">
            <div class="profile-wrap">
                <div class="profile">
                    {% if user %}   
                    
                    ...
                    
                    {% else %}
                        <form id="login-form" action="/auth/login" method="post">
                            
                            ...
                            
                            <a id="join" href="/join" class="btn">회원가입</a>
                            <button id="login" type="submit" class="btn">로그인</button>
                            <a id="kakao" href="/auth/kakao" class="btn">카카오톡</a>
                        </form>
                    {% endif %}
                </div>
            </div>
            {% block content %}
            {% endblock %}
        </div>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    {% block script %}
    {% endblock %}
    </body>
    </html>

     

    2.4.2. auth.js - router.post( '/login' )

    • auth.js에는 bcrypt모듈과 passport모듈이 import되어있다.
    • 동작하는 라우터는 '미들웨어 속에 미들웨어가 있는 구조'.

    로그인 동작은 passport모듈로 구현되는데,
    passport.authenticate( )가 동작됨과 동시에 passport/localStrategy.js에서 로그인을 처리하기 시작하고 응답을 기다린다.

    // auth.js 주로 로그인에 관련한 내용
    const express = require('express');
    const User = require('../models/user');
    const bcrypt = require('bcrypt');
    const passport = require('passport');
    
    const router = express.Router();
    
    ...
    
    // 로그인 동작
    router.post('/login', (req,res,next)=>{
        // passport 모듈로 로그인을 구현한다.
        console.log('/login 라우터 동작');
        passport.authenticate('local', (authError, user, info)=>{
            // 로그인을 위해 현재 미들웨어가 실행되면, 
            // 'local'까지만 인식되어지고, passport/localStrategy라는 곳으로 이동하여 로그인을 처리한다.
            // done()에 의해 되돌아온 전달값으로 (authError, user, info)=>{}가 실행된다.
            
            if(authError){  // 서버에러가 있다면 서버에러 처리
                console.error(authError);
                return next(authError);
            }
            if(!user){  // user가 false라면 (로그인에 실패했다면)
                console.log('user가 false여서 로그인 실패');
                return res.redirect(`/?loginError=${info.message}`);
            }
            
            //여기서부터 정상 로그인
            return req.login(user, (loginError)=>{
                console.log('정상적으로 로그인에 성공');
                //req.login을 하는 순간 index.js로 이동한다. (로그인루틴 정상실행 - 실행 후 복귀)
                if(loginError){ //index.js에서 보낸 에러가 있다면 에러처리
                    console.error(loginError);
                    return next(loginError);
                }
                // 세션위치에서 세션쿠키가 브라우저로 보내어진다.
                console.log('세션 위치에서 세션쿠키가 브라우저로 보내짐');
                return res.redirect('/');
            });
        })(req,res,next)  // 미들웨어 속 미들웨어에는 (req,res,next)를 뒤에 붙인다.
    });
    
    ...
    
    module.exports = router;

     

    2.4.3. passport/localStrategy.js

    • 로그인 처리 절차를 정의해놓은 strategy
    • usernameField와 passwordField는 어떤 폼 필드로부터 아이디와 비밀번호를 전달받을지 설정하는 옵션이다.
      • 여기서는 req.body.email과 req.body.password의 폼필드와 일치시키면 되므로 email, password로 설정한다.
    • done( 서버에러 인자, 로그인 성공시 return값, error메시지 )로 세개 까지 인자로 넣을 수 있다.
      • done( error ) - 서버에러 처리시
      • done( null, exUser ) - 로그인 성공시 return ( 에러가 없어야하므로 첫 번째 인자는 null )
      • done( null, false, { message : '비밀번호 틀림' } ) - 비번이 일치하지 않는 등의 경우에 에러메시지를 보낸다.
    const passport = require('passport');
    const LocalStrategy = require('passport-local').Strategy;
    const bcrypt = require('bcrypt');
    const User = require('../models/user');
    
    // 일반 사용자의 로그인 절차를 정의한 strategy
    module.exports = ()=>{
        passport.use(new LocalStrategy({
            usernameField:'email',      // 보내온 req.body.email의 '필드이름과 일치하게 작성', 'email'
            passwordField:'password',   // 보내온 req.body.password의 '필드이름과 일치하게 작성',
        }, async (email,password,done)=>{
            try {
                console.log('localStrategy 시작');
                console.log('이메일 조회 시작');
                const exUser = await User.findOne({
                    where:{email},
                }); // 전달된 email이 user테이블에 존재하는지 조회
    
                if(exUser){
                    console.log('회원이 존재하여 비밀번호 비교');
                    // 회원이 존재한다면?
                    // 암호-해시화 된 비번을 비교
                    const result = await bcrypt.compare(password, exUser.password);
    
                    if(result){ 
                        console.log('이메일 - 비번 일치');
                        // 비밀번호까지 같다면
                        // localStrategy.js가 호출된 위치의 익명함수로 전달된다.
                        done(null, exUser);
                        // localStrategy.js가 호출된 위치의 익명함수로 이동(에러없음 null과, 로그인한 유저를 전달)
                    }else{
                        // 비밀번호가 틀리다면
                        console.log('이메일은 일치하나 비번이 불일치');
                        done(null, false, {message:'비밀번호가 일치하지 않아요!'});
                        // localStratege.js가 호출된 위치의 익명함수로 이동(에러없음 null과, false(로그인 실패), 그리고 info 메시지 내용)
                    }
    
                }else{
                    // 회원이 존재하지 않다면?
                    console.log('회원이 일치하지 않음');
                    done(null, false, {message:'가입되지 않은 회원입니다.'});
                }
            } catch (err) {
                
            }
        }));
    };

     

    2.4.4. passport/index.js

    • serializeUser : 사용자 정보 객체를 세션에 아이디로 저장한다.
    • deserializeUser : 세션에 저장한 아이디로 사용자 정보 객체를 불러온다.
    const passport = require('passport');
    const local = require('./localStrategy');
    const User = require('../models/user');
    
    module.exports = ()=>{
        passport.serializeUser((user, done)=>{  //정상적으로 로그인 되었을 때 실행
            console.log('정상적으로 로그인되어 serializeUser 시작');
            done(null,user.id); // 세션에 아이디만 저장하는 동작.
            //이동 직후 '세션에 아이디가 저장된다'라는 것은 세션쿠키에도 암호화된 키로 쿠키가 저장된다는 뜻이다.
            // {id:3, 'condect.sid:12424123 } 세션쿠키와 같은 세션쿠키가 생성되면서
            // 브라우저에서 connect.sid값의 쿠키가 관리되고 이후로는 아래 디시리얼라이즈유저로 아이디가 사용(세션값으로 복구 및 사용)된다.
        });
    
        passport.deserializeUser((id, done)=>{
            console.log('deserializeUser 시작');
            // 세션쿠키를 사용할 때, 로그인 후 부터 사용한다.
            // 세션쿠키로 로그인된 사람이 req.user에 저장되는데, 차후에 추가로 그의 정보와 팔로워 팔로잉도 조인된 결과로 저장된다.
            User.findOne({
                where:{id},
            })
            // 세션에 저장된 아이디와 쿠키로 user를 복구, req.user로 사용
            .then(user=> done(null,user))
    
            // req.isAuthenticated()함수 결과 : 로그인되어있는 동안 트루값을 갖게된다.
            .catch(err => done(err));
        });
        
        local();
    };

    다음과 같이 데이터가 흐른다

     


     

    2.5. 로그아웃 동작

    2.5.1. layout.html

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <div class="container">
            <div class="profile-wrap">
                <div class="profile">
                    {% if user %}   
                    <!-- 로그인 유저가 null이 아니라면 (로그인 한 사람이 있다면 )
                        로그인 한 사람의 정보가 세션에 저장이 되고 
                        그 세션값이 현재 파일에 render에 의해 담겨져 왔다는 뜻-->
                        <div class="user-name">
                            {{'안녕하세요 ' + user.nick + '님😊'}}
                        </div>
                        <div class="harf"><div>팔로워</div>
                            <div class="count follower-count">{{followerCount}}</div>
                        </div>
                        <div class="half">
                            <div>팔로잉</div>
                            <div class="count following-count">{{followingCount}}</div> 
                        </div>
                        <input type="hidden" id="my-id" value="{{user.id}}">
                        <a id="my-profile" href="/profile" class="btn">내 프로필</a>
                        <a id="logout" href="/auth/logout" class="btn">로그아웃</a>
                    {% else %}
                        <form id="login-form" action="/auth/login" method="post">
                            
                            ...
                            
                        </form>
                    {% endif %}
                </div>
            </div>
            {% block content %}
            {% endblock %}
        </div>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    {% block script %}
    {% endblock %}
    </body>
    </html>

     

    2.5.2. auth.js - router.get( '/logout' )

    // auth.js 주로 로그인에 관련한 내용
    const express = require('express');
    const User = require('../models/user');
    const bcrypt = require('bcrypt');
    const passport = require('passport');
    
    const router = express.Router();
    
    ...
    
    router.get('/logout', (req,res)=>{
        req.logout();   // 세션 쿠키 삭제
        req.session.destroy();
        res.redirect('/');
    });
    
    ...
    
    module.exports = router;

     

     


     

    2.6. 내 프로필로 이동

    2.6.1. layout.html

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <div class="container">
            <div class="profile-wrap">
                <div class="profile">
                    {% if user %}   
                    <!-- 로그인 유저가 null이 아니라면 (로그인 한 사람이 있다면 )
                        로그인 한 사람의 정보가 세션에 저장이 되고 
                        그 세션값이 현재 파일에 render에 의해 담겨져 왔다는 뜻-->
                        <div class="user-name">
                            {{'안녕하세요 ' + user.nick + '님😊'}}
                        </div>
                        <div class="harf"><div>팔로워</div>
                            <div class="count follower-count">{{followerCount}}</div>
                        </div>
                        <div class="half">
                            <div>팔로잉</div>
                            <div class="count following-count">{{followingCount}}</div> 
                        </div>
                        <input type="hidden" id="my-id" value="{{user.id}}">
                        <a id="my-profile" href="/profile" class="btn">내 프로필</a>
                        <a id="logout" href="/auth/logout" class="btn">로그아웃</a>
                    {% else %}
                        <form id="login-form" action="/auth/login" method="post">
                            
                            ...
                            
                        </form>
                    {% endif %}
                </div>
            </div>
            {% block content %}
            {% endblock %}
        </div>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    {% block script %}
    {% endblock %}
    </body>
    </html>

     

    2.6.2. page.js

    const express = require('express');
    const { Post, User, Hashtag } = require('../models');
    const router = express.Router();
    
    ...
    
    // 내 프로필로 이동
    router.get('/profile',(req,res)=>{
        res.render('profile',{
            title:'내 프로필 - Nodegram',
            user:req.user,
            followerCount : 0,
            followingCount:0,
            followerIdList:[],
        });
    });
    
    ...
    
    module.exports = router;

     

    2.6.3. profile.html

    {% extends 'layout.html' %}
    
    {% block content %}
        <div class="timeline">
            <div class="following half">
                <h2>팔로잉 목록</h2>
                {% if user.Followings %}
                    {% for following in user.Followings %}
                    <div>{{following.nick}}</div>
                    {% endfor %}
                {% endif %}
            </div>
            <div class="followers half">
                <h2>팔로워 목록</h2>
                {% if user.Followers %}
                    {% for follower in user.Followers %}
                    <div>{{follower.nick}}</div>
                    {% endfor %}
                {% endif %}
            </div>
        </div>
    {% endblock %}

     


     

    2.7. 포스팅 동작 - 이미지 업로드 부분

    2.7.1. main.html - html부분

    {% extends 'layout.html' %}
    <!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
    <!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
    
    
    <!-- 블록 컨텐츠 작성-->
    {% block content %}
    <div class="timeline">
        {% if user %}
        <div>
            <form id="post-form" action="/post" method="post" enctype="multipart/form-data">
                <div class="input-group">
                    <textarea id="twit" name="content" maxlength="140"></textarea>
                </div>
                <div class="img-preview">
                    <img id="img-preview" src="" style="display:none;" width="250" alt="미리보기">
                    <input id="img-url" type="hidden" name="url">
                </div>
                <div>
                    <label id="img-label" for="img">사진 업로드</label>
                    <input id="img" type="file" accept="image/*">
                    <button id="post-btn" type="submit" class="btn">포스팅</button>
                </div>
            </form>
        </div>
        {% endif %}
        <div class="twits">
            
            ...
            
        </div>
    </div>
    {% endblock %}
    
    
    <!-- 블록 스크립트 작성 -->
    {% block script %}
    <script type="text/javascript">
        ...
    </script>
    {% endblock %}

     

    2.7.2. main.html - script 태그 부분

    {% extends 'layout.html' %}
    <!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
    <!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
    
    
    <!-- 블록 컨텐츠 작성-->
    {% block content %}
    <div class="timeline">
        
        ...
        
    </div>
    {% endblock %}
    
    
    <!-- 블록 스크립트 작성 -->
    {% block script %}
    <script type="text/javascript">
        document.getElementById('img').addEventListener('change', (e)=>{
            const formData = new FormData();
            formData.append('img', e.target.files[0]);
            axios.post('/post/img', formData)
            .then((res)=>{
                document.getElementById('img-url').value = res.data.url;
                document.getElementById('img-preview').src = res.data.url;
                document.getElementById('img-preview').style.display = 'inline';
            })
            .catch((err)=>{ console.error(err); });
        });
    </script>
    {% endblock %}

     

    2.7.3. post.js - router.post( '/img' )

    const express = require('express');
    const multer = require('multer');
    const path = require('path');
    const fs = require('fs');
    const { Post, User, Hashtag } = require('../models');
    
    const router = express.Router();
    
    try{
        fs.readdirSync('uploads');
    }catch(error){
        console.error('upload폴더가 없으므로 생성합니다.');
        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:{fieldSize:5*1024*1024},
    });
    
    
    // 이미지파일을 서버에 업로드하는 동작
    router.post('/img', upload.single('img'), (req,res,next)=>{
        console.log( `/img/${req.file.filename}` );
        res.json({ url:`/img/${req.file.filename}` });
    }); // 이미지만 업로드하고, 저장된 경로를 json형식으로 되돌려준다.
    
    
    ...
    
    module.exports = router;

     


     

    2.8. 포스팅 동작 - 포스팅

    2.8.1. main.html

    {% extends 'layout.html' %}
    <!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
    <!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
    
    
    <!-- 블록 컨텐츠 작성-->
    {% block content %}
    <div class="timeline">
        {% if user %}
        <div>
            <form id="post-form" action="/post" method="post" enctype="multipart/form-data">
                <div class="input-group">
                    <textarea id="twit" name="content" maxlength="140"></textarea>
                </div>
                <div class="img-preview">
                    <img id="img-preview" src="" style="display:none;" width="250" alt="미리보기">
                    <input id="img-url" type="hidden" name="url">
                </div>
                <div>
                    <label id="img-label" for="img">사진 업로드</label>
                    <input id="img" type="file" accept="image/*">
                    <button id="post-btn" type="submit" class="btn">포스팅</button>
                </div>
            </form>
        </div>
        {% endif %}
        <div class="twits">
            
            ...
            
        </div>
    </div>
    {% endblock %}
    
    
    <!-- 블록 스크립트 작성 -->
    {% block script %}
    <script type="text/javascript">
        ...
    </script>
    {% endblock %}

     

    2.8.2. post.js - router.post( '/' )

    const express = require('express');
    const multer = require('multer');
    const path = require('path');
    const fs = require('fs');
    const { Post, User, Hashtag } = require('../models');
    
    const router = express.Router();
    
    try{
        fs.readdirSync('uploads');
    }catch(error){
        console.error('upload폴더가 없으므로 생성합니다.');
        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:{fieldSize:5*1024*1024},
    });
    
    ...
    
    // 포스팅 동작
    const upload2 = multer();
    // 폼 내부에 <input type="file"이 있기 때문에 submit할 경우 파일을 한번 더 업로드하려고 동작한다.
    // 따라서 file업로드 동작을 생략하기 위해 비어있는 multer객체를 생성하고, upload2.none()를 한다.
    router.post('/', upload2.none(), async(req,res)=>{
        try {
            const currentPost = await Post.create({
                content: req.body.content,
                img:req.body.url,
                UserId:req.user.id,
            });
    
            // 게시물을 포스팅할 때 같이 입력한 해시태그(#)를 골라내어, 단어별로 처음 나온 단어를 해시태그 테이블에 insert하고,
            // 현재 게시물이 어떤 해시태그를 갖고 있는지의 여부를 posthashtags테이블에 insert한다. 
    
            // ** '정규표현식'을 사용한다! ** 
    
            // ↓ '#'으로 시작해서 빈칸과 '#'이 아닌 곳까지를 단어로 하여 모두 검색한다.
            const hashtags = req.body.content.match(/#[^\s#]*/g);
    
            if(hashtags){
                // 추출한 해시태그가 있다면~
                const result = await Promise.all(
                    hashtags.map((tag)=>{
                        return Hashtag.findOrCreate({
    
                            //slice(1) -> title 필드값이 해시태그 중 하나(tag)의 내용 중 #을 제외한 나머지 글자와 같은 조건으로 검색한다.
                            // 같은 title값이 있으면, 지나가고, 없으면 Hashtag테이블에 현재 해시태그로 레코드를 추가하고 그 값으로 리턴한다.
                            where:{title:tag.slice(1).toLowerCase()},   
                        });
                    }),
                );
                await currentPost.addHashtags(result.map( (r) => r[0] ) );
                // 지금 추가한 post게시물에 대한 해시태그로 해시태그들을 posthashtags테이블에 추가
    
                // addHashtags : Post모델과, Hashtag모델의 관계에서 Post모델에 자동 생성된 메서드 - Hashtag테이블에 데이터를 추가하는 메서드
            }
    
            res.redirect('/');
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

     


     

    2.9. 포스팅 동작에 해시태그 추출하기

    • 게시물을 포스팅할 때 같이 입력한 '해시태그(#)를 골라내어', 단어별로 처음 나온 단어를 해시태그 테이블에 insert하고, 현재 게시물이 어떤 해시태그를 갖고 있는지의 여부를 posthashtags테이블에 insert한다.
    • '정규 표현식' 문법을 사용한다.

    '정규 표현식'이란?

     

    2.9.1. post.js - router.post( '/' )

    const express = require('express');
    const multer = require('multer');
    const path = require('path');
    const fs = require('fs');
    const { Post, User, Hashtag } = require('../models');
    
    const router = express.Router();
    
    try{
        fs.readdirSync('uploads');
    }catch(error){
        console.error('upload폴더가 없으므로 생성합니다.');
        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:{fieldSize:5*1024*1024},
    });
    
    ...
    
    // 포스팅 동작
    const upload2 = multer();
    // 폼 내부에 <input type="file"이 있기 때문에 submit할 경우 파일을 한번 더 업로드하려고 동작한다.
    // 따라서 file업로드 동작을 생략하기 위해 비어있는 multer객체를 생성하고, upload2.none()를 한다.
    router.post('/', upload2.none(), async(req,res)=>{
        try {
            const currentPost = await Post.create({
                content: req.body.content,
                img:req.body.url,
                UserId:req.user.id,
            });
    		
            // 게시물을 포스팅할 때 같이 입력한 해시태그(#)를 골라내어, 단어별로 처음 나온 단어를 해시태그 테이블에 insert하고,
            // 현재 게시물이 어떤 해시태그를 갖고 있는지의 여부를 posthashtags테이블에 insert한다. 
    
            // ** '정규표현식'을 사용한다! ** 
    
            // ↓ '#'으로 시작해서 빈칸과 '#'이 아닌 곳까지를 단어로 하여 모두 검색한다.
            const hashtags = req.body.content.match(/#[^\s#]*/g);
    
            if(hashtags){
                // 추출한 해시태그가 있다면~
                const result = await Promise.all(
                    hashtags.map((tag)=>{
                        return Hashtag.findOrCreate({
                            //slice(1) -> title 필드값이 해시태그 중 하나(tag)의 내용 중 #을 제외한 나머지 글자와 같은 조건으로 검색한다.
                            // 같은 title값이 있으면, 지나가고, 없으면 Hashtag테이블에 현재 해시태그로 레코드를 추가하고 그 값으로 리턴한다.
                            where:{title:tag.slice(1).toLowerCase()},   
                        });
                    }),
                );
                await currentPost.addHashtags(result.map( (r)=>r[0] ) );
                // 지금 추가한 post게시물에 대한 해시태그로 해시태그들을 posthashtags테이블에 추가
    
                // addHashtags : Post모델과, Hashtag모델의 관계에서 Post모델에 자동 생성된 메서드 
                //- Hashtag테이블에 데이터를 추가하는 메서드
            }
    
            res.redirect('/');
        } catch (err) {
            console.error(err);
            next(err);
        }
    });
    
    module.exports = router;

    addHashtags( )

     


     

    2.9. 해시태그 검색

    2.9.1. main.html

    {% extends 'layout.html' %}
    <!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
    <!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
    
    
    <!-- 블록 컨텐츠 작성-->
    {% block content %}
    <div class="timeline">
        {% if user %}
        <div>
            <form id="post-form" action="/post" method="post" enctype="multipart/form-data">
                
                ...
                
            </form>
        </div>
        {% endif %}
        <div class="twits">
            <form id="hashtag-form" action="/hashtag">
                <input type="text" name="hashtag" placeholder="태그검색">
                <button class="btn">검색</button>
            </form>
            <br>
    
            {% for post in posts %}
            <div class="twit">
                
                ...
                
            </div>
            {% endfor %}
        </div>
    </div>
    {% endblock %}
    
    
    <!-- 블록 스크립트 작성 -->
    {% block script %}
    <script type="text/javascript">
        
        ...
        
    </script>
    {% endblock %}

     

    2.9.2. page.js - router.get( '/hashtag' )

    const express = require('express');
    const { Post, User, Hashtag } = require('../models');
    const router = express.Router();
    
    ...
    
    // 해시태그 검색
    router.get('/hashtag', async (req,res,next)=>{
        const query = req.query.hashtag;
        if(!query){
            return res.redirect('/');   // 도착한 검색어가 없으면 메인으로 redirect
        }
        
        try {
            // 해시태그 단어 검색
            const hashtag = await Hashtag.findOne({ where:{ title:query, } });
            let posts = [];
            
            if(hashtag){
                // 해당 해시태그로 Post테이블의 게시물들을 조회 (외래키인 User테이블을 join)
                posts = await hashtag.getPosts({ include: [{ model:User }] });
            }
            
            return res.render('main', {
                title:`${query} | NodeGram`,
                posts,
                user:req.user,
                followerCount:0,
                followingCount:0,
                followerIdList:[],
            });
        } catch (err) {
            console.error(err);
            return next(err);
        }
    });
    
    
    module.exports = router;

     


     

    2.10. 카카오톡으로 로그인 ( passport-kakao )

    passport-kakao모듈을 활용해 카카오톡으로 로그인하는 기능을 추가해본다.

    2.10.1. layout.html

    카카오로 로그인 버튼을 클릭하면 /auth/kakao 라우터가 동작한다.

    <!DOCTYPE html>
    
    ...
    
    </head>
    <body>
        <div class="container">
            <div class="profile-wrap">
                <div class="profile">
                    {% if user %}   
                    
                    ...
                    
                    {% else %}
                        <form id="login-form" action="/auth/login" method="post">
                            <div class="input-group">
                                <label id="email">이메일</label>
                                <input type="text" id="email" name="email">
                            </div>
                            <div class="input-group">
                                <label id="password">비밀번호</label>
                                <input type="password" id="password" name="password">
                            </div>
                            <a id="join" href="/join" class="btn">회원가입</a>
                            <button id="login" type="submit" class="btn">로그인</button>
                            <a id="kakao" href="/auth/kakao" class="btn">카카오톡</a>
                        </form>
                    {% endif %}
                </div>
            </div>
            {% block content %}
            {% endblock %}
        </div>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    {% block script %}
    {% endblock %}
    </body>
    </html>

     

    대략적인 동작 구성도

    출처: https://inpa.tistory.com/entry/NODE-📚-카카오-로그인-Passport-구현 [👨‍💻 Dev Scroll]

     

    [NODE] 📚 카카오 로그인 (passport-kakao) ✈️ 구현

    카카오 로그인 OAuth 신청 카카오 로그인을 위해서는 카카오 개발자 계정과 로그인용 애플리케이션 등록이 필요하다. https://developers.kakao.com 에 접속하여 개발자 계정을 만들고 아이디를 만들어

    inpa.tistory.com

     

    2.10.2. auth.js - router.get( '/kakao' )

    // auth.js 주로 로그인에 관련한 내용
    const express = require('express');
    const User = require('../models/user');
    const bcrypt = require('bcrypt');
    const passport = require('passport');
    const {isLoggedIn, isNotLoggedIn } = require('./middleware');
    
    const router = express.Router();
    
    ...
    
    // 카카오톡으로 로그인
    router.get('/kakao', passport.authenticate('kakao'));
    // 스트레티지를 통해 카카오에 한번 갔다가 콜백을 받아 돌아온 뒤 콜백을 실행
    
    ...
    
    module.exports = router;

     

    2.10.3. auth.js - router.get( '/kakao/callback' )

    // auth.js 주로 로그인에 관련한 내용
    const express = require('express');
    const User = require('../models/user');
    const bcrypt = require('bcrypt');
    const passport = require('passport');
    const {isLoggedIn, isNotLoggedIn } = require('./middleware');
    
    const router = express.Router();
    
    ...
    
    // 카카오 로그인페이지를 통해 카카오에서 담겨온 데이터와 함께 콜백을 실행
    
    // 카카오 콜백
    router.get('/kakao/callback', passport.authenticate('kakao',{
        failureRedirect:'/',
    }), (req,res)=>{
        res.redirect('/');
    });
    
    module.exports = router;

     

    2.10.4. 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 );
            // profile:계정이 동의한 항목들이 들어있는 카카오가 보내준 객체(카카오 이메일주소, 카카오 닉네임, 나이, 성명, 성별 등)
            try{
                const exUser = await User.findOne({
                    where : { snsid:profile.id, provider:'kakao' },   //카카오 아이디 검색 (이미 가입된 계정이 있는지 확인)
                });
                if(exUser){
                    done(null, exUser); // 아이디가 존재하면 검색결과 회원정보(exUser)를 갖고 바로 done(null, exUser)로 돌아가 로그인절차(세션쿠키저장 등)을 실행한다.
                }else{
                    // 아이디가 없으면 아래와 같이 회원을 Users테이블에 추가하고 로그인절차를 진행
                    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);
            }
        }));
    };

     

     

    2.10.5. Kakao Developer에 플랫폼 등록

    • 회원가입 / 로그인 후 내 애플리케이션을 클릭하여

     

    • 애플리케이션을 추가한다.

     

    • 생성된 어플리케이션을 클릭하여 설정에 들어간다.

     

    • 왼쪽의 플랫폼을 클릭하여 플랫폼 설정에 들어간다.

     

    • Web 플랫폼 등록을 선택

     

    • 인덱스 주소를 입력하고 저장한다.

     

    • 그 다음 좌측에 '카카오 로그인' 탭을 클릭

     

    • '카카오 로그인'을 활성화 시킨다.

     

    • 그리고 하단을 보면 Redirect URI가 있는데 등록버튼을 클릭하여 '/auth/kakao/callback 라우터를 추가'한다.

     

    • 좌측의 '카카오 로그인 - 동의항목' 탭으로 이동

     

    • 닉네임과 카카오 계정(이메일)을 동의설정한다.
    • 여기서는 닉네임은 필수동의, 카카오계정은 선택동의로 한다.

     

    • 좌측의 앱키 탭으로 들어가 REST API키를 복사한다.

     

    • 복사한 REST API키를 .env 환경변수파일에 kakao_ID로 붙여넣기한다.

     

    1.10.6. passport/index.js

    kakaoStrategy.js를 require하고,
    하단에 kakao();를 작성한다.

    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('정상적으로 로그인되어 serializeUser 시작');
            done(null,user.id); // 세션에 아이디만 저장하는 동작.
    
            //이동 직후 '세션에 아이디가 저장된다'라는 것은 세션쿠키에도 암호화된 키로 쿠키가 저장된다는 뜻이다.
            // {id:3, 'condect.sid:12424123 } 세션쿠키와 같은 세션쿠키가 생성되면서
            // 브라우저에서 connect.sid값의 쿠키가 관리되고 이후로는 아래 디시리얼라이즈유저로 아이디가 사용(세션값으로 복구 및 사용)된다.
        });
    
        passport.deserializeUser((id, done)=>{
            console.log('deserializeUser 시작');
            // 세션쿠키를 사용할 때, 로그인 후 부터 사용한다.
            // 세션쿠키로 로그인된 사람이 req.user에 저장되는데, 차후에 추가로 그의 정보와 팔로워 팔로잉도 조인된 결과로 저장된다.
            User.findOne({
                where:{id},
                include: [{
                    model:User,
                    attributes:['id', 'nick'],
                    as: 'Followers',
                }, {
                    model:User,
                    attributes:['id', 'nick'],
                    as:'Followings',    
                
                }],
            })
            // 세션에 저장된 아이디와 쿠키로 user를 복구, req.user로 사용
            .then(user=> done(null,user))
    
            // req의 내장함수 : req.isAuthenticated()함수 결과 : 로그인되어있는 동안 트루값을 갖게된다.
            .catch(err => done(err));
        });
        local();
        kakao();
    };

     

    1.10.7. models/user.js의 password필드 확인

    카카오로 로그인 시 패스워드는 테이블에 저장되지 않기 때문에 'null 허용'이어야 한다.

     

     


     

    1.11. 로그인 상태를 검사하는 미들웨어 추가

    가끔씩 로그인이 되어있지 않지만, 로그인이 되어있는 것 처럼 동작할 때가 있다.

    이것을 방지하기 위해 미들웨어가 동작할 때 로그인 인증을 하는 미들웨어가 동작하도록 추가한다.

     

    1.11.1. routers/middleware.js

    isAuthenticated() : 이 함수를 통하여 현재 로그인이 되어있는지 아닌지를 true, false로 return 한다.

    // middleware.js    -   도구를 만든다고 생각하면 됨.
    
    const { renderString } = require("nunjucks");
    
    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}`);
        }
    };

     

    1.11.2. 로그인 인증이 필요한 미들웨어에 추가

    routers/page.js를 예를 들면 
    middleware.js의 isLoggedIn, isNotLoggedIn를 import하고

     

    로그인이 필요한 동작isLoggedIn,
    로그인이 필요하지 않은 동작에는 isNotLoggedIn 미들웨어를 추가한다.

    const express = require('express');
    const { Post, User, Hashtag } = require('../models');
    const router = express.Router();
    const {isLoggedIn, isNotLoggedIn } = require('./middleware');
    
    ...
    
    // 회원가입 폼 블럭으로 이동
    router.get('/join', isNotLoggedIn, (req,res,next)=>{
        res.render('join', 
            {
                title:'회원가입-Nodegram',
            }
        );
    });
    
    // 내 프로필로 이동
    router.get('/profile', isLoggedIn,(req,res)=>{
        res.render('profile',{
            title:'내 프로필 - Nodegram',
            user:req.user,
            followerCount : req.user ? req.user. Followers.length: 0,    // 로그인 유저의 팔로워 수
            followingCount : req.user ? req.user. Followings.length: 0,    // 로그인 유저의 팔로잉 수
            followerIdList : req.user ? req.user. Followings.map(f=>f.id): [],  // 팔로워 리스트 (배열)
        });
    });
    
    ...
    
    module.exports = router;

     


     

    1.12. follow기능 추가

    1.12.1. main.html 

    main.html의 block content 에 아래와 같이 추가가 되고,

     

    block script 에는 아래와 같이 추가된다.

    {% extends 'layout.html' %}
    <!--'layout.html의 내용을 확장해서 이곳에 내용을 더 쓰겠다'라는 의미-->
    <!--layout.html파일을 이 위치에 호출하여 block content에 들어갈 부분을 아래에 쓰겠다.-->
    
    
    <!-- 블록 컨텐츠 작성-->
    {% block content %}
    <div class="timeline">
        {% if user %}
        <div>
            <form id="post-form" action="/post" method="post" enctype="multipart/form-data">
                <div class="input-group">
                    <textarea id="twit" name="content" maxlength="140"></textarea>
                </div>
                <div class="img-preview">
                    <img id="img-preview" src="" style="display:none;" width="250" alt="미리보기">
                    <input id="img-url" type="hidden" name="url">
                </div>
                <div>
                    <label id="img-label" for="img">사진 업로드</label>
                    <input id="img" type="file" accept="image/*">
                    <button id="post-btn" type="submit" class="btn">포스팅</button>
                </div>
            </form>
        </div>
        {% endif %}
        <div class="twits">
            <form id="hashtag-form" action="/hashtag">
                <input type="text" name="hashtag" placeholder="태그검색">
                <button class="btn">검색</button>
            </form>
            <br>
    
            {% for post in posts %}
            <div class="twit">
                <!-- 아이디 --><!-- 닉네임 -->
                <input type="hidden" value="{{post.id}}" class="twit-id">
                <input type="hidden" value="{{post.UserId}}" class="twit-user-id">
                <div class="twit-author" style="font-weight:bold; font-family:Verdana;">
                    {{post.id}} &nbsp; -&nbsp; {{post.User.nick}}
                </div>
    
                <!-- 팔로우 버튼 -->
                {% if not followerIdList.includes(post.User.id) and post.User.id !== user.id %}
                    <!-- 전달된 팔로워 리스트에 현재 게시물 작성자가 없고, 나의 게시물이 아니라면 버튼을 표시한다-->
                    <button class="twit-follow">팔로우하기</button><br>
                {% endif %}
    
                <!-- 이미지 -->
                {% if post.img %}
                    <!-- 현재 게시물의 이미지가 있다면 이미지태그 표시-->
                    <div class="twit-img"><img src="{{post.img}}"></div><br>
                {% endif %}
                <!-- content -->
                <div class="twit-content" style="font-weight:bold; font-family:Verdana;">
                    <pre>{{post.content}}</pre>
                </div>
            </div>
            {% endfor %}
        </div>
    </div>
    {% endblock %}
    
    
    <!-- 블록 스크립트 작성 -->
    {% block script %}
    <script type="text/javascript">
        document.getElementById('img').addEventListener('change', (e)=>{
            const formData = new FormData();
            formData.append('img', e.target.files[0]);
            axios.post('/post/img', formData)
            .then((res)=>{
                document.getElementById('img-url').value = res.data.url;
                document.getElementById('img-preview').src = res.data.url;
                document.getElementById('img-preview').style.display = 'inline';
            })
            .catch((err)=>{ console.error(err); });
        });
    
        // class가 twit-follow인 셀렉터를 모두 선택한 후
        // -> 그 태그들을 하나씩 tag에 전달하는 익명함수를 실행
        document.querySelectorAll('.twit-follow').forEach( function (tag) {
            // 전달된 tag를 통해 각 버튼에 모두 이벤트 리스너(click)를 붙여 사용한다.
            tag.addEventListener('click', function(){
                
                const myid = document.querySelector('#my-id');  // 로그인 한 아이디
                if( myid ){ // 로그인한 상태로 myid가 존재할 때만 실행한다.
                    
                    const userId = tag.parentNode.querySelector('.twit-user-id').value; // 게시물의 작성자
                    if( userId !== myid.value ){    // 로그인 유저와 작성자가 같지 않다면 실행
                        
                        const answer = confirm('팔로우 하시겠습니까?');
                        if(answer){
                            // 내가(로그인유저) 현재 게시물의 작성자를 팔로우하겠다. 라고 axios.post를 호출
                            axios.post(`/user/follow/${userId}`)
                            .then(()=>{
                                location.reload();
                            })
                            .catch((err)=>{
                                console.error(err);
                            });
                        }
                    }
                }
            });
        });
    
    </script>
    {% endblock %}

     

    1.12.2. routers/user.js - router.post( '/follow/:id' )

    const express = require('express');
    const User = require('../models/user');
    const { isLoggedIn, isNotLoggedIn } = require('./middleware');
    
    const router = express.Router();
    
    // 로그인유저(나)가 전달된 :id 상대를 팔로우한다.
    router.post('/follow/:id', isLoggedIn, async (req,res,next)=>{
        const loginuser = await User.findOne({
            where:{id:req.user.id},
        }); //로그인 유저의 user정보 조회
    
        if(loginuser){
            await loginuser.addFollowings( parseInt(req.params.id, 10) );
            // as : 'Followings'에 따른 메서드가 만들어짐. 복수,단수 모두 가능하다 setFollowing 수정메서드
            // getFollowings, removeFollowings 복수면 []를 사용한다.
            res.send('success');
        }else{
            res.status(404).send('no user');
        }
    });
    
    module.exports = router;

     

    반응형