본문 바로가기

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

바닐라 자바스크립트를 이용한 스크롤 트리거 애니메이션의 원리와 구현

반응형

 

이전 애니메이션 | 드래그 가능한 슬라이드 구현하기

 

캐러셀 라이브러리에서 보던 드래그 가능한 슬라이드, 원리 부터 구현까지!!

이전 애니메이션 |  액자 모양 로고 애니메이션 HTML과 CSS 만으로 이런 것도 가능해?? 액자 모양 로고 애니메이션이전 애니메이션 | 스크롤링 헤드라인, 마키 어디가 끝이지? 끝없이 흘러가는 마

duklook.tistory.com

 

 

스크롤 트리거

오늘은 바닐라 자바스크립트를 이용하여 스크롤 트리거를 구현해보는 시간을 가져볼까 합니다. 보통 스크롤 트리거는 스크롤 애니메이션, 스크롤 트리거 애니메이션 등으로 불리는데요, 스크롤의 특정 위치 마다 서로 다른 동작이나 애니메이션, 트랜잭션을 실행하는 기법을 지칭합니다.

 

시작 전에 아래 영상을 통해서 스크롤 트리거가 어떻게 동작하는지 살펴보시길 권장합니다.

 

동작원리

스크롤 애니메이션의 동작 원리는 영상을 보셨으면 아시겠지만, 특정 위치에 스크롤이 도달했을 때 실행하길 원하는 동작을 정의해 주기만 하면 됩니다

 

예를 들어, 뷰포트 최상단의 0px 지점에서 스크롤의 윗면 까지 거리가 120px 될 때 까지는 아무런 변화가 없다가, 240px 가 되는 지점에 원형의 요소의 크기를 확대하고, 중앙에 오도록 동작을 정의할 수 있습니다.

 

이런 특정 위치에 스크롤이 도달하고 있음을 알 수 있는 방법은 scroll 이벤트를 사용하는 것과 intersecitonObserver 를 사용하는 방법 두 가지가 있습니다. 두 방법 다 장단점이 있지만, 이에 대한 설명 보다는 현재 애니메이션 구현에 사용된 scroll 이벤트에 대해서만 다음에서 간략하게 언급하고 바로 구현 단계로 넘어 가겠습니다.

인터섹션 옵저버: https://developer.mozilla.org/ko/docs/Web/API/IntersectionObserver
스크롤 이벤트: https://developer.mozilla.org/ko/docs/Web/API/Document/scroll_event

 

스크롤 이벤트(scroll)와 고려사항

스크롤 이벤트는 웹 페이지에서 사용자가 페이지를 위아래로 스크롤할 때 발생하는 이벤트를 의미합니다. 스크롤에 따른 특정 동작에 대한 처리는 자바스크립트에서 다양한 방식으로 처리할 수 있는데, 특히 스크롤 이벤트의 경우에는 웹 페이지의 상호작용성을 높이는 데 중요한 역할을 합니다. 스크롤 이벤트를 활용하면 스크롤 위치에 따라 요소를 변경하거나 애니메이션을 적용하는 등의 동작을 구현할 수 있습니다.

 

다만, 스크롤 이벤트의 경우에는 잦은 이벤트 호출이 불가피하기 때문에, 성능최적화가 중요시 되는 환경에서는 디바운싱이나 쓰로틀링 등의 기법을 적용하여 이벤트 호출 빈도를 조절하는 것이 권장됩니다.

 

그리고 너무 세부적인 조정을 필요로 하지 않는 경우에는 앞서 언급했던 IntersectionObserver 를 이용하여 관찰 대상에게만 애니메이션을 적용할 수도 있습니다.

 


 

 

참고로, HTML 과 CSS 는 별도의 상세 설명없이 넘어갈 것입니다. 구조를 찬찬히 보고 이해가 안 된다면, 아직 까지는 HTML과 CSS 를 별도로 공부해야 하는 시기일 수 있습니다. 그럼 시작하겠습니다.

초기설정 (HTML과 CSS)

HTML

<section class="section">
  <ul>
    <li>
      <span class="id">01</span>
      <div class="title">서시</div>
      <span class="summary">윤동주</span>
    </li>
    <li>
      <span class="id">02</span>
      <div class="title">김영랑</div>
      <span class="summary">모란이 피기까지는</span>
    </li>
    <li>
      <span class="id">03</span>
      <div class="title">산유화</div>
      <span class="summary">김소월</span>

    </li>
    <li>
      <span class="id">04</span>
      <div class="title">향수</div>
      <span class="summary">정지용</span>
    </li>
    <li>
      <span class="id">05</span>
      <div class="title">님의 침묵</div>
      <span class="summary">한용운</span>
    </li>
  </ul>
  <div class="contents">
    <div class="content"></div>
  </div>
</section>

 

설정하면 대략 아래와 같은 모습이 될 겁니다.

CSS

주요 부분은 주석으로 별도 설명을 첨부하였습니다. 참고로 앞서 영상에서 우측의 콘텐츠 박스가 따라서 움직이는 것을 볼 수 있는데, 이는 position의 sticky 효과를 적용한 것입니다. sticky 를 적용한 요소에 top 속성을 0px 와 같이 초기 위치를 지정해두면 해당 요소는 부모 컨테이너 요소가 차지하는 공간 만큼 fixed 포지션으로 움직이다가 끝 지점에 도달하면 해당 지점에서 멈추는 효과를 부여합니다. 이 때 고려할 점은 부모 요소에 overflow:hidden 이 적용되어 있으면 안 됩니다. 

/* 초기 CSS 설정을 초기화 합니다. */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

li {
  list-style: none;
}

body {
  width: 100%;
  background: #333;
  min-height: 250vh; /* 스크롤 이벤트를 보다 시각적으로 확인하기 위해 높이를 조정해줍니다.*/
}


/* section:  스크롤 이벤트가 적용되는 요소를 감싸는 컨테이너 */
.section {
  position: relative;
  height: 150vh;
  display: flex;
  justify-content: space-between;
  max-width: 1200px;
  margin: 0 auto; /* left와 right 여백을 자동으로 조정합니다. 중앙 정렬이 되는 효과가 있습니다. */

  /* ul:  스크롤 이벤트의 대상이 되는 좌측에 위치한 아이템을 감싼 컨테이너*/
  ul {
    margin: 0 0 1rem 1rem;
    min-width: 200px;
    max-width: 340px;
    width: 100%;

    li:nth-of-type(1n) {
      margin-top: 1.2rem; /* 윗 공간에 여백을 주기 위해 설정합니다.*/
    }
    
   /* li.active : 스크롤이 특정 위치에 도달하면 활성화하는 스타일 입니다.*/
    li.active {
      transform: scale(1.05);
      box-shadow: 0 0 0 5px white;
      opacity: 1;
      .id {
        box-shadow: 0 0 0 10px white;
      }
    }
     
    li {
      box-shadow: 0 0 0 2px white;
      transition: 0.5s opacity, 0.5s transform;
      transform: scale(0.85);
      opacity: 0.3;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      position: relative;
      background: gold;
      height: 170px;
      text-align: center;
      margin: 1.8rem 0;
      border-radius: 20px;
      padding: 13px;
      color: white;

      .id {
        line-height: 2;
        font-size: 1.35rem;
        position: absolute;
        left: 0;
        top: 0;
        width: 50px;
        height: 50px;
        border-radius: 20px 0% 50% 0%;
        box-shadow: 0 0 0px 5px white, inset -2px -2px 10px 0 rgba(0, 0, 0, 0.2);
        background: black;
      }

      .title {
        font-size: 1.435em;
        padding: 3px;
        font-weight: bold;
      }
      span {
        font-size: 0.95em;
      }
    }
  }
  /*  우측 콘텐츠  */
  .contents {
    margin: 1rem 2rem;
    max-height: 700px;
    max-width: 50vw;
    min-width: 20px;
    width: 100%;
    height: 300px;
    padding: 10px;
    border-radius: 20px;
    position: sticky;
    top: 20px;
    box-shadow: 0 0 0 5px white;
    .content {
      color: white;
      transition: 1s;
      padding: 40px;
      font-size: 1.35em;
      line-height: 1.8;
      font-weight: bold;
    }
  }
  /* 약간의 그라디언트를 기존 콘텐츠 요소의 상위 레이어에 적용하기 위해 사용됩니다.*/
  .contents::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    background: white;
    width: 100%;
    height: 100%;
    border-radius: 20px;
    background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 70%, white);
    opacity: 0.4;
  }
  /* ◀ 이 모양을 만들기 위해서 사용합니다. */
  .contents::after {
    content: "";
    position: absolute;
    left: -2.5rem;
    top: 1rem;
    border-left: 20px solid transparent;
    border-top: 15px solid transparent;
    border-right: 20px solid white;
    border-bottom: 15px solid transparent;
  }
}

 

CSS 까지 적용했다면, 아래와 같은 모양과 색감을 가진 레이아웃이 만들어졌을 겁니다.

 


 

여기까지 잘 마무리되었다면 바로 자바스크립트로 넘어갑시다.

구현(자바스크립트)

바인딩할 색상과 콘텐츠 내용

우선 사용할 색상과 콘텐츠 내용을 준비해 줍니다. 

const navColors = ["#ff3900", "#63bb72", "#2bb4dd", "#2bb4dd", "#961bed"];
const lightNavColors = ["#ff9c66", "#a2d8a8", "#71d6f3", "#71d6f3", "#c68af0"];

const contentGroup = [
  "어느 먼 별에서 나를 부르는 이 있어 나는 이 길을 간다.", // 윤동주, <서시>
  "낙엽이 지거든 그리운 마음을 알리라.", // 김영랑, <모란이 피기까지는>
  "산 넘어 남촌에는 누가 살길래 해마다 봄바람이 남으로 오네.", // 김소월, <산유화>
  "흐르는 강물처럼 내 고운 님, 내 사랑.", // 정지용, <향수>
  "오늘도 내일도 우리는 그리워하며 살리라." // 한용운, <님의 침묵>
];

 

DOM 요소 가져오기

이벤트의 효과를 적용하기 위해 사용할 dom 요소를 가져와 줍니다.

const container = document.querySelector("body"); 
const content = document.querySelector(".content");
const contentContainer = document.querySelector(".contents");
const section = document.querySelector(".section");
const items = document.querySelectorAll("li"); // 좌측 항목들
const ids = document.querySelectorAll(".id"); // 좌측 항목을 식별하는 아이디

 

색상 설정

앞서 가져온 DOM 요소인 item과 id 의 style 에 접근하여 배경색을 설정해줍니다.

function setting() {
  items.forEach((item, i) => (item.style.background = navColors[i]));
  ids.forEach((id, i) => (id.style.background = lightNavColors[i]));
}
setting();

 

그럼 아래와 같이 각 요소에 색상이 매치되는 것을 볼 수 있습니다.

 

스크롤 이벤트 등록

이번에는 스크롤 이벤트를 등록 해보겠습니다. 여기서 handleScroll 이라는 함수를 별도로 선언할 것이므로 이벤트 호출 함수의 자리에 입력해줍니다.

window.addEventListener("scroll", handleScroll);

 

스크롤 이벤트 함수 구현

이제 함수를 구현해줍니다. 

function handleScroll(e) {
  items.forEach((item, i) => {
    const start = item.offsetTop - item.offsetHeight / 2; //시작지점
    const end = item.offsetHeight + start;
    const scrollY = window.scrollY;

    if (start < scrollY && end > scrollY) {
      content.textContent = contentGroup[i];
      contentContainer.style.background = navColors[i];
      item.classList.add("active");
    } else {
      item.classList.remove("active");
    }
  });
}

 

사실상 해당 함수가 제일 중요한 역할을 하므로 설명을 하면서 넘어가겠습니다.

 

items 배열 순회

일단 스크롤 이벤트를 호출하여 동작을 변경시키고자하는 대상은 li 요소 입니다. 따라서 해당 li 요소를 NodeList 형대로 가져온 items 배열에 forEach 로 접근하여 각 li 에 접근합니다.

items.forEach((item, i) => { ... }

 

위치 특정을 위한 주요 변수 초기화

여기서 추가적으로 선언해야 하는 변수는 3가지가 있습니다. 

    const start = item.offsetTop - item.offsetHeight / 2; //시작지점
    const end = item.offsetHeight + start;
    const scrollY = window.scrollY;

 

start 변수는 각 li 요소의 윗면 까지의 거리에서 자신의 높이의 절반에 해당하는 값을 더해준 결과를 담아준 변수입니다. 즉, 애니메이션의 시작 지점입니다.

 

end 변수는 start 변수에 담긴 값에 li의 높이를 더해준 값을 담고 있습니다. 즉, 애니메이션의 종료 지점을 나타냅니다.

대략 김영량이 입력된 item 의 중간에서 약간 아래 지점에 도달하면 애니메이션이 종료됨을 짐작할 수 있습니다.

 

 

scrollY 변수는 브라우저 뷰포트 높이의 0vh 지점 부터 시작하여 스크롤의 윗면 까지의 거리를 px 단위로 저장하는 변수입니다. 이는 동적으로 매번 이벤트가 호출될 때 마다 변경된 값이 scrollY 에 담기게 됩니다. 즉, 현재 스크롤이 어느 위치에 도달하였는지를 추적할 수 있도록 해줍니다.

 

조건문을 통한 애니메이션 실행을 위한 특정한 동작 설정

마지막으로 애니메이션의 시작지점과 끝 지점, scrollY 변수를 활용하여 특정 위치에 도달하는 경우 특정 동작을 트리거 하는 로직을 다음과 같이 작성해줍니다.

    if (start < scrollY && end > scrollY) {
      content.textContent = contentGroup[i];
      contentContainer.style.background = navColors[i];
      item.classList.add("active");
    } else {
      item.classList.remove("active");
    }

 

부연 설명을 하자면, start < scrollY && end > scrollY 와 같이 조건을 입력할 시 start 지점 보다는 스크롤 위치가 더 크고, end 지점보다는 작을 때 조건문을 실행하게 됩니다.

 

여기서는 해당 조건을 충족하는 경우 각 item이 되는 li 요소 마다 콘텐츠를 추가해주고 있습니다. 즉,  contents 요소의 content 에 동적으로 텍스트를 삽입하기 위해  contentGroup 의 각 요소와  index 간에 매치시켜 줍니다. 추가적으로 제대로 각 요소에 해당하는 콘텐츠가 반영되고 있는지 확인하기 위해 contentGroup 과 동일한 방식으로 좌측 요소와 동일한 색상을 동적으로 추가해 주고 있습니다.

const contentGroup = [
  "어느 먼 별에서 나를 부르는 이 있어 나는 이 길을 간다.", // 윤동주, <서시>
  "낙엽이 지거든 그리운 마음을 알리라.", // 김영랑, <모란이 피기까지는>
  "산 넘어 남촌에는 누가 살길래 해마다 봄바람이 남으로 오네.", // 김소월, <산유화>
  "흐르는 강물처럼 내 고운 님, 내 사랑.", // 정지용, <향수>
  "오늘도 내일도 우리는 그리워하며 살리라." // 한용운, <님의 침묵>
];

 

 

마지막으로, 각 item에 해당하는 li 요소에  classList.add 메소드를 사용하여 active 클래스 속성 값을 추가 해주고, 반대로 item 이 start 와 end 지점을 벗어나면, active 클래스 속성 값을 제거해주는 동작을 수행해줍니다.

function handleScroll(e) {
  items.forEach((item, i) => {
   //...
    if (start < scrollY && end > scrollY) {
         // ...
      item.classList.add("active");
    } else {
      item.classList.remove("active");
    }
  });
}

 

로드 후 초기 포커스 설정

앞서 이벤트까지 구현하고 나서 새로고침을 하면, 처음에는 아무런 요소도 포커스되어 효과가 적용되어 있지 않은 상태일 수 있습니다. 이는 초기에는 스크롤 이벤트가 호출되지 않기 때문인데, 이를 위해 하나의 이벤트를 더 추가 해줍니다.

 

바로 load 이벤트 인데 load 이벤트를 호출하면 HTML, CSS 가 모두 로드되는 것을 확인하고 등록된 콜백함수를 호출하게 됩니다.

window.addEventListener("load", handleScroll);

 

구현 결과

모든 구현이 끝났습니다. 코드 자체도 복잡하지 않고, 간단한 원리만 이해하면 구현할 수 있는 재밌는 애니메이션 효과 였던 것 같습니다. 만일 아래 예제가 정상적으로 동작하지 않는다면, codepen 에 방문하셔서 체험해보시면 좋을 듯 합니다.

 

See the Pen 스크롤 트리거 by youngwan2 (@youngwan2) on CodePen.

 

 

나가는 말

오늘은 스크롤 이벤트를 활용하여 스크롤 트리거 애니메이션을 구현해보는 시간을 가져보았습니다. 동작의 원리 자체는 그리 어렵지 않으니, 한 번 참고하셔서 재밌는 효과를 만들어보는 시간이 되셨으면 좋겠습니다.

 

그럼 이만 글을 줄여보겠습니다. 감사합니다.

 

다음 애니메이션 | 스트리밍 써클

 

JS와 GSAP trigger API 로 만드는 스트리밍 써클 애니메이션!

이전 애니메이션 | 스크롤 트리거 애니메이션 바닐라 자바스크립트를 이용한 스크롤 트리거 애니메이션의 원리와 구현이전 애니메이션 | 드래그 가능한 슬라이드 구현하기 캐러셀 라이브러리

duklook.tistory.com

 

반응형