메인 페이지 배너를 만들기 위해 앞, 뒤 화살표 버튼을 누르면 이미지가 변경되는 캐러셀(Carousel) 컴포넌트를 만들었다.

<transition> 써보려고 했는데 정확히 이해하지 못해서 그냥 없이 했다.😇

How to make Carousel Slider with Vue 3.0

*프리픽 이미지를 사용했다.

영역의 너비를 고정하고 안에 들어갈 이미지들의 위치를 조정하는 방식으로 만든다. 밑에는 '1/3'과 같이 표시되는 페이지네이션을 넣었다.

필요한 객체는 다음과 같다.

  • state: 이미지들과 슬라이드에 현재 보여지는 이미지 정보. 업데이트를 트리거할 수 있도록 reactive로 생성한다.
    • items: 이미지 이름이 들어있는 배열
    • currentIndex: items 중 현재 보여지는 이미지의 인덱스값
  • prevSlide: 클릭하면 currentIndex--를 하여 앞 이미지를 보여준다.
  • nextSlide: 클릭하면 currentIndex++를 하여 뒷 이미지를 보여준다.
  • slideTranslateX: currentIndex에 따라 각 이미지의 위치를 변경해준다.

하면서 막혔던 부분이 몇 가지 있었는데 console.log로 출력하면서 고쳤다.


이미지 위치를 "transform: translateX(값%)"으로 조정했는데, 값은 해당 이미지의 (index - state.currentIndex) * 100으로 계산하면 된다.

state.currentIndex: 0
index translateX(%)
0 0
1 100
2 200
state.currentIndex: 1
0 -100
1 0
2 100
state.currentIndex: 2
0 -200
1 -100
2 0
=> (state.currentIndex - index)*100

currentIndex의 값을 변경할 때 인덱스 값으로 유지하기 위해 (state.currentIndex-1)%state.items.length로 계산했는데, prevSlide의 경우 음수를 나누면 값이 제대로 안 나오기 때문에 state.items.length를 한 번 더해서 양수로 유지해야 한다.


테스트를 위해 같은 이미지를 색만 바꿔서 "src/assets/img/"에 저장했다. 이미지의 너비는 넣고 싶은 영역에 딱 맞도록 만들었다. 이미지가 영역보다 작으면 여백이 남아서 이미지에 맞춘 배경색을 따로 넣어야 하기 때문이다. 이미지 이름은 banner01, banner02, banner03이다. 혹시 이름을 다르게 쓸 수도 있으니까 저렇게 했는데 아예 01, 02, 03으로 하는게 나으려나...

이미지 경로는 require로 입력해야 한다. require을 사용하지 않으면 src에 써있는 게 그대로 나온다. vue에서 경로 맨 앞에 '@'를 쓰면 src라는 뜻이다.


폴더에 있는 이미지를 모두 가져오는 다른 방법이 있다고 하는데 막상 해보니까 잘 안 되어서 일단 저런 방식으로 진행했다.


결과적으로 다음과 같이 작성했다.

*chevron_left와 chevron_right는 material symbols 아이콘을 사용했다.

<template>
  <div class="slide-section">
    <div class="slides">
      <div v-for="(item, index) in state.items" :key="index" class="slide-item" :style="{ transform: `translateX(${slideTranslateX(index)}%)`,}">
        <div>{{item}}</div>
      </div>
    </div>
    <div class="slide-button-container">
      <button  @click="prevSlide" class="slide-nav-button prev-button">
        <span class="material-symbols-rounded">chevron_left</span>
      </button>
      <button  @click="nextSlide" class="slide-nav-button next-button">
        <span class="material-symbols-rounded">chevron_right</span>
      </button>
    </div>
    <div class="slide-pagenation-container">
      <button btntype="slideNav" class="slide-pagenation">
        <span>{{ state.currentIndex + 1 }}</span>/<span>{{ state.items.length }}</span>
      </button>
    </div>
  </div>
</template>

<script setup>
import { reactive } from 'vue';

const state = reactive({
  currentIndex: 0,
  items: ['banner01', 'banner02', 'banner03'],
});

const prevSlide = () => {
  state.currentIndex =
    (state.currentIndex + state.items.length - 1) % state.items.length;
};
const nextSlide = () => {
  state.currentIndex = (state.currentIndex + 1) % state.items.length;
};
const slideTranslateX = (index) => {
  return (index - state.currentIndex) * 100;
};
</script>

버튼은 양 끝에 고정되게 했고, 페이지네이션은 오른쪽 아래에 고정되게 했다.

버튼 위치가 이미지의 중요한 부분에 맞춰서 최대 1200px에 맞춰지도록 했다. 애초에 이미지를 만들 때 꽉 채우지 않고 양 옆에 여백을 두었다.

Vue 3.0 Carousel image slider example

이렇게 보니까 버튼 배경색이 너무 투명한가 싶다.
너비가 많이 줄었을 때 이미지가 지나치게 잘리는데 이건 나중에 생각해봐야 할 것 같다.

<style scoped>
.slide-section {
  height: 395px;
  width: 100%;
  position: relative;
  overflow: hidden;
  display: flex;
  justify-content: center;
  align-items: center;
}
.slides {
  margin-inline: auto;
  display: flex;
}
.slide-button-container,
.slide-pagenation-container {
  position: absolute;
  width: calc(100% - 40px);
  max-width: 1200px;
  display: flex;
  justify-content: space-between;
  margin-inline: 20px;
}
/*이미지 스타일*/
.slide-item {
  transition: transform 0.3s ease-in-out;
  box-sizing: border-box;
  display: block;
  overflow: hidden;
  position: absolute;
  inset: 0px;
}
.slides img {
  position: absolute;
  inset: 0px;
  box-sizing: border-box;
  margin: auto;
  display: block;
  width: 0px;
  height: 0px;
  min-width: 100%;
  max-width: 100%;
  min-height: 100%;
  max-height: 100%;
  object-fit: cover;
}
.slide-pagenation {
  position: absolute;
  font-size: 12px;
  font-weight: 500;
  padding: 4px 10px;
  border-radius: 12px;
  width: 36px;
  gap: 4px;
  bottom: 28px;
  right: 0;
  margin-right: 72px;
  cursor: inherit;
}
/*페이지네이션 스타일*/
.slide-pagenation-container {
  bottom: 0;
}
.slide-button-container .slide-nav-button {
  border-radius: 50%;
}
.slide-pagenation span {
  width: 16px;
}

/*원래 다른 버튼 Vue 파일에 있던 CSS 스타일*/
button {
  padding: 10px;
  border-radius: 4px;
  font-weight: 700;
  font-size: 18px;
  cursor: pointer;
  display: flex;
  justify-content: center;
  background-color: rgba(25, 28, 26, 0.2);
  color: white;
  border: none;
  }
</style>

CSS 수정하다가 잘 안 되는 부분은 마켓컬리 메인 페이지 배너를 참고했다.

튜토리얼 따라할 때는 잘 할 수 있을까 싶었는데 이제 v-for 쓰는 것도 익숙해진 것 같다.