리팩토링한 컴포넌트 구조로 기존 코드를 수정했다.
ImageSlide
최상위 부모인 ImageSliderPage의 상태로 현재 슬라이드 index와 슬라이드의 갯수를 저장해주었다.
슬라이드, 넘김 버튼, 포인터 3가지의 컴포넌트를 만들어 ImageSliderPage의 해당 태그에 각각 연결해주었다.
setup() {
this.state = {
currentSlideIndex: 0,
maxLength: imageUrlList.length,
};
}
template() {
return `
<div id='Image-slider-container'>
<div data-component="slider"></div>
<div data-component="arrow-buttons"></div>
<div data-component="dots"></div>
</div>
`;
}
mounted() {
const $slider = this.$target.querySelector('[data-component="slider"]');
const $dots = this.$target.querySelector('[data-component="dots"]');
const $arrowButtons = this.$target.querySelector(
'[data-component="arrow-buttons"]',
);
this.Slider = new Slider($slider, {
currentSlideIndex: this.state.currentSlideIndex,
imageUrlList,
});
this.Dots = new Dots($dots, {
currentSlideIndex: this.state.currentSlideIndex,
indexArr: Array.from({ length: this.state.maxLength }, (_, i) => i),
});
new ArrowButtons($arrowButtons, {
onClickPrev: this.handleClickPrevButton.bind(this),
onClickNext: this.handleClickNextButton.bind(this),
});
}
넘김 버튼 click handler함수를 ArrowButtons컴포넌트의 props으로 전달해서 이벤트를 걸어줬다.
콜백함수에는 currentSlideIndex를 변경시켜주는 로직이 작성되어있다.
0에서부터 슬라이드 최대갯수-1의 범위까지 순환하도록 구현했다.
handleClickPrevButton() {
const { currentSlideIndex, maxLength } = this.state;
const newIndex =
currentSlideIndex === 0 ? maxLength - 1 : currentSlideIndex - 1;
this.setState({ ...this.state, currentSlideIndex: newIndex }, true);
}
handleClickNextButton() {
const { currentSlideIndex, maxLength } = this.state;
const newIndex =
currentSlideIndex === maxLength - 1 ? 0 : currentSlideIndex + 1;
this.setState({ ...this.state, currentSlideIndex: newIndex }, true);
}
넘김 버튼을 클릭해 ImageSliderPage의 상태인 currentSlideIndex가 변경되면 Slider와 Dots컴포넌트가 바뀐 상태로 다시 렌더링되게 구현했다.
화면 상 변하지 않는 ArrowButtons는 리렌더링되지 않게 했다.
reRender() {
this.Slider.setState({
...this.Slider.state,
currentSlideIndex: this.state.currentSlideIndex,
});
this.Dots.setState({
...this.Dots.state,
currentSlideIndex: this.state.currentSlideIndex,
});
}
문제점
스크롤이 끝나면 useInterval을 중단 시키는 코드를 useInterval의 콜백함수 안에 작성해줬다.
문제점은 delay초마다 스크롤이 끝난지 확인하는 로직이 실행되어 비효율적이다.
useInterval(
() => {
if (!(targetDom instanceof Element)) return;
const { scrollLeft, clientWidth } = targetDom;
const currentScrolledWidth = Math.ceil(scrollLeft + clientWidth);
const isRenderedCards = currentScrolledWidth > targetDom.clientWidth;
const isFinishScroll = currentScrolledWidth === targetDom.scrollWidth;
isRenderedCards && isFinishScroll && setIsPlayMove(false);
targetDom.scrollLeft += 1;
},
15,
isPlayMove,
);
해결책
intersection observer를 도입하여, 마지막 카드를 감시 target으로 걸어준다.
마지막 카드가 전부 보여질 때 useInterval을 중단시키는 콜백함수를 실행시키는 방법이다.
ref.current의 값이 바뀌어도 컴포넌트는 그 사실을 알지 못한다.
그렇기에 ref.current가 초기값에서 연결된 돔으로 바뀐 후 어떤 로직을 실행하게 하려면
useEffect를 사용해 의존성으로 ref.current를 걸어줘야했다.
하지만 callback ref를 사용하면 ref.current변경을 알아서 인지해, 돔 값이 할당 된 후 로직을 자동 실행시킬 수 있다.
돔의 ref속성에 함수를 할당해주면, 해당 함수의 파라미터로 dom이 들어오게되고
그것을 이용해 로직을 작성할 수 있다.
주의점은 ref에 할당하려는 함수는 꼭 useCallback으로 감싸서 리렌더링이 안되게 해야한다는 것.
참고 코드
import React, { useState, useCallback } from "react";
import Cat from "./components/Cat";
import "./styles.css";
export default function App() {
const [height, setHeight] = useState(0);
const callbackRef = useCallback((node) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div>
<h4> 고양이가 세상을 구한다 ️</h4>
<p> 내 키는 : {height}px 이야</p>
<div ref={callbackRef}>
<Cat />
</div>
</div>
);
}
참고자료
intersectionobserver-support-horizontal-scrolling-observation
https://dev.to/itepifanio/horizontal-scroll-with-lazy-loading-578c
https://leehwarang.github.io/2020/11/29/ref.html