[JavaScript, CSS] SpringBoot : 페이지 슬라이드
목적
- 페이지 전환없이 좌우 슬라이드 기능 구현
- 스크롤 위치에 상관 없이 폼이 슬라이드로 등장했다가 제거시 슬라이드로 사라지도록 함.
- JavaScript와 css만 이용
진행중인 프로젝트의 갤러리 메인 페이지에서 글쓰기 버튼을 누르면 화면 오른쪽에서 슬라이드로 폼이 등장하는 기능을 만들고 싶었다.
나중에 웹뷰 앱으로 전환할 생각인데, 모바일 앱 UI상으로 페이지 새로고침은 자연스럽지 않으니 자연스레 연결해주는게 좋아보였기 때문이다.
근데 뭐.. 기본적으로 JS와 CSS, HTML밖에 다룰 줄 몰라서 여러 방법을 찾아봤으나.. 해결하지 못했다.
fadeIn, fadeOut은 페이지 호출시에 opacity 조절을 통해 나름 자연스러워 보이게 할 수 있지만 슬라이드는(보기에는 별다를게 없어보이는데..) 어떻게 해도 페이지 이동간 깜빡임이 발생하니 자연스럽게 이전 화면 위에 다음 화면이 덮어써지는 형태로 되지 않았다.
처음 시도했던 것은 a페이지에서 b페이지로 이동할 때, a페이지 화면을 캡쳐해서 캡쳐한 이미지를 b페이지에 넘겨주고 보이는 viewport에 덮어씌운 다음 b페이지 요소를 오른쪽에서 왼쪽으로 슬라이드하는 것이었다.
a페이지를 캡쳐하는 것도 생각처럼 되지 않았는데 html2canvas 라는 것을 통해 어떻게 해결할 수 있었다.
근데 이게 생각보다 캡쳐 시간이 걸려서 다음페이지에 넘겨주고 넘겨준 이미지를 또 로드하고,, 오히려 부자연스러울 것 같아 그만뒀다.
https://html2canvas.hertzen.com/
html2canvas - Screenshots with JavaScript
Try out html2canvas Test out html2canvas by rendering the viewport from the current page. Capture
html2canvas.hertzen.com
다음으로 시도한건 View Transitions API를 사용하는 것인데, 메타태그 넣고 스타일 수정만 조금 해주면 된다고 해서 해봤더니 뭘 잘못한건지 기본 fadein/out도 안됐다..
https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
View Transitions API - Web APIs | MDN
The View Transitions API provides a mechanism for easily creating animated transitions between different DOM states, while also updating the DOM contents in a single step.
developer.mozilla.org
결국 캐러샐 만드는 것과 유사한 방식을 통해 약간의 노가다(?)로 만들었다.
결과는 아래와 같다.
기본적으로 HTML, CSS를 잘 못다뤄서.. 이것저것 끼워맞추느라 제대로 작성했는지 모르겠지만, 일단 기록을 남겨두면 언젠가 쓸모가 있겠지 싶어 적는다.
기본적으로 구조는 다음과 같다.
원래 main 페이지 따로 write 페이지 따로 두고, 글쓰기 버튼을 눌렀을 때 서버에 write 페이지를 요청하도록 하는게 흐름이지만, 페이지를 전달 받아 띄우면 새로고침과정이 불가피하다.
그래서 하나의 페이지가 굉장히 무거워지긴 하겠지만 효율보단 저 기능을 하고 싶어 저렇게 붙였다.
[HTML]
전체 코드를 담지는 않겠지만 JSP 태그 구조는 아래와 같다.
<body>
<!-- 모든폼 적용 메인 헤더 시작 -->
<div id = "main_header">
<ul id = "top_menu">
<!-- 헤더 버튼들 -->
</ul>
</div>
<!-- 모든폼 적용 메인 헤더 끝 -->
<!-- 메인 바디 시작 -->
<div id = "main_body">
<div id = "for_slider_div">
<!-- 갤러리 메인 시작 -->
<div id = "gallery_main_div">
<ul id = "gallery_list_ul">
<li></li>
<!-- 리스트 -->
</ul>
</div>
<!-- 갤러리 메인 끝 -->
<!-- 글쓰기 폼 시작 -->
<div id = "gallery_write_div">
<!-- 글쓰기 폼 헤더 시작 -->
<div id = "gallery_write_div_wrap">
<ul id = "gallery_write_header">
<li>뒤로가기버튼</li>
<li>게시물 등록하기</li>
</ul>
</div>
<!-- 글쓰기 폼 헤더 끝 -->
<div id = "cont-wrap">
<-- 글쓰기 폼 콘텐츠 -->
</div>
</div>
<!-- 글쓰기 폼 끝 -->
</div>
</div>
<!-- 메인 바디 끝 -->
<!-- 메인 푸터 시작 -->
<div id = "main_footer">
</div>
<!-- 메인 푸터 끝 -->
</body>
타일스를 써서 헤더와 푸터는 엄밀하게 따지면 다른 JSP지만 합쳐서 보면 위와 같다.
그림으로 보면 더 간단하다.
그림으로 보면 바로 이해가 될 것 같은데, 가장 큰 문제가 저 헤더였다.
설계를 잘못해서(초반 설계를 타일스 기준으로 했다가 슬라이드를 넣게 되어서..) 글쓰기 폼에 따로 헤더가 있게 됐다.
저 부분이 메인 헤더 영역에 오버랩되면서 슬라이드가 되도록 하고 싶었는데 도저히 되자 않아 포기..했다.
전체 구조를 바꾸면 되지 않냐 할 수 있지만 기본적으로 타일스를 쓰다보니 더 복잡해질 것 같아 접었다.
[CSS]
직접 이것저것 바꿔보면서 작성한거라 원리나 이런건 잘 모른다..
[main_header]
#main_header{
position : sticky;
top : 0px;
width : 100vw;
height : 40px;
background : skyblue;
margin : 0 auto;
z-index : 100;
}
스크롤을 내려도 상단에 메뉴바가 계속 붙어있게 하기 위해 sticky와 top: 0을 지정해주고 z-index를 높여줘서 다른 element에 덮어 씌워지지 않게 했다.
[for_slider_div]
다음은 메인 바디에서 갤러리와 폼을 모두 담고 있는 div 태그 속성이다.
#for_slider_div{
position : relative;
left : 0; right : 0; top : 0; bottom : 0;
overflow : hidden;
z-index:0;
}
relative로 해줘야 안에 담은 두 영역 중 하나를 화면에, 다른 하나를 화면 밖에 위치시킬 수 있는듯 하다.
[gallery_main_div]
갤러리 메인에 해당하는 부분이다.
#gallery_main_div{
position : relative;
width : 100%;
height : 100%;
left : 0;
}
이 부분도 position을 relative로 두고 부모 요소 영역의 전부를 사용하도록 하기 위해 width와 height 100%를 줬다.
left는 0을 줘서 가장 화면 왼쪽으로 붙여 바로 view에 보이도록 했다.
[gallery_write_div]
글쓰기 폼에 해당하는 부분이다.
#gallery_write_div{
position : absolute;
width : 100%;
height : 100%;
left : 100%;
background : white;
transition: transform 1s cubic-bezier(0.85, 0, 0.15, 1);
}
글쓰기는 position을 absolute로 두고 left 100%를 줘서 화면 오른쪽에 보이지 않도록 위치시켰다.
마지막엔 transform이 1초에 걸쳐 이동하도록 적어줬다.
Easing Functions Cheat Sheet
Easing functions specify the speed of animation to make the movement more natural. Real objects don’t just move at a constant speed, and do not start and stop in an instant. This page helps you choose the right easing function.
easings.net
뒤에 cubic-bezier은 위 사이트에서 원하는 패턴을 선택해서 사용하면 된다.
[main_footer]
마지막으로 footer는 다음과 같다.
#main_footer{
position : fixed;
bottom : 0px;
width : 100vw;
height : 40px;
background : skyblue;
margin : 0 auto;
z-index : 1000;
}
일단 CSS로 설정할 부분은 이정도로 생각된다.
필요한 CSS 작업은 이정도로 생각되는데 혹시 해볼 사람이 있다면,, 안될 경우 알려주면 좋겠다(이것저것 적용해보고 수정하고 하다보니 놓친게 많을 가능성이 높다).
[JavaScript]
이제 위 태그들을 조작해서 슬라이드 효과처럼 보이도록 JS 코드를 짜주면 된다.
먼저 전체 코드는 아래와 같다.
document.addEventListener('DOMContentLoaded', function() {
var main_header = document.getElementById("main_header");
var main_footer = document.getElementById("main_footer");
var gallery_write_div = document.getElementById("gallery_write_div");
var gallery_write_div_wrap = document.getElementById("gallery_write_div_wrap");
var writeButton = document.getElementById('menu_icon');
var backButton = document.getElementById('back_btn');
var curURL = document.location.href.split('gallery')[0];
let curTop;
let urlCheck;
let header_height = main_header.getBoundingClientRect().height;
writeButton.addEventListener('click', function() {
curTop = window.pageYOffset;
main2write(curTop);
});
backButton.addEventListener('click', function(){
write2main(curTop);
})
window.onpopstate = () => {
write2main(curTop);
};
function main2write(curTop){
disableScroll();
main_header.style.display = "none";
main_footer.style.display = "none";
gallery_write_div_wrap.style.display = "";
if(curTop == '0'){
gallery_write_div.style.top = curTop + 'px';
}else{
gallery_write_div.style.top = (curTop - header_height) + 'px';
}
gallery_write_div.style.transform = "translateX(-100%)";
gallery_write_div.style.height = (screen.height) + 'px';
history.pushState(null, null, curURL + 'gallery/write.do');
urlCheck = 'gallery/write.do';
}
function write2main(){
if(urlCheck == 'gallery/write.do'){
if (curTop == '0') {
gallery_write_div_wrap.style.display = "none";
}
gallery_write_div.style.transform = "translateX(100%)";
main_footer.style.display = ""
main_header.style.display = "";
history.pushState(null, null, curURL + 'gallery/main.do');
urlCheck = 'gallery/main.do';
enableScroll();
}
}
//여기부턴 스크롤 막는 코드
function preventDefault(e) {
e.preventDefault();
}
var supportsPassive = false;
try {
window.addEventListener("test", null, Object.defineProperty({}, 'passive', {
get: function() { supportsPassive = true; }
}));
} catch (e) { }
var wheelOpt = supportsPassive ? { passive: false } : false;
var wheelEvent = 'onwheel' in document.createElement('div') ? 'wheel' : 'mousewheel';
// call this to Disable
function disableScroll() {
window.addEventListener('DOMMouseScroll', preventDefault, false); // older FF
window.addEventListener(wheelEvent, preventDefault, wheelOpt); // modern desktop
window.addEventListener('touchmove', preventDefault, wheelOpt); // mobile
}
function enableScroll() {
window.removeEventListener('DOMMouseScroll', preventDefault, false);
window.removeEventListener(wheelEvent, preventDefault, wheelOpt);
window.removeEventListener('touchmove', preventDefault, wheelOpt);
}
});
여기서는 jQuery도 쓰지 않긴 했지만, jQuery 사용해서 해도 상관 없다.
페이지 로드 시 실행되어야 하니까 해당 코드를 아무거나 적어준다(onload / $(function) / ready 등등).
다음으로 필요한 태그들 및 변수를 선언해줬다.
var main_header = document.getElementById("main_header"); // 메인 헤더
var main_footer = document.getElementById("main_footer"); // 메인 푸터
var gallery_write_div = document.getElementById("gallery_write_div"); // 글쓰기 폼 div
var gallery_write_div_wrap = document.getElementById("gallery_write_div_wrap"); // 글쓰기 폼 헤더묶음
var writeButton = document.getElementById('menu_icon'); // 글쓰기 버튼(+모양)
var backButton = document.getElementById('back_btn'); // 뒤로가기 버튼(글쓰기 폼에서 <-모양)
var curURL = document.location.href.split('gallery')[0]; // http://localhost:8080/
let curTop; // 스크롤 위치를 고려해 현재 가장 상단 위치값 담을 변수
let urlCheck; //페이지 이동 없이 url 변경할 것인데, 이를 체크하기 위한 변수
let header_height = main_header.getBoundingClientRect().height; // 메인 헤더 높이값 담아둔 변수
선언한 변수 설명은 주석으로 달아놨다.
[글쓰기 클릭 시]
글쓰기 버튼이 눌렸을 때 이벤트를 보자.
writeButton.addEventListener('click', function() {
curTop = window.pageYOffset;
main2write(curTop);
});
window.pageYOffset은 스크롤로 이동한 거리를 알려준다(픽셀단위).
가장 상단에 위치했을 때, 0이고 한 칸 내릴 때마다 100씩 증가하는 것으로 확인된다.
즉, 스크롤 한 칸 당 100px이라는 의미다.
스크롤의 Y값에 따라 해당 위치에 글쓰기 폼을 보여주기 위해 값을 구했다.
main2write() 함수는 아래와 같다.
function main2write(curTop){
disableScroll();
main_header.style.display = "none";
main_footer.style.display = "none";
gallery_write_div_wrap.style.display = "";
if(curTop == '0'){
gallery_write_div.style.top = curTop + 'px';
}else{
gallery_write_div.style.top = (curTop - header_height) + 'px';
}
gallery_write_div.style.transform = "translateX(-100%)";
gallery_write_div.style.height = (screen.height) + 'px';
history.pushState(null, null, curURL + 'gallery/write.do');
urlCheck = 'gallery/write.do';
}
가장 위에 있는 disableScroll() 함수는 나중에 보도록 하자.
먼저 메인 헤더와 푸터의 display에 none을 줘서 지웠다.
그리고 헤더 묶음인 gallery_write_div_wrap의 display를 비워줬는데, 이는 뒤에서 none을 줬던걸 해제하는 부분이다.
조건문에 따라 현재 curTop이 0이면, 즉 스크롤이 가장 상단에 위치하면 글쓰기 폼 전체의 top을 curTop으로 줬다(= 0).
0이 아니면 스크롤이 한 칸이라도 내려간 경우인데, 이때는 curTop에서 헤더의 높이를 뺀 값을 top으로 설정했다.
처음에 헤더가 있는 상태에서 스크롤 위치를 파악하고 지웠기 때문에 스크롤을 내린 상태일 때 해당 값에서 지워진 헤더의 높이를 빼줘야 가장 상단에 글쓰기 폼이 위치하게된다.
다음으로 transform = "translateX(-100%)"를 통해 left 100% 되어 있던 글쓰기 폼을 왼쪽으로 이동시켰고,
높이를 현재 보이는 화면의 높이값으로 설정해줬다.
history.pushState(null, null, "주소")를 해주면 현재 주소를 바꿀 수 있다.
글쓰기 폼이 슬라이드 되어 나오면 주소를 write.do로 바꿔주었다.
[뒤로가기 클릭 시]
뒤로가기는 두 가지 경우가 있다.
페이지에 제공되는 버튼을 눌러 뒤로가는 경우와 브라우저 뒤로가기를 이용하는 경우이다.
두 가지 모두 적용하기 위해 아래와 같이 작성했다.
backButton.addEventListener('click', function(){
write2main(curTop);
})
window.onpopstate = () => {
write2main(curTop);
};
onpopstate는 뒤로가기를 막기 위한 방법을 찾아보다 발견했는데 아래 포스트를 참고했다.
참고 : https://gurtn.tistory.com/192
onpopstate에 대한 설명은 아래와 같다.
설명이 와닿진 않는데, 쉽게 그냥 뒤로가기나 앞으로가기 누르면 호출된다고 생각하면 된다.
write2main() 함수를 보자.
function write2main(){
if(urlCheck == 'gallery/write.do'){
if (curTop == '0') {
gallery_write_div_wrap.style.display = "none";
}
gallery_write_div.style.transform = "translateX(100%)";
main_footer.style.display = ""
main_header.style.display = "";
history.pushState(null, null, curURL + 'gallery/main.do');
urlCheck = 'gallery/main.do';
enableScroll();
}
}
코드는 앞서 main에서와 반대로 설정해준 것이 전부인데,
curTop이 0일 때 글쓰기 폼의 헤더 묶음을 없애주는 이유는 다음과 같다.
없애지 않고 메인 헤더의 display = none을 해제했을 경우, 메인헤더가 다시 가장 상단의 자리를 차지하여, 그 아래에 글쓰기 폼 헤더가 위치하기 때문이다.
이런식으로 글쓰기 폼 호출 시 제거했던 메인 헤더가 돌아오면서, 글쓰기 폼 헤더가 아래로 깔려 이상한 모양으로 슬라이드가 닫히기 때문에 그것을 방지하고자 글쓰기 폼 헤더를 없애줬다.
마지막으로 글쓰기 폼을 호출했을 때 스크롤이 되지 않도록 하는 코드이다.
이건 참고한 페이지에서 그대로 가져온 것이라 코드와 포스트만 첨부하도록 하겠다.
function preventDefault(e) {
e.preventDefault();
}
var supportsPassive = false;
try {
window.addEventListener("test", null, Object.defineProperty({}, 'passive', {
get: function() { supportsPassive = true; }
}));
} catch (e) { }
var wheelOpt = supportsPassive ? { passive: false } : false;
var wheelEvent = 'onwheel' in document.createElement('div') ? 'wheel' : 'mousewheel';
// call this to Disable
function disableScroll() {
window.addEventListener('DOMMouseScroll', preventDefault, false); // older FF
window.addEventListener(wheelEvent, preventDefault, wheelOpt); // modern desktop
window.addEventListener('touchmove', preventDefault, wheelOpt); // mobile
}
function enableScroll() {
window.removeEventListener('DOMMouseScroll', preventDefault, false);
window.removeEventListener(wheelEvent, preventDefault, wheelOpt);
window.removeEventListener('touchmove', preventDefault, wheelOpt);
}
참고 : https://joylee-developer.tistory.com/181
설계가 좀 잘못돼서 이런식으로밖에 구현을 못했는데, HTML 구조를 조금 다듬으면 더 자연스럽게 만들 수 있지 않을까 싶다.
생각보다 너무 많은 시간을 소비했으므로.. 이정도에서 만족하고 정리해둔다.