[SNS 서비스 구축 프로젝트(미니프로젝트3)] 1. SNS앱 만들기

Shy·2023년 10월 5일
0

NodeJS(Express&Next.js)

목록 보기
39/39

SNS앱 만들기

앱 기본 구조

미니 프로젝트1에서 만든 기본 구조를 가져온다.

필요한 패키지 설치

npm install connect-flash method-override multer
  1. connect-flash
    플래시 메세지를 위한 미들웨어 모듈

  2. method-override
    HTML Form 태그에서 원래 POST, GET 메소드만 지원하는데 DELETE, PUT을 사용할 수 있게 지원해 주는 모듈

  3. multer
    파일 업로드를 위한 모듈

API 요청을 처리하기 위한 폴더와 파일 생성

모든 router.js파일에는 아래 코드를 추가해 준다.

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

module.exports = router
  • express 모듈을 가져와 express 변수에 할당한다. express는 웹 애플리케이션 프레임워크로, API 및 웹 서버를 쉽게 만들 수 있도록 도와준다.
  • express.Router() 메서드를 사용하여 새로운 라우터 인스턴스를 생성하고 이를 router 변수에 할당힌다. Router 인스턴스는 미들웨어와 라우트를 마운트하기 위한 완전한 미들웨어 및 라우팅 시스템이다.
  • 현재 정의된 router 인스턴스를 모듈로 내보낸다. 이렇게 함으로써 다른 파일에서 이 router를 가져와 사용할 수 있게 된다. 가져온 곳에서는 이 router에 추가적인 라우트를 정의하거나, 다른 미들웨어 또는 라우터와 결합하여 사용할 수 있다.

로그인, 회원가입 페이지 생성

스타일링엔 부트스트랩을 사용한다
1. login.ejs파일

<%- include('../partials/header') %>

<div class="auth-wrapper">
    <form method="POST" action="/auth/login">
        <div class="text-center">
            <h3 class="mb-3 font-weight-normal">로그인</h3>
        </div>
        <div class="mb-3">
            <label for="email">Email address</label>
            <input type="email" class="form-control" id="email" name="email">
        </div>
        <div class="mb-3">
            <label for="password">Password</label>
            <input type="password" class="form-control" id="password" name="password">
        </div>

        <div class="d-flex justify-content-between align-items-center">
            <div>
                <button type="submit" class="btn btn-primary">로그인</button>
            </div>
            <div>
                <a href="/auth/google" class="btn btn-danger" role="button">
                    구글
                </a>
                <a href="/auth/kakao" class="btn btn-warning" role="button">
                    카카오
                </a>
            </div>
        </div>

        <div class="text-end mt-2">
            <a href="/signup">회원 가입 페이지로...</a>
        </div>
    </form>
</div>

<%- include('../partials/footer') %>
  1. signup.ejs
<%- include('../partials/header') %>

<div class="auth-wrapper">
    <form method="POST" action="/auth/signup">
        <div class="text-center">
            <h3 class="mb-3 font-weight-normal">회원가입</h3>
        </div>
        <div class="mb-3">
            <label for="email">Email address</label>
            <input type="email" class="form-control" id="email" name="email" required>
        </div>
        <div class="mb-3">
            <label for="username">Username</label>
            <input type="text" class="form-control" id="username" name="username" required>
        </div>
        <div class="mb-3">
            <label for="password">Password</label>
            <input type="password" class="form-control" id="password" name="password" required>
        </div>

        <button type="submit" class="btn btn-primary">가입하기</button>

        <div class="text-end mt-2">
            <a href="/login">로그인 페이지로...</a>
        </div>
    </form>
</div>

<%- include('../partials/footer') %>

코드 분석

login.ejs

<!-- Header Include -->
<%- include('../partials/header') %>
  • include 디렉티브를 사용하여 헤더 부분을 삽입한다. ../partials/header 경로의 파일이 헤더에 해당한다.
<!-- Form Wrapper -->
<div class="auth-wrapper">
  • 로그인 폼을 감싸는 div를 시작한다. 이 div에 auth-wrapper 클래스가 부여된다.
<!-- Form Element -->
<form method="POST" action="/auth/login">
  • form 요소를 시작합니다. 이 폼은 /auth/login 경로로 POST 요청을 전송한다.
<!-- Email Input -->
<div class="mb-3">
    <label for="email">Email address</label>
    <input type="email" class="form-control" id="email" name="email">
</div>
  • 이메일 입력 필드를 포함하는 div다. 부트스트랩 클래스를 사용하여 스타일링이 적용되어 있다.
<!-- Password Input -->
<div class="mb-3">
    <label for="password">Password</label>
    <input type="password" class="form-control" id="password" name="password">
</div>
  • 비밀번호 입력 필드를 포함하는 div다. 스타일링을 위해 부트스트랩 클래스가 적용되어 있다
<!-- Login & Social Login Buttons -->
<div class="d-flex justify-content-between align-items-center">
    <div>
        <button type="submit" class="btn btn-primary">로그인</button>
    </div>
    <div>
        <a href="/auth/google" class="btn btn-danger" role="button">
            구글
        </a>
        <a href="/auth/kakao" class="btn btn-warning" role="button">
            카카오
        </a>
    </div>
</div>
  • 로그인 버튼과 구글 및 카카오로 로그인하는 버튼을 포함한다. 각 버튼은 부트스트랩 클래스로 스타일링된다.
<!-- Signup Link -->
<div class="text-end mt-2">
    <a href="/signup">회원 가입 페이지로...</a>
</div>
  • 회원 가입 페이지로의 링크를 제공한다.
<!-- Footer Include -->
<%- include('../partials/footer') %>
  • include 디렉티브를 사용하여 푸터 부분을 삽입한다. ../partials/footer 경로의 파일이 푸터에 해당된다.

작동 원리

  • 사용자가 이메일과 패스워드를 입력한 후 "로그인" 버튼을 클릭하면, /auth/login 경로로 POST 요청이 전송되며, 서버는 이 요청을 처리한다.
  • 구글 및 카카오 로그인 버튼은 각각의 소셜 로그인 경로로 이동한다.
  • "회원 가입 페이지로..." 링크는 회원 가입 페이지로 이동한다.

signup.ejs

이 코드는 EJS (Embedded JavaScript) 템플릿 언어로 작성된 HTML 코드 조각이다. 주요로 회원 가입 폼을 포함하며, include 디렉티브를 사용하여 헤더와 푸터를 포함한다. 각 부분의 상세 분석은 다음과 같다.

<!-- Header Include -->
<%- include('../partials/header') %>
  • include 디렉티브를 사용하여 헤더 부분을 삽입한다. ../partials/header 경로의 파일이 헤더에 해당한다.
<!-- Form Wrapper -->
<div class="auth-wrapper">
  • 회원 가입 폼을 감싸는 div를 시작한다. 이 div에 auth-wrapper 클래스가 부여된다.
<!-- Form Element -->
<form method="POST" action="/auth/signup">
  • form 요소를 시작한다. 이 폼은 /auth/signup 경로로 POST 요청을 전송한다.
<!-- Email Input -->
<div class="mb-3">
    <label for="email">Email address</label>
    <input type="email" class="form-control" id="email" name="email" required>
</div>
  • 이메일 입력 필드를 포함하는 div다. 부트스트랩 클래스를 사용하여 스타일링이 적용되어 있다.
<!-- Username Input -->
<div class="mb-3">
    <label for="username">Username</label>
    <input type="text" class="form-control" id="username" name="username" required>
</div>
  • 사용자 이름을 입력받는 텍스트 필드를 정의한다. 이 필드 역시 필수 입력이다.
<!-- Password Input -->
<div class="mb-3">
    <label for="password">Password</label>
    <input type="password" class="form-control" id="password" name="password" required>
</div>
  • 비밀번호를 입력받는 필드를 정의한다. 이 필드 역시 필수 입력이다.
<!-- Submit Button -->
<button type="submit" class="btn btn-primary">가입하기</button>
  • 폼을 제출하는 버튼을 정의한다.
<!-- Footer Include -->
<%- include('../partials/footer') %>
  • include 디렉티브를 사용하여 푸터 부분을 삽입한다. ../partials/footer 경로의 파일이 푸터에 해당한다.

작동 원리

  • 사용자가 이메일, 사용자 이름, 및 패스워드를 입력한 후 "가입하기" 버튼을 클릭하면, /auth/signup 경로로 POST 요청이 전송되며, 서버는 이 요청을 처리한다.
  • "로그인 페이지로..." 링크는 로그인 페이지로 이동한다.

위 확장팩을 설치해 주고, EJS Beautify의 Indentation size를 4로 바꿔준다.

Header와 Footer의 일반적인 구조는 다음과 같다.

이 header와 footer는 다음과 같이 사용이 가능하다.

<%- include('../partials/header') %>
  
  <!-- 파일의 중간 내용 -->
  
<%- include('../partials/footer') %>

부트스트랩 5 CDN

부트스트랩 링크

CDN링크를 넣어주자.

header.ejs

<!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>SNS</title>
    <link rel="stylesheet" href="/styles/main.css">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css" integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>

<body>

    <nav class="navbar navbar-dark bg-dark shadow-sm fixed-top navbar-expand-lg">
        <div class="container">
            <a class="navbar-brand">SNS</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarToggle">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarToggle">
                <% if (!currentUser) { %>
                    <ul class="navbar-nav ms-auto mb-2 mb-lg-0">
                        <li class="nav-item">
                            <a class="nav-link" href="/signup">Signup</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="/login">Login</a>
                        </li>
                    </ul> 
                <% } else { %>
                <ul class="navbar-nav ms-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                        <a class="nav-link" href="/posts">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/friends">Friends</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/profile/<%= currentUser._id %>">
                            <%= currentUser.username %>
                        </a>
                    </li>
                    <li class="nav-item">
                        <form action="/auth/logout" method="POST">
                            <button type="submit" class="nav-link no-outline">
                                Logout
                            </button>
                        </form>
                    </li>
                </ul>
                <% } %>
            </div>
        </div>
    </nav>
<br>
<br>
<br>


    <div class="container">
        <% if (error && error.length > 0) { %>
        <div class="alert alert-danger" role="alert">
            <%= error %>
        </div>
        <% } %>
        <% if (success && success.length > 0) { %>
        <div class="alert alert-success" role="alert">
            <%= success %>
        </div>
        <% } %>
    </div>

2. footer.ejs

<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js" integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.min.js" integrity="sha384-mQ93GR66B00ZXjt0YO5KlohRA5SY2XofN4zfuZxLkoj1gXtW8ANNCe9d5Y3eG5eD" crossorigin="anonymous"></script>
</body>

</html>

Header코드

이 코드는 HTML5 문서로, EJS (Embedded JavaScript) 템플릿 언어를 사용하여 동적 내용을 렌더링한다. 코드는 크게 head 섹션, 네비게이션 바 (navbar), 및 알림 (error 및 success 메시지) 섹션으로 구성된다.

  1. HTML head Section
    • 문서의 head 섹션에는 기본 메타 데이터, 문서 제목, 및 스타일시트 링크가 포함된다.
    • 여러 스타일시트 링크를 포함하여 디자인 및 스타일을 적용한다 (Bootstrap, Font Awesome, 및 custom CSS).
  2. Navigation Bar
    • Bootstrap을 사용한 어둡게 표현된 (dark) 네비게이션 바를 만든다.
    • 로그인 상태에 따라 네비게이션 링크를 동적으로 렌더링한다.
    • 로그인하지 않은 사용자는 "Signup" 및 "Login" 링크를 볼 수 있다.
    • 로그인한 사용자는 "Home", "Friends", 프로필 링크, 및 "Logout" 버튼을 볼 수 있다.
    • "Logout" 버튼은 POST 메서드를 사용하여 /auth/logout 경로로 폼을 전송한다.
  3. Notification Messages
    • error 및 success 변수의 값에 따라 알림 메시지를 동적으로 렌더링한다.
    • error 변수에 값이 있는 경우, 에러 메시지를 표시하는 빨간색 알림 박스가 표시된다.
    • success 변수에 값이 있는 경우, 성공 메시지를 표시하는 초록색 알림 박스가 표시된다.

사용되는 변수

  • currentUser: 현재 로그인한 사용자의 정보를 담고 있습니다. 로그인 상태를 확인하고, 사용자 이름을 표시하는 데 사용된다.
  • error: 에러 메시지를 담고 있는 변수입니다. 이 변수에 값이 있을 때만 에러 알림 박스가 표시된다.
  • success: 성공 메시지를 담고 있는 변수입니다. 이 변수에 값이 있을 때만 성공 알림 박스가 표시된다.

partial 사용법

<%- include('../partials/header') %>
  <div class="form-signin">
    <form method="POST" action="/auth/login">
      <!-- ... -->
    </form>
  </div>
<%- include('../partials/footer') %>

위 처럼 include를 사용하여 헤더와 푸터를 사용한다.

main.css

.auth-wrapper {
    width: 100%;
    max-width: 330px;
    padding: 15px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%); 
    border: 1px solid lightgray;
    border-radius: 4px;
    background-color: #f5f5f5;
}

헤더에서 rel을 해줬다.
이제 로그인페이지와 회원가입 페이지도 스타일링이 잘 되었을 것이다.

모델 생성하기

현재 로그인을 할 때는 이메일을 이용해서 하며, 가입할 때는 이메일과 유저네임 둘 다 받는다. 하지만 OAuth 로그인을 할 때는 유저네임을 DB에 안 넣고 있었기에 그 부분을 임의로 넣어주겠다.

users.model.js

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
    email: {
        type: String,
        unique: true
    },
    password: {
        type: String,
        minLength: 5
    },
    googleId: {
        type: String,
        unique: true,
        sparse: true,
    },
    kakaoId: {
        type: String,
        unique: true,
        sparse: true,
    },
    username: {
        type: String,
        required: true,
        trim: true,
    },
    firstName: {
        type: String,
        default: 'First Name'
    },
    lastName: {
        type: String,
        default: 'Last Name'
    },
    bio: {
        type: String,
        default: '데이터 없음'
    },
    hometown: {
        type: String,
        default: '데이터 없음'
    },
    workspace: {
        type: String,
        default: '데이터 없음'
    },
    education: {
        type: String,
        default: '데이터 없음'
    },
    contact: {
        type: String,
        default: '데이터 없음'
    },
    friends: [{ type: String }], // 배열 안의 문자 타입 ['RTPC', 'qwerty', 'pikachu1']
    friendsRequests: [{ type: String }] // 배열 안의 문자 타입
}, { timestamps: true })

const saltRounds = 10;
userSchema.pre('save', function (next) {
    let user = this;
    // 비밀번호가 변경될 때만
    if (user.isModified('password')) {
        // salt를 생성합니다.
        bcrypt.genSalt(saltRounds, function (err, salt) {
            if (err) return next(err);

            bcrypt.hash(user.password, salt, function (err, hash) {
                if (err) return next(err);
                user.password = hash;
                next();
            })
        })
    } else {
        next();
    }
})


userSchema.methods.comparePassword = function (plainPassword, cb) {
    // bcrypt compare 비교 
    // plain password  => client , this.password => 데이터베이스에 있는 비밀번호

    bcrypt.compare(plainPassword, this.password, function (err, isMatch) {
        if (err) return cb(err);
        cb(null, isMatch);
    })
}



const User = mongoose.model('User', userSchema);

module.exports = User;

코드 분석

해당 코드는 Mongoose를 사용하여 MongoDB에 저장되는 사용자의 비밀번호를 안전하게 관리하기 위한 로직을 구현한 것입니다. 코드는 크게 두 부분으로 나뉜다.

  1. 비밀번호의 해시 생성: 사용자 정보를 데이터베이스에 저장하기 전에 비밀번호를 해시 처리한다.
  2. 비밀번호 비교: 로그인 시, 사용자가 입력한 비밀번호와 데이터베이스에 저장된 해시된 비밀번호를 비교한다.

코드에 대한 자세한 내용은 다음과 같다.

  1. 비밀번호의 해시 생성
    • userSchema.pre('save', function (next) { ... } ): Mongoose 스키마에 정의된 pre 메서드를 사용하여 'save' 이벤트 전에 실행될 콜백 함수를 정의한다. 이 콜백은 주로 데이터를 데이터베이스에 저장하기 전에 미리 처리해야 할 작업을 수행하는 데 사용된다.
    • if (user.isModified('password')) { ... }: isModified 메서드를 사용하여 'password' 필드가 수정되었는지 확인한다. 즉, 비밀번호가 변경될 때만 이 로직을 실행하도록 한다.
    • bcrypt.genSalt(saltRounds, function (err, salt) { ... } ): bcrypt 라이브러리의 genSalt 메서드를 사용하여 salt를 생성한다. 이 salt는 비밀번호를 해시하는 데 사용된다.
    • bcrypt.hash(user.password, salt, function (err, hash) { ... } ): bcrypt의 hash 메서드를 사용하여 실제 비밀번호를 해시한다. 해싱이 성공하면 원래의 비밀번호를 해시 값으로 대체한다.
  2. 비밀번호 비교
    • userSchema.methods.comparePassword = function (plainPassword, cb) { ... }: 사용자 스키마에 comparePassword 메서드를 추가한다. 이 메서드는 로그인 시 사용자에게 입력받은 비밀번호 (plainPassword)와 데이터베이스에 저장된 해시된 비밀번호를 비교하는 데 사용된다.
    • bcrypt.compare(plainPassword, this.password, function (err, isMatch) { ... } ): bcrypt 라이브러리의 compare 메서드를 사용하여 입력받은 비밀번호와 해시된 비밀번호를 비교한다. 비교 결과는 콜백 함수의 두 번째 인수인 isMatch를 통해 반환된다. isMatch 값이 true면 비밀번호가 일치하며, false면 일치하지 않는다.

posts.model.js

const { default: mongoose } = require("mongoose");

const postSchema = new mongoose.Schema({
    description: {
        type: String,
        required: true
    },
    comments: [{
        type: mongoose.Schema.Types.ObjectId,
        ref: "Comment",
    }],
    author: {
        id: {
            type: mongoose.Schema.Types.ObjectId,
            ref: "User",
        },
        username: String
    },
    image: {      
        type: String
    },
    likes: [{ type: String }]
}, {
    timestamps: true,
})

module.exports = mongoose.model("Post", postSchema);
  1. postSchema Definition
    • postSchema는 mongoose.Schema의 새 인스턴스로 생성된다.
  2. Fields
    • description: String 타입이며, 필수 필드다.
    • comments: 배열 형태로, Comment 모델을 참조하는 ObjectId 타입이다. 여러 댓글의 ObjectId를 배열로 저장한다.
    • author: author 필드는 id 및 username 서브필드를 가지며, id는 User 모델을 참조하는 ObjectId 타입이다.
    • image: String 타입을 가지며, 이미지 URL 또는 파일 경로를 저장할 수 있다.
    • likes: String 타입의 배열로, 좋아요를 누른 사용자의 정보 또는 식별자를 저장한다.
  3. Timestamps
    • timestamps: true 옵션은 createdAt 및 updatedAt 필드를 자동으로 관리한다.
  4. Model Export
    • 정의한 postSchema를 사용하여 Post 모델을 생성하고, 이를 내보낸다. 이를 통해 다른 파일에서 Post 모델을 가져와 사용할 수 있다.
type: mongoose.Schema.Types.ObjectId?

mongoose.Schema.Types.ObjectId는 Mongoose에서 MongoDB의 ObjectId 데이터 유형을 나타낸다. MongoDB는 각 도큐먼트에 대해 고유한 ID를 자동으로 생성하며, 이 ID의 데이터 유형은 ObjectId이다.

코드에서 mongoose.Schema.Types.ObjectId를 사용하는 부분은 주로 다른 도큐먼트와의 관계를 나타내기 위해 사용된다. 다른 모델의 도큐먼트와 연결할 때 해당 도큐먼트의 ObjectId를 참조하여 관계를 설정한다.

  1. comments: 배열 안에서 type: mongoose.Schema.Types.ObjectId, ref: "Comment"를 사용하면 comments 필드는 "Comment" 모델의 ObjectId들로 구성된 배열이다. 즉, 각 Post 도큐먼트는 여러개의 Comment 도큐먼트들을 참조할 수 있다.
  2. author.id: type: mongoose.Schema.Types.ObjectId, ref: "User"를 사용하면 author.id 필드는 "User" 모델의 특정 도큐먼트를 참조한다. 즉, 각 Post 도큐먼트는 하나의 User 도큐먼트에 연결되어 있으며, 이는 포스트의 작성자를 나타낸다.

comment.model.js

const { default: mongoose } = require("mongoose");

const commentSchema = new mongoose.Schema({
    text: String,
    author: {
        id: {
            type: mongoose.Schema.Types.ObjectId,
            ref: "User",
        },
        username: String
    }
}, {
    timestamps: true,
})

module.exports = mongoose.model("Comment", commentSchema);
  1. commentSchema Definition
    • commentSchema는 mongoose.Schema의 새 인스턴스로 정의되며, 이 스키마는 Comment 문서의 구조를 정의한다.
  2. Fields
    • text: String 타입이며, 댓글의 본문을 나타낸다.
    • author: author는 두 개의 서브 필드를 가진다. (id, username)
    • id: User 모델을 참조하는 ObjectId 타입이며, 댓글 작성자의 사용자 ID를 저장한다.
    • username: 작성자의 이름을 나타내는 String 타입이다.
  3. Timestamps
    • { timestamps: true } 옵션은 Mongoose가 createdAt과 updatedAt 필드를 자동으로 관리하도록 설정한다. 이 필드는 문서가 생성되거나 업데이트될 때 자동으로 갱신된다.
  4. Model Export
    • commentSchema를 사용하여 Comment 모델을 생성하고, 이를 export하여 다른 파일에서 이 모델을 사용할 수 있게 한다.

사용 방법

  • Comment 모델은 MongoDB 데이터베이스 내에 Comment 컬렉션의 각 문서에 대한 스키마를 정의한다.
  • 다른 코드에서 이 Comment 모델을 import하여 댓글 데이터를 생성, 조회, 수정, 삭제할 수 있다.

기본적인 인증 기능 체크하기

로그인 성공 시 posts 경로로 이동

posts경로에 왔을 때 posts/index.ejs 보여주기

포스트 UI 생성하기

이런 팝업창은 부트스트랩의 Modal을 이용하는 거다.

UI 생성하기

posts/index.ejs

<%- include('../partials/header') %>
<div class="container">

    <div style="max-width: 600px; margin: 1rem auto;">
        <%- include('../partials/create-post') %>
    </div>
    <% posts.forEach((post) => { %>
        <!-- 나의 포스트인지 -->
        <% if (post.author.id.equals(currentUser._id) || 
        // 나의 친구의 포스트인지
        currentUser.friends.find(friend => friend === post.author.id.toString())) { %>
        
        <div style="max-width: 600px; margin: 1rem auto;">
            <%- include('../partials/post-item', { post: post }) %>        
        </div>

        <% } %>
    <% }) %>

</div>

<%- include('../partials/post-modal') %>

<%- include('../partials/footer') %>

partials/create-posts/ejs

<div class="card">
    <h5 class="card-header text-start"> 소식 알리기</h5>
    <div class="card-body">
        <div class="card-text text-muted create-post"
            data-bs-toggle="modal" data-bs-target="#newpost" 
        >
            <%= currentUser.username %>의 소식을 공유해주세요!
        </div>
    </div>
</div>

partials/post-modal.ejs

<div class="modal fade" id="newpost">
    <div class="modal-dialog modal-dialog-centered">
        <div class="modal-content">
            <div class="modal-header">
                <h5>소식 알리기</h5>
                <button type="button" class="btn-close close" data-bs-dismiss="modal"></button>
            </div>
            <form method="POST" action="/posts" enctype="multipart/form-data">
                <div class="modal-body">
                    <div class="form-group">
                        <textarea name="desc" id="desc" class="form-control" row="6" cols="50" placeholder="<%= currentUser.username %>소식을 알려주세요!" required></textarea>
                    </div>
                    <div class="form-group mt-1">
                        <input class="form-control" type="file" name="image">
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="submit" class="btn btn-primary">소식공유</button>
                </div>
            </form>
        </div>
    </div>
</div>

포스트 찾아와서 보여주기

routes/posts.router.js

router.get('/', checkAuthenticated, (req, res) => {
    Post.find()
        .populate('comments')
        .sort({ createdAt: -1 })
        .exec((err, posts) => {
            if (err) {
                console.log(err);
            } else {
                res.render('posts', {
                    posts: posts,
                });
            }
        })
})

코드는 Express.js 라우터의 한 부분으로, HTTP GET 요청을 / 경로로 처리하는 라우터 핸들러를 정의하고 있다. 요청이 이 경로로 들어오면, 다음의 동작을 수행한다.

  1. Middleware: checkAuthenticated
    • checkAuthenticated는 미들웨어 함수입니다. 이 미들웨어는 요청이 처리되기 전에 실행되며, 일반적으로 사용자가 인증되었는지 확인하는 데 사용된다. 사용자가 인증되지 않았다면, 일반적으로 로그인 페이지로 리디렉션하거나 401 Unauthorized 응답을 반환할 것이다.
  2. Query Database: Post.find()
    • Post.find()는 MongoDB에서 모든 Post 도큐먼트를 찾는 Mongoose 쿼리다.
  3. Populate Comments
    • .populate('comments')는 Mongoose의 populate 메서드를 사용하여 각 포스트에 연결된 'comments' 필드를 채운다. 각 포스트의 'comments' 필드에는 Comment 도큐먼트의 ObjectId들이 저장되어 있다. populate를 사용하면 해당 ID들을 실제 Comment 도큐먼트로 대체하여, Post 도큐먼트와 연관된 Comment 도큐먼트들의 정보를 한 번의 쿼리로 가져올 수 있다.
  4. Sort Posts
    .sort({ createdAt: -1 })는 생성 날짜 (createdAt 필드)를 기준으로 포스트를 내림차순으로 정렬한다. 이렇게 하면 최신 포스트가 리스트의 상단에 위치하게 된다.
  5. Execute the Query
    • .exec()는 Mongoose 쿼리를 실행하고, 결과를 반환한다. 쿼리의 결과 (또는 오류)는 제공된 콜백 함수를 통해 처리된다.
  6. Render the View
    • res.render('posts', { posts: posts }): 성공적으로 포스트를 검색한 경우, posts라는 뷰 템플릿을 렌더링하고, 검색된 포스트를 해당 템플릿에 전달한다. 뷰 템플릿은 일반적으로 EJS, Pug 등의 템플릿 엔진을 사용하여 HTML을 생성한다.
  7. Error Handling
    • 만약 데이터베이스 쿼리 중 오류가 발생한다면, 콘솔에 오류 메시지를 출력한다.

이 코드는 사용자가 인증된 경우에만 웹 페이지에서 모든 포스트를 가져와서 표시하는 기능을 제공한다.

CurrentUser 데이터 가져오기

router.get("/", checkAuthenticated, (req, res) => {
  Post.find()
  .populate("comments")
  .sort({ createdAt: -1 })
  

이미지 업로드 및 포스트 생성하기

posts.router.js

const storageEngine = multer.diskStorage({
    destination: (req, file, callback) => {
        callback(null, path.join(__dirname, '../public/assets/images')); // 오류가 없다먼 'null'을 전달.
    },
    filename: (req, file, callback) => {
        callback(null, file.originalname); // 오류가 없다먼 'null'을 전달.
    }
})
// 이 callback을 사용함으로써, multer는 해당 파일을 지정된 위치와 이름으로 저장할 수 있게 된다.

const upload = multer({ storage: storageEngine }).single('image');

router.post('/', checkAuthenticated, upload, (req, res, next) => {
    let desc = req.body.desc; // description. post-modal.ejs로 가보자.
    let image = req.file ? req.file.filename : ""; // 파일이 있는 경우

    Post.create({
        image: image,
        description: desc,
        author: {
            id: req.user._id,
            username: req.user.username
        },
    }, (err, _) => { // post는 사용을 안하니 언더바(_)를 사용했다.
        if (err) {
            req.flash('error', '포스트 생성 실패');
            res.redirect("back"); // 원래 페이지로 되돌아간다.

            // next(err);
        } else {
            req.flash('success', '포스트 생성 성공');
            res.redirect("back"); // 원래 페이지로 되돌아간다.
        }
    })

})

코드 분석

이 코드는 Express.js를 사용한 라우팅 및 파일 업로드를 관리하는 부분이다.

  1. Multer Storage Engine Configuration
    • multer.diskStorage를 사용하여 업로드되는 파일의 저장 구성을 정의한다.
    • destination: 파일이 저장될 위치를 정의한다. 코드에 따르면, 파일은 프로젝트의 ../public/assets/images 디렉토리에 저장된다.
    • filename: 저장될 파일의 이름을 정의한다. 이 코드에서는 업로드된 파일의 원래 이름(file.originalname)이 사용된다.
  2. Multer Middleware Configuration
    • upload는 Multer 미들웨어로, HTTP 요청에 첨부된 파일을 처리하고, 지정된 스토리지 엔진에 따라 저장한다. .single('image')는 요청에서 'image'라는 이름의 단일 파일을 처리할 것임을 나타낸다.
  3. Route Configuration
    • router.post('/', checkAuthenticated, upload, (req, res, next) => { ... }:
      • HTTP POST 요청을 / 경로로 처리하는 라우터 핸들러를 정의한다.
      • checkAuthenticated: 사용자가 인증되었는지 확인하는 미들웨어이다.
      • upload: Multer 미들웨어로, 파일 업로드를 처리한다.
  4. Route Handler
    • let desc = req.body.desc;: 클라이언트에서 전송된 설명(description)을 가져온다.
    • let image = req.file ? req.file.filename : "";: req.file이 존재하면(즉, 파일이 첨부되었으면) 업로드된 파일의 이름을 image에 저장하고, 그렇지 않으면 빈 문자열을 저장한다.
    • Post.create(...): Mongoose를 사용하여 새로운 Post 도큐먼트를 데이터베이스에 생성한다. 생성할 도큐먼트의 정보는 { image: image, description: desc, author: { ... } } 객체에 담겨 있다.
    • Error Handling
      • 포스트 생성 중 오류가 발생하면, 오류 메시지를 사용자에게 플래시 메시지로 표시하고 이전 페이지로 리디렉션한다.
      • 오류가 발생하지 않으면, 성공 메시지를 플래시 메시지로 표시하고 이전 페이지로 리디렉션한다.

이 코드는 인증된 사용자가 이미지와 설명(description)을 포함하여 새로운 포스트를 생성할 수 있게 해준다. 사용자가 이미지를 첨부하여 업로드하면 해당 이미지는 서버에 저장되며, 포스트 정보는 MongoDB 데이터베이스에 저장된다.

server.js

app.use((err, req, res, next) => {
    res.status(err.status || 500);
    res.send(err.message || "Error Occurred");
})

이 코드는 Express.js에서 전역 오류 핸들러 미들웨어를 정의하는 부분이다. 오류 핸들러 미들웨어는 다른 미들웨어나 라우트 핸들러에서 발생한 오류를 처리하는 데 사용된다.

  1. app.use((err, req, res, next) => { ... }
    • app.use는 Express 애플리케이션에 미들웨어 함수를 등록하는 메서드이다.
    • 오류 핸들러는 4개의 매개변수를 가지는데, 이것이 일반 미들웨어와의 주요 차이점이다. 이 네 개의 매개변수는 순서대로 err, req, res, next이다.
  2. res.status(err.status || 500);:
    • 발생한 오류 객체에 status 프로퍼티가 있으면 해당 상태 코드를 응답으로 설정하고, 그렇지 않으면 기본값인 500을 사용한다.
  3. res.send(err.message || "Error Occurred");:
    • 발생한 오류 객체에 message 프로퍼티가 있으면 해당 메시지를 응답 본문으로 보내고, 그렇지 않으면 기본 메시지인 "Error Occurred"를 보낸다.

전체적으로, 이 오류 핸들러는 애플리케이션 내의 어디에서든 발생할 수 있는 예상치 못한 오류를 처리하는 역할을 한다. 오류 정보가 제공되면 해당 정보를 사용하여 응답을 반환하고, 제공되지 않으면 기본값을 사용하여 응답한다.

포스트 리스트 나열하기

데이터베이스에 저장한 포스트를 가져와서 Posts페이지에서 보여주겠다.

Post 데이터베이스에서 가져오기

posts.router.js

router.get('/', checkAuthenticated, (req, res) => {
    Post.find()
        .populate('comments')
        .sort({ createdAt: -1 })
        .exec((err, posts) => {
            if (err) {
                console.log(err);
            } else {
                res.render('posts', {
                    posts: posts,
                });
            }
        })
})

에러 없애기 위해 임시로 가져오기

포스트 UI 생성하기

나의 포스트와 나의 친구들의 포스트만 나열해주기!

posts/index.ejs

<% posts.forEach((post) => { %>
<!-- 나의 포스트인지 -->
   <% if (post.author.id.equals(currentUser._id) || 
   // 나의 친구의 포스트인지
   currentUser.friends.find(friend => friend === post.author.id.toString())) { %>
   <div style="max-width: 600px; margin: 1rem auto;">
      <%- include('../partials/post-item', { post: post }) %>        
   </div>
   <% } %>
<% }) %>
코드 설명

이 코드는 EJS 템플릿 엔진을 사용하여 데이터를 웹 페이지에 동적으로 표현하는 코드이다.

  1. <% posts.forEach((post) => { %>
    • posts라는 배열을 순회합니다. 이 배열은 포스트 객체들의 컬렉션이다.
  2. <% if (post.author.id.equals(currentUser._id) || ... %>
    • 각 포스트에 대해, 현재 로그인한 사용자(currentUser)가 포스트의 작성자(post.author.id)인지 확인한다. .equals() 메서드는 Mongoose ObjectId를 비교하기 위해 사용된다.
    • 또는, 로그인한 사용자의 친구 목록(currentUser.friends)에 포스트의 작성자가 포함되어 있는지 확인한다. 여기서 .find() 메서드는 배열 내에서 조건에 맞는 첫 번째 요소를 반환한다. 조건은 친구의 ID와 포스트의 작성자 ID가 일치하는지 여부이다.
  3. <div style="max-width: 600px; margin: 1rem auto;">
    • 조건에 맞는 포스트를 웹 페이지에 표시하기 위한 <div> 요소를 생성한다.
  4. <%- include('../partials/post-item', { post: post }) %>
    • EJS의 include 구문을 사용하여, 다른 EJS 템플릿 파일인 '../partials/post-item'을 현재 템플릿에 삽입한다. 이 템플릿은 각 포스트의 내용을 렌더링하기 위해 사용된다.
    • post-item 템플릿에는 post라는 변수를 통해 현재 순회 중인 포스트 데이터가 전달된다.

요약하면, 이 코드는 posts 배열을 순회하면서, 현재 로그인한 사용자 또는 사용자의 친구가 작성한 포스트만 웹 페이지에 표시한다.

partials/post-item.ejs

<div class="card mb-2">
    <div class="card-body text-start">
        <div class="d-flex align-items-center">
            <a href="/profile/<%= post.author.id %>">
                <h5>
                    <%= post.author.username %>
                </h5>
            </a>
            <small class="text-muted ms-auto">
                <%= post.createdAt.toDateString() %>
            </small>
            <% if (post.author.id.equals(currentUser._id)) { %>
            <div class="dropdown">
                <button class="no-outline" type="button" data-bs-toggle="dropdown" aria-expanded="false">
                    <img src="/assets/images/ellipsis.png" height="20px">
                </button>
                <ul class="dropdown-menu">
                    <li><a class="dropdown-item text-center" href="/posts/<%= post._id %>/edit">Edit</a></li>
                    <li>
                        <form class="dropdown-item text-center" action="/posts/<%= post._id %>?_method=DELETE" method="POST">
                            <button type="submit" class="no-outline">Delete</button>
                        </form>
                    </li>
                </ul>
            </div>
            <% } %>
        </div>

        <p class="card-text mt-2">
            <%= post.description %>
        </p>

        <% if (post.image) { %>
            <img class="w-100" src="/assets/images/<%= post.image %>" />
        <% } %>

           

        <hr class="mt-1" >
        <div class="d-flex justify-content-between">
            <div class="row">
                <form action="/posts/<%= post._id %>/like?_method=PUT" method="POST">
                    <!-- 이미 좋아요를 눌렀는지  -->
                    <% if (post.likes.find(like => like === currentUser._id.toString())) { %>
                        <button type="submit" class="no-outline">
                            <img src="/assets/images/like-1.png" height="20px" >
                            <span class="ms-1"> <%= post.likes.length %></span>
                        </button>
                    <% } else { %>
                        <button type="submit" class="no-outline">
                            <img src="/assets/images/like.png" height="20px" >
                            <span class="ms-1"> <%= post.likes.length %></span>
                        </button>
                    <% } %>
                </form>
            </div>

            <a class="ms-auto pe-2"  data-bs-toggle="collapse" href="#post<%= post._id %>">
                댓글 <%= post.comments.length %>
            </a>
        </div>


        <hr class="mt-1" >
        <div class="collapse show" id="post<%= post._id %>">
            <% if (post.comments.length > 0) { %>
                <div class="card-body comment-section">
                    <% post.comments.forEach((comment) => { %>
                        <div class="d-flex justify-content-between">
                            <div class="font-weight-bold">
                                <%= comment.author.username %>
                            </div>
                            <small>
                                <%= comment.createdAt.toDateString() %>
                            </small>
                        </div>
                        <div class="d-flex justify-content-between mt-2">
                            <p>
                                <%= comment.text %>
                            </p>
                            <% if (comment.author.id.equals(currentUser._id)) { %>
                                <div class="dropdown">
                                    <button class="no-outline" type="button" data-bs-toggle="dropdown">
                                        <img src="/assets/images/ellipsis.png" height="20px" >
                                    </button>
                                    <div class="dropdown-menu">
                                        <a 
                                        class="dropdown-item text-center"
                                        href="/posts/<%= post._id %>/comments/<%= comment._id %>/edit">Edit</a>
                                        <form class="dropdown-item text-center"
                                            action="/posts/<%= post._id %>/comments/<%= comment._id %>?_method=DELETE"
                                            method="POST"
                                            >
                                          <button class="no-outline" type="submit">Delete</button>  
                                        </form>
                                    </div>
                                </div>
                            <% } %>
                        </div>
                    <% }) %>
                </div>
            <% } %>
        </div>
        <div>
            <form method="POST" action="/posts/<%= post._id %>/comments">
                <div class="form-group">
                    <input  name="text" id="desc" class="comment-section" placeholder="댓글을 작성해주세요." required>
                    <p class="small ms-2"> 엔터를 눌러주세요.</p>
                </div>
            </form>
        </div>
        
    </div>
</div>

Connect-Flash

포스트 생성을 성공했을 때나 실패했을 때 다른 페이지로 이동해 주는데 그때 다른 페이지에서 성공이나 실패에 대한 메시지를 화면에서 보여주고 싶다.

그때 connect-flash라는 모듈을 이용해서 간단하게 보여줄 수 있다.

connect-flash를 사용할 때는 정보를 유지해줘야 하기 때문에 세션을 사용한다.

Connect-Flash 사용법

npm install connect-flash

connect-flash 미들웨어 등록

const flash = require("connect-flash");
app.use(flash());

페이지를 이동하면서, 이동한 페이지에서 메시지를 보여준다.

app.get('/send', (req, res) => {
  
  res.send('message send page');
})

app.get('/receive', (req, res) => {
  
  res.send('message receive page');
})
app.get('/send', (req, res) => {
  res.flash('post success', '포스트가 생성되었습니다.');
  res.send('message send page');
})

app.get('/receive', (req, res) => {
  res.send(req.flash('post success')[0]);
})
// 페이지를 새로고침 하면 세션에서 메시지가 사라지기에(flash) 화면에도 아무것도 안 보이게 된다.
// connect-flash는 휘발성으로 한번 실행되면 세션에서 저장값이 사라진다. 

이 코드는 두 개의 Express 라우터 핸들러를 정의하고 있다. 라우터 핸들러는 특정 URL 경로에 대한 HTTP GET 요청을 처리한다. 여기서는 req 객체를 통해 요청을 받고, res 객체를 통해 응답을 보낸다.

  1. app.get('/send', (req, res) => {...}:
    • /send 경로에 대한 GET 요청이 들어오면 이 핸들러가 실행된다.
    • res.flash('post success', '포스트가 생성되었습니다.'); 구문은 flash 메시지를 설정한다. 이 flash 메시지는 한 번 보여지면 삭제되므로 다음 요청에서는 사용할 수 없다. post success는 메시지의 key이고, '포스트가 생성되었습니다.'는 해당 메시지의 값이다.
    • res.send('message send page');로 사용자에게 'message send page'라는 텍스트 메시지를 응답으로 보낸다.
  2. app.get('/receive', (req, res) => {...}:
    • /receive 경로에 대한 GET 요청이 들어오면 이 핸들러가 실행된다.
    • req.flash('post success')[0]를 통해 앞서 설정한 flash 메시지의 값을 가져온다. req.flash('post success')는 배열 형태로 값을 반환하므로, [0]을 통해 첫 번째 값을 가져온다.
    • res.send(req.flash('post success')[0]); 구문은 해당 flash 메시지의 값을 응답으로 보낸다.
  3. app.get('/receive', (req, res) => {...}
    • /receive 경로에 대한 GET 요청이 들어오면 이 핸들러가 실행된다.
    • req.flash('post success')[0]를 통해 앞서 설정한 flash 메시지의 값을 가져온다. req.flash('post success')는 배열 형태로 값을 반환하므로, [0]을 통해 첫 번째 값을 가져온다.
    • res.send(req.flash('post success')[0]); 구문은 해당 flash 메시지의 값을 응답으로 보낸다.

connect-flash 앱에 적용

포스트 생성을 잘했거나 실패했으면 Header.ejs에서 메시지를 보여주겠다.

    <div class="container">
        <% if (error && error.length > 0) { %>
        <div class="alert alert-danger" role="alert">
            <%= error %>
        </div>
        <% } %>
        <% if (success && success.length > 0) { %>
        <div class="alert alert-success" role="alert">
            <%= success %>
        </div>
        <% } %>
    </div>

포스트 생성 성공이나 실패 시 메시지 보내기

router.post('/', checkAuthenticated, upload, (req, res, next) => {
    let desc = req.body.desc;
    let image = req.file ? req.file.filename : "";

    Post.create({
        image: image,
        description: desc,
        author: {
            id: req.user._id,
            username: req.user.username
        },
    }, (err, _) => {
        if (err) {
            req.flash('error', '포스트 생성 실패');
            res.redirect("back");

            // next(err);
        } else {
            req.flash('success', '포스트 생성 성공');
            res.redirect("back");
        }
    })

})

해당 코드는 Express 라우터 핸들러를 정의하고 있으며, 클라이언트로부터 받은 POST 요청을 처리한다. 구체적인 설명을 아래에서 확인할 수 있다.

  1. 라우터 경로와 미들웨어
    • router.post('/', checkAuthenticated, upload, (req, res, next) => {...}):
      • 이 핸들러는 클라이언트로부터 '/' 경로로 들어오는 POST 요청을 처리한다.
      • checkAuthenticated 미들웨어: 요청이 인증된 사용자에 의해 만들어졌는지 확인한다.
      • upload 미들웨어: multer 라이브러리를 사용하여 요청으로부터 업로드된 파일(이미지)을 처리한다.
  2. 요청 데이터 처리
    • let desc = req.body.desc;: POST 요청의 body로부터 desc 값을 가져와 변수에 저장한다.
    • `let image = req.file ? req.file.filename : "";: multer를 통해 업로드된 파일이 있다면 그 파일명을 저장하고, 없다면 빈 문자열을 저장한다.
  3. 데이터베이스에 포스트 생성
    • Post.create({..}, (err, _) => {...}): MongoDB 데이터베이스에 새로운 Post를 생성하는 명령이다.
      • 포스트의 image, description, author 정보를 저장한다.
      • author 정보는 현재 로그인한 사용자의 _idusername으로 구성된다.
  4. 에러 및 응답 처리
    • 만약 포스트 생성 중에 에러가 발생한다면:
      • req.flash('error', '포스트 생성 실패');: 에러 메시지를 flash에 저장한다.
      • res.redirect("back");: 사용자를 이전 페이지로 리다이렉트한다.
    • 에러가 없고 포스트 생성에 성공했다면:
      • req.flash('success', '포스트 생성 성공');: 성공 메시지를 flash에 저장한다.
      • res.redirect("back");: 사용자를 이전 페이지로 리다이렉트한다.

이 코드는 사용자가 웹 어플리케이션에서 새로운 포스트를 생성할 때 사용되는 로직을 포함하고 있다. 사용자가 이미지와 설명을 업로드하면 해당 정보를 데이터베이스에 저장하고, 성공 또는 실패 메시지를 플래시에 저장하여 다음 페이지에서 사용자에게 보여줄 수 있다.

flash메시지를 받아서 ejs에 넣어주기

router.get('/', checkAuthenticated, (req, res) => {
  Post.find()
    .populate('comments')
    .sort({ createdAt: -1 })
    .exec((err, posts) => {
    if (err) {
      console.log('Error occured', err);
    } else {
      res.render('posts/index', {
        posts: posts,
        currentUser: req.user,
        error: req.flash('error'),
        success: req.flash('success')
      });
    }
  })
})
mainRouter.get('/', checkNotAuthenticated, function(req, res, next) {
  res.render('auth/login', {
    error: req.flash('error'),
    success: req.flash('success')
  });
});

mainRouter.get('/login', checkNotAuthenticated, function(req, res, next) {
  res.render('auth/login', {
    error: req.flash('error'),
    success: req.flash('success')
  });
});

mainRouter.get('/signup', checkNotAuthenticated, function(req, res, next) {
  res.render('auth/signup', {
    error: req.flash('error'),
    success: req.flash('success')
  });
});

코드 설명

해당 코드는 Express에서 사용되는 라우터 핸들러를 정의하고 있으며, 클라이언트로부터 받은 특정 GET 요청을 처리한다.

  1. 라우터 경로와 미들웨어
    • router.get('/', checkAuthenticated, (req, res) => {...}):
      • 이 핸들러는 클라이언트로부터 '/' 경로로 들어오는 GET 요청을 처리한다.
      • checkAuthenticated 미들웨어는 요청이 인증된 사용자에 의해 만들어졌는지 확인하는 기능을 담당한다.
  2. 데이터베이스 쿼리
    • Post.find(): MongoDB에서 Post 모델에 해당하는 모든 문서를 검색하는 쿼리를 시작한다.
    • .populate('comments'): 각 Post 문서의 comments 필드에 연결된 Comment 모델의 데이터를 가져와 함께 채워넣는다. 이렇게 하면 데이터베이스에서 별도의 쿼리를 실행하지 않고도 관련된 코멘트 데이터에 접근할 수 있다.
    • .sort({ createdAt: -1 }): Post 문서를 createdAt 필드 기준으로 내림차순 정렬한다. 이는 최신의 포스트부터 반환되도록 한다.
    • .exec((err, posts) => {...}): 쿼리를 실행하고, 결과나 에러를 콜백 함수로 받는다.
  3. 에러 및 응답 처리
    • 만약 데이터베이스 쿼리 중 에러가 발생한다면:
      • console.log('Error occured', err);: 에러 메시지를 콘솔에 출력한다.
    • 에러가 없다면:
      • res.render('posts/index', {...}): 'posts/index' 뷰 템플릿을 사용하여 HTML 페이지를 렌더링한다.
      • 뷰 템플릿에 전달되는 데이터는:
        • posts: 쿼리의 결과로 받은 포스트 목록이다.
        • currentUser: 현재 인증된 사용자의 정보이다.
        • error: 에러 메시지를 flash에서 가져와 전달한다.
        • success: 성공 메시지를 flash에서 가져와 전달한다.

코드는 인증된 사용자가 요청한 모든 포스트와 관련된 코멘트를 데이터베이스에서 가져와서 웹 페이지에 렌더링하는 역할을 한다.

이 코드는 Express 웹 서버에서 주요 라우팅 로직을 담고 있다.

  1. 미들웨어 checkNotAuthenticated
    • 모든 라우터 핸들러에서 checkNotAuthenticated 미들웨어를 사용한다. 이 미들웨어는 사용자가 이미 인증되지 않은 경우에만 특정 라우트에 액세스 할 수 있도록 하는 기능을 수행한다. 예를 들면, 이미 로그인한 사용자가 로그인 페이지나 회원가입 페이지에 다시 액세스하는 것을 방지할 수 있다.
  2. 루트 경로('/')
    • mainRouter.get('/', checkNotAuthenticated, function(req, res, next) {...}
      • 이 라우트는 웹사이트의 루트 URL(예: http://example.com/)에 대한 GET 요청을 처리한다.
      • 인증되지 않은 사용자만 액세스할 수 있으며, 로그인 페이지(auth/login)를 렌더링한다.
      • 또한, 에러나 성공 메시지를 req.flash를 통해 가져와서 렌더링한다.
  3. 로그인 경로('/login')
    • mainRouter.get('/login', checkNotAuthenticated, function(req, res, next) {...}:
      • 이 라우트는 로그인 페이지에 대한 GET 요청을 처리한다.
      • 기능은 루트 경로와 거의 동일하다. 로그인 페이지(auth/login)를 렌더링하며, 에러나 성공 메시지를 함께 전달한다.
  4. 회원가입 경로('/signup')
    • mainRouter.get('/signup', checkNotAuthenticated, function(req, res, next) {...}:
      • 이 라우트는 회원가입 페이지에 대한 GET 요청을 처리한다.
      • 인증되지 않은 사용자만 액세스할 수 있으며, 회원가입 페이지(auth/signup)를 렌더링한다.
      • 에러나 성공 메시지도 함께 렌더링된다.

이 코드는 사용자 인증과 관련된 주요 페이지들에 대한 라우팅을 설정하고 있으며, 각 페이지를 요청할 때마다 인증되지 않은 사용자만 접근할 수 있도록 하는 미들웨어를 사용하고 있다.

더 깔끔한 방법 사용하기 (res.locals)

// res.locals 객체 안에 있는 프로퍼티들은 
// 아래에 있는 routes 들에서 변수로 사용가능하게 된다. 
// views에서도 변수를 사용할 수 있게 된다.
app.use((req, res, next) => {
  res.locals.error = req.flash('error');
  res.locals.success = req.flash('success');
  res.locals.currentUser = req.user;
  next();
})

app.use('/', mainRouter);
app.use('/auth', usersRouter);
app.use('/posts', postsRouter);
app.use('/posts/:id/comments', commentsRouter);
app.use('/profile/:id', profileRouter);
app.use('/friends', friendsRouter);
app.use(likeRouter);
// 또한 res.locals의 프로퍼티들은 요청이 끝날때 값이 사라지게 된다.

포스트 수정하기

수정 페이지 보여주는 핸들러 생성

router.get("/:id/edit", checkPostOwnership, (req, res) => {
  Post.findById(req.params.id (err, post) => {
    if (err) {
      res.redirect("/posts")
    } else {
      res.render("posts/edit", {
        post: post
      })
    }
  })
})

checkPostOwnership 미들웨어 생성

auth.js

function checkPostOwnerShip(req, res, next) {
  if (req.isAuthenticated()) {
    // id에 맞는 포스트가 있는 포스트인지 
    Post.findById(req.params.id, (err, post) => {
      if (err || !post) {
        req.flash('error', '포스트가 없거나 에러가 발생했습니다.');
        res.redirect('back');
      } else {
        // 포스트가 있는데 나의 포스트인지 확인
        if (post.author.id.equals(req.user._id)) {
          req.post = post;
          next();
        } else {
          req.flash('error', '권한이 없습니다.');
          res.redirect('back');
        }
      }
    })

  } else {
    req.flash('error', '로그인을 먼저 해주세요.');
    res.redirect('/login');
  }
}

Header 올바르게 조건 처리하기

Post 수정 페이지 UI 생성하기 (MethodOverride)

profile
초보개발자. 백엔드 지망. 2024년 9월 취업 예정

0개의 댓글