react swiper.js와 useEffect

Lybell·2024년 3월 15일
0

본 아티클은 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.elref.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를 초기화한다.

좀 더 자세하게 살펴보면 다음과 같다.

  1. mySwiper 컴포넌트를 처음 렌더링을 시도한다. mySwiper 함수의 useSwiperRef 함수를 실행한다.
  2. useSwiperRef 함수가 실행되고 wrapper state와 ref가 리턴된다.
  3. 리턴값을 평가한다. Swiper 컴포넌트를 렌더링 시도하는데, 아직 ref가 바인딩되지 않았으므로 ref는 null으로 취급한다.
  4. ref.current가 실제 nav 태그의 dom으로 대입된다.
  5. 컴포넌트가 렌더링 완료되면 커스텀 훅 안에 있는 useEffect를 실행한다. setWrapper로 state를 변경한다.
  6. 리액트는 mySwiper의 state가 변경되었다고 판단하고, mySwiper를 리렌더링한다.
  7. useSwiperRef 함수가 실행되고, wrapper state와 ref를 가져온다.
  8. 리턴값을 평가한다. Swiper 컴포넌트를 렌더링 시도하면서 nav 태그의 dom으로 대입된 ref.current를 갖고 Swiper를 렌더링한다.
  9. 컴포넌트가 렌더링 완료되면 커스텀 훅 안에 있는 useEffect를 실행하려고 하나, 2번째 인자가 빈 배열이므로 실행하지 않는다.

useEffect의 2번째 인자

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는 실행되지 않는다.

레퍼런스

profile
홍익인간이 되고 싶은 꿈꾸는 방랑자

0개의 댓글