💻 AirPods Max - Apple 클론코딩
스크린리더 및 보조기기 등을 사용하는 사용자들에게 페이지의 내용과 데이터가 바뀌는 영역에 역할(Role), 속성(Property), 상태(State) 정보를 추가하여 동적인 컨텐츠에 보다 원활하게 접근하고 페이지에 접근성을 높여 여러 사용자들에게 원활한 페이지 이용을 도와준다.
이러한 정보들은 Web Browser에 의해 자동으로 해석되어 각각의 운영체제(OS)의 접근성 API로 변환되어서 스크린리더와 같은 보조기기들이 이를 통해 Desktop Application과 동일한 방법으로 JavaScript control을 인식하고 상호작용을 할 수 있게 된다.
role은 HTML 요소의 역할을 정의하며 HTML native 요소만으로는 세세하고 다양한 설정이 힘들 때 role을 사용하여 해당 요소들의 역할을 명시한다.
즉, 특정 element의 기능을 정의하는 것으로 페이지의 검색 영역인지, 내비게이션 요소인지, 특정 section의 제목(heading) 인지 등의 명확한 기능을 부여할 수 있다.
#예시
<a href="#" role="button">...</a>
<!--
role="button"이 없는 경우 스크린 리더의 해석:
버튼을 링크의 용도로 이해
role="button"이 있는 경우 스크린 리더의 해석:
버튼의 용도로 이해
-->
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>
사용예시
role="banner"
비슷한 의미로 <header>
사용가능
<header role="banner">로 사용시 1페이지에서 1개만 사용하기를 권장
role="contentinfo"
<footer>
와 비슷, <footer role="contentinfo">로 사용시 한 페이지에 한 개의 요소만 사용하기를 권장
role="navigation"
nav
와 동일
role="main"
<main>
과 동일
본문의 주요 컨텐츠 영역으로 한페이지 내에 1개만 사용 가능
role="search"
검색 역할을 담당하는 서식영역
<div>
또는 <form>
에 사용권장
role="button"
p, span, div에서도 버튼컨트롤로 사용된다는 것을 스크린리더에 인식시킬 수 있다.
하지만 가능하면 기본 html의 <button>
, <input type="button">
, <input type="submit">
을 사용해야 한다.
기본 html 요소들은 추가 사용자 정의 없이 키보드 포커스를 지원한다.
role ="tablist"
탭메뉴 등의 리스트임을 사용자에게 전달한다.
<ul class="category-list" role="tablist">
...
</ul>
role="tab"
보조기기가 탭으로 인식
<div class="category-list" role="tablist">
<a href="#" role="tab">패션뷰티</a>
<a href="#" role="tab">웹툰</a>
<a href="#" role="tab">레시피</a>
</div>
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>
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>
CSS 포지션(Position)에서 화면을 고정하는 방법은 Fixed 속성과 Sticky 속성이 있는데 간략한 차이는 아래와 같다.
position: fixed -> 최상위 브라우저 창을 기준으로 절대 위치를 지정
position: sticky -> 부모 태그의 크기를 기준으로 절대 위치를 지정
sticky 속성을 선언한 영역의 위치 값을 고정시켜주는 역할을 하며 top, left와 같은 위치값을 반드시 작성해줘야 sticky 속성을 적용할 수 있다.
또한 sticky 를 적용할 HTML 태그의 부모 태그에 무조건 height이 있어야 하며 상위 부모태그에 height 를 준 만큼만 고정된다.
아래 예시의 header처럼 구현하고 싶은 경우 컨텐츠의 내용에 따라 자동으로 높이 속성을 가지는 body
태그를 사용하면 된다.
sticky를 사용해야 한다면 그 부분만 <body> 아래에 놓고 CSS를 주도록 할 것
💡 실제 화면 보기
💡 실제 화면 보기
gsap.to('.box', {
x: 500
});
gsap.to('.box2', {
x: 200,
y: 200,
backgroundColor: 'black',
scale: 1.5,
rotate: 360,
duration: 1.5
});
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
});
let tl = gsap.timeline({
defaults: {
duration: 2
}
});
tl.to('.box1', {x: 200})
.to('.box1', {y: 200})
.to('.box1', {x: 100})
값을 하나만 넣을 경우 viewport 기준 scrollTrigger의 시작점
ex) 500px -> 스크롤 시작 후 500px 떨어진 시점)
값을 두 개 넣을 경우
첫 번째 값: trigger가 시작되는 기준점
두 번째 값: viewport에서 스크롤의 위치
ex) start: top center // trigger의 윗부분이 viewport의 중간에 닿았을 때 시작
start
와 같은 방식으로 사용되면 끝나는 시점을 입력함start
시점에 class가 추가되고 end
시점에서 class 삭제scrub: true
외에 숫자 값도 넣을 수 있음 ex) scrub: 1 -> 스크롤바를 잡는데 1초가 걸림pin: true
시 trigger 요소 고정됨pin: true
또는 pin: '요소의 id/class명'
입력가능pinSpacing: true
는 고정되는 요소 아래에 padding을 줘서 스크롤이 끝난 후 다음 요소가 이어서 보일수 있게 만들어줌 pinSpacing: 'margin'
은 padding 대신 margin 사용const fadeIn = gsap.fromTo('h1', {
opacity: 0.2
}, {
opacity: 1
});
ScrollTrigger.create({
trigger: '.area.here',
start: 'top center',
scrub: 1,
animation: fade
});
💡 실제 화면 보기
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`;
}; // 리턴 필수
구문
str.padStart(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);
}
img
태그를 사용하지 않고 자바스크립트 코드로 Image 객체를 생성하여 이미지를 로딩할 수 있다.
new Image()에 의해 생성된 Image 객체는 이미지를 로딩하여 저장해 두는 목적으로만 사용되며 Image 객체에 로딩된 이미지는 img
태그를 통해서만 화면에 출력된다.
구문
array.push(item1, item2, ..., itemX)
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);
}
구문
clearRect(x, y, width, height)
구문
drawImage(image, dx, dy)
drawImage(image, dx, dy, dWidth, dHeight)
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)