[8주차] Node.js와 MongoDB 2 - Express.js와 MongoDB로 웹 서비스 만들기 2

minLuna·2023년 4월 22일
0

엘리스 AI트랙 7기

목록 보기
47/62

본 자료는 Elice 플랫폼의 자료를 사용하여 정리하였습니다.

회원가입 구현하기

회원가입 설명

  • 이메일, 이름, 패스워드의 간단한 정보만 사용
    • 이메일 형식 확인
    • 비밀번호 최소길이 확인
    • 패스워드와 패스워드 확인 일치여부 확인

비밀번호 저장방법

  • Hash

    • 문자열을 되돌릴 수 없는 방식으로 암호화
    • 로그인 시 전달된 비밀번호를 Hash하여 저장된 값과 비교
  • Hash 사용하기

  const hash = crypto.createHash('sha1');    // sha224, sha256 등 사용가능
  hash.update(password);
  hash.digest('hex');

회원가입 구현하기

  1. 회원가입 페이지 구현
  2. script를 이용해 이메일 형식, 비밀번호 확인 문자 확인
  3. form을 이용해 post 요청 전송
  4. 회원가입 처리 및 redirect

passport.js와 로그인

passport.js란?

  • Express.js 어플리케이션에 간단하게 사용자 인증 기능을 구현하게 도와주는 패키지
  • 유저 세션 관리 및 다양한 로그인 방식 추가 가능

passport-local

  • passport는 다양한 로그인 방식을 구현하기 위해 strategy라는 인터페이스를 제공
  • passport-local은 username, password를 사용하는 로그인의 구현체

로그인 기능 구현하기

  1. 로그인 화면 구성하기
  2. passport-local strategy로 로그인 구현하기
  3. passport.js 설정하기
  4. passport로 요청 처리하기

Session Store

Session이란?

  • 웹 서버가 클라이언트의 정보를 구분하여 서버에 저장하고, 클라이언트 요청 시 Session ID를 사용하여 클라이언트의 정보를 다시 확인하는 기술

Session 작동방식

  1. 서버는 세션을 생성하고 Session ID를 클라이언트에게 전달
  2. 클라이언트는 요청 시 Session ID를 함께 요청에 담아서 전송
  3. 서버는 전달받은 Session ID로 해당하는 세션을 찾아 클라이언트 정보 확인

Express.js의 Session

  • Express-session 패키지로 자동으로 session 동작 구현

Session Store 사용 이유

  • express-session은 session을 메모리에 저장한다. (재부팅 시 모든 로그인 해제)
  • 서버 간 세션정보를 공유하지 않는다.

Session Store 구성

MongoDB를 Session Store로 사용하기

const MongoStore = require('connect-mongo');

app.use(session({
  secret: 'secret',
  resave: false,
  saveUninitialized: true,
  store: MongoStore.create({
    mongoUrl: 'mongoUrl',
  }),
}));
  • connect-mongo 패키지를 사용해 express-session 설정 시 store 옵션에 전달하고, mongoUrl을 설정
  • 세션데이터를 몽고디비에 저장하고 관리하는 기능을 자동으로 수행해준다.

회원과 게시글의 연동

회원과 게시글 연동 기능 설명

  • 게시글 작성 시 로그인된 회원정보를 작성자로 추가
  • 게시글 - 작성자는 populate하여 사용하도록 구현
  • 게시글 수정, 삭제 시 로그인된 유저와 작성자가 일치하는지 확인
  • 작성자의 게시글 모아보기 기능 구현

PostSchema 수정

  • populate를 사용하기 위해 ObjectID 사용

게시글 등록 요청 수정

const author = await User.find({
  shortId: req.user.shortId,
});
if (!author) {
  throw new Error('No User');
}

await Post.create({
  title,
  content,
  author,
});
  • req.user에는 strategy에서 최소한의 정보로 저장한 shortId, email, username만 가지고 있다.
  • Post 생성 시 user의 ObjectID를 전달해야 하는데, 이를 위해 User에서 shortId로 회원을 검색하여 한 번 더 검증한다.
  • type: ObjectID로 선언된 필드에 객체가 주어지면 자동으로 ObjectID을 사용한다.

게시글에 작성자 연동

// ./routes/posts.js

router.get('/', ... {
  ...
  const posts = await Post.find({})
    .populate('author');

res.render('posts/list', { posts });

// ./views/posts/list.pug
...
  td post.author.name
  • 게시글 find 시 populate를 추가하여 ObjectID로 저장된 author를 각 게시글에 주입
  • 사용 시 post.author.{field}로 사용 가능하다.

게시글 수정, 삭제 시 유저확인

const post = await Post.find({ shortId, }).populate('author')

if (post.author.shortId !== req.user.shortId) {
  throw new Error('Not Authorized');
}
  • 게시글 수정, 삭제 시 작성자를 populate하여 로그인된 사용자와 일치하는지 확인

작성자 게시글 모아보기 기능 구현

  • 기본적으로 MongoDB는 Document 검색 시, 전체 문서를 하나씩 확인한다.
    \rarr 비효율적, 속도 저하의 원인
  • Index
    • 쿼리를 효육적으로 수행하여 성능을 향상시키게 할 수 있다.

author에 index 설정하기

author: {
  types: Schema.Types.ObjectId,
  ref: 'User',
  required: true,
  index: true,
},
  • PostSchema의 author 속성에 index: true 옵션을 사용하면 mongoose가 자동으로 MongoDB에 인덱스를 생성해준다.
  • 이미 데이터가 많은 상태에서 인덱스를 추가할 시 작업 시간이 길어져, MongoDB가 응답하지 않을 수 있다.
  • 따라서, 예상되는 인덱스를 미리 추가하는 것이 좋다.

회원 게시글 라우팅 추가하기

// ./routes/users.js

router.get('/:shortId/posts', ... => {
  ...
  const { shortId } = req.params;
  const user = await User.find({ shortId });
  const posts = await Post.find({ author: user }).populate('author');
  res.render('posts/list', { posts, user });
});
  • RESTful 한 구성을 위해, 회원 \rarr 게시글의 경로를 /users/{userId}/posts로 구성
  • 게시글 목록 view는 기존에 작성한 posts/list.pug를 재활용한다.

게시글 목록 화면 수정

h2= user ? `${user.name}의 게시글`: "전체 게시글"
...
td: a(href=`/users/${post.author.shortId}/posts`) = post.author.name
  • 게시글 목록 화면을 재활용하기 위해 수정
  • 유저의 게시글인 경우 "###의 게시글"이라는 제목 사용
  • 게시글의 사용자 이름에 유저의 게시글 link 추가

CSR로 댓글 기능 구현하기

CSR을 구현하는 방법

  1. 페이지 로드 시 필요한 리소스를 클라이언트에 선언
  2. 클라이언트에서 필요한 데이터를 비동기 호출
  3. 클라이언트가 전달받은 데이터를 가공, 리소스를 사용하여 화면에 표시

클라이언트에 리소스 선언 - HTML Template

  • 브라우저에 표시되지 않는 HTML Element를 작성해두고, JavaScript로 이를 화면에 반복적으로 그릴 수 있게 하는 기술

댓글 화면 작성하기

// posts/view.pug

...
table
  thead
    tr
      td(colspan="2")
        input#content(type="text")
      td: button(onclick="writeComment()")
        댓글 작성
  tbody#comments
template#comment-template
  tr
    td.content
    td.author
    td.createdAt
  • 게시글 상세 화면 하단에 댓글작성, 목록 화면 추가
  • HTML Template 사용하여 한 개의 댓글이 표시될 모양을 선언
  • JavaScript로 조작하기 위해 id, class를 선언하는 것이 유용하다.

데이터 비동기 호출 - API 작성하기

  • 지금까지는 HTTP 응답으로 HTML을 전송하는 방식
  • CSR에서는 데이터만 주고받을 수 있는 API를 구성해야 한다.(JSON 사용)

게시글에 댓글 추가하기

const CommentSchema = new Schema({
  content: String,
  author: {
    type: Schema.Types.ObjectId,
    ref: 'User',
  },
}, {
  timestamps: true,
});

const PostSchema = new Schema({
  ...
  comments: [CommentSchema],
  • mongoose의 sub-schema를 이용하여 Post 스키마에 Comment를 배열로 추가
  • populate를 사용할 때, ObejctID만 저장하는 것과는 다르게 Comment의 내용을 게시글이 포함하게 된다.
  • sub-schema 내부에서도 populate가 가능하다.

API 작성하기

댓글 작성

...
router.post('/posts/:shortId/comments', ... {
  const { shortId } = req.params;
  const { content } = req.body;
  const authro = await User.findOne({ shortId: req.user.shortId });

  await Post.updateOne({ shortId }, {
    $push: { comments: {
      content,
      author,
    }},
  });

  res.json({ result: 'success' });
});
  • api 라우터를 추가하고, RESTful하게 api/posts/{postId}/comments 경로로 댓글 작성 기능 구현
  • 게시글 업데이트 시 $push를 사용하여 comments 배열에 새로 작성된 댓글 추가 \rarr 동시에 들어오는 요청에 대해 정확하게 처리
  • api는 render 하지 않고 json으로 응답

댓글 목록

...
router.post('/posts/:shortId/comments', ... {
  const { shortId } = req.params;
  const post = await Post.findOne({ shortId });

  await User.populate(post.comments, {
    path: 'authro'
  });

  res.json(post.comments);
});
  • /api/posts/{postId}/comments로 RESTful 경로 설정
  • find에 populate하지 않고 User (model)의 populate를 사용하는 방법도 가능하다.

데이터 비동기 호출 - fetch로 클라이언트에서 API 호출하기

  • 브라우저는 비동기 HTTP 요청을 fetch 함수를 이용해 제공
  • jQuery의 Ajax와 유사한 기능
  • fetch를 이용하면 간단하게 JavaScript로 HTTP요청을 비동기 처리할 수 있다.

fetch로 API 호출하고 처리하기

댓글 작성하기

...
script.
  function writeComment() {
    const input = document.querySelector('#content')
    const content = input.value;
    fetch('/api/posts/#{post.shortId}/comments', {
      method: 'post',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content }),
    })
    .then(() => {
      if (res.ok) {
        input.value = '';
          loadComments();
      } else {
        alert('오류가 발생했습니다.');
      }
    });
  • 댓글 작성 버튼 클릭 시 writeComment() 실행
  • input#content에서 내용을 읽어 fetch로 댓글 작성 api 호출
  • 호출 결과의 성공 여부를 확인하여, 댓글 다시 불러오기 실행

댓글 목록 불러오기

// 댓글 목록 api 호출하기
script.
  loadComments();
  function loadComments() {
    document
      .querySelector('#comments')
      .innerHTML = '', // 이전 목록 삭제
    fetch('/api/posts/#{post.shortId}/comments')
      .then((res) => {
        if (res.ok) {
          return res.json();
        } else {
          throw new Error('댓글을 불러오지 못했습니다.');
        }
    })
    .then((comments) => {
      comments.forEach(addComment);
    });
    .catch((err) => alert(err.message));
  }
                      
// HTML Template 사용하여 댓글 화면에 표시하기
function addComment(comment) {
  const template = document.querySelector('#comment-template');
  const node = document.importNode(template.content, true);
  node.querySelector('.content').textContent = comment.content;
  node.querySelector('.authro').textContent = comment.author.name;
  node.querySelector('.createdAt').textContent = comment.createdAt;
  document.querySelector('#comments').appendChild(node);
}

MongoDB Aggregation

Aggregation이란?

  • MongoDB에서 Document들을 가공하고, 연산하는 기능
  • RDBMS에서 SQL로 수행할 수 있는 기능들을 유사하게 구현할 수 있다.

Aggregation을 사용하는 이유

  • MongoDB의 find는 검색필터링과 정렬이외의 기능을 제공하지 않는다.
  • 다른 Collection에서 데이터를 가져오거나 검색된 데이터를 그룹화하는 등의 작업에 필요

간단한 Aggregation 예제

db.posts.aggregate([
  { $group: { _id: '$authro', count { $sum: 1 } } },
  { $match: { sum: { $gt: 10 } } },
  { $lookup: { from: 'users', localField: '_id', foreignField: '_id', as: 'users' } },
]);
  • aggregation은 Stage들의 배열로 이루어지고 각 Stage는 순차적으로 수행된다.
  1. 작성자별 게시글 수를 취합하고
  2. 게시글 수가 10개보다 많은 작성자를 찾아서
  3. 해당 작성자를 회원 collection에서 검색한다.
profile
열심히

0개의 댓글