AirPods Max - Apple

전혜린·2022년 8월 1일
0

Portfolio

목록 보기
3/10

💻 AirPods Max - Apple 클론코딩

  • 사이트명: AirPods Max - Apple
  • 제작기간: 22.07.16 ~ 22.07.19 (4일 소요)
  • 사용언어: html, css, js
  • 분류: PC, 클론코딩

🔍 Main Point

  • WAI-ARIA
  • sticky 속성
  • 2개의 Swiper 연동시키기
  • GSAP ScrollTrigger
  • Canvas

1. WAI-ARIA

  •  스크린리더 및 보조기기 등을 사용하는 사용자들에게 페이지의 내용과 데이터가 바뀌는 영역에 역할(Role), 속성(Property), 상태(State) 정보를 추가하여 동적인 컨텐츠에 보다 원활하게 접근하고 페이지에 접근성을 높여 여러 사용자들에게 원활한 페이지 이용을 도와준다.

  • 이러한 정보들은 Web Browser에 의해 자동으로 해석되어 각각의 운영체제(OS)의 접근성 API로 변환되어서 스크린리더와 같은 보조기기들이 이를 통해 Desktop Application과 동일한 방법으로 JavaScript control을 인식하고 상호작용을 할 수 있게 된다. 

👉 role

  • role은 HTML 요소의 역할을 정의하며 HTML native 요소만으로는 세세하고 다양한 설정이 힘들 때 role을 사용하여 해당 요소들의 역할을 명시한다.

  • 즉, 특정 element의 기능을 정의하는 것으로 페이지의 검색 영역인지, 내비게이션 요소인지, 특정 section의 제목(heading) 인지 등의 명확한 기능을 부여할 수 있다.

    #예시

    <a href="#" role="button">...</a>
    
    <!-- 
    role="button"이 없는 경우 스크린 리더의 해석:
    버튼을 링크의 용도로 이해 
    
    role="button"이 있는 경우 스크린 리더의 해석: 
    버튼의 용도로 이해 
    -->

👉 WAI-ARIA 사용 방법

  • HTML5 Section 요소와 중복 사용하지 않는다.
    html의 각 태그에는 기본적으로 갖고 있는 역할과 의미가 있으므로 태그의 기본 속성에 덧붙여서 속성을 중복하여 정의할 필요가 없다.

    <nav role="navigation">...</nav> (x)
    <!-- 동일한 역할을 하는 네이티브요소와의 중복마크업 -->
  • native요소의 의미, 기능 변경을 하지 않는다.

    <button role="heading">search</button> (x)
    <!-- button의 본래의 기능을 버리고 다른 역할을 부여한 잘못된 케이스 -->
  • 키보드 사용성 보장
    상호작용이 가능한 대화형 UI(사용자가 클릭 가능한 정보, 탭, drag&drop, slide, scroll등이 필요한 기능)를 span이나 div로 마크업 후, role="button"속성 부여시, 사용자가 키보드로 접근이 가능하도록 해야 한다.

  • 아래와 같이 <span> 태그를 버튼으로 사용하기 위해서는 role="button" 속성 부여시 tabindex도 반드시 설정해서 키보드 접근이 가능하도록 한다.

    <span role="button" tabindex="0">버튼</span>
  • 사용예시

  1. role="banner"
    비슷한 의미로 <header> 사용가능
    <header role="banner">로 사용시 1페이지에서 1개만 사용하기를 권장

  2. role="contentinfo"
    <footer>와 비슷, <footer role="contentinfo">로 사용시 한 페이지에 한 개의 요소만 사용하기를 권장

  3. role="navigation"
    nav와 동일

  4. role="main"
    <main>과 동일
    본문의 주요 컨텐츠 영역으로 한페이지 내에 1개만 사용 가능

  5. role="search"
    검색 역할을 담당하는 서식영역
    <div>또는 <form>에 사용권장

  6. role="button"
    p, span, div에서도 버튼컨트롤로 사용된다는 것을 스크린리더에 인식시킬 수 있다.
    하지만 가능하면 기본 html의 <button>, <input type="button">, <input type="submit">을 사용해야 한다.
    기본 html 요소들은 추가 사용자 정의 없이 키보드 포커스를 지원한다.

  7. role ="tablist"
    탭메뉴 등의 리스트임을 사용자에게 전달한다.

    <ul class="category-list" role="tablist">
      ...
    </ul>
  8. role="tab"
    보조기기가 탭으로 인식

    <div class="category-list" role="tablist">
        <a href="#" role="tab">패션뷰티</a>
        <a href="#" role="tab">웹툰</a>
        <a href="#" role="tab">레시피</a>
    </div>
  9. role="none", role="presentation"
    semantic 의미를 요소와 그 자식요소로부터 제거하기 위해서 사용한다. 즉, 의미 없음(role="none")을 선언하는 경우 보조기기는 마크업의 의미를 제거 후 내용만 사용자에게 전달한다. role="none" 속성은 role="presentation"과 동일하며 role="presentation"을 대체한다.

    HTML을 의미에 맞지 않게 마크업한 경우, 또는 스타일링에 필요한 마크업을 추가한 경우 role="none" 속성을 사용할 수 있다.

    #예시

    <ul role="tablist">
        <li role="none">
            <a href="#" role="tab"></a>
        </li>
        <li role="none">
            <a href="#" role="tab">연재</a>
        </li>
        <li role="none">
            <a href="#" role="tab">랭킹</a>
        </li>
    </ul>
    
    <ul class="category-list" role="tablist">
        <li class="category-item" role="presentation">
            <a href="#" role="tab" class="btn-category">엔터</a>
        </li>
        <li class="category-item" role="presentation">
            <a href="#" role="tab" class="btn-category">스포츠</a>
        </li>
        <li class="category-item" role="presentation">
            <a href="#" role="tab" class="btn-category">웹툰</a>
        </li>
    </ul>
  10. role="group"
    라디오 버튼과 같이 여러개의 옵션 중 한가지를 선택 할 때, name 속성에 같은 값을 넣어 그룹화 하더라도 스크린리더 사용자는 묶여있는 그룹이라는 것을 인식하기 어렵다. 따라서 이러한 경우에는 role="group"를 부여하여 같은 그룹이라는 것을 인식시킨다.

    #예시
    항공권 구매 시 좌석 등급을 선택해야 할 때, 이코노미/프레스티지/일등석을 라디오버튼으로 제작하였으나 시각적인 타이틀만 눈에 보일 뿐, 그룹으로 묶지 않아 스크린리더 사용자는 정보를 인지하지 못하기 때문에 이해하기 어렵게 된다.

    따라서 전체를 감싸는 <ul>요소에 ARIA role="group"과 aria-labelledby를 사용하여 그룹으로 묶고 그룹 제목을 아래와 같이 명시한다.

    <h2>Cabin Class</h2>
    <ul role="group">
      <li>
        <input id="economy" type="radio" value="economy" checked="checked" name="cabin">
        <label for="economy">Economy Class</label>
      </li>
      <li>
        <input id="Prestige" type="radio" value="Prestige" name="cabin">
        <label for="Prestige">Prestige Class</label>
      </li>
      <li>
        <input id="First" type="radio" value="First" name="cabin">
        <label for="First">First Class</label>
      </li>
    </ul>

    참고사이트) https://aoa.gitbook.io/skymimo/improve/group



2. sticky 속성

  • CSS 포지션(Position)에서 화면을 고정하는 방법은 Fixed 속성과 Sticky 속성이 있는데 간략한 차이는 아래와 같다.

    position: fixed -> 최상위 브라우저 창을 기준으로 절대 위치를 지정
    position: sticky -> 부모 태그의 크기를 기준으로 절대 위치를 지정

  • sticky 속성을 선언한 영역의 위치 값을 고정시켜주는 역할을 하며 top, left와 같은 위치값을 반드시 작성해줘야 sticky 속성을 적용할 수 있다.

  • 또한 sticky 를 적용할 HTML 태그의 부모 태그에 무조건 height이 있어야 하며 상위 부모태그에 height 를 준 만큼만 고정된다.

  • 아래 예시의 header처럼 구현하고 싶은 경우 컨텐츠의 내용에 따라 자동으로 높이 속성을 가지는 body 태그를 사용하면 된다.
    sticky를 사용해야 한다면 그 부분만 <body> 아래에 놓고 CSS를 주도록 할 것

💡 실제 화면 보기





3. 2개의 Swiper 연동시키기

  • 슬라이드가 바뀔 때마다 이벤트를 호출하는 메서드 사용
    activeIndexChange는 활성화된 인덱스가 바뀔 때마다 호출
    slideChange는 활성화된 슬라이드(swiper-slide-active)가 바뀔 때마다 호출
  • 활성화(active)된 슬라이드의 index를 반환하는 파라미터 사용
    realIndex는 현재 활성화(active)된 슬라이드의 index를 반환
    activeIndex도 현재 활성화된 슬라이드의 index를 반환하는데 둘의 차이는 activeIndex는 loop에서 복제된 슬라이드까지 포함한다. 따라서 진짜 슬라이드의 index를 반환하기 위해서는 realIndex를 사용해야 한다.
  • 지정된 슬라이드로 전환하는 slideToLoop사용

💡 실제 화면 보기



4. GSAP ScrollTrigger

gsap.to()

  • gsap.to에는 대상과 속성 2가지 값은 필수이다.
  • x:200은 css의 transform: translateX(200px)과 동일한 효과이다.
  • duration의 기본값은 0.5초
gsap.to('.box', {
  x: 500
});

  • 아래 출력 화면과 같이 opacity나 rotate, scale 등 다양한 css를 적용할 수 있다.
gsap.to('.box2', {
  x: 200,
  y: 200,
  backgroundColor: 'black',
  scale: 1.5,
  rotate: 360,
  duration: 1.5
});

gsap.from(), fromTo()

  • from()은 시작 값을 정하고 원래대로 되돌아 오는 애니메이션 실행
  • fromTo()에서 from 속성은 시작 값, to 속성은 종료 값으로 시작 값에서 종료 값으로 변경되는 애니메이션 실행
gsap.from('.box1', {
  x: 200,
  width: 300,
  height: 300,
  opacity: .2,
  duration: 3
});

gsap.fromTo('.box2', {
  // from 
  scale: 0.85,
  opacity: 0,
  x: 80,
  y: 80
}, {
  // to
  scale: 1,
  opacity: 1,
  x: 0,
  y: 0,
  duration: 1.5
});

gsap.play(), .pause(), resume(), reverse(), restart()

  • 애니메이션을 멈추거나 재실행하는 등의 핸들링도 가능하다.

TimeLine

  • 순차적으로 애니메이션을 실행하고자 할 때 timeline사용
let tl = gsap.timeline({
  defaults: {
    duration: 2
  }
});
tl.to('.box1', {x: 200})
.to('.box1', {y: 200})
.to('.box1', {x: 100})

ScrollTrigger

  • 스크롤을 해서 해당 요소가 보이는 지점에 애니메이션 효과가 나타나도록 한다.

속성

  1. start
  • 값을 하나만 넣을 경우 viewport 기준 scrollTrigger의 시작점
    ex) 500px -> 스크롤 시작 후 500px 떨어진 시점)

  • 값을 두 개 넣을 경우
    첫 번째 값: trigger가 시작되는 기준점
    두 번째 값: viewport에서 스크롤의 위치
    ex) start: top center // trigger의 윗부분이 viewport의 중간에 닿았을 때 시작

  1. end
  • start와 같은 방식으로 사용되면 끝나는 시점을 입력함
  1. toggleClass
  • start 시점에 class가 추가되고 end 시점에서 class 삭제
  1. markers
  • 스크롤이 시작되고 끝나는 시점을 마킹해줌
  1. scrub
  • 스크롤이 사용될 때만 재생되도록 만들어 줌
  • 스크롤을 멈출 경우 이벤트도 함께 멈춤
  • 스크롤이 요소 이전으로 돌아가면 애니메이션도 함께 되돌아 감 (되감기 가능)
  • 즉, 애니메이션을 재사용 할 수 있는 기능
  • scrub: true 외에 숫자 값도 넣을 수 있음 ex) scrub: 1 -> 스크롤바를 잡는데 1초가 걸림
  • 숫자 값을 넣을 경우 이벤트가 더욱 부드러움
  1. pin
  • 특정 요소가 고정되도록 만들어주는 속성 (화면에 고정하는 기능)
  • pin: true 시 trigger 요소 고정됨
  • pin: true 또는 pin: '요소의 id/class명' 입력가능
  • pinSpacing: true 는 고정되는 요소 아래에 padding을 줘서 스크롤이 끝난 후 다음 요소가 이어서 보일수 있게 만들어줌 pinSpacing: 'margin'은 padding 대신 margin 사용
  1. 독립형
  • ScrollTrigger를 애니메이션에 굳이 넣지 않고도 사용 가능
  • 독립형으로 사용할 경우 앞글자는 대문자 'S' 사용
const fadeIn = gsap.fromTo('h1', {
  opacity: 0.2
}, {
  opacity: 1
});

ScrollTrigger.create({
  trigger: '.area.here',
  start: 'top center',
  scrub: 1,
  animation: fade
});

💡 실제 화면 보기



5. Canvas

  • canvas 요소는 웹 페이지에 실시간으로 그래픽을 그리는 데 사용된다. 자바스크립트를 통해 다양한 그림을 그릴 수 있는 공간을 제공해주며, 그래프, 게임 그래픽 및 기타 다른 이미지를 실시간으로 그려 사용할 수 있는 기능들을 제공한다.

  • img 태그와 달리 canvas 요소는 닫는태그(</canvas>) 를 필요로 한다.

  • 캔버스의 크기를 정의하기 위해서 width와 height 속성을 지정하며 width의 기본값은 300, height의 기본값은 150이다. 픽셀 단위이며 CSS로도 크기를 지정할 수 있다. 그러나, CSS를 사용할 경우 렌더링 과정에서 CSS 크기에 맞추기 위해 이미지의 크기를 조절하므로, 최종 그래픽이 변형될 수 있다.

  • 캔버스 태그 안에 콘텐츠를 추가해 대체 콘텐츠로서 사용할 수 있다. 대체 콘텐츠는 캔버스를 지원하지 않는 구형 브라우저와, JavaScript를 비활성화한 브라우저에서 표시된다.

<canvas id="canvas" width="300" height="150">캔버스의 내용을 설명하는 대체 텍스트</canvas>
  • 캔버스는 처음에 비어있으며, 무언가를 표시하기위해서는 어떤 스크립트가 랜더링 컨텍스트에 접근하여 그리도록해야 한다. canvas 요소는 getContext() 메서드를 호출해서, 랜더링 컨텍스트와 그리기 함수들을 사용할 수 있다. 컨텍스트 종류는 문자열이므로 반드시 따옴표로 감싸 '2d'라고 적어야 하며 대문자를 인정하지 않으므로 '2D'라고 적어서는 안된다.
  const canvas = document.querySelector('#canvas');
  const ctx = canvas.getContext('2d'); //ctx = context의 약자

이미지 가져오기

  const frameCount = 45;
  const currentFrame = (idx) => {
    return `https://www.apple.com/105/media/us/airpods-max/2020/996b980b-3131-44f1-af6c-fe72f9b3bfb5/anim/turn/large/large_${idx.toString().padStart(4, '0')}.jpg`;
  }; // 리턴 필수
  • 위 코드는 애플사이트의 large_0000 ~ large_0045.jpg 라는 이름의 이미지 45개를 가져오는 코드이며 여기서 사용된 padStart() 메서드에 대해 알아보겠다.

padStart()

  • padStart() 메서드는 현재 문자열의 시작을 다른 문자열로 채워, 주어진 길이를 만족하는 새로운 문자열을 반환한다. 채워넣기는 좌측부터 적용된다.

구문
str.padStart(targetLength [, padString])

  • targetLength(길이): 원하는 새로운 문자열의 길이를 지정한다. 현재 문자열의 길이보다 작다면 채워넣지 않고 그대로 반환한다. (필수 지정)
  • padString(문자열): 현재 문자열에 채워넣을 다른 문자열을 지정한다. 문자열이 너무 길어 목표 문자열 길이를 초과한다면 좌측 일부를 잘라서 넣으며 (기본값은 공백(" ") 이다.)
'abc'.padStart(10);         // "       abc"
'abc'.padStart(10, "foo");  // "foofoofabc"
'abc'.padStart(6,"123465"); // "123abc"
'abc'.padStart(8, "0");     // "00000abc"
'abc'.padStart(1);          // "abc"

이미지 삽입하기

  const frameCount = 45;
  const currentFrame = (idx) => {
    return `https://www.apple.com/105/media/us/airpods-max/2020/996b980b-3131-44f1-af6c-fe72f9b3bfb5/anim/turn/large/large_${idx.toString().padStart(4, '0')}.jpg`;
    
  const images = [];
  const card = {
    frame: 0,
  };

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

new Image()

  • img 태그를 사용하지 않고 자바스크립트 코드로 Image 객체를 생성하여 이미지를 로딩할 수 있다.

  • new Image()에 의해 생성된 Image 객체는 이미지를 로딩하여 저장해 두는 목적으로만 사용되며 Image 객체에 로딩된 이미지는 img 태그를 통해서만 화면에 출력된다.

Array.prototype.push()

  • push() 메서드는 배열 끝에 새 항목을 추가하고, 배열의 새로운 길이를 반환한다.

구문
array.push(item1, item2, ..., itemX)

  • push 메서드는 배열 끝에 여러 값을 추가
  • 반환 값은 배열의 새 길이
var sports = ['축구', '야구'];
var total = sports.push('미식축구', '수영');

console.log(sports); // ['축구', '야구', '미식축구', '수영']
console.log(total);  // 4 (total 변수는 추가한 배열의 새 길이 값을 포함)

이미지 지우고 그리기

  images[0].onload = render;

  function render() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(images[card.frame], 0, 0);
  }

CanvasRenderingContext2D.clearRect()

  • Canvas 2D API 의 CanvasRenderingContext2D.clearRect() 메서드는 사각형 영역의 픽셀을 투명 검정(rgba(0, 0, 0, 0))으로 설정하여 지운다.

구문
clearRect(x, y, width, height)

  • x: 제거할 영역이 시작되는 x 좌표를 설정한다.
  • y: 제거할 영역이 시작되는 y 좌표를 설정한다.
  • width: 제거할 사각 영역의 가로 크기를 설정한다.
  • height: 제거할 사각 영역의 세로 크기를 설정한다.

CanvasRenderingContext2D.drawImage()

  • Canvas 2D API 의 CanvasRenderingContext2D.drawImage() 메서드는 캔버스에 이미지를 그리는 다양한 방법을 제공한다.

구문
drawImage(image, dx, dy)
drawImage(image, dx, dy, dWidth, dHeight)
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

  • image: 사용될 이미지 요소를 지정한다. 문서내 DOM에서 찾거나 새롭게 생성하여 지정할 수도 있다. (필수 지정)
  • dx: 캔버스 내에서 이미지가 그려질 이미지의 좌측 상단의 x 위치값을 캔버스 영역 기준으로 지정한다. (필수 지정)
  • dy: 캔버스 내에서 이미지가 그려질 이미지의 좌측 상단의 y 위치값을 캔버스 영역 기준으로 지정한다.
  • dWidth: 캔버스 내에서 그려질 이미지의 너비를 지정한다.
  • dHeight: 캔버스 내에서 그려질 이미지의 높이를 지정한다.
  • sx: 이미지가 그려지는 위치(dX)에서 캔버스 기준으로 이미지의 좌측 상단이 실제로 시작되는 x 위치를 지정한다. 따라서 이미지가 잘릴 수도 있다.
  • sy: 이미지가 그려지는 위치(dY)에서 캔버스 기준으로 이미지의 좌측 상단이 실제로 시작되는 y 위치를 지정한다. 따라서 이미지가 잘릴 수도 있다.
  • sWidth: sX 위치로부터 원래 이미지 가로 픽셀수에서 더하거나 뺄 수 있는 픽셀수이다.
  • sHeight: sY 위치로부터 원래 이미지 세로 픽셀수에서 더하거나 뺄 수 있는 픽셀수이다.
profile
코딩쪼아

0개의 댓글