👀글을 작성하기에 앞서
css는 하나도 모르는 초보이며, javascript는 에러조차뜨지않고 문법을 인도해주는 것도 없어 헤매던 사람이 찾아 만든 것이기때문에 최대한 초보친화적으로 만들어 졌습니다.
0. 개요 / 무한 스크롤이란?
게시판, 데이터등을 조회할때 우리는 페이징 처리를 통해 글을 나누어서 조회 하는 기능을 페이지네이션이라고 부르기로 했다.
이러한 페이지네이션은 편리한 조회에 유용하지만, 페이지를 이동한다는 것때문에 연속적이지 못한 경험을 제공한다.
페이지의 변화 없이 연속적인 경험을 위해서 생긴것이 무한스크롤이다.
이러한식으로 페이지가 끊이지 않는다는 경험을 주는것이 무한 스크롤이어서 프로젝트에서 이것을 구현해보았다.
✔ 필요한 제원
JAVA, HTML, ThymeLeaf, Javascript를 이용한다.
환경으로는 SpringBoot(Gradle)을 이용한다.
❗ 미리 알고갈 요점
html을 기준으로 구역을 나누는것은 <div> 태그이다. 그것에 class를 주어 구분하는데 그중에도 Scroller라는 div안에 card라는 div을 만들어 내부에 scroller를 채우겠다.
우리의 목적은
1. 스크롤이 최하단에 닿았음을 감지 하면
2. append를 javascript에서 사용하어 card들을 추가하여 내용물을 늘려주겠다. 이때 정보는 JSON으로 받아와야한다.
한줄에 4개씩 넣을 예정이며 최초로 8개의 card를 불러놓고, 스크롤이 아래로 내려가면 4개씩의 데이터를 불러오도록 하겠다.
1. 무한스크롤을 구성하기 위해 구성할 공간 만들기
우선 html과 css를 이용해 scroller라는 클래스명을 가진 div에 card라는 클래스명을 가진 div를 넣어주겠다.
어렵지않게 px 단위로 고정적으로 진행하겠다. %단위로 갔을때는 생각보다 계산이 까다롭고 불편하기 때문이다.
✔ HTML
<div class="scroller">
<div class="card">
<img src="#">
<div class="middle"></div>
<div class="bottom"></div>
</div>
</div>
✔ CSS
.scoller{
display: flex; /* 이것을 flex요소들을 담는 컨테이너로 만들겠다는 뜻*/
flex-wrap: wrap; /* 한 줄이 가득 차면 다음줄로 자동으로 줄바꿈 하겠다는 뜻 */
justify-content: space-between; /* flex요소들 사이에 동일한 간격을 두겠다는 뜻 */
width: 1300px; /* %를 이용하면 화면의 크기에따라 다르기때문에 고정 pixel로 지정함*/
margin: 30px; /* 동일한 이유 */
}
.card{
width: 400px;
height: 700px;
margin: 10px;
}
일단 여기까지 진행하여 컨테이너 내부에 카드를 딱 한장 두었다.
2. ThymeLeaf를 이용해 기존 내용을 채우기
😮 기존의 ThymeLeaf의 사용법은 지금의 내용과 연관성이 깊지 않기때문에 HTML에 빨리채워주고 넘어가겠다. 예시를 든것이니 실제 DB와 맞춰주자.
Content라는 객체의 배열(List)를 model에서 attribute로 지정해주겠다. 여기까지는 스프링부트를 배웠다면 동일!
✔ HTML
<div class="scroller">
<div class="card" th:each="content, ${content}">
<img class="thumbnail" th:src="${content.image}">
<div class="middle" th:src="${content.title}"></div>
<div class="bottom" th:src="${content.content}"></div>
</div>
</div>
content에는 사진정보가 담긴 image, 제목인 title, 내용인 content라는 컬럼이 있다고 가정하겠다.
그리고 이것을 controller에서 pageable을 통해 8개 가져온다고 정해보자.
✔ JAVA 의 Controller
// 자동주입을 위해 사용하는 기본
@Autowired
private ContentService = contentService;
// "localhost:포트번호/list" 입력시 이 페이지로 연결
@RequestMapping("/list")
// model지정을 위해 Model 파라미터 사용
public String listView(Model model){
//8개를 한쪽으로 볼때 첫번째 페이지이므로 1~8번!
Pageable firstPage = PageRequest.of(0,8);
//목록을 가져와 페이징처리한다.
Page<Content> contentList = contentService.findAllByOrderBytitle(firstPage);
//페이징처리한 내용이 있다면 Page타입을 List타입으로 바꾸자
if(contentList!=null){
//ContentList에서 getContent라는 메서드를 사용해서 가져온다. Content라서 쓰는게 아님
List<Content> contents = contentList.getContent();
model.addAttribute("content",contents);
//매핑된 경로의 list.html파일을 읽어온다. 지금 우리가 작업해둔 파일이다. 이름을 맞추자.
return "list";
} else {
// 안나오면 null주고 끝내라
return null;
}
}
천천히 읽어보면 이렇다. SpringBoot 초입 사용자를 위해 자세히 적어두었으니 알고있는 내용이라면 그냥 넘어가도 무관
이렇게하면 8개의 card가 생길것이다.
3. @RestController 또는 @ResponseBody를 사용하여 JSON형식으로 추가데이터를 불러오기(★)
@ResponseBody는 return값이 view를 지정하는 것이 아니라 String값을 그대로 출력한다.
따라서 HTML형식을 갖추지 못하지만, JSON 형식의 데이터를 전달 할 수 있다.
@RestController는 Controller에 붙이는것으로 해당클래스의 내용물을 모두 @ResponseBody 처리하는것이다.
👀잘 보아야 할 것은 우리가 JSON 데이터를 이것을 이용해서 페이지를 새로고침 하지 않더라도 페이지에 전달하여 사용 할수 있다는 것이다!
✔ JAVA 의 Controller에 추가할 메서드
@ResponseBody
@RequestMapping("/more/{page}") /*page는 전달 받을 인자이다.*/
/* 주소에 담긴 page를 int로 받는다, 그리고 어트리뷰트 지정을 위한 모델.
리턴타입은 담을것을 ResonponseEntity에 담아 작성한다.*/
public ResponseEntity<List<Content>> more(@PathVariable("page") int page, Model model){
/* 4개씩 가져올 것이고, 페이지는 이미 0과 1을 썼으니 2부터 불러오겠다*/
Page<Content> contentList = contentService.findAllByOrderByTitle(page, 4);
if(contentList!=null){
List<Content> contents = contentList.getContent();
/* ResponseEntity.ok의 parameter에 담아 return 하면 Json형식으로 전달을 받을수 있다. */
return ResponseEntity.ok(content);
}else{
return null;
}
}
이렇게 할경우 만약 page가 5라면 6번째 페이지인 21~24번째 객체들을 가져온다.
4. 스크롤이 아래에 닿으면 작동하는 function을 JavaScript로 작성(★★)
내용이 길기때문에 가능하면 js파일을 따로 만들어 작성하는게 좋다.
// 우리가 HTML에서 정한 scroller라는 id를 가진 div에서 찾아 scroller 로 const 선언
const scroller = document.querySelector('.scroller');
function checkScroll() {
// ScrollY는 스크롤된 위치를 가져와서 담았다. 둘다 스크롤 위치를 뜻한다.
const scrollY = window.scrollY || window.pageYOffset;
// 브라우저 창의 내부 높이를 나타낸다.
const windowInnerHeight = window.innerHeight;
// scroller의 높이를 나타낸다.
const contentHeight = container.offsetHeight;
// 둘의 합이 창 전체의 높이를 넘어버린다면 더이상 표시할 내용이 있어도 공간이 없게된다.
if (scrollY + windowInnerHeight >= contentHeight) {
// 그때 fetchMoreData라는 function을 실행 시켜준다.
fetchMoreData();
}
}
// 이벤트발생을 대기하도록 설정했다. 이벤트이름은 scroll
window.addEventListener('scroll', checkSrcoll);
이제 fetchMoreData라는 function이 필요하겠다.
4. 스크롤이 아래에 닿으면 작동하는 function을 JavaScript로 작성(★★★)
/*이미 앞에서 0번, 1번페이지를 불러왔으니 2번부터 불러와야겠다. */
let page=2;
function fetchMoreDate(){
/* 위에서 만든 페이지에서 정보를 받아온다. */
fetch('/more/${page}')
/* 그 정보를 json으로 변환*/
.then(response => response.json())
/* 그 정보를 data로 다룬다. 고정이름 data*/
.then(data =>{
/* 그 정보가 존재하는지 정보의 길이로 검사한다 */
if(data.length>0){
/* 여러개의 content객체가 각자 들어있으므로 foreach로 꺼내자. */
data.forEach(content =>{
/* div 태그를 card이름을 붙인다음에, 그 class명을 card로 지정했다. */
const card = documnet.createElement('div');
card.classList.add('card');
/* card에 넣을 것들도 똑같은 원리로 생성해서 준비해두자. */
const image = document.createElement('img');
image.classList.add('thumb');
const middle = document.createElement('div');
middle.classList.add('middle');
const bottom = document.createElement('div');
bottom.classList.add('bottom');
/* 이제 만들어둔 thumb, middle, bottom에 값을 넣자. */
image.setAttribute('src', content.image);
middle.textContent = content.title;
bottom.textContent = content.content;
/* 그다음으로 그것들을 카드에 모두 추가한다. */
card.appendChild(image);
card.appendChild(middle);
card.appendChild(bottom);
/* 마지막으로 container에 card를 추가 (container는 앞의 javascript에서 srcoller로 선언) */
scroller.appendChild(card);
});
/* 여기까지가 forEach 문이다. 이것을 반복 끝낸 후 page를 1 증가시켜 다음에 호출되면 그 다음페이지를 가져오게함*/
page++;
}
})
/* 에러가 발생했다면 어디서 에러가 발생하는지 알기 위해서 error메세지를 console에 띄우도록 설정함. */
.catch(error => cosole.error('fetchMoreData Error', error))
}
이렇게 설정을 했다면 이제 4개씩의 정보를 가져오는 RestController를 실행하여 4개씩의 정보를 가져와 forEach를 이용해 모두 card에 추가하게 되므로 무한 스크롤이 구현이 되었다.
😎 추가적인 내용 - 로딩시간을 통해 중복내용 소환 방지
이것을 만들면 창의 크기만 만족되면 조건없이 무조건 정보를 불러오는 일이 일어나며, 그 연속적인 행동사이에 시간이 존재하지 않기에 계속 정보를 불러와 page++; 가 실행되지 않고 똑같은것을 겹쳐 불러오는 현상도 있었다.
그것을 방지하기위해 한번 불러온 후 짧은 로딩시간을 주고, 로딩중에는 Now Loading이라는 메세지를 띄워 로딩중임을 나타내겠다.
✔ Loading중이라는 것을 표시하는 LoadingIndicator
1. HTML
<div id="loadingIndicator" class="loading">
Now Loading...
</div>
2. CSS
.loading {
display: none; /* 기본적으로는 안보이게한다. 로딩중에만 보이게 속성지정 */
position: fixed; /* 하단에 고정시켜주자. */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.8);
padding: 10px 20px;
border-radius: 5px;
}
✔ 로딩중에 로딩을 띄우며 로딩시간을 지정하는 JavaScript
let page=2;
/* <추가내용1 시작> */
/* 현재 로딩중인지 검사할 loading이라는 변수와, loading이면 띄울 요소를 변수로 정했다. */
let loading = false;
let loadingIndicator = document.getElementById('loadingIndicator');
/* <추가내용1 끝> */
function fetchMoreDate(){
/* <추가내용2 시작> */
/* 만약 이미 loading중이어서 loading이 true라면 return을 통해 function을 마무리짓자.
그리고 로딩중이라는 요소를 display 속성을 none으로 해둔걸 block으로 바꾸어 나타낸다.*/
if(loading) return;
loading = true;
loadingIndicator.style.display='block';
/* <추가내용2 끝> */
/* <SetTimeOut 시작절>*/
/* fetchMoreData의 내용 전체를 감싸서 대기시간을 정한다.*/
setTimeout( () => {
/* <SetTimeOut 시작절>*/
fetch('/more/${page}')
.then(response => response.json())
.then(data =>{
if(data.length>0){
data.forEach(content =>{
const card = documnet.createElement('div');
card.classList.add('card');
const image = document.createElement('img');
image.classList.add('thumb');
const middle = document.createElement('div');
middle.classList.add('middle');
const bottom = document.createElement('div');
bottom.classList.add('bottom');
image.setAttribute('src', content.image);
middle.textContent = content.title;
bottom.textContent = content.content;
card.appendChild(image);
card.appendChild(middle);
card.appendChild(bottom);
scroller.appendChild(card);
});
page++;
}
})
.catch(error => cosole.error('fetchMoreData Error', error))
/* <추가내용3 시작> */
/* finally절을 이용해 어떤상황에서든 loading을 false로 돌려놓고, loadingIndicator를 다시 숨긴다. */
.finally( () =>{
loading = false;
loadingIndicator.style.display = 'none';
});
/* <추가내용3 끝> */
/* <SetTimeOut 마무리절>*/
/* 다 감싸준 내용에 0.5초의 대기시간을 넣는다. */
}, 500});
/* <SetTimeOut 마무리절>*/
}
Finally) 만들어보고 느낀점
javascript는 작성중 에러메세지도 안나오고 불편해 미뤄둔 과제였는데, 이번 기회를 통해 웹에서 많은 가능성을 가지고 있는 언어이며, 이것을 활용 할 가능성이 무궁무진하다는걸 알았다. 꼭 페이지를 넘기지 않아도 정보의 제출만이 아니라 정보의 수령 후 html 태그에 맞춰 표현까지 가능하다는것이 정말 흥미로웠다.
'프로젝트 > 중앙 정보처리학원 2차(음원 경매 사이트)' 카테고리의 다른 글
중앙정보처리학원 / 중앙정보기술인재개발원 4월말기수 2차 프로젝트 후기(23.08~09) (0) | 2023.09.13 |
---|---|
JavaScript를 이용해 음악을 재생하는 뮤직플레이어 만들기 (2) | 2023.09.11 |
마감기한을 기준으로 DB 값을 변경하기(MYSQL) (0) | 2023.09.07 |