ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Node] 개발자 지망생 스터디 - 21일차
    스터디/KAKAOCLOUDSCHOOL 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);
Designed by Tistory.