React - Simple Carousel

January 15, 2023

라이브러리 없이 Element.scrollIntoView, useRef를 이용해 만든 리액트 캐러셀




Index

1. css : overflow hidden
2. scrollIntoView
3. useRef



1. css

.carousel {
  display: flex;
  overflow: hidden;
  gap: 10px;
}

( required )

display : flex - 이미지 요소들을 가로로 펴줍니다.

overflow : hidden - 이미지들이 화면의 너비를 초과하는 경우엔 화면의 너비이외의 이미지는 자릅니다.

만약 캐러셀의 영역을 특정 너비로 제한하고 싶다면 width 속성을 사용합니다.

ex) width : 500px; width : 50vw;


( optional )

gap: 10px - gap은 이미지들이 일정한 간격으로 위치할 수 있도록 사용하는 스타일 속성입니다. 여기선 10px만큼 간격을 띄워줍니다.



2. Element.scrollIntoView

element.scrollIntoView({
    inline: "center",
    block: "nearest",
    behavior: "smooth",
});

JavaScript의 scrollIntoView는 화면의 특정 위치로 이동시켜주는 함수입니다.

이 캐러셀에선 특정 이미지의 위치로 이동하기 위해 scrollIntoView 함수를 활용합니다.

scrollIntoView 엔 option을 지정할 수 있습니다.

아래는 캐러셀에 사용된 옵션입니다.


  • inline: "center"

스크롤의 좌우방향(horizontal)을 정합니다. 수평 정렬의 중앙을 맞춥니다.


  • block: "nearest"

스크롤의 상하방향(vertical)을 정합니다. 수직 정렬을 가까운 곳으로 설정합니다.


  • behavior: "smooth"

스크롤 움직임(transition animation)을 정합니다. smooth 속성으로 스크롤을 부드럽게 이동시킵니다.


이 캐러셀에서 사용되지 않은 옵션은 공식문서에서 확인 가능합니다. [MDN]Element.scrollIntoView()



3. useRef

리액트에서 특정 DOM을 제어해야하는 상황엔 useRef란 리액트 훅을 사용합니다.

useRef의 기본 사용법은 공식문서를 참조해주세요. [Hooks API Reference] useRef


const imageRef = useRef<HTMLDivElement[]>([]);

imageRef변수에 초기값으로 array을 넣습니다.

div태그를 배열안에 넣어주기 때문에 타입으로 HTMLDivElement[] 를 지정해줍니다.



{images.map((image, index) => (
    <div ref={(el) => (imageRef.current[index] = el)}>
      <img {...} />
    </div>
	)
}

div element에 ref 속성을 콜백함수의 형태로 작성하여 해당 div element ref를 파라미터로 받습니다.

ref 파라미터를 imageRef의 current 배열에 index 순서대로 넣어줍니다.

이 과정을 통해 image들은 imageRef 배열에 담기게 됩니다.

imageRef.png

imageRef.current 배열에 담긴 div element를 이용하여 이미지들의 scroll 위치를 지정할 수 있습니다.



위 3가지 개념들을 적용하여 코드를 작성하면 아래와 같은 형태로 작성 가능합니다.

type OnSelectImageType = {
    currentIndex: number;
    behavior?: "auto" | "smooth";
};

export default function Carousel() {
    // 선택된 이미지의 인덱스를 담아둔 state
    const [currentImageIndex, setCurrentImageIndex] = useState<number>(0);

    // 이미지들의 위치를 배열로 담아둔 ref 변수
    const imageRef = useRef<HTMLDivElement[] | null[]>([]);

    // 선택된 이미지의 위치로 이동시키는 함수
    function onSelectImage({ currentIndex, behavior = "smooth" }: OnSelectImageType) {
        setCurrentImageIndex(currentIndex);
        imageRef.current[currentIndex]?.scrollIntoView({
            inline: "center",
            block: "nearest",
            behavior,
        });
    }

    // 버튼 클릭시 동작하는 함수
    function handleImageMove(currentIndex: number) {
        const firstIndex = 0;
        const lastIndex = images?.length - 1;

        // 첫 번째 이미지에서 마지막 이미지로 이동할 때 동작하는 조건
        if (currentIndex < firstIndex) {
            return onSelectImage({ currentIndex: lastIndex, behavior: "auto" });
        }
        // 마지막 이미지에서 첫 번째 이미지로 이동할 때 동작하는 조건
        if (currentIndex > lastIndex) {
            return onSelectImage({ currentIndex: firstIndex, behavior: "auto" });
        }

        onSelectImage({ currentIndex });
    }

    return (
        <main>
            <section className="carousel">
                {images.map((image, index) => (
                    <div ref={(el) => (imageRef.current[index] = el)}>
                        <img
                            className={`image ${currentImageIndex === index ? "selected" : ""}`}
                            alt={`carousel-img-${index}`}
                            src={image.location}
                        />
                    </div>
                ))}
            </section>

            <section className="controller">
                <button onClick={() => handleImageMove(currentImageIndex - 1)}>previous</button>
                <button onClick={() => handleImageMove(currentImageIndex + 1)}>next</button>
            </section>
        </main>
    );
}

위 코드는 CodeSandbox에서 테스트할 수 있습니다.