본 아티클은 2022년 8월에, Stardew Dressup을 개발하다가 생긴 탐구를 개인 Notion에 작성한 것을 재구성한 것입니다.
<div class="swiper">
<div class="swiper-wrapper">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</div>
</div>
<nav class="pagination">
<div class="pagination-button"></div>
<div class="pagination-button"></div>
<div class="pagination-button"></div>
<div class="pagination-button"></div>
</nav>
위와 같은 구조의 html을 짜고 싶었다. swiper 엘리먼트 내부에서 각 엘리먼트가 스와이프되고, pagination 엘리먼트 내에 있는 각 원소를 클릭하면 스와이퍼가 자동으로 이동하도록 만들고 싶었다. 이걸 react와 swiper.js로 짜야 한다. 어떻게 해야 할까?
Swiper.js는 기본적으로 Swiper 컴포넌트 내에 pagination, navigation 등 엘리먼트를 자동으로 생성한다. 하지만 Swiper 내용과 페이지네이션을 분리하고 싶을 때가 있다.
Swiper 컴포넌트의 pagination
속성에는 el
이라는 속성이 있다. null로 지정하면 기본적으로 Swiper 컴포넌트 내에 새로운 페이지네이션 태그를 생성하지만, CSSSelector 문자열이나 html 엘리먼트를 넣을 수 있다. 리액트에서 어떠한 컴포넌트의 실제 html 엘리먼트를 가져오려면? ref
를 이용하면 된다.
import {useRef} from "react";
import {Pagination} from "swiper";
import {Swiper, SwiperSlide} from "swiper/react";
function mySwiper()
{
const paginationElement = useRef(null);
return (
<>
<Swiper
pagination={
el=paginationElement.current,
clickable=true
}
modules={[Pagination]}
>
<SwiperSlide><div>1</div></SwiperSlide>
<SwiperSlide><div>2</div></SwiperSlide>
<SwiperSlide><div>3</div></SwiperSlide>
<SwiperSlide><div>4</div></SwiperSlide>
</Swiper>
<nav ref={paginationElement}></nav>
</>
);
}
그래서 Swiper 컴포넌트 밖에 nav 태그를 추가하고, ref를 준 뒤 pagination.el
에 ref.current
를 대입하는 것을 시도했지만 실패했다. 왜 실패했을까?
paginationElement ref는 처음에 null
로 초기화된다. mySwiper 함수형 컴포넌트가 렌더링되면 Swiper 컴포넌트를 초기화할 때 pagination.el
에 null이 대입되고, 이를 기반으로 스와이퍼를 생성한다. 이후 ref
가 컴포넌트에 부착되면 ref.current
에 컴포넌트의 실제 엘리먼트가 대입된다. 즉 이 경우는 Swiper 컴포넌트가 이미 null
로 초기화된 후 paginationElement
ref를 nav 태그에 부착했기 때문에 안 되는 것이다.
function useSwiperRef()
{
const [wrapper, setWrapper] = useState(null);
const ref = useRef(null);
useEffect(() => {
setWrapper(ref.current);
}, []);
return [wrapper, ref];
};
이 방법을 해결하기 위해 구글링을 하다, 위의 커스텀 hook을 이용하면 가능하다고 한다. 실제로 돌려보니 잘 되었다. 그렇다면 대체 왜 이 커스텀 hook을 이용하면 swiper의 pagination을 잘 부착할 수 있는 것일까?
useSwiperRef를 이용하면 mySwiper 컴포넌트의 렌더링이 2번 실행된다. 1번은 null을 이용하여 Swiper를 초기화하지만, nav 태그에 ref가 부착되면 mySwiper 컴포넌트가 다시 렌더링되어, nav 태그의 실제 dom을 이용하여 Swiper를 초기화한다.
좀 더 자세하게 살펴보면 다음과 같다.
useEffect는 2개의 인자를 받으며, 첫 번째 인자로 컴포넌트가 마운팅되고 업데이트될 때 실행하는 콜백 함수를, 두 번째 인자로 배열을 받는다. 2번째 인자의 의미가 무엇일까?
기본적으로 useEffect는 렌더링 함수가 마무리되면 바로 실행된다. 하지만 모든 업데이트 때마다 useEffect의 함수를 실행하면 성능이 저하될 것이다. 그래서 일부 상태가 변경될 때에만 실행되게 하도록 만들고 싶은데 이것이 useEffect의 2번째 인자의 존재 의의다.
useEffect가 실행될 때, 직전에 실행했던 배열의 각 원소와 현재 실행되는 배열의 원소를 비교하여, 그 내용이 변경되면 콜백 함수를 실행한다. 그 예시는 다음과 같다.
function MyComponent()
{
const [toggle, setToggle] = useState(false);
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
useEffect( ()=>{
console.log("yes!");
}, [toggle, count1] );
return <>
<button onClick={()=>setToggle( prev=>!prev )} >toggle</button>
<button onClick={()=>setCount1( prev=>prev+1 )} >count1</button>
<button onClick={()=>setCount2( prev=>prev+2 )} >count2</button>
</>
}
MyComponent 컴포넌트가 렌더링되고, [false,0]
을 기준으로 useEffect를 실행했다. 만약 toggle이 true가 되었다면, [true,0]
으로 배열의 내용이 바뀌었으므로 useEffect가 실행된다. 반면, count2가 2로 변해도 배열의 내용은 [false, 0]
그대로이므로 useEffect는 실행되지 않는다.