ETC -

무한 롤링 기능 구현

  • -

🎯무한 롤링 기능

안녕하세요
TriplexLab입니다. 
오늘은 무한롤링 기능에 대해서 이야기를 해보겠습니다.

저 같은 경우에는 Swiper(v8.4.7)을 커스텀해서 기능을 완성할 수 있었습니다.
그리고 반응형으로 제작을 해야해서 마크업 상에서 PC, 모바일 둘 다 만들어 놓고 css 미디어쿼리를 사용해서 해상도가 PC일 때 PC버전 보여주고, 해상도가 모바일일 때 모바일 버전을 보여주는 방식을 택했습니다.

그렇다 보니 JS에서 Swiper인스턴스를 다음과 같이 두 번 호출했습니다.

/** HISTORY 모바일 영역 */
new Swiper(".history-swiper.mo", {
	//...
}

/** HISTORY PC 영역 */
const historySwiperPc = new Swiper(".history-swiper.pc", {
	//...
}

👉🏻문제 및 이슈 EX1)

기존에는 무한롤링 기능이 없었는데 메뉴 항목이 늘어날수록 누적이 되어야 하는 UI라서
기획단에서 뒤늦에 문제를 확인하고 제가 무한롤링 기능을 추가해야만 했었습니다.

현재의 마크업 구조상으로는 구현이 불가능할 것 같아서 완전히 마크업 구조를 변경해야만 했었고, 반응형이라는 것도 고려해야 했습니다.
PC일 때는 위, 아래로 메뉴들이 무한 롤링이 되어야만 하는 기능이었고, 
모바일 일때는 이미지들이 일반적인 양옆으로 슬라이드 되는 거였습니다.

공수는 약 5일정도 소유한 것 같습니다.
Swiper 옵션에 대해서 설명을 한다면 다음과 같이 간단하게 주석으로 설명하겠습니다.

const historySwiperPc = new Swiper(".history-swiper.pc", {
    direction: 'vertical', //세로로 변경
    slidesPerView: 7,// 메뉴들 보여지는 객수
    spaceBetween: 0, // 여백 간격
    loop: true, // 반복 여부
    slideToClickedSlide: true, //클릭하면서 마지막 요소일때 loop으로 실행 할수 있습니다.
    centeredSlides: true, //가운데로 정렬하겠다
    allowTouchMove : false //마우스 드래그엔드랍 여부
}

👉🏻고민한 부분

slideToClickedSlide옵션을 찾기 전에는 메뉴 요소가 마지막일 때 더 이상 다음클릭을 할 수 없어서 어떻게 해결해야 하나 많이 고민했었습니다. 따로 함수로 만들어서 마지막시점에 위로 지나간 엘리먼트를 복사해서 밑에 추가해야 하나 생각했지만 찾아보니 저런 옵션을 지원해주고 있어서 정말 다행 있었습니다.

그리고 또 하나의 문제점은 메뉴들을 마구 클릭했을때 버그인 것 마냥 한번 클릭했을 때 제대로 동작을 못하고 두 번을 클릭해야 제대로 동작하는 문제를 발견했습니다. 이 부분도 혹시나 싶어서 다음과 같이 클릭 이벤트를 추가해서 slideTo(clickedIndex) 메서드를 호출해서 클릭인텍스를 인자로 넣었더니 문제 해결이 잘 되었습니다.

그리고 클릭 이벤트와, 드래그엔드랍 이벤트도 둘 다 같이 적용되면 가끔 제대로 동작을 못하는 것 같아서 마우스 드래그 앤 드롭 이벤트를 일부러 비활성화시켰습니다.

$('.history .history-swiper').on('click', '.swiper-slide', function(e) {
	e.preventDefault(); // 슬라이드 전환 중 클릭 이벤트 방지
	e.stopPropagation();
	const clickedIndex = $(this).index();
	historySwiperPc.slideTo(clickedIndex);
});

👉🏻전체 소스 코드 보기 EX1)

(() => {
  const isDesktop = window.innerWidth > 1080;

  // 성공 템플릿 PC
  const successTemplatePc = ({year, month}) => {
    return `
      <div class="swiper-slide">
        <p>${month} <span>${year}</span></p>
      </div>
    `;
  };

  // 성공 템플릿 MO
  const successTemplateMo = ({year, month, thumb, link}) => {
    return `
      <div class="swiper-slide" data-year=${year} data-month=${month}>
        <img class="thumb" src=${thumb} alt="thumb" />
        <a href=${link}></a>
      </div>
    `;
  };

  // 실패 템플릿
  const errorTemplate = (errorThrown) => { 
    return`
      <div class="error">
        <h2>통신 실패!</h2>
        <p>에러 메시지: ${errorThrown}</p>
      </div>
    `;
  };

  const fetchData = async (path) => {
    $.ajax({
      url: path, // 서버의 엔드포인트를 지정합니다.
      method: 'GET',
      dataType: 'json',
      async: false, // 동기식으로 통신함.
      success: function(response) {
        // 통신 성공 시 템플릿을 만들어 추가합니다.
        const templateHtmlPc = response.map((datas) => {
          return successTemplatePc(datas)
        }).join('');
        const templateHtmlMo = response.map((datas) => {
          return successTemplateMo(datas)
        }).join('');

        $('.history_pc .swiper-wrapper').html(templateHtmlPc);
        $('.history_mo .swiper-wrapper').html(templateHtmlMo);
      },
      error: function(jqXHR, textStatus, errorThrown) {
        // 통신 실패 시 템플릿을 만들어 추가합니다.
        const errorHTML = errorTemplate(errorThrown);
        $('.history_pc .swiper-wrapper').html(errorHTML)
        $('.history_mo .swiper-wrapper').html(errorHTML);
      }
    });
  };

  //첫 로드될때 실행
  fetchData("https://younhoso.github.io/younhoso/blogExample/infinite_rolling/data/history.json");

  /** HISTORY 모바일 영역 */
  new Swiper(".history-swiper.mo", {
    slidesPerView: 1.4,
    centeredSlides: true,
    spaceBetween: 0,
    loop: true,
    initialSlide: 0,
    allowTouchMove: true,
    on: {
      slideChange: function (swiper) {
        $(".active-title").children("img").css("display", "none");

        //하얀글자로 바뀔 때 애니메이션 (only 모바일)
        $(".history-swiper-pagination .swiper-pagination-bullet-active").addClass("animation");
      },
    },
    pagination: {
      el: ".history-swiper-pagination",
      clickable: true,
      type: "custom",
      clickable: isDesktop ? true : false,
      renderCustom: function (swiper, current, total) {
        let text = "<div class='bullet-container'>";
        for (let i = 1; i <= total; i++) {
          const year = $('.history .history-swiper.mo .swiper-slide').eq(i+1).data('year');
          const month = $('.history .history-swiper.mo .swiper-slide').eq(i+1).data('month');
          if (current === i) {
            text += `<div class='swiper-pagination-bullet swiper-pagination-bullet-active' style='order:${-1};'>
              <p class="year">${year}</p>
              <p class="month">${month}</p>
              </div>`;
          } else {
            text += `<div class='swiper-pagination-bullet' style='order:${i - current > 0 ? i - current : i + current};'>
              <p class="year">${year}</p>
              <p class="month">${month}</p>
              </div>`;
          }
        }
        text += "</div>";
        return text;
      },
    },
  });
  /** HISTORY 모바일 영역 끝*/

   // 히스토리 이미지 Pagination
   const historyPaginationImgs = [
    "history_2023_6_img",
    "history_2023_4_img",
    "history_2023_2_img",
    "history_2022_12_img",
    "history_2022_10_img",
    "history_2022_8_img",
    "history_2022_6_img",
    "history_2022_4_img",  
  ];

  /** HISTORY PC 영역 */
  const historySwiperPc = new Swiper(".history-swiper.pc", {
    direction: 'vertical',
    slidesPerView: 7,
    spaceBetween: 0,
    loop: true,
    slideToClickedSlide: true, //true 해줘야지 클릭했을때 loop을 실행 할수 있습니다.
    centeredSlides: true,
    allowTouchMove : false,
    on: {
      slideChange: function(swiper) {
        const activeIndex = swiper.activeIndex;
        const current = $(swiper.slides[activeIndex]).data('swiper-slide-index');
        const marginLeft = parseInt($('.history .history-swiper .swiper-pagination-bullet').css('margin-left')?.replace('px', ''));
        const marginRigth = parseInt($('.history .history-swiper .swiper-pagination-bullet').css('margin-right')?.replace('px', ''));
        const itemWidth = $('.history .history-swiper .swiper-pagination-bullet').width();
        const leftValue = -((itemWidth + marginLeft + marginRigth) * current);
        $('.history .bullet-container').css({ 'transform': `translateX( ${leftValue}px )`});

        if(isDesktop){
          let slideImg = $('.history .swiper-pagination-bullet');
          for(let i = 0; i < 3; i++){
            $('.history .bullet-container').append($(slideImg[i]).clone());
          }
        }
      },
    },
    pagination: {
      el: '.swiper-pagination',
      type: "custom",
      renderCustom: function (swiper, current, total) {
        const activeIndex = swiper.activeIndex;
        const activeTxt = swiper.slides[activeIndex]?.innerHTML;

        //이미지 Pagination
        let text = `<div class='bullet-container'>`
        for (let i = 1; i <= total; i++) {
          const linkNumber = total - i + 1;//인덱스 반대로 뒤집기
          if (current === i) {
            text += `<div class='swiper-pagination-bullet swiper-pagination-bullet-active'>
            <div class="activetxt">${activeTxt}</div>
            <a href="http://tastyzine.co.kr/main/index/${linkNumber}">
              <img src='./imgs/${historyPaginationImgs[i - 1]}.png' art="#" />
            </a>
          </div>`;
          } else {
            text += `<div class='swiper-pagination-bullet'>
              <a href="http://tastyzine.co.kr/main/index/${linkNumber}">
                <img src='./imgs/${historyPaginationImgs[i - 1]}.png' art="#" />
              </a>
            </div>`;
          }
        }
        text += "</div>";
        return text;
      },
    },
  });

  $('.history .history-swiper').on('click', '.swiper-slide', function(e) {
    e.preventDefault(); // 슬라이드 전환 중 클릭 이벤트 방지
    e.stopPropagation();
    const clickedIndex = $(this).index();
    historySwiperPc.slideTo(clickedIndex);
  });
  /** HISTORY PC 영역 끝*/
})();

👉🏻Live Demo EX1)

무한롤링 기능이 되는 Live Demo링크와 해당 레포지토리 링크입니다. 참고하세요.

👉🏻문제 및 이슈 EX2)

추가적 이슈 상황이 생겼는데 위에 Live Demo를 보신 분들은 이해될 것입니다. 오른쪽 이미지 영역이 "좌우 슬라이드 모션"을 추가해 달라는 것입니다. (음.... 역시 회사 생활 쉽지 않군...)

암튼 해당문제에 대해서 해결 방법을 고민해 봤는데 현재 구조를 최대한 유지하고 스크립트에서 "좌우 슬라이드 모션"을 추가해 보려고 반나절동안 작업 해봤는데 의도하는 데로 잘 작동하지 않고 이상하게 움직였다.ㅠㅠ

그래서 나의 생각은 음... 일단 밥 먹고 하자ㅋㅋㅋ 일부러 한 가지 생각에 골임 되면 오히려 문제가 더 안 풀려서 일부러 좀 쉬었하자는 생각임.
(이 또한 문제 해결을 위한 전략이다.)

그리고 밥 먹으면서도 생각해 봤다. 해결 방법을 모르겠어서 결국 CodePen사이트에서 벤치마켓을 했다. 나랑 비슷한 UI로 어떻게 구현했는지 레퍼런스를 확인했다.

다른 분들이 만든 것 확인해 보니 결국 마크업 구조를 두 덩어리로 만들어서 controller로 서로 연결을 시켜준 것입니다.

// 예를 들면 이런식으로
slider.controller.control = thumbs;
thumbs.controller.control = slider;

여기서 저도 힌트를 얻었고, 현재 마크업 구조를 변경해서 두 덩어리로 만들어서 controller로 서로 연결을 시켰습니다.

근데 이것 또한 문제가 있었습니다. 이것 작업을 하다 보면 메뉴와, 이미지가 1:1로 메칭이 되어서 "좌우 슬라이드 모션"이 잘 될 순 있어도
맨 마지막, 혹은 첫 번째가 되었을 때부터 의도하지 않게 이상하게 움직임이 되는 것을 확인했습니다.

왜 그런가 했더니 맨 마지막, 혹은 첫 번째가 되었을 때 인덱스가 2번 연속으로 찍힙니다. 버그인가 싶었다...ㅠ
문제가 되는 상황 마지막 번째 인덱스 번호 16번째 때 연속으로 8번도 찍히는 현상👇🏻

문제의 화면

그래서 이것 저러고 수정해 보고, 찾아보다가 알게 되었다. 위와 같이 controller로 연결하지 말고 다음과 같이 연결하라는 것을!!

 $('.history .swiper-wrapper').on('click', '.swiper-slide', function(e) {
  e.preventDefault(); // 슬라이드 전환 중 클릭 이벤트 방지
  e.stopPropagation();
  const clickedIndex = $(this).index();
  subThumbs.slideTo(clickedIndex); //subThumbs해당 인스턴스에 clickedIndex를 연결
  historySwiperPc.slideTo(clickedIndex); //historySwiperPc해당 인스턴스에 clickedIndex를 연결
});

이렇게 수정해서 확인을 해봤더니 이제 문제없이 잘 작동합니다.!! 👍🏻👏✨

👉🏻Live Demo EX2)

무한롤링 기능이 되는 Live Demo링크와 해당 레포지토리 링크입니다. 참고하세요.
"좌우 슬라이드 모션" 추가!!

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.