이미지를 클릭하면 나오는 팝업 슬라이드(바닐라JS)

soyeon·2022년 1월 4일
9

TIL

목록 보기
5/32
post-thumbnail
post-custom-banner

👻서론

퍼블리셔 포트폴리오 사이트에 들어간 슬라이드입니다.
포트폴리오 사이트 제작하면서 두번째로 시간을 많이 잡아먹은 파트입니다. (첫번째는 디자인..😨😭)

기능을 설명하자면..

  • 갤러리의 썸네일을 클릭하면 썸네일에 걸려있는 원본 이미지 링크들을 팝업창에 띄움
  • 원본 이미지의 수만큼 슬라이드 밑에 버튼(bullet)들이 생성되고 몇번째 이미지인지 표시됨
  • 슬라이드 밑 버튼을 클릭하면 해당 번호의 이미지로 슬라이딩됨
  • 이전, 다음 슬라이드로 넘어가는 버튼
  • 무한 슬라이드

정도가 되겠습니다.

이것도 저번 포스트처럼 codepen에 간단한 데모버전을 만들었습니다. 코드펜에 작성한 내용을 바탕으로 설명하겠습니다.

👻HTML

    <h2>Gallery</h2>
    <ul class="box__gallery">
        <li>
            <a href="https://picsum.photos/id/237/500/300">
                <img src="https://picsum.photos/id/237/150/150" alt="이미지1"></li>
            </a>
            <li>
                <a href="https://picsum.photos/id/238/500/300">
                    <img src="https://picsum.photos/id/238/150/150" alt="이미지2"></li>
                </a>
            <li>
                <a href="https://picsum.photos/id/239/500/300">
                    <img src="https://picsum.photos/id/239/150/150" alt="이미지3"></li>
                </a>
            <li>
                <a href="https://picsum.photos/id/240/500/300">
                    <img src="https://picsum.photos/id/240/150/150" alt="이미지4"></li>
                </a>
            <li>
                <a href="https://picsum.photos/id/241/500/300">
                    <img src="https://picsum.photos/id/241/150/150" alt="이미지5"></li>
                </a>
            <li>
                <a href="https://picsum.photos/id/242/500/300">
                    <img src="https://picsum.photos/id/242/150/150" alt="이미지5"></li>
                </a>
    </div>
</section>

    <!--팝업창-->
<div class="slide-overlay">
    <button class="close-btn">close</button>
    <button class="slide-btn --prev">
        prev
    </button>
    <button class="slide-btn --next">
        next
    </button>
    <div class="slide__container">
        <ul class="slides">
            <li><img src="" alt="이미지1"></li>
            <li><img src="" alt="이미지2"></li>
            <li><img src="" alt="이미지3"></li>
            <li><img src="" alt="이미지4"></li>
            <li><img src="" alt="이미지5"></li>
            <li><img src="" alt="이미지6"></li>
        </ul>
    </div>
</div>

갤러리 썸네일 li 요소에 앵커로 이미지 원본(큰 사진) 주소를 걸어주고
img 태그에는 썸네일(작은 사진) 주소를 집어넣습니다.
그리고 슬라이드 이미지 주소는 비우거나 임시 이미지를 넣어줍니다. 나중에 자바스크립트로 이미지 원본 주소를 슬라이드 이미지 주소에 집어넣을거임

👻CSS

slide

.slide__container {
  position: absolute;
  width: 500px;
  height: 300px;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  overflow: hidden;
}
.slides {
  width: 600%;
  position: relative;
  left: 0;
}
.slides::after {
  content: "";
  display: block;
  clear: both;
}
.slides > li {
  float: left;
}

길어서 중요한 슬라이드 부분만!
slide의 li들을 float을 이용해 일자로 정렬해줍니다.
ul 요소인 slides는 이미지의 개수만큼 너비를 지정해줍니다.
여기서는 slide__container의 크기 = 이미지 하나의 크기이기 때문에 너비를 600%로 했습니다.

👻javaScript

썸네일을 눌렀을 때 팝업창이 등장하는 함수&이미지 주소 연결

const thumbnails = document.querySelectorAll(".box__gallery > li");
const overlay = document.querySelector(".slide-overlay");
let slidePhoto = document.querySelectorAll(".slides > li>img");

thumbnails.forEach((thumbnail) => {
  thumbnail.addEventListener("click", (e) => {
    e.preventDefault();
    overlay.style.display = "block";
    // 썸네일 원본 사진과 갤러리 슬라이드 이미지 소스 링크 연결
    for (let i = 0; i < thumbnails.length; i++) {
      let photo = thumbnails[i].lastElementChild;
      slidePhoto[i].src = photo.href;
    }
  });
});
document.querySelector(".close-btn").addEventListener("click", () => {
  overlay.style.display = "none";
});

이미지 개수만큼 버튼(bullet)을 생성하는 함수

const overlay = document.querySelector(".slide-overlay");
let slides = document.querySelectorAll(".slides > li");
let bullets = 0;

function createBullets() {
  // bullet들의 리스트를 생성
  const bulletsList = document.createElement("ul");
  bulletsList.setAttribute("id", "bullets");
  overlay.appendChild(bulletsList);
  // 이미지 개수대로 bullet를 생성
  slides.forEach((slide, index) => {
    const a = document.createElement("a");
    a.setAttribute("href", "#");
    // 이미지의 index를 a의 html에 집어넣음 
    a.innerHTML = `${index}`;
    const li = document.createElement("li");
    li.appendChild(a);
    bulletsList.appendChild(li);
  });
  return (bullets = document.querySelectorAll("#bullets > li > a"));
}
createBullets();

1. 버튼들을 담을 ul 요소 생성

  const bulletsList = document.createElement("ul");
  bulletsList.setAttribute("id", "bullets");
  overlay.appendChild(bulletsList);
  1. 먼저 createElement 메서드로 버튼들을 담을 ul 요소를 생성해줍니다.
  2. 그리고 그 ul 요소에 id를 설정해줍니다. (#bullets)
  3. 팝업 오버레이에 ul를 삽입합니다.

2. 이미지의 개수만큼 버튼을 생성

  slides.forEach((slide, index) => {
    const a = document.createElement("a");
    a.setAttribute("href", "#");
    a.innerHTML = `${index}`;
    a.setAttribute("href","#");
    const li = document.createElement("li");
    li.appendChild(a);
    bulletsList.appendChild(li);
  1. forEach 메서드를 이용하여 slides를 조작합니다.
    forEach()는 주어진 콜백함수를 배열에 있는 각 요소에 적용하는 반복문입니다.
    forEach의 첫번째 인자는 처리할 현재요소 즉, slides 배열의 각 요소이고
    두번째 인자는 처리 중인 현재 요소의 인덱스입니다.
  2. createElement() 메서드로 버튼이 되어줄 a 요소를 생성합니다. 배열에 들어있는 요소의 개수만큼 반복되는 원리이니 이미지의 개수만큼 만들어지겠죠?
  3. 웹 접근성을 위해 innerHTML를 이용해 버튼마다 이미지의 번호를 넣어줍니다.
  4. a 요소가 들어갈 li 요소를 만들고 li 요소 안에 a 요소를 넣습니다.


저장하고 새로고침을 하면 이렇게 이미지의 개수만큼 버튼이 만들어진 것을 확인할 수 있습니다.

슬라이드 버튼 클릭 이벤트

const overlay = document.querySelector(".slide-overlay");
let slides = document.querySelectorAll(".slides > li");
const slide = document.querySelector(".slides");
const thumbnails = document.querySelectorAll(".box__gallery > li");
const photoCount = slides.length;
const duration = 400;
let bullets = document.querySelectorAll("#bullets > li > a")
let photoIndex = 0;

// 슬라이드 버튼 클릭 이벤트
document.querySelector(".--next").addEventListener("click", nextSlideImage);
document.querySelector(".--prev").addEventListener("click", prevSlideImage);

// 다음 사진으로 슬라이드
function nextSlideImage() {
  photoIndex++;
  photoIndex %= photoCount;
  slide.style.left = "-100%";
  slide.style.transition = duration + "ms";
  window.setTimeout(() => {
    slide.appendChild(slide.firstElementChild);
    slide.removeAttribute("style");
  }, duration);
  bulletClassReset();
  //해당하는 bullet에 on 클래스 넣기
  bulletIndex();
}
// 이전 사진으로 슬라이드
function prevSlideImage() {
  photoIndex--;
  console.log(photoIndex);
  photoIndex %= photoCount;
  slide.insertBefore(slide.lastElementChild, slide.firstChild);
  slide.style.left = "-100%";
  slide.style.transition = "0ms";
  window.setTimeout(() => {
    slide.style.left = 0;
    slide.style.transition = duration+"ms";
  });
  bulletClassReset();
  //해당하는 bullet에 on 클래스 넣기
  bulletIndex();
}

❓ 이전 사진으로 슬라이드 하는 함수의 setTimeout에 delay를 따로 설정하지 않은 이유는?
A. delay를 넣으면 이전 이미지로 넘어갈 때 반응이 느리게 느껴집니다. 그래서 slide를 잡아당기자마자 "즉시" slide가 움직일 수 있도록 delay를 생략했습니다.

모든 bullet의 on 클래스를 삭제

function bulletClassReset() {
  bullets.forEach((bullet) => {
    bullet.classList.remove("on");
  });
}

해당하는 bullet에 on 클래스 넣기

function bulletIndex() {
  // photoIndex가 음수일 때를 고려
  let index = photoIndex + bullets.length;
  index %= bullets.length;
  console.log(index);
  bullets[index].classList.add("on");
}

💖bullet을 클릭하면 해당하는 번호의 이미지로 슬라이드 되는 함수

function bulletLink() {
  bullets.forEach((bullet, index) => {
    bullet.addEventListener("click", (e) => {
      e.preventDefault();
      // 클릭된 bullet의 인덱스
      const clickedIndex = index;
      // 현재 bullet과 클릭된 bullet의 차이
      let step = clickedIndex - photoIndex;
      photoIndex = clickedIndex;
      //모든 bullet의 클래스를 없애고 클릭된 bullet에만 on 클래스 추가
      bulletClassReset();
      bullets[clickedIndex].classList.add("on");

      // 클릭할 때마다 순서가 바뀌는 slides들 업뎃
      slides = document.querySelectorAll(".slides>li");
      let currentSlides = [...slides];
      //step이 양수: 현재 요소보다 뒤에 오는 요소로 이동
      if (step > 0) {
        // 이미지 슬라이드 step의 수 만큼 앞에서 자른다
        let sliceSlides = currentSlides.slice(undefined, step);
        slide.style.transition = duration+"ms";
        slide.style.left=step * -100+"%";
        window.setTimeout(() => {
          slide.removeAttribute("style");
          // 잘린 요소들을 맨 뒤로 집어넣기..
          slide.append(...sliceSlides);
        }, duration);
      } else {
        // step이 음수: 현재 요소보다 앞에 있는 요소로 이동
        sliceSlides = currentSlides.slice(step);
        // 잘린 요소들을 맨 앞으로 집어넣기
        slide.prepend(...sliceSlides);
        slide.style.left = step * 100 + "%";
        window.setTimeout(()=>{ 
            slide.style.left = 0;
            slide.style.transition = duration+"ms";
        })
      }
      //서로 같은 경우 이동할 필요가 없기 때문에 함수 즉시 종료
      if (step==0) return;
    });
  });
}

기본 이벤트 삭제

bullets.forEach((bullet, index) => {
    // 클릭 이벤트 추가
    bullet.addEventListener("click", (e) => {
      e.preventDefault();
    });

클릭된 bullet의 index를 변수로 설정

      // 클릭된 bullet의 인덱스
      const clickedIndex = index;

헷갈려서 변수 할당해줌

현재 bullet과 클릭된 bullet의 인덱스의 차이를 이용하기

      let step = clickedIndex - photoIndex;

뜬끔없이 이 둘의 인덱스의 차이를 구하냐면 인덱스의 차이만큼 슬라이드들을 분리시켜서 이동-삽입할 거기 때문입니다.
이 둘의 차가 양수(>0)면 현재 요소보다 뒤에 있는 요소로 가겠다는 뜻이고
음수(<0)면 현재 요소보다 앞에 있는 요소로 가겠다는 뜻입니다.

아래에 예시로 현재 요소보다 뒤에 있는 요소로 갈 경우의 설계도를 그렸습니다.

      photoIndex = clickedIndex;
      //모든 bullet의 클래스를 없애고 클릭된 bullet에만 on 클래스 추가
      bulletClassReset();
      bullets[clickedIndex].classList.add("on");

먼저 선택한 bullet에 클래스가 들어가는 코드를 짰습니다.
forEach(bulletClassReset)로 모든 bullet의 클래스를 지워주고 선택한 x번째의 bullet에만 on 클래스가 들어가도록 작성합니다.

      // 클릭할 때마다 순서가 바뀌는 slides들 업뎃
      slides = document.querySelectorAll(".slides>li");
      let currentSlides = [...slides];
  1. 다른 bullet을 클릭할 때마다 이미지들의 순서가 달라지기 때문에 변수를 다시 할당했습니다.
  2. querySelectorAll은 "배열"이 아닌 "노드리스트"를 반환하기 때문에 배열로 변환했습니다.
    (참고 https://developer.mozilla.org/ko/docs/Web/API/NodeList)

💜step이 양수일 경우(현재보다 뒤에 있는 요소로 이동)

      //step이 양수: 현재 요소보다 뒤에 있는 요소로 이동
      if (step > 0) {
        // 이미지 슬라이드 step의 수 만큼 앞에서 자른다
        let sliceSlides = currentSlides.slice(undefined, step);
        slide.style.transition = duration+"ms";
        slide.style.left=step * -100+"%";
        window.setTimeout(() => {
          slide.removeAttribute("style");
          // 잘린 요소들을 맨 뒤로 집어넣기..
          slide.append(...sliceSlides);
        }, duration);
      } 

step 값 기준으로 배열의 원소들을 잘라내는 과정을 거치기 위해 slice() 메서드를 사용했습니다.

arr.slice([begin[, end]])
begin: 시작점 (undefined일 경우 0번 인덱스부터 자르고 음수일 경우 끝에서부터 자릅니다)
end : 종료점 (생략되면 배열의 끝까지 잘라냅니다.)

  1. 슬라이드 효과를 위해 step * -100%만큼 slide를 이동시켜주고
  2. 이동이 끝난 후, 잘라놓은 요소들을 배열의 맨 뒤로 이동시켜줍니다.
  3. 인라인 스타일을 제거하여 위치를 조정합니다.

💜step이 음수일 경우(현재보다 앞에 있는 요소로 이동)

원리는 위와 같습니다.

else {
        // step이 음수: 현재 요소보다 앞에 있는 요소로 이동
        sliceSlides = currentSlides.slice(step);
        // 잘린 요소들을 맨 앞으로 집어넣기
        slide.prepend(...sliceSlides);
        slide.style.left = step * 100 + "%";
        window.setTimeout(()=>{ 
            slide.style.left = 0;
            slide.style.transition = duration+"ms";
        })
      }
      //서로 같은 경우 이동할 필요가 없기 때문에 함수 즉시 종료
      if (step==0) return;
  1. slice()의 인자에 step을 넣어 뒤에서부터 step번째까지 요소들을 잘라줍니다.
  2. 자른 요소들을 slide 맨 앞으로 삽입한 후
  3. 슬라이딩하는 효과를 내기 위해 step * 100로 앞에서부터 step번째까지의 이미지까지 slide를 이동시킵니다.
  4. left: 0, transition을 설정하여 2번에서 0번으로 가는 애니메이션 효과를 만들어줍니다. (고무줄을 잡아당겼다가 놓아준다고 생각하면 됩니다)

썸네일을 클릭하면 해당하는 이미지가 띄워지는 함수

const thumbnails = document.querySelectorAll(".box__gallery > li");
let photoIndex = 0;
let slides = document.querySelectorAll(".slides > li");
const slide = document.querySelector(".slides");

thumbnails.forEach((thumbnail, index) => {
    // 클릭 이벤트 추가
    thumbnail.addEventListener("click", (e) => {
      e.preventDefault();
      const clickedIndex = index;
      let step = clickedIndex - photoIndex;
      photoIndex = clickedIndex;
      bulletClassReset();
      bullets[clickedIndex].classList.add("on");
      // 클릭할 때마다 순서가 바뀌는 slides들 업뎃
      slides = document.querySelectorAll(".slides>li");
      let currentSlides = [...slides];
      if (step > 0) {
        // 이미지 슬라이드 step의 수 만큼 앞에서 자른다
        let sliceSlides = currentSlides.slice(undefined, step);
          // 잘린 슬라이드들 맨 뒤로 집어넣기..
        slide.append(...sliceSlides);
      } else {
        sliceSlides = currentSlides.slice(step);
        // 잘린 슬라이드들 맨 앞으로 집어넣기
        slide.prepend(...sliceSlides);
      }
    });
  });

bullet을 클릭하면 해당하는 번호의 이미지로 슬라이드 되는 함수와 같은 원리입니다.
애니메이션 효과만 뺐습니다.

👻마치며

생각보다 고려해야할 점들도 많았고.. 제이쿼리의 eq()를 어떻게 구현할까 많은 고민을 했습니다.
삽질 끝에 slice() 메서드로 어떻게 돌아가게 만들었는데 이게 맞는지는 모르겠네요ㅎ_ㅎ...
슬라이드를 적용한 포트폴리오 페이지가 궁금하시다면
https://soonmac.github.io/portfolio_page/ 이쪽으로~

profile
공부중
post-custom-banner

2개의 댓글

comment-user-thumbnail
2022년 1월 13일

포폴 사이트 디자인 너무 취향이라 치이고 갑니다! 너무 멋져요 +_+bbb 계속 예쁜 거 많이 만들고 공유 해주세요!!

1개의 답글