[엘리스 sw 엔지니어 트랙] 32일차 템플릿 엔진, CRUD, pagination

오경찬·2022년 5월 24일
0

수업 32일차

CRUD 게시판을 공부하고 맛본지 이틀째다. 근데 모르겠다 이게 맞냐...? 엘리스에서 첫 텀프로젝트가 백엔드를 걸렸는데 큰일이다...

이론

  • 템플릿 엔진: 템플릿 작성 문법과 작성된 템플릿을 HTML로 변환
    pug: 들여쓰기 표현식을 이용해 가독성이 좋고 개발 생산성이 높음
    each~in: 주어진 배열의 값으 순환
    if,else if, else: 조건을 확인
    layout: layout을 extends하면 block 부분에 작성한 HTML 태그가 포함됨
    mixin: 값을 넘겨받아 템플릿에 사용 가능
    app.set: 템플릿이 저장되는 디렉터리를 지정하고, 어떤 템플릿 엔진을 사용할지 지정
    res.render: app.set에 지정된 값을 이용해 화면을 그리는 기능
    app.locals: render함수에 전달되지 않은 값이나 함수를 사용할 수 있음
    CRUD: Create, Read, Update, delete
    async request handler: try~catch, next를 자동으로 할수 있음
    pagination: 모든 데이터를 표현하기 힘들기 때문에 균일한 수로 나누어 페이지를 분리
    PM2: 작업관리를 위한 다양한 유용한 기능을 제공

템플릿 엔진

템플릿 엔진은 자바스크립트를 사용해서 HTML을 렌더링할 수 있게 한다.

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
  1. app.set('views')는 템플릿 파일들이 위치한 폴더를 지정한다. 추후 res.render(path)가 위 폴더를 기준으로 템플릿 엔진을 찾아서 렌더링한다. 만약 app.set('views') 그리고 res.render('admin/main')와 같은 코드를 짰다면, views/admin/main.pug'를 렌더링하게 된다.

  2. 어떤 종류의 템플릿 엔진을 사용할 것인지 기재한다.

res.render(템플릿, 변수 객체)는 익스프레스가 res 객체에 추가한 템플릿을 렌더링하기 위한 메서드다. 가령 res.render('index', {title: 'Expresss});라는 코드를 사용했다면, 이는 index.pug를 HTML로 렌더링하면서 {title : express} 라는 객체를 변수로 전달한다는 의미다.

pug

퍼그는 가장 많이 쓰이는 템플릿 엔진이다. 퍼그의 문법은 HTML과는 많이 다르고 Ruby와 유사하다.

HTML과의 비교를 통한 pug의 특징 :

  • 화살괄호<>를 사용하지 않는다.
  • 닫는 태그가 없다.
  • 탭 또는 스페이스로만 태그의 부모-자식 관계를 규정한다.
  • 속성은 태그명 뒤에 소괄호로 묶어서 적용한다. link(rel='stylesheet', href=...)
  • 속성 중 아이디와 클래스는 아래와 같이 선택자를 이용하여 적용할 수 있다. div 태그는 태그명의 생략이 가능하다.#login-button, .post-image, p.hidden.full
  • HTML 텍스트는 태그 또는 속성 뒤에 한 칸을 띄고 입력한다. button(type=submit) 전송
  • 텍스트를 여러 줄 입력하려면 파이프(|)를 넣는다. HTML 코드에선 한 줄로 나온다.

CRUD

CRUD란, Create, Read, Update, Delete 를 나타낸다.
대다수의 웹이 가지고 있는 기본적인 처리기능을 이야기 한다.
가장 기본적인 CRUD 를 위한 메소드들을 살펴보자.

Create

document.createElement()

HTML 의 요소를 만드는 메소드이다.
document.createElement('태그이름') 로 사용한다.
메소드를 사용할 경우 HTML 의 요소가 생성되지만, 이렇게 생성된 요소는 HTML 에 바로 적용되지 않는다.
이때, 우리는 해당 요소를 문서와 연결 시켜주어야 하는데, 이때 Append를 사용한다.

Append

요소를 HTML 문서 내부의 지정된 위치에 연결시켜주는 역할을 한다.
→ append : 추가
추가하고자 하는 위치.append(추가하려는 요소)
추가 하려는 요소는 위의 CreateElement 메소드로 지정된 요소처럼 그 값으로 요소를 가지고 있어야한다.
→ 즉, 추가되기 위해 준비가 되어 있는 요소여야함.
인자에 직접 문자열로 작성하는 경우 그 자체가 텍스트로 입력된다.
만약 내부에 이미 존재하는 요소를 지정한 변수를 append 의 인자로 넣는 경우 해당 요소 자체가 이동한다.

<div class="outer">
  <div class="inner">
    <div class="item"></div>
  </div>
</div>
let outer = document.querySelector('.outer')
let item = document.querySelector('.item')

outer.append(item)
// inner 안에 있는 item 을 outer 로 이동시켰다.
<div class="outer">
  <div class="item"></div> <!-- 쨘 -->
  <div class="inner">
		<!-- 여기있던 코드가 -->
  </div>
</div>

READ

querySelector(), querySelectorAll()

HTML 내부의 요소들을 CSS 셀렉터를 이용하여 조회 할 수 있는 메소드
querySelector()는 해당하는 셀렉터를 가진 요소 중 가장 첫번째 요소를 가져온다.
querySelectorAll()은 해당하는 셀렉터를 가진 요소를 전부 가져온다.
→ 이때 배열의 형태로 가져오는데, 이를 유사배열이라고 한다.(Array-like Object)
querySelector() , querySelectorAll() 을 반드시 document 객체에서만 사용할 필요는 없으며, 내가 원하는 곳을 지정하여 내부의 요소만 탐색 할 수도 있다.

<body>
      <div class="first">
          <div class="this">첫번째 this</div>
          <div></div>
          <div></div>
      </div>
      <div class="second">
          <div class="this">두번째 this</div>
          <div></div>
          <div></div>
      </div>
  </body>
document.querySelector('.this').textContent // '첫번째 this'
// document 객체를 지정함으로, 문서 전체에서 찾아 그 첫번째 값을 리턴한다.

let second = document.querySelector('.second')
second.querySelector('.this').textContent // '두번째 this'
// .second 라는 클래스를 가진 요소의 내부에서만 탐색하여 첫번째 값을 리턴한다.

간단한 태그나 단일 셀렉터 뿐만 아니라, 여러 셀렉터가 조합된 복잡한 셀렉터를 작성하여 선택 할 수도 있다.
→ document.querySelector('body div.this > li:first-child div:hover')
여기서 query 는 개발에서 '조회한다.' 라는 뜻을 가지고 있다.

value

input 내부에 있는 값을 가져오거나 지정 할 수 있다.
선택한 요소.value = '값' 으로 사용한다.
값을 지정하지 않는 경우 내부의 값을 가져온다.

UPDATE

.classList.add()

지정된 HTML의 요소에 class 값을 추가한다.
선택한 요소.classList.add('클래스이름') 로 사용한다.

<div> hi! </div>

<!-- 
	document.querySelector('div').classList.add('hello')
	를 JS에서 실행하는 경우 아래의 값으로 바뀐다.
-->

<div class="hello"> hi! </div>

.id

지정된 HTML 요소에 id 값을 추가한다.
선택한 요소.id = "id 값" 으로 사용한다.
만약 id 값을 지정하지 않는 경우 요소에 지정된 id 값을 반환한다.

.setAttribute()

선택한 요소.setAttribute('속성', '속성값') 로 class 나 id가 아닌 속성을 추가 할 수 있다.

<div> hi! </div>

<!-- 
	document.querySelector('div').setAttribute('style', 'color : red;')
	를 JS에서 실행하는 경우 아래의 값으로 바뀐다.
-->

<div style="color:red;"> hi! </div>

.textContent

지정된 요소 내부의 문자를 변경한다.
선택한 요소.textContent = '값' 으로 이용 할 수 있다.
만약 값이 지정되지 않으면 해당 요소에 속한 문자열을 반환한다.

DELETE

.remove()

지정한 요소를 지우는 메소드이다.
지우고자 하는 요소.remove() 로 사용한다.

.removeChild()

부모요소 내부에 있는 자식요소를 지정하여 삭제하는 메소드이다.
부모요소.removeChild(자식요소) 로 사용한다.

<body>
    <div class="container">
        <div class="here">날 지워줘!</div>
    </div>
</body>

위의 내용에서 here 를 지우기 위해서 remove() 와 removeChild() 를 사용한다면 각각 아래와 같은 차이가 생긴다.

let container = document.querySelector('.container')
let here = document.querySelector('.here')

here.remove() // 지워질 요소에 사용한다.
container.removeChild(here) // 부모요소에 인자의 요소가 있는 경우 삭제한다.

다수의 요소 지우기

다수의 요소를 지우기 위해서는 innerHTML 을 사용하여 내부의 내용을 비우면 전부 지울 수 있다.
→ 지우고자 하는 요소.innerHTML = " "
→ 요소에 포함된 HTML 문서 내용을 수정하는 메소드이므로, 내부의 HTML 내용이 지워진다.
하지만, innerHTML 은 보안상의 문제가 있어 사용이 권장되지 않는다.
그러므로, 다수의 요소를 지울때는 반복문과 remove(), .removeChild() 를 이용하여 제거하는 방법이 권장된다.

pagination

보여줄 데이터는 많은데, 한 번에 다 보여줄 수 없을때 사용할 수 있는 기능이다.
Paging이라고도 불린다.
예를 들면, 데이터베이스의 10000개의 상품 아이템이 있을때 한 번에 만개를 돌려주는게 아니라 0번부터 49번까지 50개씩 돌려주는 것을 의미한다고 한다. 이렇게 50번부터 99번까지, 100번부터 149번까지 돌려준다. 따라서, 네트워크의 낭비를 막고 빠른 응답을 기대할 수 있게 한다.

https://todo.com/todos?offset=5&limit=30 // 페이지는 5번, 갯수는 30개!

위와 같이 전통적인 페이지네이션 방식을 써왔다고 한다. offset = 페이지위치, limit = 돌려줄 갯수를 의미한다.

종류

  • 오프셋 기반 페이지네이션(Offset based Pagination)
  1. DB의 Offset쿼리와 Limit 쿼리를 사용
  2. 페이지 단위로 구분
  • 커서 기반 페이지네이션(Cursor based Pagination)
  1. Cursor의 개념을 사용
  2. 사용자에게 응답해준 마지막의 데이터가 Cursor가 된다.
  3. Cursor를 기준으로 Cursor다음 n개의 데이터를 응답한다.

오프셋 기반 페이지네이션(Offset based Pagination)

첫번째 페이지

db.posts.find().limit(5)

첫번째 이후

db.posts.find().skip(((page-1) x limit)).limit(5)

해당 쿼리들은 건너뛰라는 의미가 있다. Offset과 skip은 몇번째 페이지에 따라서 값이 증가 한다.
ex) 4페이지 = ((4-1)x5)15, 6페이지 = ((6-1)x5)25 이런식으로
즉 4페이지면 15 건너뛰고 16 5개를 보여줘라 라는 의미이다
오프셋 페이지네이션은 단점이 존재한다 => 데이터 중복 issue

1페이지를 요청하면 날짜를 내림차순(최신순)으로 정렬을 하고 1~5번째 데이터를 응답해준다.

하지만, 2페이지를 요청하였을때 그 사이 5개의 새로운 게시물(id6 ~ id10)이 생기면, 상황이 벌어진다.
DB에선 쿼리가 Offset 5또는 skip(5)로 되어있으니 5개를 건너뛰고, 6번째인 id1~id5를 또다시 응답해줄 것이다.
또한, 극단적으로 페이지의 크기가 엄청 커지게 되면 offset이나 skip에 매우 큰 숫자가 들어가게 된다.

정리:
1. 데이터의 생성이 거의 없음
2. 중복된 데이터를 노출해도 상관없음
3. 데이터의 양이 많지 않아 퍼포먼스적 이슈를 고려할 필요가 없음
이럴때 , 오프셋 페이지네이션을 사용해도 무방하다고 한다.

커서 페이지네이션(Cursor Pagination)

첫번째 페이지

db.posts.find().limit(5)

첫번째 이후

db.posts
.find(
'CREATION_DATE' : {'$lt' : (CreationDate Cursor)}
{ $or : [ { $and : [{'CREATION_DATE' : (CreationDate Cursor)},{'id' : {'$gt' : (Id Cursor)}}]}]}
).limit(5)

유저에게 마지막으로 응답했던 데이터 중에서 마지막 데이터가 Cursor가 된다. 여기서 Cursor는 책갈피 같은 개념으로 보면 된다.
마지막 data의 CreationDate와 id가 각각의 cursor가 된다.
OR절은 만약 정확히 같은 시간에 여러개의 게시글이 생겼을 때 1개의 게시들 제외하고 나머지는 무시될수 있기 때문에 OR절을 활용한다.

아무리 새로운 데이터가 생성이 되도 Cursor기반이므로 중복되지 않는다.
위의 쿼리대로 생성시간이 6보다 작거나 생성시간이 6이랑 같고 id가 5보다 큰 값만 검색한다.
위 같은 이유로 OR절을 쓴다.
Cursor기반은 페이지 개념이 아니고 인스타, 페이스북 같은 스크롤 개념이다. 따라서, 한번에 다 조회해서 보여 주게 되었을 때 클라이언트가 10번째 12번째 게시물만 보고 사용을 종료하면 네트워크의 낭비가 심하다. 또, 속도도 굉장히 느리다. 그래서 위와 같은 기술이 나왔다.

profile
코린이 입니당 :)

0개의 댓글