Apple Design
최초 응답
이 스킬이 특정 질문 없이 처음 호출되면, 다음만 응답하세요:
Apple 스타일의 유려한 웹 인터페이스를 구축하는 데 도움을 드릴 준비가 되었습니다. 제 지식은 Apple의 WWDC 디자인 강연을 웹에 맞게 번역한 것에서 비롯되었습니다.
사용자가 질문할 때까지 다른 정보는 제공하지 마십시오.
Apple이 컴퓨터처럼 느껴지지 않고 사용자의 연장처럼 느껴지는 인터페이스를 구축하는 방법입니다. 이 지식은 Apple의 WWDC 디자인 강연 — 주로 Designing Fluid Interfaces (WWDC 2018) — 에서 비롯되었으며, 웹 플랫폼(CSS, Pointer Events, requestAnimationFrame, Motion/Framer Motion과 같은 스프링 라이브러리)으로 정제되고 번역되었습니다.
핵심: 인터페이스는 현재 화면 값에서 모션이 시작되고, 사용자의 속도를 상속받으며, 모멘텀을 앞으로 투영하고, 언제든지 잡히고 역전될 수 있을 때 살아있는 것처럼 느껴집니다. 스프링은 이 모든 것을 자연스럽게 만드는 도구입니다. 왜냐하면 스프링은 본질적으로 인터럽트가 가능하고 속도를 인식하기 때문입니다.
핵심 아이디어
"인터페이스를 우리가 생각하고 움직이는 방식에 맞추면, 마법 같은 일이 일어납니다. 컴퓨터처럼 느껴지지 않고 우리의 매끄러운 연장처럼 느껴지기 시작합니다."
인터페이스는 물리적 세계처럼 행동할 때 유연합니다. 즉, 사물이 즉시 반응하고, 연속적으로 움직이고, 모멘텀을 전달하고, 경계에서 저항하며, 움직이는 중간에 방향을 바꿀 수 있습니다. 아래의 모든 내용은 이에 더 가까워지기 위한 방법입니다.
Apple은 디자인이 네 가지 인간의 요구를 충족시키는 것으로 봅니다: 안전/예측 가능성, 이해, 성취, 기쁨. 여기의 모든 규칙은 그중 하나를 위한 것입니다.
1. 응답 — 지연 시간 제거
지연이 나타나는 순간, 직접성의 느낌은 "절벽에서 떨어집니다." 응답은 다른 모든 것이 구축되는 기초입니다.
- 포인터-다운 시 응답하고, 릴리즈 시가 아닙니다. 버튼을 누르는 즉시 강조 표시하세요.
click/터치 업까지 기다려서 피드백을 보여주는 것은 죽은 것처럼 느껴집니다. - 모든 지연 시간에 주의하세요. 디바운스, 인위적인 타이머, 전환 대기, ~300ms 탭 지연을 감사하세요. 입력 경로에서 필수적이지 않은 모든 것은 퇴보입니다.
- 피드백은 상호작용 중에 연속적이어야 하며, 끝에서만이 아닙니다. 드래그, 슬라이더 또는 서랍의 경우, 전체 과정 동안 포인터와 1:1로 UI를 업데이트하세요. 제스처가 완료될 때만 애니메이션을 적용하지 마십시오.
/* 피드백은 누름에 있으며 즉각적입니다 */
.button:active {
transform: scale(0.97);
transition: transform 100ms ease-out;
}
2. 직접 조작 — 1:1 추적
"터치와 콘텐츠는 함께 움직여야 합니다."
사용자가 무언가를 드래그할 때, 손가락에 붙어 있어야 하며 — 잡은 위치의 오프셋을 존중해야 합니다. 잡을 때 요소의 중앙으로 스냅하면 즉시 착각이 깨집니다.
setPointerCapture와 함께 Pointer Events를 사용하여 포인터가 요소의 경계를 벗어나더라도 추적이 계속되도록 하십시오.- 짧은 속도/위치 기록(마지막 몇 개의
pointermove이벤트)을 현재 지점뿐만 아니라 추적하십시오. 릴리즈 시 속도가 필요합니다.
el.addEventListener('pointerdown', (e) => {
el.setPointerCapture(e.pointerId);
const grabOffset = e.clientY - el.getBoundingClientRect().top; // 잡은 위치를 존중합니다
// ...속도를 위한 위치 + 타임스탬프 기록을 추적합니다
});
3. 인터럽트 가능성 — 가장 중요한 원칙
"생각과 제스처는 병렬로 일어납니다."
모든 애니메이션은 언제든지 인터럽트 가능하고 방향을 바꿀 수 있어야 합니다. 사용자는 움직이는 요소를 비행 중에 잡아서 애니메이션이 끝날 때까지 기다리지 않고 역전시킬 수 있어야 합니다. 사용자가 다시 잡는 닫히는 모달은 손가락을 따라야 합니다. 먼저 닫힌 다음 다시 열리지 않아야 합니다.
- 전환 중에 입력을 절대 잠그지 마십시오.
- 항상 표현(현재) 값에서 애니메이션을 시작하고, 대상 값에서 시작하지 마십시오. 인터럽트 시, 요소의 라이브 화면 변환을 읽고 거기서 새 애니메이션을 시작하십시오. 논리적/대상 값에서 시작하면 눈에 띄는 점프가 발생합니다.
- 제스처 기반의 모든 것에 CSS 전환 및
@keyframes를 사용하지 마십시오 — 비행 중에 부드럽게 잡히고 역전될 수 없습니다. 스프링은 기본적으로 현재 값에서 애니메이션을 시작하는데, 이는 인터럽트에 정확히 필요한 것입니다. - 제스처가 반전될 때 속도를 혼합하십시오. 하드 컷하지 마십시오. 반전 지점에서 하나의 애니메이션을 다른 애니메이션으로 교체하면 속도 불연속성, 즉 "벽돌 벽"이 생성됩니다. 재타겟을 통해 속도를 전달하는 스프링 라이브러리는 이를 피합니다. (이것이 iOS의 additive animations이 기본적으로 하는 일입니다. 웹에서는 현재 속도에서 재타겟하는 스프링 라이브러리를 선택하십시오.)
- 2D 모션을 독립적인 X 및 Y 스프링으로 분해하십시오. 2D 거리에 대한 단일 스프링은 X와 Y의 속도가 다를 때 동기화가 해제됩니다.
4. 애니메이션보다 거동 — 스프링 사용
"애니메이션을 인터페이스가 규정하는 것이 아니라 사용자와 객체 간의 대화로 생각하십시오."
미리 스크립트된 고정 지속 시간 애니메이션은 새로운 입력에 응답할 수 없습니다. 스프링은 가능합니다. 새로운 입력은 대상을 변경할 뿐이며 모션은 연속적으로 유지됩니다. 사용자가 만질 수 있는 모든 것에 스프링을 사용하십시오.
Apple은 의도적으로 물리학 삼중항(질량/강성/감쇠)을 디자이너에게 친숙한 두 가지 매개변수로 대체했습니다. 이것들로 생각하십시오:
- 감쇠비(Damping ratio) — 오버슈트를 제어합니다.
1.0= 임계 감쇠, 바운스 없음, 부드러운 안착.< 1.0= 오버슈트 및 진동. 낮을수록 더 탄력적입니다. - 응답(Response) — 값이 대상에 도달하는 속도(초). 낮을수록 더 빠릅니다. 이는 "지속 시간"이 아닙니다 — 스프링에는 고정된 지속 시간이 없습니다. 안착 시간은 매개변수에서 발생합니다.
기본값:
- 대부분의 UI는 감쇠
1.0(임계 감쇠)으로 시작하십시오 — 우아하고 방해가 되지 않습니다. - 제스처 자체가 모멘텀을 전달할 때만 (튕기기, 던지기, 드래그 릴리즈) 바운스를 추가하십시오(감쇠
~0.8). 방금 페이드 인된 메뉴의 오버슈트는 어색하게 느껴집니다. 사용자가 튕긴 카드의 오버슈트는 자연스럽게 느껴집니다.
Apple이 제공하는 구체적인 값:
| 상호작용 | 감쇠 | 응답 |
|---|---|---|
| 이동 / 위치 변경 (예: PiP) | 1.0 | 0.4 |
| 회전 | 0.8 | 0.4 |
| 서랍 / 시트 | 0.8 | 0.3 |
웹 매핑 (Motion / Framer Motion): bounce + duration 스프링 API는 Apple의 감쇠 + 응답에 가깝게 매핑됩니다. 안전한 하우스 스타일은 기본적으로 모든 곳에서 damping: 1.0 스프링을 사용하는 것입니다. 모멘텀 기반의 물리적 상호작용을 위해 바운스를 예약하십시오.
import { animate } from 'motion';
// 임계 감쇠 기본값 (오버슈트 없음)
animate(el, { y: 0 }, { type: 'spring', bounce: 0, duration: 0.4 });
// 모멘텀 상호작용 — 약간의 바운스, 튕기기가 선행되었기 때문
animate(el, { y: target }, { type: 'spring', bounce: 0.2, duration: 0.4 });
5. 속도 핸드오프 — 드래그와 애니메이션 사이의 이음매
제스처가 끝날 때, 애니메이션은 손가락의 정확한 속도로 계속되어 드래그와 애니메이션 사이에 보이는 이음매가 없어야 합니다. 이것이 "유연함"과 "괜찮음"을 가장 구분짓는 세부 사항입니다.
포인터의 릴리즈 속도를 스프링의 초기 속도로 전달하십시오. 일부 스프링 API는 상대 속도를 원합니다. 남은 거리로 정규화하십시오:
relativeVelocity = gestureVelocity / (targetValue − currentValue)
예: y=50에 있는 요소, 대상 y=150 (100px 남음), 손가락이 50px/s로 움직임 → 초기 스프링 속도 = 50 / 100 = 0.5. Framer Motion / Motion은 절대 px/s 속도를 직접 받아들이므로(velocity 옵션), 일반적으로 원시 값을 전달합니다.
6. 모멘텀 투영 — 제스처가 향하는 곳으로 애니메이션
"작은 입력을 받아 큰 출력을 만드십시오."
릴리즈 지점에서 가장 가까운 경계로 스냅하지 마십시오. 속도를 사용하여 정지 위치를 투영하십시오. 정확히 스크롤 감속처럼 — 그런 다음 투영된 지점에 가장 가까운 대상으로 스냅하십시오. 이것이 튕기기가 요소를 던지는 것처럼 느껴지게 만드는 것입니다.
Apple의 정확한 투영 함수(Designing Fluid Interfaces 샘플 코드에서):
// decelerationRate ≈ 일반 스크롤 느낌의 경우 0.998; 더 빠른 경우 0.99
function project(initialVelocity /* px/s */, decelerationRate = 0.998) {
return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate);
}
const projectedEndpoint = currentPosition + project(releaseVelocity);
const target = nearestSnapPoint(projectedEndpoint); // 투영에서 대상 선택
animateSpringTo(target, { velocity: releaseVelocity }); // 그런 다음 속도 핸드오프 (§5)
참고: 물리 교과서의 v²/(2·decel)은 Apple이 제공하는 것이 아닙니다. 위의 지수 감쇠 형태를 사용하십시오. 이것은 좋은 바텀 시트와 캐러셀(Vaul, Embla)의 표준 동작입니다.
7. 공간적 일관성 — 대칭 경로, 고정된 원점
"무언가가 한 방향으로 사라지면, 우리는 그것이 왔던 곳에서 나타날 것으로 예상합니다."
- 동일한 경로로 들어오고 나가십시오. 오른쪽에서 슬라이드되는 패널은 오른쪽으로 사라져야 합니다. 오른쪽에서 들어와서 아래로 나가는 것은 연결되지 않고 혼란스럽게 느껴집니다.
- 상호작용을 소스에 고정하십시오. 메뉴, 팝오버 또는 시트는 그것을 트리거한 요소에서 시작해야 합니다.
transform-origin을 트리거로 설정하여 버튼과 콘텐츠 간의 공간적 관계가 명확하도록 하십시오. (이것은 팝오버가 중앙이 아닌 트리거에서 크기가 조정되는 것과 동일한 원점 인식 지점입니다.) - 가역 전환에 이징을 미러링하여 나가는 경로가 돌아오는 경로와 일치하도록 하십시오(두 방향에 대해 역 큐빅-베지어 제어점 사용).
8. 제스처 방향으로 힌트 제공
인간은 궤적에서 최종 상태를 예측합니다. 중간 프레임은 사물이 어디로 가고 있는지 알려주어야 합니다. — 제어 센터 모듈은 "손가락을 향해 위아래로 자랍니다." 중간 프레임이 맹목적으로 보간하는 대신 결과를 가리키도록 만드십시오.
9. 고무줄 효과(Rubber-banding) — 부드러운 경계
경계에서는 딱딱하게 멈추는 대신 점진적으로 저항하십시오. 딱딱한 멈춤은 "얼어붙은" 것으로 읽힙니다. 연속적인 저항은 "반응하지만 더 이상은 없다"로 읽힙니다. 사용자가 경계를 넘어 드래그할수록 증가하는 감쇠를 적용하십시오.
// 경계를 넘을수록 요소가 덜 따라옵니다. 실제 사물은 멈추기 전에 느려집니다.
function rubberband(overshoot, dimension, constant = 0.55) {
return (overshoot * dimension * constant) / (dimension + constant * Math.abs(overshoot));
}
10. 제스처 디자인 세부 사항 ("느낌" 체크리스트)
- 탭: 터치-다운 시 강조 표시(즉시), 터치-업 시 커밋. 대상 주위에 ~10px의 히스테리시스/히트 패딩을 추가하고, 드래그-멀리 및 뒤로 취소를 허용하십시오.
- 드래그/스와이프: 방향을 커밋하기 전에 작은 움직임 임계값(히스테리시스, ~10px)을 요구한 다음 1:1로 추적하십시오.
- 첫 번째 움직임부터 모든 그럴듯한 제스처를 병렬로 감지한 다음 의도가 명확해지면 패자를 자신 있게 취소하십시오. 최종 상태만 보고하는 인식기(
swipeleft유형 이벤트)는 피하십시오. 피드백에 필요한 연속 추적을 버립니다. - 구분 지연을 최소화하십시오. 더블 탭 감지는 불가피하게 단일 탭을 지연시킵니다. 더블 탭이 실제로 존재하는 경우에만 해당 비용을 지불하십시오.
11. 프레임 수준의 부드러움
부드러움은 프레임 속도뿐만 아니라 프레임의 내용에 관한 것입니다.
- 지각 임계값 아래로 프레임당 위치 변화를 유지하여 스트로빙을 피하십시오.
- 매우 빠른 모션의 경우 미묘한 모션 블러 / 스트레치가 속도를 인코딩하며 날카로운 하드 스트릭보다 더 잘 읽힙니다.
requestAnimationFrame은 웹의 디스플레이 동기화 클록입니다(Apple은CADisplayLink사용).transform및opacity와 같은 컴포지터 친화적인 속성에만 애니메이션을 적용하고, 모션이 임박한 곳에will-change로 힌트를 제공하십시오.
12. 재료 및 깊이 — 반투명이 계층 구조 전달
Apple은 반투명 재료를 구조를 제공하면서도 주의를 빼앗지 않는 떠 있는 기능적 레이어로 사용합니다. 웹에서는 backdrop-filter로 근사하십시오.
- 네비게이션/툴바/시트를 반투명 레이어로 구축하십시오(
backdrop-filter: blur()+ 반투명 배경) 고정된 스트립을 소비하는 불투명 막대가 아니라 콘텐츠가 아래에서 스크롤되도록 하십시오. - 재료 무게는 계층 구조를 인코딩합니다: 더 어둡고 무거운 재료는 구조적 영역(사이드바)을 구분합니다. 더 가벼운 재료는 대화형 요소(버튼)에 주의를 끕니다. 다른 것 위에 밝은 반투명 표면을 절대 쌓지 마십시오 — 가독성이 무너집니다.
- 더 큰 표면은 더 두껍게 읽혀야 합니다: 작은 칩보다 더 강한 블러와 더 깊은 그림자. 상황 인식 그림자를 고려하십시오. — 분리를 위해 바쁜/텍스트 콘텐츠 위에서는 더 무겁게, 평범한 배경 위에서는 더 가볍게.
- 어둡게 하여 초점을 맞추고, 분리하여 흐름을 유지하십시오. 모달 작업은 표면을 어두워지는 스크림과 짝을 이루고 배경을 뒤로/아래로 밉니다. 병렬, 비차단 패널은 흐름이 끊어지지 않도록 스크림 없이 반투명과 오프셋을 사용합니다. 쌓인 시트의 경우 각 상위 레이어를 점진적으로 어둡게 하고 뒤로 밉니다.
- 생생함(Vibrancy)은 변화하는 배경 위에서 텍스트 가독성을 유지합니다. 흐릿/반투명 표면 위에서는 평평한 회색 텍스트를 사용하지 마십시오. 더 높은 대비, 약간 더 무거운 두께, 그리고 작은 자간 증가를 사용하십시오. 색상은 반투명 전경이 아닌 단색 레이어에 배치하십시오.
- 스크롤 가장자리 효과, 하드 구분선이 아닙니다. 고정 헤더 아래에 1px 테두리 대신, 떠 있는 UI가 실제로 콘텐츠와 겹치는 곳에서만 콘텐츠가 떠 있는 크롬을 만나는 곳에 작은 블러/그라데이션 마스크를 페이드 하십시오.
- 페이드만 하지 말고, 물질화하십시오. 유리/블러 표면의 경우, 진입/퇴장 시 블러 반경과 크기를 함께 애니메이션하여 표면이 단순한 불투명도 페이드보다는 실제 재료가 도착하는 것으로 읽히도록 하십시오.
.toolbar {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px) saturate(180%);
border-top: 1px solid rgba(255, 255, 255, 0.4); /* 밝은 상단 가장자리 = 재료에 닿는 빛 */
}
13. 다중 감각 피드백 — 모션 + 사운드 + 햅틱
감각을 결합하기 위한 세 가지 규칙(Designing Audio-Haptic Experiences에서):
- 인과성 — 무엇이 피드백을 일으켰는지 분명해야 합니다. 실제 인과 이벤트(토글 플립, 항목 스냅)에서 트리거하고 그 특성을 동작의 물리성과 일치시키십시오.
- 조화 — 시각, 소리 및 햅틱은 동일한 프레임에서 실행되어야 합니다. 그들 사이의 지연은 환상을 파괴합니다.
...