본문 바로가기
웹 코딩/티스토리

사이드 바 - 스크롤 따라 다니게 만들기

by 알릭2 2020. 9. 11.

따라다니는 메뉴? 보통 Sticky 라고 하나요? 

 

평소엔 자기 위치에 있다가 위아래로 스크롤 될 때는 화면상 위치가 고정되는거요.

(이미 좌측 사이드 바에 적용되어 있습니다.)

 

특히 요샌 포스팅 길이도 많이들 길고해서 사이드 바에 이 기능이 적용되서 따라다니면 좋겠다는 생각을 해봤습니다.

 

그래서 살펴보니 float 이 적용된 제 사이드 바를 그렇게 만드는 방법은 3가지 정도 있을 것 같습니다.

 

우선 제일 쉬운 방법부터...

 

 

방법 1. CSS 의 position : sticky 사용하기

#aside {
	position: sticky;
	top: 0;
}

위 내용을 css에, 아니면 html에 style 태그로 감싸서 넣어줍니다.

(#aside 는 북클럽 스킨 사이드바에 지정된 아이디입니다. 필요 시 수정!)

 

끝!

 

차암~ 쉽죠잉? 다만 아래와 같은 몇가지 단점이 있습니다.

 

1 - IE에서는 작동하지 않습니다.

2 - 사이드 바가 길어서 그 높이가 브라우저 창의 높이보다 높을 때, 화면 밖 사이드바 하단의 내용은 스크롤이 바닥으로 가야만 보여집니다. (때에 따라선 이게 장점일 수도 있겠으나, 사용성 측면에선 좀 아닌것 같네요)

 

 

방법 2. Javascript 사용하기 - marginTop

 

위에 언급한 사용성 관련 단점을 극복하기 위한 방법입니다.

스크롤되는 상황과 사이드 바이 높이에 따라 사이드바의 margin-top 스타일을 조정해주는 방식이 되겠습니다.

//// 아래 offset 함수는 퀵점프에 이용된 함수라, 이미 html상 존재한다면 삭제해도 됨
function offset(el) {
  var rect = el.getBoundingClientRect(),
    scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
    scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  return { top: rect.top + scrollTop, left: rect.left + scrollLeft }
}
////

window.addEventListener("load", function() {
	var lastScrollTop = 0;
	var article = document.getElementById("content"); //본문영역 id
	var aside = document.getElementById("aside"); //사이드바 id
	if (document.documentElement.clientWidth > 767 && article.offsetHeight > aside.offsetHeight) {
		window.addEventListener("scroll", function() {
			var scrT = window.pageYOffset || document.documentElement.scrollTop;
			var winH = document.documentElement.clientHeight;
			var sideH = aside.offsetHeight;
			var dir = (scrT > lastScrollTop) ? "down" : "up"; lastScrollTop = scrT;
			var sideT = offset(aside).top;
			var topLine = offset(article).top;
			var bottomLine = topLine + article.offsetHeight - winH;

			if (sideH > winH) {
				if (dir == "down") {
					if (scrT >= (sideT + sideH - winH) && scrT < bottomLine) {
						aside.style.marginTop = scrT - topLine - (sideH - winH) + 'px';
					}
					if (scrT >= bottomLine) {
						aside.style.marginTop = Math.max(topLine + article.offsetHeight - sideH - topLine, 0) + 'px';
					}
				} else {
					if (scrT <= sideT && scrT > topLine) {
						aside.style.marginTop = scrT - topLine + 'px';
					}
					if (scrT <= topLine) {
						aside.style.marginTop = 0;
					}
				}
			} else {
				bottomLine = topLine + article.offsetHeight - sideH;
				if (dir == "down") {
					if (scrT > topLine && scrT < bottomLine) {
						aside.style.marginTop = scrT - topLine + 'px';
					}
					if (scrT >= bottomLine) {
						aside.style.marginTop = bottomLine - topLine + 'px';
					}
				} else {
					if (scrT > topLine && scrT < bottomLine) {
						aside.style.marginTop = scrT - topLine + 'px';
					}
					if (scrT <= topLine) {
						aside.style.marginTop = 0;
					}
				}
			}
		});
	}
});

스킨의 html 에 위 스크립트를 추가하고,

모바일 접속 시를 대응하기 위한 css도 살짝 추가되어야 하겠습니다.

@media screen and (max-width:767px) {
	#aside {
		margin-top: 0 !important;
	}
}
#aside {
	transition: margin-top 0.5s ease-in-out 0s, right .5s;
}

이 방법의 유일한 단점은, 스크롤에 맞춰 사이드 바를 즉각 움직이게 했을 때 약간의 지글거림(덜컹거림?)이 있을 수 있다는 건데요, 그래서 바로 움직이진 않더라도 대신 부드럽게 쫓아오는 느낌이 나도록 css 의 transition 을 적용했네요.

 

 

방법 3. Javascript 사용 - absolute/fixed

 

스크롤 시 화면에 사이드바가 더 정교하게 고정되는 느낌을 원한다면 사이드바의 position을 상황에 따라 absolute 나 fixed 가 되도록 바꿔주고 top 값을 조정해주면 됩니다.

 

실제 이렇게 구현도 해봤는데 스크롤 할 때의 반응은 역시 css: sticky 만큼이나 깔끔해서 만족스러웠습니다.

 

다만...

 

지금 쓰고 있는 북클럽 스킨에서는 사이드 바가 float를 사용해서 위치를 잡는데, 여기에 fixed 로 적용되는 과정에서 %로 지정된 사이드 바의 넓이에 미세한 변화가 생긴다거나, 본문 내용이 짧을 때 푸터가 위로 올라오는 등의 처리해줘야 하는 자잘한 요소들이 많이 있더군요.

 

거기에 모바일 대응을 위해 원복시켜야 하는 내용들까지 더해지면 수정해야 하는 부분도 더 생기고... 

사이드바 위치가 왼쪽인지 오른쪽인지도 체크해야 하고... 기타 등등...

 

결국엔 이런저런 처리까지 다 하다보면 너무 현재 스킨에서만 돌아갈 것 같고, 소스도 길어지고, 이렇게 헤비하게 처리해가면서까지 써야 할 일인가 싶고...  특히 2번 방법에서 적용한 스르륵 움직이는 방식도 살짝 마음에 들고..

 

그러다보니 3번용 코드는 결국 살짝 지저분해지고, 너무 특정 스킨에 맞춰지고....

 

그래도 일단 북클럽 스킨을 위한 코드를 올리긴 합니다만...

다른 스킨에 응용이 잘 안되면 쓰실 수 있게 글 하단에 범용으로 쓸 수 있는 솔루션을 추가했으니 참고하세요.

@media screen and (max-width:767px) {
	#aside {
		position: fixed !important;
		top: 0 !important;
		width: 280px !important;
		right: -280px !important;
	}
	.mobile-menu #aside {
		right: 0 !important;
	}    
}
//// 아래 offset 함수는 퀵점프에 이용된 함수라, 이미 html상 존재한다면 삭제해도 됨
function offset(el) {
	var rect = el.getBoundingClientRect(),
			scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
			scrollTop = window.pageYOffset || document.documentElement.scrollTop;
	return { top: rect.top + scrollTop, left: rect.left + scrollLeft }
}
////

var sidebarSticked = false;
window.addEventListener("load", stickySidebar);
window.addEventListener("resize", stickySidebar);
window.addEventListener("orientationChange", stickySidebar);

function stickySidebar() {
  var aside = document.getElementById("aside");
  var article = document.getElementById("content");
	var winW = document.documentElement.clientWidth;
	if (winW > 767 && article.offsetHeight > aside.offsetHeight) {
		var parentWidth = aside.parentNode.offsetWidth - parseInt(window.getComputedStyle(aside.parentNode, null).paddingLeft) - parseInt(window.getComputedStyle(aside.parentNode, null).paddingRight);
		var initW = (100 * aside.offsetWidth / parentWidth);
		if (!aside.dataset.w) {
			aside.dataset.w = initW;
		}
		var newW = (aside.dataset.w / 100 * parentWidth);
		aside.style.width = newW + 'px';
		if (window.getComputedStyle(aside, null).float == "right" || aside.style.right) {
			aside.style.right = (winW - parentWidth)/2 + 'px';
		}
		aside.style.position = "absolute";	
        aside.style.top = "unset";
        aside.style.bpttom = "unset";
		if (!sidebarSticked) {
			sidebarSticked = true;	
			var lastScrollTop = 0;
			window.addEventListener("scroll", function() {
				if (document.documentElement.clientWidth <= 767 && article.offsetHeight < aside.offsetHeight) { return; }
				var scrT = window.pageYOffset || document.documentElement.scrollTop;
				var winH = document.documentElement.clientHeight;
				var sideH = aside.offsetHeight;
				var dir = (scrT > lastScrollTop) ? "down" : "up";
				lastScrollTop = scrT;
				var sideT = offset(aside).top;
				var topLine = offset(article).top;
				var bottomLine = topLine + article.offsetHeight - winH;
				if (sideH > winH) {
					if (dir == "down") {
						if (scrT >= (sideT - winH + sideH) && scrT < bottomLine && aside.style.position != 'fixed') {
							aside.style.top = 'unset';
							aside.style.bottom = 0;
							aside.style.position = 'fixed';
						}
						if (scrT >= bottomLine && aside.style.position == 'fixed') {
							aside.style.top = 'unset';
							aside.style.bottom = 0;
							aside.style.position = 'absolute';
						}
						if (scrT < bottomLine && aside.style.top == '0px' && aside.style.position == 'fixed') {
							aside.style.top = sideT - topLine + aside.parentNode.offsetTop + 'px';
							aside.style.bottom = 'unset';
							aside.style.position = 'absolute';
						}
					} else {
						if (scrT <= sideT && scrT > topLine && aside.style.position != 'fixed') {
							aside.style.top = 0;
							aside.style.bottom = 'unset';
							aside.style.position = 'fixed';
						}
						if (scrT <= topLine && aside.style.position == 'fixed') {
							aside.style.top = 'unset';
							aside.style.bottom = 'unset';
							aside.style.position = 'absolute';
						}
						if (scrT > topLine && aside.style.bottom == '0px' && aside.style.position == 'fixed') {
							aside.style.top = sideT - topLine + aside.parentNode.offsetTop + 'px';
							aside.style.bottom = 'unset';
							aside.style.position = 'absolute';
						}
					}
				} else {
					bottomLine = topLine + article.offsetHeight - sideH;
					if (dir == "down") {
						if (scrT >= topLine && scrT < bottomLine && aside.style.position != 'fixed') {
							aside.style.top = 0;
							aside.style.position = 'fixed';
						}
						if (scrT >= bottomLine && aside.style.position == 'fixed') {
							aside.style.top = 'unset';
							aside.style.bottom = 0;
							aside.style.position = 'absolute';
						}
					} else {
						if (scrT >= topLine && scrT < bottomLine && aside.style.position != 'fixed') {
							aside.style.bottom = 'unset';
							aside.style.top = 0;
							aside.style.position = 'fixed';
						}
						if (scrT <= topLine && aside.style.position == 'fixed') {
							aside.style.top = 'unset';
							aside.style.position = 'absolute';
						}
					}
				}
			});
		}
	}
}

 

결론적으로,

 

내 사이드 바는 짧다! 하시면 무조건 1번 적용!

내 사이드 바는 조금 길이가 있다 하시면 2번 적용!

내 사이드 바는 길지만 난 즉각적으로 움직이는걸 원해 (또는 이미 내 스킨의 사이드 바는 position: absolute 이라구)!! 라면 3번에 도전! 

 

이런 기준으로 선택하시면 될 것 같습니다.

 

물론.. 따라다니는걸 모두가 다 좋아하는 건 아니겠죠?

ㅋ 싫으시면 패스!

 

 

부록

 

티스토리가 아닌 곳에서도 범용으로 쓸만한,

 

특히 스크롤 상태에 따라 이런저런 callback 함수를 호출해야 하는 상황이라면!!

(예를들어 사이드바가 바닥을 치면 이런 이런걸 해라.. 등등)

 

잘 짜여진 js 플러그인이 있어 소개합니다.

 

abouolia.github.io/sticky-sidebar

 

Sticky Sidebar ⬆⬇

Sticky Sidebar ⬆⬇ is a pure JavaScript plugin for making smart and high performance sticky sidebar, works with sidebar if it’s taller or shorter than the viewport, integrated with resize sensor to re-calculate the dimensions automatically when the si

abouolia.github.io

(이 아이는 고정이 아닐때 absolute 가 아닌 relative로 처리하되 css 의 transform-3d 를 쓰는군요?! 넓이나 위치도 스크롤 전에 알아서 맞춰주고... 그냥 가져다 바로 써도 별 문제 없겠네요)

 

잘 쟁여두면 언제간 필요할 떄 휘릭 꺼내서 간단히 쓰기에 좋겠어요.

댓글