scroll arrow button으로 scroll을 제어해보자

박종대·2022년 10월 22일
1

Convi

목록 보기
8/9

Scrolling

tab header의 전체 width를 넘어가면 tab header element는 어떻게 표현해야 할까에 대한 고민입니다. 단순히 생각하면 scroll bar를 생성하여 출력하면 되겠지만 기본 스크롤바는 보기 좋지도 않고 tab header의 height도 변경되어서 마음에 들지 않았습니다. 그래서 scroll bar를 생성하지 않고 scroll button으로 scrolling을 제어 하려고 합니다.

언제 scroll button이 생겨야 할까?

scroll bar가 생성될 때 scroll button을 생성하면 됩니다. 그럼 scroll bar는 언제 생성되냐고 했을 때 clientWidth가 scrollWidth 작거나 같을때로 답할 수 있겠습니다.

clientWidth : scrollbar를 포함하지 않은 width
scrollWidth : scrollbar를 포함한 width

clientWidth와 scrollWidth는 overflow가 발생하기 전까지는 같은 값을 유지하다가 overflow 발생 시, 즉 scroll bar가 생성될 때 달라집니다. 해당 특징을 이용하려 합니다.

추가적으로 scroll bar가 생성되는 것은 원치 않으므로 tab list의 overflow는 hidden으로 설정하겠습니다. 또 runtime에 width를 체크할 필요가 생겼으므로 header 컴포넌트에 대한 ref 값도 유지하도록 하겠습니다.

  • button 생성 시점 : header의 clientWidth < scrollWidth
  • tab list의 overflow : hidden
  • header의 ref값 저장, useRef

scroll button의 표현

해당 부분은 css가 중요합니다. ssafy 교육 중 교수님이 강조하셨던 부분이 있습니다. 해당 기법을 중요하게 이용할 것입니다.

position:absolute 속성 사용 시 안에 들어가길 원하는 부모 컴포넌트를 position:relative로 만들어라.

일단 우리는 scroll button 생성 시점을 알 수 있게 되었으니까 scroll button을 생성하려는 시점에 scroll button을 표현할 자리를 마련할 겁니다. header 컴포넌트의 양 옆(화살표 부분)에 padding을 두는 방식으로 말이죠.

해당 padding을 구하기 위해 getPadding이라는 메소드를 생성합니다.

export const getPadding = (open: boolean) => {
	let paddingLeft = 0;
	let paddingRight = 0;
	if (open) {
		paddingLeft += 20;
		paddingRight += 20;
	}
	return `0 ${paddingRight}px 0 ${paddingLeft}px`;
};

scroll button을 open 해야 하는 상태라면 양 옆에 padding을 추가합니다.

20의 의미는?

현재 scroll 버튼의 width를 20으로 고정해서 scroll button의 width 만큼 자리를 비워주기 위해서입니다. 개발자 입장에서 당연히 찝찝함이 느껴집니다. 추후 리팩토링 과정에서는 size 리스트를 두고 이 size 변수에 접근하는 방식으로 리팩토링 할 예정입니다.

그리고 다음과 같이 적용합니다.

if (clientWidth < scrollWidth) {
	setOpen(true); // scroll button rendering
	headerRef.current.style.padding = getPadding(true); // scroll button 자리 마련하기
} else {
	setOpen(false); // scroll button not rendering
	headerRef.current.style.padding = getPadding(false); // scroll button을 위해 마련했던 자리 없애기
}

이제 자리를 비워뒀습니다. scroll button을 이 빈 곳에 띄워주면 됩니다. 단순하게 left arrow 버튼을 맨 앞에, right arrow 버튼을 맨 뒤로 배치한다면 right arrow 버튼은 보이지 않을 겁니다. scroll 맨 끝으로 가 있을 테니까요.

그래서 scroll button은 다음과 같이 작성했습니다.

<div>
	<span>
		<ConviTabArrowButton
			direction="left"
			scrollLocation={scrollLocation}
			onClick={() => headerElement?.scrollBy(-move, 0)}
		/>
	</span>
	<span>
		<ConviTabArrowButton
			direction="right"
			scrollLocation={-scrollLocation}
			onClick={() => headerElement?.scrollBy(move, 0)}
		/>
	</span>
</div>

header의 scroll width를 포함하지 않는 client width 만큼의 양 옆에 left button, right button이 존재하는 div 태그를 scroll button으로 선언한 것입니다.

이것을 header 태그 위에 덮어 씌우는 느낌으로 띄워야 합니다. 이제 position: absolute를 사용할 때가 왔습니다.

absolute는 부모 태그 중 position:static이 아닌 가장 가까운 부모의 위치를 기준으로 위치를 선정합니다. 그래서 header를 relative로 선정하여 header를 기준으로 absolute position을 지정할 수 있도록 하였습니다.

양 끝에 어떻게 배치하나요?

left arrow button에는 left: 0, right arrow button에는 right: 0을 주면 됩니다. 하지만 추가적으로 고려할 부분이 있습니다.

const LeftButton = styled(AiOutlineCaretLeft)<{ scrollLocation: number }>`
	width: 20px;
	background-color: #eeeeee;
	height: 25px;
	display: inline-block;
	filter: none;
	position: absolute;
	justify-content: center;
	text-align: center;
	z-index: 500;
	left: ${props => `${props.scrollLocation}px`};
	cursor: pointer;
	:hover {
		background-color: #87cefa;
	}
`;

양 옆에 배치하려면 left를 0, right를 0으로 두면 된다고 했지만 위의 소스코드에서는 scrollLocation이라는 props를 활용하고 있습니다. scroll 시에는 해당 scroll button div 컴포넌트도 함께 이동해야 하기 때문입니다.

만약 scroll 버튼을 클릭해서 scroll을 move만큼 이동시켰다면 scroll 버튼의 위치도 move만큼 이동되어야 한다는 의미입니다.

left 버튼은 left: move, right 버튼은 right: -move로 지정해야 겠습니다. 그래서 위쪽의 scroll button jsx return 값을 보시면 left button에는 scrollLocation, right button에는 -scrollLocation을 props로 넘겨주고 있음을 확인할 수 있습니다.

scroll 위치와 scroll button의 위치의 동기화

위에서 scroll position과 scroll button의 위치가 동기화 되는 것이 중요한 것을 알았습니다. 그래서 scrollLocation이라는 props로 scroll button의 left, right 값을 지정 해 주고 있습니다.

해당 scrollLocation은 어떻게 구하고 있을까요?

onScroll={e => setScrollLocation(e.currentTarget.scrollLeft)}

header의 onScroll 이벤트를 통해 스크롤 될 때마다 scroll location 상태를 변경시켜 주고 있습니다.

이쯤에서 의문점을 가질 수 있습니다. headerRef를 가지고 있는데 headerRef.current.scrollLeft를 사용하면 안될까요. 문제없이 동작합니다.

그런데 scroll-behavior: smooth css를 적용시키는 순간 이야기가 달라집니다. scroll이 바로 적용되지 않고 약 100ms의 텀을 두고 적용되기 때문에 scroll이 모두 적용 되기 전의 scrollLeft 값을 사용하게 되어서 scroll button이 정상적으로 이동하지 못하게 됩니다. 그래서 완벽하게 둘의 위치를 동기화 시키기 위해서는 scroll location state를 scroll event를 통해 유지할 필요가 있었습니다.

얼마만큼 이동할 것이냐?

element들의 width 평균만큼 이동할 것입니다. 우리는 headerRef를 유지하고 있기 때문에 다음과 같이 쉽게 구할 수 있습니다.

headerElement.scrollWidth / headerElement.childElementCount

결과

꼬박 이틀 동안 많은 시행착오를 겪으며 개발에 성공했습니다. 고민할 것도 은근히 많았고 scroll 이벤트가 생소했던 부분이 컸던 것 같습니다.

요약

  1. 언제 scroll button 생성할래?
  2. scroll button 어떻게 양 옆에 배치할래?
  3. scroll button과 scroll position 어떻게 동기화 시킬래?
  4. scroll-behavior: smooth css 적용 시 scroll이 일정 간격을 두고 진행 되는 데에서 오는 문제점을 어떻게 해결할까?
  5. scrollBy, onScroll, scroll-behavior, scrollLeft, scrollWidth 등 많은 scroll 관련 속성들의 이용 방법
profile
Frontend Developer

0개의 댓글