SOITZ

VanilaJS 로 Fade In / Out 구현하기

Published on

웹 페이지에 이미지를 동적으로 표시하는 것은 사용자의 관심을 끌고, 웹 사이트의 시각적 매력을 향상시킬 수 있는 훌륭한 방법입니다. 이미지가 서서히 나타나고 (Fade In) 사라지는 (Fade Out) 효과는 사용자 경험에 부드러움을 더해 줍니다. 이 글에서는 VanillaJS로 이미지에 Fade In 및 Fade Out 효과를 적용하는 방법을 단계별로 설명합니다.

fade-in-out

시작하기

HTML

먼저 HTML 구조를 만듭니다. 이미지를 표시할 img 태그와 효과를 트리거할 버튼 두 개가 필요합니다.

<div>
  <button id="btn-fade-in">Fade In</button>
  <button id="btn-fade-out">Fade Out</button>
</div>
<img
  src="https://images.unsplash.com/photo-1707343843598-39755549ac9a?q=80&w=600&auto=format&fit=crop"
  width="600"
  id="image"
  style="display: none"
/>

자바스크립트로 동작 추가하기

fadeIn 함수 내부에서 requestAnimationFrame으로 지정된 duration 동안 애니메이션을 실행합니다. 애니메이션에 대한 정보는 element의 fadeState에 추가하며 이 정보는 추후에 애니메이션을 취소하기 위해서 필요합니다.

function fadeIn(element, duration = 300) {
  // 기존에 fade 애니메이션이 있으면 취소
  cancelFade(element);

  // element css 초기화
  element.style.opacity = 0;
  element.style.display = 'block';

  const state = {
    startTime: performance.now(), // 애니메이션 시작 시간
    animationFrameId: null, // requestAnimationFrame의 id
  };

  function animate(currentTime) {
    const elapsed = currentTime - state.startTime;
    element.style.opacity = Math.min(1, elapsed / duration); // 0 - 1
    if (elapsed < duration) {
      // duration 보다 이전 시간이면 애니메이션 실행
      state.animationFrameId = requestAnimationFrame(animate);
    } else {
      console.log('finished fade in');
    }
  }
  state.animationFrameId = requestAnimationFrame(animate);
  element.fadeState = state; // 애니메이션 취소를 위한 애니메이션 정보
}

function fadeOut(element, duration = 300) {
  // 기존에 fade 애니메이션이 있으면 취소
  cancelFade(element);

  const state = {
    startTime: performance.now(), // 애니메이션 시작 시간
    animationFrameId: null, // requestAnimationFrame의 id
  };

  function animate(currentTime) {
    const elapsed = currentTime - state.startTime;
    element.style.opacity = Math.max(0, 1 - elapsed / duration); // 1 - 0

    if (elapsed < duration) {
      state.animationFrameId = requestAnimationFrame(animate);
    } else {
      element.style.display = 'none';
      console.log('finished fade out');
    }
  }

  state.animationFrameId = requestAnimationFrame(animate);
  element.fadeState = state; // 애니메이션 취소를 위한 애니메이션 정보
}

기존에 실행중인 fade 애니메이션이 있다면 취소하는 함수입니다. element의 fadeState를 찾아서 null이 아니면 fade 애니메이션을 취소합니다.

// fade in/out 애니메이션을 취소
function cancelFade(element) {
  const state = element.fadeState;
  if (state && state.animationFrameId) {
    cancelAnimationFrame(state.animationFrameId);
    element.fadeState = null;
  }
}

Fade In / Fade Out 버튼에 EventListener를 추가합니다. #btn-fade-in 버튼을 클릭하면 이미지의 opacity가 0에서 1로 0.3초간 변경됩니다. 반면 #btn-fade-out 버튼을 클릭하면 이미지의 opacity가 1에서 0으로 0.3초간 변경됩니다.

// Fade In 버튼 클릭
document.querySelector('#btn-fade-in').addEventListener('click', function () {
  fadeIn(document.querySelector('#image'), 300);
});

// Fade Out 버튼 클릭
document.querySelector('#btn-fade-out').addEventListener('click', function () {
  fadeOut(document.querySelector('#image'), 300);
});

전체 코드

아래는 전체 코드입니다.

<!doctype html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="initial-scale=1.0, minimum-scale=1.0, width=device-width, viewport-fit=cover"
    />
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      html,
      body {
        height: 100%;
      }
    </style>
    <title>VanilaJS Fade In / Fade Out</title>
  </head>

  <body>
    <div>
      <button id="btn-fade-in">Fade In</button>
      <button id="btn-fade-out">Fade Out</button>
    </div>
    <img
      src="https://images.unsplash.com/photo-1707343843598-39755549ac9a?q=80&w=600&auto=format&fit=crop"
      width="600"
      id="image"
      style="display: none"
    />

    <script>
      function fadeIn(element, duration = 300) {
        // 기존에 fade 애니메이션이 있으면 취소
        cancelFade(element);

        // element css 초기화
        element.style.opacity = 0;
        element.style.display = 'block';

        const state = {
          startTime: performance.now(), // 애니메이션 시작 시간
          animationFrameId: null, // requestAnimationFrame의 id
        };

        function animate(currentTime) {
          const elapsed = currentTime - state.startTime;
          element.style.opacity = Math.min(1, elapsed / duration);
          if (elapsed < duration) {
            state.animationFrameId = requestAnimationFrame(animate);
          } else {
            console.log('finished fade in');
          }
        }
        state.animationFrameId = requestAnimationFrame(animate);
        element.fadeState = state; // 애니메이션 취소를 위한 애니메이션 정보
      }

      function fadeOut(element, duration = 300) {
        // 기존에 fade 애니메이션이 있으면 취소
        cancelFade(element);

        const state = {
          startTime: performance.now(), // 애니메이션 시작 시간
          animationFrameId: null, // requestAnimationFrame의 id
        };

        function animate(currentTime) {
          const elapsed = currentTime - state.startTime;
          element.style.opacity = Math.max(0, 1 - elapsed / duration);

          if (elapsed < duration) {
            state.animationFrameId = requestAnimationFrame(animate);
          } else {
            element.style.display = 'none';
            console.log('finished fade out');
          }
        }

        state.animationFrameId = requestAnimationFrame(animate);
        element.fadeState = state; // 애니메이션 취소를 위한 애니메이션 정보
      }

      // fade in/out 애니메이션을 취소
      function cancelFade(element) {
        const state = element.fadeState;
        if (state && state.animationFrameId) {
          cancelAnimationFrame(state.animationFrameId);
          element.fadeState = null;
        }
      }

      // Fade In 버튼 클릭
      document
        .querySelector('#btn-fade-in')
        .addEventListener('click', function () {
          fadeIn(document.querySelector('#image'), 300);
        });

      // Fade Out 버튼 클릭
      document
        .querySelector('#btn-fade-out')
        .addEventListener('click', function () {
          fadeOut(document.querySelector('#image'), 300);
        });
    </script>
  </body>
</html>