본문 바로가기

UI 디자인 애니메이션 연구소

HTML+CSS+JS로 초간단 무한 슬라이드 만들기 [1탄]

반응형

 


이전 애니메이션

 

HTML+CSS+JS 를 이용한 써클 프로그래스 애니메이션을 만들어 봅시다

이전 애니메이션 CSS +JS 를 이용한 톡톡 튀는 폭죽(스파크) 애니메이션 만들기이전 애니메이션 | 로딩 스피너 HTML/CSS 를 이용한 로딩 스피너 만들기3탄 | 마우스 호버 시 따라다니는 언더라인 만

duklook.tistory.com

 

미리보기

이번에 만들 슬라이드의 핵심은 첫 번째 자리에 오는 슬라이드가 확대 되면서 커다란 영역을 차지하도록 해주는 겁니다. 그리고 슬라이드1 부터 4 까지 진행 후 다시 1로 돌아갈 때, 자연스럽게 다시 슬라이드 4에서 슬라이드 1로 이어지도록 해주는 것이 포인트 입니다.

 

HTML

우선 마크업은 다음과 같이 해줍니다. 각 .item 이 부여한 div 태그가 슬라이드1~5 까지 총 5개가 있고, 그러한 슬라이드를 감싸고 있는 래퍼로서 #container 가 부여된 div 태그가 있습니다. 편의상 div 태그를 사용하였지만, 시멘틱한 웹을 위한다면 각 상황에 맞게 적절한 태그를 사용하시면 됩니다.

<div id="container">
  <!-- 슬라이드1 -->
  <div class="item">
    <div class="content">
      <h2 class="name">인생이란</h2>
      <div class="description">인생은 롤러코스터와 같아. 둘다 오르막과 내리막이 있으니깐  하지만 두려움에 떨거나 즐기는 것은 너의 선택이야
      </div>
    </div>
  </div>
  <!-- 슬라이드2 -->
  <div class="item">
    <div class="content">
      <h2 class="name">책- 결국 결말은 해피엔딩</h2>
      <div class="description">내가 좋아하는 것에만 집중하기로 했다. 그 밖의 것들까지 신경 쓰기엔 인생은 너무 짧으니깐
      </div>
    </div>
  </div>
  <!-- 슬라이드3 -->
  <div class="item">
    <div class="content">
      <h2 class="name">쉽지 않아</h2>
      <div class="description">원래 원하는 것을 얻는 것은 결코 쉽지 않아. 그런데, 쉽지 않기 때문에 그 만큼 가치가 있는 거야. 생각해봐, 우리가 이 때 까지 쉽게 얻은 결과들이 결국 어떻게 되었는지. </div>
    </div>
  </div>
  <!-- 슬라이드4 -->
  <div class="item">
    <div class="content">
      <h2 class="name">될 때 까지</h2>
      <div class="description">난 포기라는 말이 정말 싫어. 왠지 그것은 스스로의 한계를 정하는 말 같거든. 처음의 도전, 두 번의 도전, 세번의 도전.. 그 모든 도전이 실패할 수 있어. 하지만, 그게 불가능을 말하는 것은 아니야. 안 되면 될 때 까지, 미친듯이 했을 때, 그럼에도 안 된다면, 잠시 놓아줄 수는 있더라도 그게 너의 한계라고 단정짓지마. 느리더라도 결국 해낼 수 있음을 스스로 의심하지마.</div>
    </div>
  </div>
  <!-- 슬라이드5 -->
  <div class="item">
    <div class="content">
      <h2 class="name">끝</h2>
      <div class="description">마지막 슬라이드</div>
    </div>
  </div>
</div>

<!-- 컨트롤 버튼 -->
<div class='control_button'>
  <button id="prev"><</button>
  <button id="next">></button>
</div>

 

CSS

폰트 및 기본 여백 제거

이 부분은 해당 구현에 사용된 폰트와 브라우저가 기본으로 제공하는 margin 과 padding 값을 0으로 초기화 시켜주는 부분 입니다.  디자인에 크게 신경 써서 만드는게 아니라서, 원하는 폰트를 찾아서 쓰시면 됩니다.

@font-face {
  font-family: "Freesentation-9Black";
  src: url("https://fastly.jsdelivr.net/gh/projectnoonnu/2404@1.0/Freesentation-9Black.woff2")
    format("woff2");
  font-weight: 900;
  font-style: normal;
}

@font-face {
  font-family: "NanumBarunGothic";
  font-style: normal;
  font-weight: 400;
  src: url("//fastly.jsdelivr.net/font-nanumlight/1.0/NanumBarunGothicWeb.eot");
  src: url("//fastly.jsdelivr.net/font-nanumlight/1.0/NanumBarunGothicWeb.eot?#iefix")
      format("embedded-opentype"),
    url("//fastly.jsdelivr.net/font-nanumlight/1.0/NanumBarunGothicWeb.woff")
      format("woff"),
    url("//fastly.jsdelivr.net/font-nanumlight/1.0/NanumBarunGothicWeb.ttf")
      format("truetype");
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

 

container 영역과 body, button 태그 설정

body {
  width: 100%;
  height: 100vh;
  overflow: hidden;
}

#container {
  width: 100%;
  height: 100%;
  background: orange;
}

button {
  border: none;
  padding: 5px;
}

 

body 태그

이 부분은 슬라이드가 정상적으로 화면의 중앙에 위치할 수 있도록 body 태그가 차지하는 영역을 뷰포트의 100%에 맞춰주기 위해 width, height 를 각각 100%, height 를 100vh 로 지정해줍니다. 또한 슬라이드가 바깥으로 나가는 경우 오버플로우로 인한 스크롤이 생기는 것을 방지하기 위해 overflow 를 hidden 으로 설정해줍니다.

#container

container 태그의 경우에도 슬라이드의 래퍼이기 때문에 body 태그에 설정된 넓이와 높이의 100% 비율로 맞추기 위해 width 와 height 를 각각 100% 으로 지정해주었습니다. background 의 경우는 현재 구현하고자 하는 색상을 주황색, 하얀색, 노란색 계열로 맞추었기 때문에 orange 로 베이스 색상을 맞춰주었습니다.

 

button

버튼은 브라우저에서 기본 제공하는 border (선)을 제거하기 위해 none 으로 설정하였고, border 바깥으로 약간의 여백을 만들어 주기 위해 5px 로 지정하여 상하좌우 모두 5px 씩 공간이 확보되도록 설정해주었습니다.

 

슬라이드에 해당하는 .item 초기 설정

이 부분은 슬라이드에 해당하는 부분을 중앙에 정렬하고 각 아이템 마다 간격을 두고 배치하기 위한 설정을 하는 부분입니다. 

.item {
  position: absolute;
  left: 50%;
  top: 50%;
  border: none;
  transition: 0.5s;
  background: gold;
  width: 160px;
  height: 200px;
  .name {
    font-family: "Freesentation-9Black";
    color: tomato;
  }
  .description {
    margin-top: 0.85em;
    font-size: 0.9em;
    line-height: 1.6;
  }
  .content {
    width: 270px;
    max-width: 270px;
    margin: 15px 0 0 0;
    visibility: hidden;
    padding: 15px;
    font-style: italic;
  }
}

.item:nth-of-type(1) {
  width: 90%;
  height: 100%;
  background: white;
  border-radius: 5px;
  left: 0;
  top: 0;
  transform: translate(0, 0);
  .content {
    position: absolute;
    opacity: 1;
    visibility: visible;
    animation: 1s 1 appear ease forwards;
  }
}

.item:nth-of-type(2) {
  transform: translate(calc(220px * 0), -50%);
}
.item:nth-of-type(3) {
  transform: translate(calc(220px * 1), -30%);
}
.item:nth-of-type(4) {
  transform: translate(calc(220px * 2), -50%);
}
.item:nth-of-type(5) {
  transform: translate(calc(220px * 3), -30%);
}

 

 

.item 을 중앙으로

해당 로직에서 주요 부분은   position: absolute;   left: 50%;   top: 50%; 이 3 가지가 끝입니다. 카드를 중앙으로 정렬하여 부모 영역에 크게 영향을 받지 않고 배치할 수 있도록 position 을 absolute 로 지정하여 현재 레이아웃의 흐름에서 벗어 나도록 해주었고, left 와 top 을 각각 50% 로 지정하여 수직-수평 정렬이 될 수 있도록 해주었습니다.

.item {
  position: absolute;
  left: 50%;
  top: 50%;
  border: none;
  transition: 0.5s;
  background: gold;
  width: 160px;
  height: 200px;
  .name {
    font-family: "Freesentation-9Black";
    color: tomato;
  }
  .description {
    margin-top: 0.85em;
    font-size: 0.9em;
    line-height: 1.6;
  }
  .content {
    width: 270px;
    max-width: 270px;
    margin: 15px 0 0 0;
    visibility: hidden;
    padding: 15px;
    font-style: italic;
  }
}

 

item 각 요소 배치하기

다만 앞서 경우는 완전 정렬이 아닙니다. 각 슬라이드 요소가 균등한 간격으로 배치될 수 있도록 nth-of-type 과 같은 가상 클래스 선택자를 활용하여 각 요소 마다 transform:translate 를 지정 해줍니다.

.item:nth-of-type(1) {
  width: 90%;
  height: 100%;
  background: white;
  border-radius: 5px;
  left: 0;
  top: 0;
  transform: translate(0, 0);
  .content {
    position: absolute;
    opacity: 1;
    visibility: visible;
    animation: 1s 1 appear ease forwards;
  }
}

.item:nth-of-type(2) {
  transform: translate(calc(220px * 0), -50%);
}
.item:nth-of-type(3) {
  transform: translate(calc(220px * 1), -30%);
}
.item:nth-of-type(4) {
  transform: translate(calc(220px * 2), -50%);
}
.item:nth-of-type(5) {
  transform: translate(calc(220px * 3), -30%);
}

 

여기서 주요한 부분은 calc를 사용한 부분입니다. 현재 지정한 item 의 width가 160px 이므로 각 카드 마다 60px 씩 간격을 두고 배치하기 위해서  translate(calc(220px * i) 를 사용하였습니다. 이렇게 되면, i = 0, 1, 2, 3 일 때 각 각 0, 220, 440, 660 (px) 단위로 x 축을 이동하게 되고, 각 카드 사이의 간격은 60px 로 균등하게 배치되게 됩니다. 또한 각 아이템 을 Y 축을 기준으로  지그재그로 표현하기 위해 -50%, -30%, -50%, -30% 로 설정하였습니다.

.item:nth-of-type(2) {
  transform: translate(calc(220px * 0), -50%);
}
.item:nth-of-type(3) {
  transform: translate(calc(220px * 1), -30%);
}
.item:nth-of-type(4) {
  transform: translate(calc(220px * 2), -50%);
}
.item:nth-of-type(5) {
  transform: translate(calc(220px * 3), -30%);
}

 

 

 

슬라이드를 통제하는 버튼 꾸미기

이 부분은 슬라이드의 진행 방향을 통제하는 버튼을 꾸미는 부분입니다. 앞서 이미지의 하단 버튼 입니다. 별도로 설명할 것이 없으므로 다음으로 넘아 갑니다. 

.control_button {
  transform: translate(-50%);
  position: absolute;
  bottom: 1em;
  left: 50%;

  button {
    background: orange;
    color: white;
  }
  button:hover {
    cursor: pointer;
    box-shadow: 0 0 2px 2px gold;
  }
}
#prev {
  border-radius: 5px;
}
#next {
  border-radius: 5px;
}

 

슬라이드 등장 시 텍스트 애니메이션 설정

이 부분은 슬라이드가 등장하는 순간 텍스트가 투명하고 흐릿한 상태에서 서서히 등장하도록 애니메이션을 추가하는 부분입니다. transform:translate(-200px, _ ) 를 지정하여  좌측 방향 지점에서 텍스트가 등장하도록 설정해주었습니다.

@keyframes appear {
  0% {
    transform: translate(-200px, 0);
    opacity: 0;
    filter: blur(5px);
  }
}

 

JS

const container = document.querySelector("#container");

const prev = document.getElementById("prev");
const next = document.getElementById("next");

// 슬라이드의 첫 번째 요소를 제일 끝으로 이동
next.addEventListener("click", () => {
  const items = document.querySelectorAll(".item");
  container.appendChild(items[0]); 
});

// 슬라이드의 마지막 요소를 첫 번째 자리로 이동
prev.addEventListener("click", () => {
  const items = document.querySelectorAll(".item");
  container.prepend(items[items.length - 1]); 
});

// 자동으로 슬라이드 변경을 2초 마다 진행하도록
setInterval(() => {
  const items = document.querySelectorAll(".item");
  container.appendChild(items[0]);
}, 2000);

 

자바스크립트 코드 자체는 크게 설명할 것은 없습니다. 다만, 핵심이 되는 로직이 있습니다. 바로  const items = document.querySelectorAll(".item"); 와 같이 요소를 선택하여 가져오는 메소드를 전역에 선언하는 것이 아니라 각 이벤트 리스너의 호출함수 내에서 매번 생성되도록 해야 한다는 점 입니다.

 

이는 NodeList 로 반환되는 요소들의 변화가 실제 DOM에 실시간 나타나도록 하기 위해서는 변경된 내역이 이벤트 호출과 동시에 갱신되어야 하기 때문입니다.

 

실제로 해당 로직을 전역에 정의하고 이벤트 호출 로직 내부에 삽입하여 사용하는 경우 container 요소의 자식 요소로 추가(appendChild, prepend) 하더라도 그 이후의 변경 내역이 반영되지 않아 정상적으로 애니메이션이 동작하지 않습니다.

const items = document.querySelectorAll(".item");

next.addEventListener("click", () => {
  container.appendChild(items[0]); // 한 번은 정상적으로 동작하지만, 두 번째 호출 시에는 변화가 없음
});

 

구현 결과

이대로 구현하면 다음과 같은 애니메이션을 확인할 수 있습니다. 다만 현재 애니메이션의 경우 Next 버튼을 클릭하여 새로운 슬라이드가 사이즈 업 하여 표시될 때, 기존에 있던 슬라이드가 사라지며 노란색 배경이 그대로 보이는 문제가 발생하고 있는데요. 이 부분은 다음 2탄에서 개선된 버전을 설명하면서 알아보겠습니다. 

 

See the Pen 슬라이드1 by youngwan2 (@youngwan2) on CodePen.

 

 

다음 애니메이션

※ 2탄은 작성 예정인데, 사정상 순위가 뒤로 밀리고, 그 다음 버전인 3탄은 작성되었기에 이어지는 통로로 달아둡니다.

 

HTML/CSS/JS 로 초간단 무한 슬라이드 갤러리 만들기 [2탄] (1탄 이후 작성예정)

이전 애니메이션 | 1탄(작성중) HTML+CSS+JS로 초간단 무한 슬라이드 만들기 [1탄] (작성예정) duklook.tistory.com 미리보기HTMLCSSJS

duklook.tistory.com

 

반응형