swiper를 메인 페이지 인기 아고라 리스트를 출력할 때 사용하려고 했는데 shallow routing과의 충돌로 문제가 발생했다.
이를 해결한 과정을 간단하게 담았다.
나는 메인 화면에서 인기 아고라를 swiper로 출력하고, 카테고리 리스트도 swiper로 적용해서 출력하고자 했다.
하지만, swiper는 기본적으로 .swiper
클래스를 사용해 swiper를 적용할 컴포넌트를 구분하기 때문에 클래스 이름이 겹치면 swiper 충돌로 인해 나머지 swiper 컴포넌트에는 적용되지 않는다.
그래서 한 페이지에서 swiper를 2개 이상 적용하고 싶을 때는 아래와 같이 최상위 클래스를 다른 이름으로 지정해주어야 한다.
const swiper = new Swiper('.lively-agora-swiper', {
direction: 'horizontal',
loop: false,
centeredSlides: false,
touchRatio: 1,
freeMode: true,
grabCursor: true,
slidesPerView: 'auto',
spaceBetween: 10,
keyboard: {
enabled: true,
onlyInViewport: false,
},
});
설정을 추가할 땐 서비스에서 사용하고자 하는 설정들을 추가하면 된다.
나는 사용자가 원하는대로 swipe 되게 하고 싶었기에 freeMode
를 사용했다.
나는 메인 화면에서 리스트를 보여줄 때, 카테고리를 선택하여 리스트 데이터를 다르게 보여주기 때문에, 부분적으로만 리렌더링이 되게 하고싶었다.
하지만, 위 영상처럼 router.push
를 사용해서 url을 바꾸며 데이터를 변경했기 때문에 페이지 전체가 리렌더링되는 문제가 있었다.
그래서 이를 해결하기 위해 부분적으로만 리렌더링이 되도록 shallow routing
을 사용해 전역 상태로 카테고리와 검색 텍스트를 관리하여 리스트를 갱신해주었다.
shallow routing
을 적용해서 필요한 부분만 리렌더링이 되게 한 것까지는 좋았다.
하지만, 프로젝트가 MVP2 버전으로 넘어가면서 새로운 기능이 추가되었고,
shallow routing
이 적용된 위 화면의 메인 페이지에 인기 아고라 swiper를 추가해주어야 했다.
처음엔 카테고리 swiper랑 동일하게 적용해주면 될 것이라 생각했었는데,
문제가 발생했다.
위 사진처럼 swiper가 적용이 안되는 것이다.
일단 한 페이지에 swiper가 두개이기 때문에, 맨 처음에 작성했던 대로 swiper 컴포넌트를 구분해주어야 했다.
컴포넌트를 구분해준 뒤에도 계속 swiper가 적용이 되지 않았다.
새로고침해서 첫 렌더링 시에만 적용이 되고, 이후 다른 페이지에서 다시 넘어온다거나 하는 경우에는 작동되지 않았다.
그래서 swiper를 출력해봤는데,
swiper 객체가 생성이 되긴 하는데,
swiper를 적용한 element가 렌더링이 되기 전에 먼저 생성이 되어 제대로 초기화 되지 않았다.
다른 페이지를 갔다가 돌아와도 element가 mount되지 않아서 swiper가 할당할 element를 찾지 못한채 초기화하는 문제였다.
그래서 이곳 저곳 인터넷을 찾아봐도 비슷한 경우가 없어서 gpt의 힘도 열심히 빌렸다.
그 결과!
shallow routing이 적용되면, 제대로 컴포넌트가 unmount되지 않아서 다른 페이지에서 돌아와도 mount 되지 않을 수 있다는 것이다!!
shallow routing을 적용하면 url이 변경되어도 컴포넌트 상태는 변하지 않는다.
그래서 데이터가 바뀌어도 컴포넌트가 리렌더링 되지 않기에 나는 아고라 리스트를 리렌더링 시키기 위해 전역 상태를 사용해서 강제 리렌더링을 시켜주었었다.
위와 동일한 이유로 unmount와 mount가 제대로 되지 않는 문제인 것 같다는 판단을 내렸다.
shallow routing을 뺄 수는 없기에 element 객체를 강제 unmount 시키고 강제 mount를 시켜줘야 내가 원하는대로 동작시킬 수 있다.
element 객체를 강제 리렌더링 시키는 방법으론 key
를 사용할 수 있다.
key 속성을 넣어주면, key 값이 바뀔 때마다 element가 리렌더링이 된다.
const [key, setKey] = useState(0); // 강제 리렌더링을 위한 키
const swiperContainerRef = useRef<HTMLDivElement>(null);
const [swiperInstance, setSwiperInstance] = useState<Swiper | null>(null);
const [isMounted, setIsMounted] = useState(false);
<div
key={key}
id="lively-agora-swiper"
ref={swiperContainerRef}
className="lively-agora-swiper pr-1rem w-full h-full"
>
<div className="swiper-wrapper h-full">
{agoras.map((agora) => (
<div key={agora.id} className="swiper-slide h-full">
<CategoryAgora agora={agora} />
</div>
))}
</div>
</div>
lively-agora-swiper
를 적용한 태그에 key 값을 주었다.
이 element 객체가 제대로 mount된 후에 swiper를 적용해주기 위해 ref도 넣어주고 isMounted 상태도 관리해주었다.
그리고 swiper가 제대로 destroy되고 초기화되도록 swiper 객체도 ref에 추가해주었다.
제발 잘 작동해주길 기도하며 코드 작성
이제!
위에 정의해둔 state와 ref를 사용해주자
useEffect(() => {
setIsMounted(true);
return () => {
setIsMounted(false);
if (swiperInstance && !swiperInstance.destroyed) {
swiperInstance.destroy();
}
};
}, [swiperInstance]);
먼저 swiper를 의존성 배열에 넣어두고 mount되었는지를 확인해주는 로직을 추가해준다.
unmount시엔 swiper를 제대로 destroy시켜준다.
useEffect(() => {
if (swiperInstance && !swiperInstance.destroyed) {
swiperInstance.destroy();
}
setSwiperInstance(null);
// 페이지 변경 시 key를 업데이트하여 강제 리렌더링
setKey((prevKey) => prevKey + 1);
}, [pathname]);
만약 mount를 하려고 하는데, swiper 객체가 이미 할당되어 있다면 destroy를 시켜준다.
그리고 pathname을 의존성 배열에 추가해줘서 페이지 변경이 일어나면 key 값을 변경해준다.
useLayoutEffect(() => {
if (
agoras &&
!isFetching &&
isMounted &&
swiperContainerRef.current &&
!swiperInstance
) {
setTimeout(() => {
const element = document.getElementById('lively-agora-swiper');
if (element) {
const swiper = new Swiper('.lively-agora-swiper', {
direction: 'horizontal',
loop: false,
centeredSlides: false,
touchRatio: 1,
freeMode: true,
grabCursor: true,
slidesPerView: 'auto',
spaceBetween: 10,
keyboard: {
enabled: true,
onlyInViewport: false,
},
});
setSwiperInstance(swiper);
}
}, 0);
}
}, [isMounted, agoras, key, swiperContainerRef, swiperInstance, isFetching]);
useLayoutEffect을 사용해서 mount가 끝난 다음에 실행되도록 해준다.
의존성 배열에 key값도 넣어줘서 key 값이 변경되면 swiper 객체가 다시 생성되도록 해준다.
그리고 화면을 보니!!
다행히 제대로 작동한다!!!
이 문제를 4일 동안 해결하려고 열심히 싸웠다...