프론트 개발 블로그

[Interactive Web] 스크롤 UX 이해하기 본문

Interactive Web

[Interactive Web] 스크롤 UX 이해하기

maybe.b50 2021. 8. 20. 15:52

[번역] Apple 제품 페이지 스크롤 애니메이션

https://css-tricks.com/lets-make-one-of-those-fancy-scrolling-animations-used-on-apple-product-pages/

 


기본 개념

빠르게 연속되는 이미지 시퀀스와 같은 애니메이션을 만드는 것. (플립북같은)

복잡한 WebGL 이나 고급 Javascript 라이브러리가 필요하지 않습니다.

각 프레임을 사용자의 스크롤 위치와 동기화하여 사용자가 페이지를 아래로(또는 위로) 스크롤 할 때 애니메이션을 재생할 수 있습니다.

https://res.cloudinary.com/css-tricks/video/upload/v1588889054/flipbook_tisnxf.mp4

 

마크업과 스타일로 시작하기

HTML과 CSS에 ID를 부여하여 <Canvas>요소 내에 자바스크립트로 제어하기 때문에 매우 쉽습니다.

CSS에서 <body> 문서 높이를 100vh로 설정하고 이 작업을 수행하는 데 필요한 스크롤 길이를 제공하기 위해 그보다 5배 더 크게 만듭니다. 또한 문서의 배경색을 이미지의 배경색과 일치 시킵니다.

마지막으로 할 일은 <canvas>의 위치를 중앙에 지정하는 것 입니다. 그리고 뷰포트의 크기를 초과하지 않도록 최대 너비와 높이를 제한합니다.

html {
	height: 100vh;
}

body {
	height: 500vh;
	backround: #000; 
}

canvas {
	position: fixed;
    left: 50%;
    top: 50%;
    max-width: 100vw;
    max-height: 100vh;
    transform: translate(-50%, -50%);
}

지금은 페이지의 아래로 스크롤 할 수 있으며 (콘텐츠가 뷰포트 높이를 초과하지 않더라도) <canvas>는 뷰포트의 맨 위에 머물러 있습니다. 이제 이미지 불러오기로 넘어갑시다.

 

올바르게 이미지 가져오기

이미지 시퀀스로 작업할 것이므로 파일 이름이 오름차순(0001.jpg, 0002.jpg … )에서 순차적으로 번호가 매겨진다고 가정합니다.

사용자의 스크롤 위치에 따라 원하는 이미지 파일의 번호로 파일 경로를 반환하는 함수를 작성합니다.

const currentFrame = index => (
  `https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/
  anim/sequence/large/01-hero-lightpass/${index.toString().padStart(4, '0')}.jpg`
)

 

이미지 번호는 정수이므로 문자열로 변환하고(toString), 파일 이름과 일치하는 4자리에 도달할 때까지 padStart(4, '0') 함수를 사용하여 인덱스 앞에 0을 추가합니다. 예를 들어 이 함수에 1을 전달하면 0001이 반환됩니다.

 

이는 이미지 경로를 처리하는 방법을 제공합니다. 다음은 <canvas>요소에 그려진 시퀀스의 첫 번째 이미지입니다.

*ES6 문법을 사용할 경우 Babel 로 컴파일 해줘야함.

 

위의 코드는 단지 정적 이미지를 로드하는 일뿐이고, 우리가 원하는 것은 사용자의 스크롤 위치에 따라 업데이트 하는 것.

우리는 이미 전달한 번호를 기반으로 이미지 파일 경로를 생성하는 함수를 만들었음으로 이제 해야 할 일은 사용자의 스크롤 위치를 추적하고 해당 스크롤 위치에 해당하는 이미지 프레임을 결정하는 것입니다.

 

사용자의 스크롤 진행 상황에 이미지 연결 

시퀀스에서 전달해야 하는 숫자(따라서 로드할 이미지)를 알기 위해서는 사용자의 스크롤 진행률을 계산해야 합니다.

이벤트 리스너를 만들어 이를 추적하고 로드할 이미지를 계산하는 수학을 처리합니다.

 

알아야 할 사항 : 

  • 스크롤이 시작되고 끝나는 위치
  • 사용자의 스크롤 진행률(즉, 사용자가 페이지 아래로 얼마나 떨어져 있을지에 대한 백분율)
  • 사용자의 스크롤 진행에 해당하는 이미지 

scrollTop을 사용하여 요소의 수직 스크롤 위치를 얻을 것입니다. 이 경우 문서의 맨 위에 있습니다.

그것이 시작점 값으로 사용됩니다.

문서 스크롤 높이에서 창 높이를 빼서 끝(또는 최대 값)을 얻습니다. 

거기에서 scrollTop 값을 사용자가 아래로 스크롤할 수 있는 최대값으로 나누어 사용자의 스크롤 진행률을 알 수 있습니다.

 

그런 다음 해당 위체애 다한 올바른 이미지를 반환하기 위해 해당 스크롤 진행률을 이미지 번호 매기기 시퀀스에 해당하는 인덱스 번호로 전환해야 합니다. 진행률에 우리가 가지고 있는 프레임(이미지) 수를 곱하면 됩니다.

Math.floor() 함수를 사용하여 해당 숫자를 반올림하고 최대 프레임 수로 Math.min()으로 래핑하여 총 프레임 수를 초과하지 않도록 합니다.

 

Math.min() 함수는 주어진 숫자들 중 가장 작은 값을 반환합니다.

Math.floor() 함수는 주어진 숫자와 같거나 작은 정수 중에서 가장 큰 수를 반환합니다.

window.addEventListener('scroll', () => {  
  const scrollTop = html.scrollTop;
  const maxScrollTop = html.scrollHeight - window.innerHeight;
  const scrollFraction = scrollTop / maxScrollTop;
  const frameIndex = Math.min(
    frameCount - 1,
    Math.floor(scrollFraction * frameCount)
  );
});

 

올바른 이미지로 <canvas> 업데이트

이제 사용자의 스크롤 진행률이 변경됨에 따라 어떤 이미지를 그려야 하는 지 알았습니다. 여기에서 <canvas>의 마법이 작용합니다.

<canvas>에는 멋진 기능들이 있는데, 이러한 기능 중 하나로 브라우저와 함께 작동하는 requestAnimationFrame 이라는 메서드로 대신 스트레이트 이미지 파일로 작업하는 경우에는 할 수 없는 방식으로 <canvas>를 업데이트합니다.

이것이 <img> 요소나 배경 이미지가 있는 <div> 대신 <canvas>로 사용하는 이유입니다.

requestAnimationFrame 은 브라우저 새로고침 빈도와 일치하고 WebGL을 사용하여 장치의 비디오 카드 또는 통합 그래픽을 사용하여 렌더링함으로써 하드웨어 가속을 활성화합니다. 즉, 프레임 간에 매우 부드러운 전환을 얻을 수 있습니다. (이미지 깜빡임 X)

스크롤 이벤트 리스너에서 이 함수를 호출하여 사용자가 페이지를 위 아래로 스크롤 할 때 이미지를 교체해보겠습니다.

requestAnimationFrame 는 콜백 인수를 사용하므로 이미지 소스를 업데이트하고 <canvas>에 새 이미지를 그리는 함수를 전달합니다.

requestAnimationFrame(() => updateImage(frameIndex + 1))

이미지 시퀀스는 0001.jpg에서 시작하지만 스크롤 진행률 계산은 실제로 0에서 시작하기 때문에 frameIndex를 1로 올립니다.

이렇게 하면 두 값이 항상 정렬됩니다.

이미지를 업데이트 하기 위해 전달하는 콜백 함수는 다음과 같습니다 : 

const updateImage = index => {
	img.src = currentFrame(index);
    context.drawImage(img, 0, 0);
}

frameIndex를 함수에 전달합니다. <canvas> 요소에 그려진 시퀀스의 다음 이미지로 이미지 소스를 설정합니다.

 

이미지 사전 로딩

이 시점에서 기술적으로 완료되었습니다.

이미지 사전 로딩을 통해 코드를 보완할 수 있습니다.

예를 들어 빠르게 스크롤하면 이미지 프레임 간에 약간의 지연이 발생하는데 이는 모든 새 이미지가 새 네트워크 요청을 보내서 새로 다운로드 해야 하기 때문에 발생합니다. 

 

이미지 새 네트워크 요청을 미리 로드해야 합니다. 그렇게 하면 각 프레임이 이미 다운로드되어 전환 속도가 훨씬 빨라지고 애니메이션이 훨씬 부드러워집니다! 

 

우리가 해야 할 일은 전체 이미지 시퀀스를 반복하고 로드하는 것뿐입니다 : 

const frameCount = 148;

const preloadImages = () => {
  for (let i = 1; i < frameCount; i++) {
    const img = new Image();
    img.src = currentFrame(i);
  }
};

preloadImages();

 

성능에 대한 참고 

이 효과는 매우 매끄럽지만 이미지가 많습니다. 정확히는 148.

우리가 이미지를 얼마나 최적화하든, 또는 이미지를 제공하는 CDN이 얼마나 빠르든, 수백 개의 이미지를 로드하면 항상 페이지가 부풀려집니다. 같은 페이지에 이것의 여러 인스턴스가 있다고 가정해 보겠습니다. 다음과 같은 성능 통계를 얻을 수 있습니다.

데이터 제한이 없는 고속 인터넷 연결에는 괜찮을지 모르지만 모든 사용자에게는 동일하게 적용할 수 없습니다.

- 모든 사람의 경험을 염두에 두어야 합니다. 

이러한 균형을 유지하기 위해 우리가 할 수 있는 몇 가지 사항은 다음과 같습니다.

  • 전체 이미지 시퀀스 대신 단일 대체 이미지 로드
  • 특정 장치에 대해 더 작은 이미지 파일을 사용하는 시퀀스 만들기
  • 시퀀스를 시작하고 중지하는 버튼을 사용하여 사용자가 시퀀스를 활성화 할 수 있도록 허용

Apple은 첫 번째 옵션을 사용합니다. 느린 3G 연결에 연결된 모바일 장치에서 AirPods Pro 페이지를 로드하면 성능 통계가 훨씬 좋아지기 시작합니다.

여전히 무거운 페이지이지만, 그러나 성능을 전혀 고려하지 않은 것보다 훨씬 가볍습니다. 이것이 Apple이 한 페이지에 많은 복잡한 시퀀스를 얻을 수 있는 방법입니다.

 

추가 글

캔버스에서 스프라이트 이미지를 사용하는 방법

https://dev.to/martyhimmel/animating-sprite-sheets-with-javascript-ag3

반응형