스터디/KAKAOCLOUDSCHOOL

[Node] 개발자 지망생 스터디 - 21일차

shineIT 2022. 11. 30. 10:11

5. Node + MongoDB + mongoose(Node의 ODM)

1) RDBMS와 NoSQL의 차이

  • RDBMS는 스키마를 생성하고 데이터를 저장하는 형식을 취하지만 NoSQL은 스키마를 생성하지 않고 데이터를 저장하는 것이 가능
NoSQL은 스키마의 구조가 변경이되도 구조를 변경할 필요없이 데이터 저장이 가능
  • RDBMS는 데이터의 값으로 다른 테이블의 데이터나 배열을 삽입할 수 없기 때문에 테이블을 분할하고 Foreign Key와 Join의 개념을 이용해서 여러 종류의 데이터를 저장하지만 NoSQL들은 데이터의 값으로 객체나 배열이 가능하기 때문에 하나의 Collection에 여러 종류의 데이터를 저장할 수 있어서 Join을 하지 않아도 되기 때문에 처리 속도가 빠를 수 있음
    Join 대신 Embedding이나 Linking의 개념을 사용
  • RDBMS는 일반적으로 엄격한 트랜잭션을 적용하지만 NoSQL들은 느슨한 트랜잭션을 적용
  • NoSQL은 복잡한 거래가 없는 경우나 비정형 데이터만을 저장하기 위한 용도로 많이 사용

2) ODM

  • Relation이라는 개념 대신에 Document를 하나의 객체에 매핑하는 방식
  • 하나의 Document에 대한 모양을 만들고 사용해야 하기 때문에 NoSQL의 Collection도 하나의 정형화된 모양을 가져야 함
  • MongoDB에서 ODM을 사용할 수 있도록 해주는 대표적인 라이브러리가 mongoose
※ 프로그래밍 언어가 데이터베이스와 연동하는 방식
> 드라이버의 기능만을 이용해서 사용하는 방식
> SQL Mapper : 관계형 데이터베이스에만 존재하는 방식으로 SQL과 프로그래밍 언어 코드의 분리를 이용하는 방식
> ORM이나 ODM 같은 객체 지향 문법을 이용해서 사용할 수 있도록 해주는 방식

관계형 데이터베이스 = 3Line 모두 고려, NoSQL = 1,3Line 고려

Authentication

1. Authentication(인증) 과 Authorizarion(인가)

  • 인증 : 계정 관련, 로그인 관련
  • 인가 : 권한 관련

2. Authentication(인증)을 구현하는 방법

  • 로컬 로그인 : 회원 정보를 저장하고 있다가 인증
회원 정보를 저장할 때는 비밀번호는 복호화가 불가능한 방식을 사용하고
개인을 식별할 수 있는 정보는 마스킹 처리를 하거나 복호화가 가능한 방식의 암호화를 활용해야 함
  • OAuth(공통된 인증 방식) 로그인 : 다른 서버에 저장된 인증 정보를 활용해서 인증을 하는 방식

3. 인증을 위한 프로젝트 기본 설정

  • 로그인을 할 수 있도록 회원 가입을 하고 로그인 처리를 수행하고 간단한 글을 업로드 할 수 있는 프로젝트
#Project 생성
📁 authentication

#필요한 패키지 설치
> npm install express morgan dotenv compression morgan file-stream-rotator multer cookie-parser express-session express-mysql-session mysql2 sequelize sequelize-cli nunjucks
> npm install --save -dev nodemon

#package.json 파일을 수정
📁 authentication -> 📄 package.json
"scripts": {
	"start":"nodemon app",
	...​

#sequelize(node 의 ORM) 초기화
> npx sequelize init

#디렉토리 생성
📁 views : 화면에 출력할 파일이 저장되는 디렉토리
📁 routes : 사용자의 요청이 왔을 때 처리하는(Controller) 라우팅 파일이 저장되는 디렉토리
📁 public : 정적인 파일(resource)들이 저장되는 디렉토리

#Project에 .env 파일을 생성하고 작성
소스 코드에 노출되서 안되는 내용이나 개발 환경에서 운영 환경으로 이행(Migration)할 때 변경될 내용을 작성하는데 이 내용은 실행 중에는 변경되지 않는 내용이어야 함. [Migration : DevOps, CI/CD에서 중요]

대표적인 내용이 데이터베이스 접속 정보 나 암호화를 하기 위한 키 또는 서버 포트 번호와 같은 것들임.
이러한 내용은 대부분 실행 중에는 변경되지 않지만 개발 환경에서 운영 환경으로 이행 할 때 변경될 가능성이 높은 내용임

📁 authentication -> ⚙️.env
PORT=80
COOKIE_SECRET=authentication
HOST='localhost'
MYSQLPORT=3306
USERID='root'
PASSWORD='qlxkals'
DATABASE='itorigin'

"PORT 번호 80번을 사용하면 localhost:9000 -> localhost로만 접속 가능"


#App.js 파일을 만들고 기본 설정을 추가

📁 authentication -> 📄 App.js
const express = require('express');

//.env 파일을 읽어서 process.env 에 대입해주는 설정
const dotenv = require('dotenv');
dotenv.config();

//서버 설정
const app = express();
app.set('port', process.env.PORT);

//로그 출력 설정을 위해서 파일을 읽기 위한 모듈 과 경로 설정 모듈
const fs = require('fs');
const path = require('path');

//static 파일의 경로 설정
app.use(express.static(path.join(__dirname, 'public')));

//view template(template engine)
//서버의 데이터를 html 과 합쳐서 다시 html로 변환해주는 라이브러리
const nunjucks = require('nunjucks');
app.set('view engine', 'html'); 
nunjucks.configure('views', {
    express:app,
    watch: true, 
});

//로그 설정
const morgan = require('morgan');
const FileStreamRotator = require('file-stream-rotator');

const logDirectory = path.join(__dirname, 'log');

// 로그 디렉토리 생성
fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory);

// 로그 파일 옵션 설정
const accessLogStream = FileStreamRotator.getStream({
  date_format: 'YYYYMMDD',
  filename: path.join(logDirectory, 'access-%DATE%.log'),
  frequency: 'daily',
  verbose: false
});

// 로그 설정
app.use(morgan('combined', {stream: accessLogStream}));

//출력하는 파일 압축해서 전송
const compression = require('compression');
app.use(compression());

//post 방식에서 form 이 아닌 형태로 데이터를 전송하는 경우 파라미터 읽기
let bodyParser = require('body-parser');
app.use( bodyParser.json());       // to support JSON-encoded bodies
app.use(bodyParser.urlencoded({     // to support URL-encoded bodies
	extended: true
})); 

//쿠키 설정
const cookieParser = require('cookie-parser');
app.use(cookieParser(process.env.COOKIE_SECRET));

//세션 설정
const session = require("express-session");
let options = {
    host :process.env.HOST,
	port : process.env.MYSQLPORT,
	user : process.env.USERID,
	password : process.env.PASSWORD,
	database : process.env.DATABASE
};

const MySQLStore = require('express-mysql-session')(session);

app.use(
    session({
      secret: process.env.COOKIE_SECRET,
      resave: false,
      saveUninitialized: true,
      store : new MySQLStore(options)
    })
);

//404 에러가 발생한 경우 처리
app.use((req, res, next) => {
    const err = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
	err.status = 404;
	next(err);
});

//404 이외의 에러가 발생한 경우 처리
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'), '번 포트에서 대기 중');
});​

디렉토리 안에 index.js 파일을 가져올 때는 디렉토리 이름만 require 하면 됨.
App.js : 모든 클라이언트의 요청이 들어오는 곳 - Front Controller
routers 디렉토리 안의 js 파일 : 특정 요청에 대한 처리를 수행 - PageController


#기본적인 처리
routes 디렉토리에 page.js 파일을 만들고 메인화면, 프로필 화면 그리고 회원가입 화면을 위한 라우팅 코드를 작성
📁 routes -> 📄 page.js

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

// 공통된 처리 - 무조건 수행
router.use((req, res, next) => {
    // 로그인한 유저 정보
    res.locals.user = null;
    // 게시글을 follow하고 되고 있는 개수
    res.locals.followCount = 0;
    res.locals.followingCount = 0;
    // 게시글을 follow 하고 이는 유저들의 목록
    res.locals.followIdList = [];

    next();
})

//메인화면
router.get('/',(req, res, next) => {
    const twits = [];
    // 템플릿 엔진을 이용한 출력
    // views 디렉토리의 main.html로 출력
    res.render('main', {title:"Node Authentication", twits});
});

//회원가입
router.get('/join', (req, res, next) => {
    res.render('join', {title: "회원가입 - Node Authencation"});
});

//프로필 화면 처리
router.get('/profile', (req, res, next) => {
    res.render('profile', {title: "나의 정보 - Node Authencation"});
});

module.exports = router;

# App.js 파일에 라우팅 파일을 포함시켜주는 코드를 작성
//라우터 설정
const pageRouter =require('./routes/page');
//여기 설정한 URL과 page.js에 설정된 URL의 조합으로 URL을 설정
app.use ('/',pageRouter);​
Routing
#공통된 레이아웃을 위한 내용을 views 디렉토리에 layout.html 파일을 만들고 작성
📁 views -> 📄 layout.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>{{title}}</title>
    <!-- 모바일 페이지 만들 때 사용 -->
    <meta name="viewport" content="width=device-width, user-scalable=no">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <link rel="stylesheet" href="/main.css">
  </head>
  <body>
    <div class="container">
      <div class="profile-wrap">
        <div class="profile">
            <!-- 로그인 되어 있으면 -->
          {% if user and user.id %}
            <div class="user-name">{{'안녕하세요! ' + user.nick + '님'}}</div>
            <div class="half">
              <div>팔로잉</div>
              <div class="count following-count">{{followingCount}}</div>
            </div>
            <div class="half">
              <div>팔로워</div>
              <div class="count follower-count">{{followerCount}}</div>
            </div>
            <input id="my-id" type="hidden" 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 for="email">이메일</label>
                <input id="email" type="email" name="email" required autofocus>
              </div>
              <div class="input-group">
                <label for="password">비밀번호</label>
                <input id="password" type="password" name="password" required>
              </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>
          <footer>
            Made by&nbsp;
            <a href="https://ggangpae1.tistory.com/" target="_blank">ADAM</a>
          </footer>
        </div>
        {% block content %}
        {% endblock %}
      </div>
      <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
      <script>
        window.onload = () => {
          if (new URL(location.href).searchParams.get('loginError')) {
            alert(new URL(location.href).searchParams.get('loginError'));
          }
        };
      </script>
      {% block script %}
      {% endblock %}
    </body>
</html>​

 

#메인 화면을 위한 내용을 views 디렉토리에 main.html 파일을 만들고 작성
📁 views -> 📄 main.html
{% extends 'layout.html' %}

{% block content %}
    <div class="timeline">
      {% if user %}
        <div>
          <form id="twit-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="twit-btn" type="submit" class="btn">게시</button>
              </div>
            </form>
          </div>
        {% endif %}
  
        <div class="twits">
            {% for twit in twits %}
              <div class="twit">
                <input type="hidden" value="{{twit.User.id}}" class="twit-user-id">
                <input type="hidden" value="{{twit.id}}" class="twit-id">
                <div class="twit-author">{{twit.User.nick}}</div>
                {% if not followerIdList.includes(twit.User.id) and twit.User.id !== user.id %}
                  <button class="twit-follow">팔로우하기</button>
                {% endif %}
                <div class="twit-content">{{twit.content}}</div>
                {% if twit.img %}
                  <div class="twit-img"><img src="{{twit.img}}" alt="섬네일"></div>
                {% endif %}
              </div>
            {% endfor %}
          </div>
        </div>
    {% endblock %}
    {% block script %}
    <script>
      if (document.getElementById('img')) {
        document.getElementById('img').addEventListener('change', function(e) {
          const formData = new FormData();
          console.log(this, this.files);
          formData.append('img', this.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);
            });
        });
      }
  
      document.querySelectorAll('.twit-follow').forEach(function(tag) {
      tag.addEventListener('click', function() {
        const myId = document.querySelector('#my-id');
        if (myId) {
          const userId = tag.parentNode.querySelector('.twit-user-id').value;
          if (userId !== myId.value) {
            if (confirm('팔로잉하시겠습니까?')) {
              axios.post(`/user/${userId}/follow`)
                .then(() => {
                  location.reload();
                })
                .catch((err) => {
                  console.error(err);
                });
            }
          }
        }
      });
    });
  </script>
{% endblock %}​


#팔로워 목록을 출력할 내용을 views 디렉토리에 profile.html 파일을 생성하고 작성

📁 views -> 📄 profile.html

{% extends 'layout.html' %}
{% block content %}
  <div class="timeline">
    <div class="followings 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 %}


#회원가입 화면을 위한 내용을 views 디렉토리에 join.html 파일을 만들고 작성
📁 views -> 📄 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="text" 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>
    window.onload = () => {
      if (new URL(location.href).searchParams.get('error')) {
        alert('이미 존재하는 이메일입니다.');
      }
    };
  </script>
{% endblock %}


#에러가 발생한 경우 출력할 내용을 views 디렉토리에 error.html 파일을 만들고 작성
📁 views -> 📄 error.html

{% extends 'layout.html' %}

{% block content %}
  <h1>{{message}}</h1>
  <h2>{{error.status}}</h2>
  <pre>{{error.stack}}</pre>
{% endblock %}


#스타일 관련된 내용을 public 디렉토리에 main.css 파일을 생성하고 작성
📁 public -> 📄 main.css

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; }
.btn {
  display: inline-block;
  padding: 0 5px;
  text-decoration: none;
  cursor: pointer;
  border-radius: 4px;
  background: white;
  border: 1px solid silver;
  color: crimson;
  height: 37px;
  line-height: 37px;
  vertical-align: top;
  font-size: 12px;
}

input[type='text'], input[type='email'], input[type='password'], textarea {
    border-radius: 4px;
    height: 37px;
    padding: 10px;
    border: 1px solid silver;
  }
  
  .container { width: 100%; height: 100%; }
  
  @media screen and (min-width: 800px) {
    .container { width: 800px; margin: 0 auto; }
  }
  
  .input-group { margin-bottom: 15px; }
.input-group label { width: 25%; display: inline-block; }
.input-group input { width: 70%; }
.half { float: left; width: 50%; margin: 10px 0; }
#join { float: right; }

.profile-wrap {
  width: 100%;
  display: inline-block;
  vertical-align: top;
  margin: 10px 0;
}

@media screen and (min-width: 800px) {
    .profile-wrap { width: 290px; margin-bottom: 0; }
}

.profile {
    text-align: left;
    padding: 10px;
    margin-right: 10px;
    border-radius: 4px;
    border: 1px solid silver;
    background: lightcoral;
}

.user-name { font-weight: bold; font-size: 18px; }
.count { font-weight: bold; color: crimson; font-size: 18px; }

.timeline {
    margin-top: 10px;
    width: 100%;
    display: inline-block;
    border-radius: 4px;
    vertical-align: top;
}
@media screen and (min-width: 800px) { .timeline { width: 500px; } }

#twit-form {
    border-bottom: 1px solid silver;
    padding: 10px;
    background: lightcoral;
    overflow: hidden;
}

#img-preview { max-width: 100%; }
#img-label {
    float: left;
    cursor: pointer;
    border-radius: 4px;
    border: 1px solid crimson;
    padding: 0 10px;
    color: white;
    font-size: 12px;
    height: 37px;
    line-height: 37px;
}
#img { display: none; }

#twit { width: 100%; min-height: 72px; }
#twit-btn {
    float: right;
    color: white;
    background: crimson;
    border: none;
}
.twit {
    border: 1px solid silver;
    border-radius: 4px;
    padding: 10px;
    position: relative;
    margin-bottom: 10px;
}

.twit-author { display: inline-block; font-weight: bold; margin-right: 10px; }
.twit-follow {
    padding: 1px 5px;
    background: #fff;
    border: 1px solid silver;
    border-radius: 5px;
    color: crimson;
    font-size: 12px;
    cursor: pointer;
}
.twit-img { text-align: center; }
.twit-img img { max-width: 75%; }
  
.error-message { color: red; font-weight: bold; }
#search-form { text-align: right; }
#join-form { padding: 10px; text-align: center; }
#hashtag-form { text-align: right; }
footer { text-align: center; }​

#서버를 실행하고 localhost 로 접속한 후 화면 출력을 확인하고 회원가입 클릭해보기

 

4. 데이터베이스 작업

1) 테이블 구조

# 회원 테이블
이메일
닉네임
패스워드
로그인 방법 : 직접 로그인 했는지 아니면 카카오로 로그인 했는지 여부를 저장
카카오 아이디
생성 시간
수정 시간
삭제 시간 - 삭제할 때 실제 지워지지 않고 삭제한 시간을 기록

# POST 테이블
게시글 내용
이미지 파일의 경로

# HashTag 테이블
테이블 이름

# 관계
User와 Post는 1:N 관계
HashTag 와 Post는 N:N 관계
User와 User는 N:N 관계

2) models 디렉토리에 user 테이블의 모델을 위한 user.js 파일을 생성하고 작성

📁 models -> 📄 user.js

# 기본 구조
const Sequelize = require('sequelize');
module.exports = class 모델이름 extends Sequelize.Model{
    // 테이블에 대한 설정
    static init(sequelize){
        return super.init({
            // 컬럼에 대한 설정
        }, {
            // 테이블에 대한 설정
        });
    }
    // 관계에 대한 설정
    static associate(db){
        
    }
}​
const Sequelize = require('sequelize');
module.exports = class User extends Sequelize.Model{
    // 테이블에 대한 설정
    static init(sequelize){
        return super.init({
            // 컬럼에 대한 설정
            email:{
                type:Sequelize.STRING(40),
                allowNull:true,
                unique:true
            },
            // 다른 경로의 로그인 방식 때문에 allowNull의 값은 false가 된다.
            // "카카오 로그인 시 닉네임은 불러와짐"
            nick:{
                type:Sequelize.STRING(40),
                allowNull:false
            },
            // 64의 배수
            password:{
                type:Sequelize.STRING(128),
                allowNull:true
            },
            provider:{
                type:Sequelize.STRING(10),
                allowNull:false,
                defaultValue:'local'
            },
            snsId:{
                type:Sequelize.STRING(50),
                allowNull:true
            }
        }, {
            // 테이블에 대한 설정
            sequelize,
            timestamps:true,
            underscored:false,
            modelName:'User',
            tableName:'snsuser',
            paranoid:true,
            charset:'utf8',
            collate:'utf8_general_ci'
        });
    }
    // 관계에 대한 설정
    static associate(db){
        db.User.hasMany(db.Post);
        db.User.belongsToMany(db.User, {
            foreignKey:'followingId',
            as:'Followers',
            through:'Follow'
        });
        db.User.belongsToMany(db.User, {
            foreignKey:'followerId',
            as:'Followings',
            through:'Follow'
        });
    }
}

3) models 디렉토리에 post 테이블의 모델을 위한 post.js 파일을 생성하고 작성

📁 models -> 📄 post.js

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,
            timestamps:true,
            underscored:false,
            modelName:'Post',
            tableName:'posts',
            paranoid:true,
            charset:'utf8mb4',
            collate:'utf8mb4_general_ci'
        });
    }
    // 관계에 대한 설정
    static associate(db){
        //User 와의 관계는 1:N
        db.Post.belongsTo(db.User);
        //Hashtag 와는 N:M
        //다대다 관계는 테이블이 생성되는데 through가 테이블 이름
        db.Post.belongsToMany(db.Hashtag, {
            through:'PostHashtag'
        })
    }
}

4) models 디렉토리에 hashtag.js 파일을 생성하고 HashTag에 대한 정보를 설정

📁 models -> 📄 hashtag.js

데이터 베이스 설계 : 저장할 항목 그리고 테이블 간의 관계(1:1인지 1:N인지)
const Sequelize = require('sequelize');
module.exports = class Hashtag extends Sequelize.Model{
    // 테이블에 대한 설정
    static init(sequelize){
        return super.init({
            // 컬럼에 대한 설정
            title:{
                type:Sequelize.STRING(15),
                allowNull:false,
                unique:true
            }
        }, {
            // 테이블에 대한 설정
            sequelize,
            timestamps:true,
            underscored:false,
            modelName:'Hashtag',
            tableName:'hashtags',
            paranoid:false,
            charset:'utf8mb4',
            collate:'utf8mb4_general_ci'
        });
    }
    // 관계에 대한 설정
    static associate(db){
        //다대다
        db.Hashtag.belongsToMany(db.Post,{
            through:'PostHashtag'
        })
    }
}

5) models 디렉토리의 index.js 파일의 내용 수정

📁 models -> 📄 index.js

const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/config.json')[env];

const sequelize = new Sequelize(
  config.database, 
  config.username, 
  config.password, 
  config
);
const db = {};

db.sequelize = sequelize;
db.Sequelize = Sequelize;

//모델들 가져오기
const User = require('./user');
const Post = require('./post');
const Hashtag = require('./hashtag');

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;

6) config 디렉토리의 config.json 파일의 내용을 수정해서 데이터베이스 접속 정보를 작성

📁 config -> 📄 config.js

자신의 정보로 수정

7) App.js 파일에서 모델 과 서버를 연결하는 코드를 추가

📁 authentication -> 📄 App.js

// 모델과 서버를 연결
const {sequelize} = require('./models');
sequelize.sync({force:false})
    .then(() => {
        console.log('데이터베이스 연결 성공')
        ;})
    .catch((err)=>{
        console.error(err)
        ;})

 

8) 데이터 베이스가 존재하지 않는경우는 아래 명령을 한 번 수행

> npx sequelize-cli db:create

9) 서버를 실행하면 데이터베이스에 4개의 테이블 생성

snsuser, posts, hashtags, PostHashtag

 

5. Passport 모듈

  • Node 에서 인증 작업을 도와주는 모듈
  • 세션이나 쿠기 처리를 직접하지 않고 이 모듈의 도움을 받으면 쉽게 구현이 가능함
  • Social 로그인 작업을 쉽게 처리할 수 있도록 해줌
  • https://www.passportjs.org
인증 작업
로그인에 성공하면 세션을 생성해서 세션에 아이디나 기타 정보를 저장하고 다음부터 로그인을 확인할 때는 세션의 정보가 있는지를 확인해서 로그인 여부를 판단하고 로그아웃을 하면 세션의 정보를 삭제함


❖ 쿠키와 세션
쿠키 : 클라이언트 (조작이 가능한 정보를 쿠키에 저장해서는 안됨)
세션 : 서버

 

6. 로컬 로그인 구현

1) 필요한 모듈 설치

> npm install passport passport-local bcrypt
(암호화를 해서 비교는 가능하지만 복호화는 안되는 암호화 모듈)

2) App.js 파일에 Passport 모듈을 사용할 수 있는 설정을 추가

📁 authentication -> 📄 App.js

const passport = require('passport');
const passportConfig = require('./passport');
passportConfig();
app.use(passport.initialize());
// 세션 기능은 passport 모듈이 알아서 사용
app.use(passport.session());

3) Passport 모듈 사용 설정

  • passport 디렉토리 생성

📁 passport -> 📄 index.js

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

module.exports = () => {
    //login 성공시 정보를 deserializeUser 함수에게 넘기는 함수
    passport.serializeUser((user, done) => {
        done(null, user.id);
    });
    // 넘어온 Id에 해당하는 데이터가 있으면 데이터베이스에서 찾아서 세션에 저장
    passport.deserializeUser((id, done) => {
        User.findOne({where:{id}})
            .then(user => done(null, user))
            .catch(err => done(err));
    });
    local();
}​

> 이렇게 하면 로그인 여부를 request 객체의 isAuthenticated() 함수로 할 수 있게 됨

 

4) 로그인 여부를 판단할 수 있는 함수를 routes 디렉토리의 middlewares.js 파일을 추가하고 작성

📁 routes -> 📄 middlewares.js

// 로그인 여부 판단
exports.isLoggedIn = (req, res, next) => {
    if(req.isAuthenticated()){
        next();
    }else{
        res.status(403).send('로그인 필요');
    }
}

// 로그인하지 않은 경우를 판단
exports.isNotLoggedIn = (req, res, next) => {
    if(!req.isAuthenticated()){
        next();
    }else{
        //메시지를 생성하는 query string(parameter)로 사용할 것이라서 encoding을 해주어야 함
        const message = encodeURIComponent('로그인 한 상태입니다.');
        // 이전 request 객체의 내용을 모두 삭제하고
        // 새로운 요청 흐름을 만든것으로 새로고침을하면 결과 화면만 새로고침 됨
        res.redirect(`/?error=${message}`);
    }
}​
❖ 로그인 여부 판단
웹 애플리케이션을 구현하게 되면 로그인 여부를 판단해서 작업을 해야 하는 경우가 발생하는데
이는 비지니스 로직과는 상관이 없고 이런 부분은 별도로 처리해야 함.
> Node는 이런 로직을 middleware로 처리
> Java WEB에서는 Filter로 처리
> Spring에서는 AOP나 interceptor를 이용해서 처리

5) routes 디렉토리의 page.js 파일을 수정

📁 routes -> 📄 page.js

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

const router = express.Router();

// 공통된 처리 - 무조건 수행
router.use((req, res, next) => {
    // 로그인한 유저 정보
    // 유저 정보를 res.locals.user에 저장
    res.locals.user = req.user;
    // 게시글을 follow하고 되고 있는 개수
    res.locals.followCount = 0;
    res.locals.followingCount = 0;
    // 게시글을 follow 하고 이는 유저들의 목록
    res.locals.followIdList = [];

    next();
})

//메인화면
router.get('/',(req, res, next) => {
    const twits = [];
    // 템플릿 엔진을 이용한 출력
    // views 디렉토리의 main.html로 출력
    res.render('main', {title:"Node Authentication", twits});
});

//회원가입 - 로그인이 되어있지 않은 경우만 수행
router.get('/join', isNotLoggedIn, (req, res, next) => {
    res.render('join', {title: "회원가입 - Node Authencation"});
});

//프로필 화면 처리 - 로그인이 되어 있는 경우만
router.get('/profile', isLoggedIn, (req, res, next) => {
    res.render('profile', {title: "나의 정보 - Node Authencation"});
});

module.exports = router;

6) 회원 가입, 로그인, 로그아웃 처리를 위한 내용을 routes 디렉토리에 auth.js 파일을 만들고 작성

 

  • 이 내용은 page.js 에 작성해도 됨
  • page.js 는 화면을 보여주는 역할을 하고 auth.js는 처리하는 역할을 하도록 분리한 것임

📁 routes -> 📄 auth.js

const express = require("express");

//로그인 및 로그아웃 처리를 위해서 가져오기
const passport = require("passport");
//회원 가입을 위해서 가져오기
const bcrypt = require("bcrypt");

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

const User = require("../models/user");

const router = express.Router();

//회원 가입 처리 - /auth/join 인데 라우팅 할 때 /auth 추가
router.post('/join', isNotLoggedIn, async(req, res, next)=>{
    //데이터 찾아오기
    //req.body에서 email, nick, password를 찾아서 대입
    const {email, nick, password} = req.body;
    try{
        //email 존재 여부 확인
        const exUser = await User.findOne({where:{email}});
        //이미 존재하는 이메일
        if(exUser){
            //회원 가입 페이지로 리다이렉트하는데
            //error 키에 메시지를 가지고 이동
            return res.redirect('/join?error=exist');
        }else{
            //비밀번호를 해싱
            const hash = await bcrypt.hash(password, 12);
            //저장
            await User.create({
                email, nick, password:hash
            })
            return res.redirect('/');
        }
    }catch(error){
        console.error(error);
        return next(error);
    }
});

//로그인 처리
router.post('/login', isNotLoggedIn, (req, res, next) => {
    //passport 모듈을 이용해서 로그인
    passport.authenticate('local', (authError, user, info) => {
        if(authError){
            console.error(authError);
            return next(authError);
        }
        //일치하는 User 가 없을 때
        if(!user){
            return res.redirect(`/?loginError=${info.message}`)
        }
        return req.login(user, (loginError) => {
            if(loginError){
                console.error(loginError);
                return next(loginError);
            }
            //로그인 성공하면 메인 페이지로 이동
            return res.redirect('/')
        })
    })(req, res, next);
})

//로그아웃 처리
router.get('/logout', isLoggedIn, (req, res, next) => {
    req.logout((error) => {
        if(error){return next(error);}
        //세션을 초기화
        req.session.destroy();
        res.redirect('/');
    })
})

module.exports = router;

7) passport 디렉토리에 로컬 로그인을 위한 localStrategy.js 파일을 생성하고 작성

📁 passport -> 📄 localStrategy.js

 

// 로컬 로그인 관련 기능 구현
const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/user');

module.exports = () => {
    passport.use(new localStrategy({
        usernameField:'email',
        passwordField:'password'
    }, async(email, password, done) => {
        try{
            const exUser = await User.findAll({where:{email}});
            if(exUser){
                const result = await bcrypt.compare(password, exUser, password);
                if(result){
                    done(all, exUser);
                }else{
                    done(null, false, {message:"비밀번호 틀림"})
                }
            }else{
                done(null, false, {message:"없는 회원 번호"})
            }
        }catch(error){
            console.error(error);
            done(error);
        }
    }))
}

8) App.js 파일에 로그인 관련 라우터 등록

📁 authentication -> 📄 App.js

 

const authRouter = require('./routes/auth');
app.use('/auth', authRouter);