Node.js

node>글목록 여러 페이지로 나누기

연습노트 2024. 7. 25. 17:21

pagination

 

지금은 글이 100개 있으면 아마 이렇게 하면 100개 전부 가져와서 보여주도록 코드짜놨는데 

근데 그러면 DB쨩도 부담되고 유저 브라우저도 좀 부담이 되지 않겠습니까

그래서 보통 웹서비스들은 글이 너무 많으면 글을 여러 페이지로 쪼개놓는데

이걸 pagination이라고 부릅니다. 

같이 만들어봅시다. 

 

당연히 페이지네이션 만드는 강의를 들어야 만들 수 있는게 아니라 

한글로 기능설명부터 하고 알아서 코드짜면 되는데 

어떻게 돌아가는지 모르겠으면 다른 사이트를 참고하면 되는 것이고요 

 

1. 일단 페이지 이동 버튼들을 만들어둡니다 

2. 버튼1 누르면 첫글부터 5번째글 들어있는 페이지 보여주고 

버튼2 누르면 6번째글부터 10번째글 들어있는 페이지 보여주고

하면 끝입니다. 

 

저는 글이 13개 있어서 그럼 페이지가 세개정도 필요할거같군요. 

3개의 페이지 URL 부터 미리 맘대로 작명해보면 

/list/1

/list/2

/list/3

이런 식으로 해보겠습니다.  

 

그럼 3개 URL마다 페이지를 만들어두면 될 것 같은데 

생각해보면 URL 파라미터 문법을 가져다쓰면 비슷한 URL을 3개 작성할 필요가 없을 것 같습니다. 

URL 파라미터 어려우면 그냥 쌩코딩합시다. 

 

 

app.get('/list/1', async (요청, 응답) => {
  //1번부터 5번째 글 찾아서 result 변수에 저장하기
  응답.render('list.ejs', { 글목록 : result })
}) 

첫 페이지를 쌩코딩으로 만들어봤습니다. 

이제 주석부분을 코드로 옮기기만 하면 되는데

컬렉션에 있던 document들 중 맨 앞의 5개만 찾아오고 싶으면 어떻게 하죠? 

모르는건 당연히 검색해보면 되는데 .limit()을 뒤에 붙여주면 됩니다.

 

 

 

await db.collection('post').find().limit(5).toArray() 

이러면 1 ~ 5번째 글을 가져와줍니다. 

 

 

await db.collection('post').find().skip(5).limit(5).toArray() 

이러면 6 ~ 10번째 글을 가져와줍니다. 

.skip(x) 은 맨 앞에서 x개 스킵하고 가져와달라는 뜻입니다. 

그럼 11~15번째 글을 가져오고 싶으면 어떻게 코드짤까요 

.skip() 안에 10 넣으면 될 것 같군요. 

 

 

 

 

 

app.get('/list/1', async (요청, 응답) => {
  let result = await db.collection('post').find().skip(0).limit(5).toArray() 
  응답.render('list.ejs', { 글목록 : result })
}) 

그래서 /list/1 페이지를 완성해봤습니다. 들어가면 게시물 5개만 잘보임 

/list/2, /list/3 페이지도 만들고 싶은데 이것들은 비슷하기 때문에 URL 파라미터 문법을 써봅시다. 

 

 

 

app.get('/list/:id', async (요청, 응답) => {
  let result = await db.collection('post').find().skip(0).limit(5).toArray() 
  응답.render('list.ejs', { 글목록 : result })
}) 

이러면 /list/2, /list/3 페이지도 완성입니다. 

근데 이렇게만 냅두면 어디로 접속하든 1~5번 게시물만 가져올 것 같습니다. 

/list/2로 접속하면 6~10번 게시물 

/list/3으로 접속하면 11~15번 게시물 가져오고 싶으면 어떻게 하죠?

 

.skip() 안에 0이라고 하드코딩된 부분을

/list/2로 접속시 .skip(5)

/list/3으로 접속시 .skip(10) 이 되도록 바꾸면 되겠군요. 

 

 

 

 

app.get('/list/:id', async (요청, 응답) => {
  let result = await db.collection('post').find()
    .skip( (요청.params.id - 1) * 5 ).limit(5).toArray() 
  응답.render('list.ejs', { 글목록 : result })
}) 

(유저가 URL에 입력한거 - 1) * 5 하면 될 것 같군요. 

그럼 /list/2 접속하면 6~10번째 글 잘 보이나 확인해봅시다. 

 

 

오늘의 교훈은 

URL 파라미터 어려우면 그냥 쌩코딩하고 나중에 URL파라미터 문법으로 축약해보면 됩니다. 

마무리하고 싶으면 글목록페이지에 /list/1, /list/2 페이지로 이동하는 버튼도 만들어봅시다. 

 

 

 

 

 

skip() 의 단점

 

.skip().limit() 사용하는 경우 문제가 있는데 

.skip() 안에 100만 이상의 숫자를 넣으면 매우 오래걸립니다.  

왜냐면 skip() 쓰는 경우 document를 하나하나 세어서 수행해주기 때문에 매우 오래걸리는 것인데 

그래서 페이지네이션 기능 만들 때 간단한 사이트에선 이 방법 가져다써도 되는데

너무 많은 게시물을 스킵하는건 if문 써서 금지시켜놓든가 하면 됩니다. 

 

 

 

 

 

 

페이지네이션 만드는 다른 방법

 

다른 방법도 있습니다.

이거 쓰면 document가 몇개있든간에 빠르게 찾아올 수 있는데

어떻게 찾냐면 .find() 안에 조건을 입력하면 그 조건에 맞는 모든 document를 찾아올 수 있는데  

 

await db.collection('post').find({_id : {$gt : 방금본 마지막게시물_id}}).limit(5).toArray() 

조건을 이렇게 입력하는 겁니다. 

뭔 뜻이냐면 '방금본 마지막 게시물의 _id'보다

더 큰 _id를 가진 document들을 5개 찾아오라는 뜻입니다.

이거 실행하면 지금 보는 게시물에서 다음 게시물 5개를 가져올 수 있겠군요.

 

그래서 엄밀히 말하면 페이지네이션이라기보다는

다음 게시물 5개 가져오는 기능인데 

이렇게 하면 장단점이 있습니다. 

 

장점 : 매우 빠릅니다. 

원래 _id 기준으로 뭔가 찾는건 찾는건 DB가 가장 빠르게 하는 작업입니다. 

 

단점 : 페이지네이션 버튼을 다음버튼으로 바꿔야합니다.

그리고 다음게시물만 가져올 수 있어서  

1페이지에서 갑자기 3페이지로 뛰어넘어서 게시물들을 가져오고 그런건 못합니다. 

 

 

 

 

 

 

다음 게시물 5개 가져오기 

 

진짜 되는지 구현해봅시다.

 

app.get('/list/next/:id', async (요청, 응답) => {
  let result = await db.collection('post').find({_id : {$gt : 마지막게시물_id}}).limit(5)
  응답.render('list.ejs', { 글목록 : result })
}) 

/list/next/:id 라고 URL을 새로 만들어놨습니다.

그리고 방금 본 마지막게시물_id 부분만 채우면 될텐데 이 정보는 어디있죠? 

유저에게 보내라고 합시다. 유저가 마지막게시물을 웹페이지에서 보고있을거아닙니까 그거의 _id를보내라고 합시다. 

그럼 ejs 파일로 돌아가서 '다음' 버튼 누르면

/list/next/어쩌구로 GET요청날리고 _id도 보내라고 코드를 짜야겠군요. 

 

 

 

 

<a href="/list/next/???">다음</a>

list.ejs 페이지에 다음버튼을 만들어봤습니다. 

버튼 눌렀을 때 GET요청날리고 싶으면 fetch같은거 써도 되겠으나

a태그 써도 GET요청이 날라갑니다. 

대신 새로고침 되어서 새로고침되는거 상관없으면 씁시다. 

근데 GET요청시 현재 페이지에 있는 맨 마지막 글의 _id를 같이 전송해야합니다. 

서버가 그 정보가 필요하댔으니까요. 

그거 어딨습니까?

<%= 글목록 %> 이라는 변수 출력해보면 들어있지 않을까요?

 

<%= 글목록[0]._id %> 이러면 첫 글의 _id

<%= 글목록[1]._id %> 이러면 둘째 글의 _id

가 출력됩니다. 

 

 

<%= 글목록[글목록.length - 1]._id %> 

그래서 위처럼 코드짜면 array에서 마지막 항목을 출력할 수 있기 때문에 

저러면 마지막 글의 _id가 잘 나올거같군요.

 

 

<a href="/list/next/<%= 글목록[글목록.length - 1]._id %>">다음</a>

마지막 글의 _id를 GET요청시 서버로 보내보도록 합시다. 

URL 파라미터자리에 보내봤음 

 

 

 

 

app.get('/list/next/:id', async (요청, 응답) => {
  let result = await db.collection('post').find({_id : {$gt : new ObjectId(요청.params.id) }}).limit(5)
  응답.render('list.ejs', { 글목록 : result })
}) 

그럼 다음버튼 눌렀을 때 서버에서 요청.params.id 쓰면 마지막 게시물 _id를 쓸 수 있습니다. 

완성인 것 같은데 

다음 버튼 누르면 다음 게시물들 잘 보여주나 테스트해봅시다. 

 

 

 

 

 

유저가 1000번째 페이지를 보고싶어하는데요

 

하지만 이렇게 구현해놓으면

유저가 1페이지부터 차근차근 조회하는게 아니라

1페이지에 있다가 갑자기 1000페이지부터 조회하는건 불가능합니다. 

 

근데 애초에 1000번째 페이지를 보고싶어하는 유저는 보통 봇일 확률이 99%라서 

대부분의 상황에선 다음버튼 하나로 만족해도 되겠지만 

항상 여러분 맘대로 코드를 짜는건 아니지 않겠습니까 

싫어도 요구사항도 들어줘야할 때가 있습니다. 

 

1000번째 페이지도 가끔 보여줘야하고 + 속도도 빠르게 하고 싶으면 

해결책이 하나 있는데

애초에 글의 _id를 자동부여되는 _id를 쓰지 말고 정수로 님들이 직접 부여하는겁니다. 

첫 글은 { _id : 1 }

둘째 글은 { _id : 2 } 

이러라는 소리입니다. 

 

그러면 예를 들어 1001~1005번 글이 필요하면 

db.collection('post').find({_id : { $gt : 1000 }}).limit(5)

이렇게 코드짜면 끝입니다. 그리고 매우 빠르게 가져옴 

근데 _id를 정수로 부여하려면 auto increment 하는 법을 찾아보면 되는데

강의하단에 요약해놨는데 필요할 때 더 찾아봅시다. 

 

 

 

 

오늘의 결론 : 

잘 생각해보면 글의 정확한 순서나 번호가 크게 중요한 서비스는 거의 없습니다.

예를 들면 쇼핑몰을 운영하는데 갑자기 10000번째 등록된 상품을 출력해달라고 요청하는사람이 어딨습니까. 

그건 100% 데이터 수집 봇임

그래서 글의 _id나 순서가 정수로 꼭 필요한지는 한 번 생각해봅시다.

관계형 DB만 쓰던 사람들의 고정관념일 수 있습니다. 

 

 

 

 

auto increment 기능

 

글발행할 때 마다 정수로 글번호같은걸 부여하고 싶으면 

관계형 DB의 경우 auto increment 기능을 켜면 되는데

MongoDB는 그게 없으니 하나 직접 만들어야합니다. 

 

일단 counter 라는 다른 이름의 컬렉션을 하나 만들고 

그 안에 { _id : 자동부여된거, count : 0  } 이런 document를 하나 발행해둡니다. 

그 다음에 게시물을 하나 발행할 때 마다 어떻게 하냐면 

 

1. counter 컬렉션에 있던 document 를 찾아와서 count : 에 기재된 값을 출력해봅니다. 

2. 그 값에 +1을 한 다음 그걸 _id란에 기입해서 새로운 글을 발행합니다.

그럼 { _id : 1, title : 어쩌구 } 이런게 발행되겠군요.

3. 성공적으로 발행된걸 확인하면 counter 컬렉션의 document에 있던 count : 항목을 +1 해줍니다.

updateOne 쓰면 되겠군요. 

 

 

근데 실은 trigger 기능을 쓰는게 더 정확하게 잘되기 때문에 

https://www.mongodb.com/basics/mongodb-auto-increment

시간많으면 그걸 쓰도록 합시다. 

trigger는 DB에 뭔가 변화가 일어날 때 자동으로 실행되는 코드를 뜻합니다.