GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

input 자동완성 검색창 — 하이라이트·디바운스·편집거리 계산을 얹은 9종

입력한 글자가 제안 항목 안에서 실제로 어디에 있는지 찾아 그 부분만 굵게 칠하고, 300ms 디바운스로 요청을 모아 키보드로도 목록이 순환합니다. 최근 검색·오타 제안·인기 순위 막대까지 — input 자동완성 검색창 9종, 코드는 비밀번호 zip으로 드립니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

데모

아홉 검색창 모두 같은 role="searchbox" 입력 하나에서 시작해 그 위에 뜨는 제안 목록만 다르게 계산합니다. input 자동완성 만들기를 검색하면 나오는 예제 대부분이 목록을 정적으로 나열하는 것과 달리, 이 아홉 개는 입력이 바뀔 때마다 일치 위치·타이머·편집거리·비율 같은 값을 다시 구해 화면에 반영합니다. 사람이 손대지 않을 때는 정해진 단어를 한 글자씩 자동으로 쳐서 같은 장면이 반복 재생되며, 01·04·06 세 항목은 그 타이핑 사이에 짧은 대기가 있어 다른 항목보다 프레임이 적게 잡히는 게 정상입니다.

실측 한눈에

항목 초점 누적면적(%) 강도평균 움직인프레임(23 기준)
01 일치 하이라이트 indexOf 로 찾은 구간을 mark 15.73 177.2 6
02 디바운스 쉬머 clearTimeout 리셋 + pendingId 22.25 33.3 22
03 키보드 순환 (i+d+len) % len 35.73 94.6 23
04 최근 검색 칩 저장 시각을 다시 계산 7.15 59.8 7
05 그룹 고정 헤더 헤더 간격을 그림자 비율로 34.70 59.6 23
06 오타 제안 레벤슈타인 DP 표 45.98 89.8 7
07 인기 급상승 막대 count/max 를 scaleX 4.80 184.9 12
08 모바일 시트 변신 visualViewport 로 lerp 29.61 66.5 13
09 리듀스 모션 스태거만 0으로 30.96 47.1 20

정지 구간이 있는 게 결함이 아니다

01·04·06 은 자동 재생 사이 타이핑을 멈추는 순간이 들어 있어 23프레임 중 6~7프레임만 값이 바뀝니다. 03·05 처럼 스크롤·포인터로 계속 움직이는 항목과 같은 잣대로 비교하면 안 되는 이유가 여기 있습니다 — 06 오타 제안은 누적면적이 45.98% 로 아홉 개 중 가장 넓은데도 움직인프레임은 7 뿐이라, "얼마나 넓게 바뀌는가"와 "얼마나 자주 바뀌는가"는 서로 다른 수치입니다. 강도평균은 07 인기 급상승 막대가 184.9 로 가장 세고 02 디바운스 쉬머가 33.3 으로 가장 차분한데, 둘 다 BD3 게이트 하한 15 를 넉넉히 넘습니다.

input 자동완성의 해부 — input·목록·선택의 3층과 aria-combobox 구조

입력창은 role="combobox" 를 가진 wrapper 안에 있다

실제로 글자를 받는 <input role="searchbox" aria-autocomplete="list"> 는 혼자 있지 않고, role="combobox" aria-expanded aria-haspopup="listbox" 를 가진 감싸는 요소 안에 들어 있습니다. 목록이 열리면 이 wrapper 의 aria-expanded 가 true 로 바뀌고, aria-controls 와 aria-owns 가 그 목록의 id 를 가리켜 스크린리더가 "지금 이 입력창이 저 목록을 열었다"를 알 수 있습니다.

목록은 role="listbox", 항목은 role="option" 이다

제안 목록은 <ul role="listbox">, 그 아래 각 줄은 <li role="option"> 로 표시됩니다. 이 두 role 이 없으면 시각적으로는 똑같은 드롭다운이어도 스크린리더는 그냥 나열된 텍스트로만 읽어, 몇 번째 항목인지·몇 개 중 몇 번째인지를 안내하지 못합니다.

선택 위치는 focus 이동이 아니라 aria-activedescendant 로 알린다

키보드로 항목을 옮길 때 실제 DOM 포커스는 여전히 input 에 있고, "지금 몇 번째가 활성인가"는 input 의 aria-activedescendant 속성이 그 항목의 id 를 가리켜서 전달합니다. 포커스를 목록 항목으로 옮기면 입력이 끊기므로, 이 3층(입력·목록·선택 표시)이 나뉘어 있는 구조 자체가 자동완성이 동작하는 방식입니다.

아홉 검색창 단계별 코드

01일치 하이라이트 — 입력 글자가 제안 안 어디에 있는지 찾아 굵게 칠하는 계산

제안 문자열을 소문자로 바꾼 사본에서 입력어의 위치를 indexOf 로 찾고, 찾은 구간만 <mark> 로 잘라 감싼 뒤 다음 위치부터 다시 찾는 반복으로 다중 일치를 전부 강조합니다. 대소문자는 비교할 때만 통일하고 실제로 화면에 그리는 문자열은 원래 대소문자를 그대로 씁니다.

일치 하이라이트 — 입력 글자가 제안 안 어디에 있는지 찾아 굵게 칠하는 계산
새 창에서 열기

01 · 일치 하이라이트 — 입력 글자가 제안 안 어디에 있는지 찾아 굵게 칠하는 계산 — 측정 누적면적 15.731% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

beforeinputCSS custom propertiesaria-expanded

indexOf 가 -1 을 돌려주는 순간 반복이 끝난다

function highlightAll(text, query) {
  if (!query) return escapeHtml(text);
  var lowerText = text.toLowerCase();
  var lowerQuery = query.toLowerCase();
  var out = '', i = 0;
  while (i < text.length) {
    var idx = lowerText.indexOf(lowerQuery, i);
    if (idx === -1) { out += escapeHtml(text.slice(i)); break; }
    out += escapeHtml(text.slice(i, idx));
    out += '<mark class="hl__mark">' + escapeHtml(text.slice(idx, idx + query.length)) + '</mark>';
    i = idx + query.length;
  }
  return out;
}

02디바운스 쉬머 — 300ms 동안 입력을 모으고 기다리는 동안 목록이 반짝이는 상태

매 입력마다 clearTimeout 으로 이전 타이머를 지우고 새 타이머를 걸어, 300ms 동안 다음 글자가 오지 않을 때만 실제로 목록을 갱신합니다. 대기하는 동안은 스켈레톤 행이 쉬머로 흐르고, pendingId 값으로 늦게 도착한 이전 요청의 결과를 버려 화면이 엉키지 않게 합니다.

디바운스 쉬머 — 300ms 동안 입력을 모으고 기다리는 동안 목록이 반짝이는 상태
새 창에서 열기

02 · 디바운스 쉬머 — 300ms 동안 입력을 모으고 기다리는 동안 목록이 반짝이는 상태 — 측정 누적면적 22.247% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

setTimeoutrequestAnimationFrametransition

pendingId 가 안 맞으면 이미 낡은 응답이다

function onChange(value) {
  clearTimeout(debounceTimer);
  pendingId++;
  var myId = pendingId;
  if (!value) {
    state = 'idle';
    renderIdle();
    return;
  }
  state = 'loading';
  renderSkeleton();
  debounceTimer = setTimeout(function () {
    setTimeout(function () {
      if (myId !== pendingId) return;
      var q = value.toLowerCase();
      results = WORDS.filter(function (w) { return w.toLowerCase().indexOf(q) !== -1; });
      state = 'done';
      renderResults(value);
    }, FAKE_LATENCY_MS);
  }, DEBOUNCE_MS);
}

03키보드 순환 — ↑↓ 로 항목을 고르고 끝에서 반대편으로 돌아오는 mod 계산

활성 인덱스를 (activeIndex + delta + length) % length 로 계산하면 마지막 항목에서 ↓ 를 눌러도 인덱스가 음수나 범위 밖으로 새지 않고 자연스럽게 첫 항목으로 돌아옵니다. 활성 항목이 바뀔 때마다 aria-activedescendant 를 갱신하고 scrollIntoView({block:'nearest'}) 로 목록 밖에 가려지지 않게 끌어옵니다.

키보드 순환 — ↑↓ 로 항목을 고르고 끝에서 반대편으로 돌아오는 mod 계산
새 창에서 열기

03 · 키보드 순환 — ↑↓ 로 항목을 고르고 끝에서 반대편으로 돌아오는 mod 계산 — 측정 누적면적 35.733% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

keydownaria-activedescendantscrollIntoView

length 를 더해 음수 나머지를 피한다

function updateActive() {
  for (var i = 0; i < items.length; i++) items[i].classList.remove('is-active');
  var el = items[activeIndex];
  if (!el) return;
  el.classList.add('is-active');
  input.setAttribute('aria-activedescendant', el.id);
  el.scrollIntoView({ block: 'nearest' });
}

function moveActive(delta) {
  activeIndex = (activeIndex + delta + WORDS.length) % WORDS.length;
  updateActive();
}

04최근 검색 칩 — 저장 시각을 재계산해 '3분 전·2일 전' 라벨을 다시 쓰는 칩

검색어와 저장 시각을 localStorage 에 쌓아 두고, 칩을 열 때마다 지금 시각과의 차를 분·시간·일 단위로 다시 계산해 라벨을 새로 씁니다. 값 자체를 문자열로 저장하지 않고 숫자 타임스탬프로 저장하기 때문에, 페이지를 다시 열어도 "3분 전"이 시간이 지난 만큼 정확하게 "1시간 전"으로 바뀝니다.

최근 검색 칩 — 저장 시각을 재계산해 '3분 전·2일 전' 라벨을 다시 쓰는 칩
새 창에서 열기

04 · 최근 검색 칩 — 저장 시각을 재계산해 '3분 전·2일 전' 라벨을 다시 쓰는 칩 — 측정 누적면적 7.149% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

localStoragetransform: scaleCSS custom properties

세 단위 중 가장 작은 것부터 순서대로 확인한다

function label(delta) {
  if (delta < MIN) return '방금';
  if (delta < HOUR) return Math.floor(delta / MIN) + '분 전';
  if (delta < DAY) return Math.floor(delta / HOUR) + '시간 전';
  return Math.floor(delta / DAY) + '일 전';
}

05그룹 고정 헤더 — 목록을 그룹으로 나누고 스크롤에 헤더가 붙어 내려오는 구조

제안을 카테고리별로 그룹화하고 각 그룹 헤더를 position: sticky 로 두면, 목록을 스크롤해도 지금 보는 그룹의 이름이 상단에 붙어 따라옵니다. 다음 그룹 헤더가 지금 헤더 밑까지 올라온 거리를 매 스크롤마다 계산해 0~1 비율로 바꾸고, 그 비율만큼 그림자와 밑선을 짙게 만들어 헤더가 겹치는 순간을 눈으로 읽히게 합니다.

그룹 고정 헤더 — 목록을 그룹으로 나누고 스크롤에 헤더가 붙어 내려오는 구조
새 창에서 열기

05 · 그룹 고정 헤더 — 목록을 그룹으로 나누고 스크롤에 헤더가 붙어 내려오는 구조 — 측정 누적면적 34.696% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

position: stickyscrollCSS custom properties

다음 헤더가 가까워질수록 그림자가 짙어진다

function applyShadow(i, ratio) {
  var head = heads[i];
  if (!head) return;
  var alpha = 0.10 + 0.28 * ratio;
  head.style.boxShadow = '0 6px 10px rgba(23,20,26,' + alpha.toFixed(3) + ')';
  head.style.borderBottomColor = 'rgba(23,20,26,' + (0.08 + 0.4 * ratio).toFixed(3) + ')';
}

function measureStuck() {
  var listTop = list.getBoundingClientRect().top;
  for (var i = 0; i < heads.length; i++) {
    var next = heads[i + 1];
    if (!next) { applyShadow(i, 0); continue; }
    var headH = heads[i].offsetHeight;
    var nextTop = next.getBoundingClientRect().top - listTop;
    var ratio = 1 - Math.min(1, Math.max(0, (nextTop - headH) / headH));
    ratio = Math.min(1, Math.max(0, ratio));
    applyShadow(i, ratio);
  }
}

06오타 제안 — 편집거리를 계산해 '이것을 찾으셨나요?'를 띄우는 계산

입력어와 후보 사이의 레벤슈타인 편집거리를 2차원 DP 표로 계산해, 거리가 1~2 이하면 오타로 판정하고 원래 단어를 "이것을 찾으셨나요?" 줄에 제안합니다. 표의 각 칸은 두 글자가 같으면 왼쪽 위 칸을, 다르면 위·왼쪽·왼쪽 위 세 칸 중 가장 작은 값에 1을 더한 값을 가집니다.

오타 제안 — 편집거리를 계산해 '이것을 찾으셨나요?'를 띄우는 계산
새 창에서 열기

06 · 오타 제안 — 편집거리를 계산해 '이것을 찾으셨나요?'를 띄우는 계산 — 측정 누적면적 45.977% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

beforeinputCSS custom propertiesaria-expanded

글자가 같으면 대각선 칸을 그대로 물려받는다

function normalize(s) {
  return s.toLowerCase().trim();
}

function editDistance(a, b) {
  var m = a.length, n = b.length;
  var dp = [];
  for (var i = 0; i <= m; i++) { dp.push([i]); }
  for (var j = 0; j <= n; j++) { dp[0][j] = j; }
  for (i = 1; i <= m; i++) {
    for (j = 1; j <= n; j++) {
      if (a[i - 1] === b[j - 1]) dp[i][j] = dp[i - 1][j - 1];
      else {
        dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
      }
    }
  }
  return dp[m][n];
}

07인기 급상승 막대 — 검색 횟수를 최댓값 대비 비율로 스케일해 그리는 순위 막대

각 제안의 검색 횟수를 목록 안 최댓값으로 나눠 0~1 비율을 구하고, 그 값을 transform: scaleX(var(--ratio)) 로 막대 길이에 그대로 매핑합니다. 순위가 오르내리면 이전 순위와 지금 순위를 비교해 위·아래·변화 없음 세 아이콘 중 하나를 고릅니다.

인기 급상승 막대 — 검색 횟수를 최댓값 대비 비율로 스케일해 그리는 순위 막대
새 창에서 열기

07 · 인기 급상승 막대 — 검색 횟수를 최댓값 대비 비율로 스케일해 그리는 순위 막대 — 측정 누적면적 4.804% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

transform: scaleXCSS custom propertiestransition

막대 길이는 count 를 max 로 나눈 값 그 자체다

function render() {
  var rows = ranked.map(function (d, i) {
    var currRank = i + 1;
    var ratio = d.count / max;
    return '<li class="tr__row" role="option" style="--i:' + i + '">' +
      '<span class="tr__rank">' + currRank + '</span>' +
      '<span class="tr__q">' + d.q + '</span>' +
      '<span class="tr__delta">' + deltaIcon(d.prevRank, currRank) + '</span>' +
      '<span class="tr__track"><span class="tr__bar" style="--ratio:' + ratio.toFixed(3) + '"></span></span>' +
      '<span class="tr__count">' + d.count + '</span>' +
      '</li>';
  });
  list.innerHTML = rows.join('');
}

08모바일 시트 변신 — 화면 좁으면 드롭다운이 바텀시트로 바뀌는 미디어쿼리 전환

window.matchMedia('(max-width: 340px)') 로 뷰포트 폭을 감지해 좁은 화면에서는 같은 목록 DOM 을 하단에 고정된 시트로 다시 배치합니다. 실제 기기에서는 visualViewport 로 가상 키보드가 밀어 올린 높이를 읽어 시트가 그만큼 줄어들고, 정적 캡처에서는 그 값을 목표값 사이를 오가는 lerp 로 대신 흉내 냅니다.

모바일 시트 변신 — 화면 좁으면 드롭다운이 바텀시트로 바뀌는 미디어쿼리 전환
새 창에서 열기

08 · 모바일 시트 변신 — 화면 좁으면 드롭다운이 바텀시트로 바뀌는 미디어쿼리 전환 — 측정 누적면적 29.61% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

visualViewporttransitionmatchMedia

current 가 target 을 매 프레임 12% 씩 따라간다

var vv = window.visualViewport;
function realKbPx() {
  if (!vv) return 0;
  return Math.max(0, window.innerHeight - vv.height);
}
// 정적 캡처에서는 realKbPx() 가 대부분 0 이라 화면에 보이는 열림·닫힘은
// 아래 목표값 lerp 가 만든다 — 실제 기기에서는 이 값이 목표값을 대신한다.
var current = 0, target = 0;
var lastFlip = 0;

function tick(now) {
  if (now - lastFlip > 900) {
    target = target === 0 ? 1 : 0;
    lastFlip = now;
  }
  current += (target - current) * 0.12;
  sheet.style.setProperty('--kb', current.toFixed(4));
  if (playing) requestAnimationFrame(tick);
}

09리듀스 모션 — 쉬머·스태거를 끄고 목록이 즉시 뜨는 정적 대체

prefers-reduced-motion 을 matchMedia 로 감지하면 각 항목이 순서대로 나타나는 스태거 지연 값을 0으로 바꿔, 목록이 한 번에 뜨게 합니다. 디바운스가 기다리는 300ms 자체는 그대로 두는데, 그 대기는 요청 절약을 위한 것이라 모션과는 아무 관계가 없기 때문입니다.

리듀스 모션 — 쉬머·스태거를 끄고 목록이 즉시 뜨는 정적 대체
새 창에서 열기

09 · 리듀스 모션 — 쉬머·스태거를 끄고 목록이 즉시 뜨는 정적 대체 — 측정 누적면적 30.961% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

prefers-reduced-motiontransitionaria-expanded

step 이 0 이면 --delay 도 전부 0ms 다

function open() {
  var step = reduced ? 0 : STEP_MS;
  list.innerHTML = WORDS.map(function (w, i) {
    return '<li class="rm__item" role="option" style="--delay:' + (i * step) + 'ms">' + w + '</li>';
  }).join('');
  input.setAttribute('aria-expanded', 'true');
  requestAnimationFrame(function () {
    requestAnimationFrame(function () {
      list.classList.add('is-open');
    });
  });
}

검색창 아홉 종, 실제로 어디에 앉히나

사이트 상단의 통합 검색창, 쇼핑몰의 상품 검색, 헬프센터의 문서 검색 — 이 세 자리가 자동완성 목록을 가장 자주 요구합니다. 01 일치 하이라이트는 상품명처럼 사용자가 입력한 글자가 어디에 걸렸는지 바로 보여줘야 하는 검색에 맞고, 04 최근 검색 칩은 같은 단어를 반복해서 찾는 헬프센터 검색에 맞습니다.

인기 검색어가 자주 바뀌는 검색창에서는 07 인기 급상승 막대가 지금 몰리는 단어를 순위로 보여줍니다. 세 화면 모두 입력 한 글자마다 목록이 다시 계산돼야 한다는 점이 같아서, 디바운스와 순위 계산을 한 데 묶은 이 구조가 그대로 들어맞습니다.

목록은 계산이다 — 부분 일치 검색·편집거리·순위 점수 공식

부분 일치는 정렬 없이 필터만 한다

01·02 가 쓰는 indexOf(query) !== -1 은 "포함하는가"만 답하는 가장 단순한 검색이라, 결과 순서는 원래 배열 순서를 그대로 따라갑니다. 순위를 매기고 싶다면 07 처럼 값(검색 횟수)을 따로 두고 그 값으로 다시 정렬해야 하며, 문자열 일치 자체에는 순서 정보가 없습니다.

편집거리는 삽입·삭제·교체 세 연산의 최소 합이다

06 의 DP 표에서 대각선으로 물려받는 값은 "고칠 필요 없음"이고, 위·왼쪽·대각선 중 최솟값에 1을 더하는 것은 각각 삽입·삭제·교체 한 번을 의미합니다. 표의 마지막 칸(dp[m][n])이 두 문자열을 완전히 같게 만드는 데 필요한 최소 횟수이고, 이 값이 임계값(보통 1~2) 이하일 때만 오타로 판정합니다.

순위 막대는 나눗셈 하나로 완성된다

07 의 막대 길이는 별도의 스케일 함수 없이 count / max 한 줄로 끝나는데, 이 나눗셈이 0~1 사이 값을 만들어 그대로 --ratio CSS 변수에 들어가기 때문입니다. 최댓값이 목록 안에서 바뀌면(예: 1위가 내려가면) 모든 막대의 비율이 같은 기준으로 다시 계산되어, 순위표 전체가 한 번에 재조정됩니다.

어디서 깨지는가 — 모바일 키보드·IME·스크롤 함정

모바일 키보드가 목록을 밀어 올린다

08 처럼 visualViewport 를 안 읽고 window.innerHeight 만 기준으로 목록 높이를 잡으면, 가상 키보드가 올라온 만큼 목록 아래쪽이 키보드 밑에 가려집니다. 바텀시트로 바꾸는 미디어쿼리 자체는 화면 폭만 보므로, 키보드가 차지한 실제 여유 공간은 visualViewport.height 를 따로 확인해야 합니다.

한글 입력 중에는 아직 완성되지 않은 글자가 온다

한글·일본어 같은 조합형 입력(IME)에서는 input 이벤트가 자음·모음이 합쳐지는 중간 글자마다 먼저 발생해, 02 의 디바운스가 끝나는 순간에도 사용자가 아직 "타이핑 중"일 수 있습니다. 이런 입력에서는 compositionstart·compositionend 이벤트로 조합이 끝났는지 확인한 뒤에 검색을 걸어야, 의미 없는 중간 글자로 목록이 계속 바뀌는 것을 막을 수 있습니다.

스크롤이 activedescendant 위치를 놓칠 수 있다

05 의 그룹 목록에서 3 처럼 mod 순환으로 활성 항목을 옮기면, 그 항목이 고정 헤더 바로 아래 가려질 수 있어 scrollIntoView({block:'nearest'}) 만으로는 헤더 높이만큼 위치가 어긋날 수 있습니다. 고정 헤더가 있는 목록에서는 스크롤 컨테이너의 padding-top 을 헤더 높이만큼 미리 잡아 두는 편이 안전합니다.

반짝임 시간과 순환 속도를 늦추면 접근성도 함께 정리된다

값 하나만 바꾼 세 변형

02 의 디바운스 대기 시간을 300ms 에서 500ms 로 늘리면 요청 수는 절반 아래로 줄지만 목록이 "가라앉은 뒤"에만 바뀌는 침착한 느낌이 됩니다. 06 의 오타 판정 거리를 2 에서 1 로 좁히면 aple 에 apple·maple 둘 다 붙던 것이 가장 확실한 정정 하나만 남아 제안이 조심스러워지고, 01 의 목록 최대 노출 수를 8개에서 12개로 늘리면 스크롤이 생기면서 05·07 의 그룹·순위 계산이 본격적으로 눈에 들어옵니다. 세 변형은 값 하나만 바꿔 느낌이 실제로 달라짐을 보여주는 확인용이고, 아홉 항목 전체와 함께 결과 zip 에 들어 있습니다 — zip 을 여는 암호는 8gshpuef 입니다.

09 는 계산을 지우지 않고 화면 표시만 지운다

리듀스 모션이 꺼도 되는 건 쉬머·스태거 같은 "보여주기 위한" 지연뿐이고, 하이라이트 위치 계산·편집거리·순위 정렬 같은 "결과를 만들기 위한" 계산은 모션 여부와 무관하게 그대로 실행됩니다. input 자동완성 css 로 애니메이션만 걷어내도 기능이 그대로 남는 이유가 여기 있습니다.

검색창에 자주 붙는 질문 세 가지

input 자동완성 만들기에 서버 API 가 꼭 있어야 하나요?

아닙니다. 이 아홉 항목은 전부 고정 배열 안에서 indexOf·편집거리·비율 계산만으로 목록을 만들고, 외부 API 호출은 없습니다. 실제 서비스에서 검색 API 를 붙일 때도 02 의 디바운스·pendingId 폐기 구조는 그대로 얹을 수 있습니다.

input 자동완성 끄기 css 로 브라우저 자체 자동완성만 없애고 싶다면요?

브라우저가 과거 입력을 기억해 보여주는 팝업은 이 아홉 개 목록과 다른 기능이라, input 태그에 autocomplete="off" 를 주면 됩니다. 이번 데모의 입력창도 우리가 그리는 목록과 브라우저 팝업이 겹치지 않도록 전부 autocomplete="off" 를 붙여 뒀습니다.

09 항목만 켜면 나머지 여덟 개도 같이 리듀스 모션이 되나요?

아닙니다. 각 항목은 독립된 데모라 09 안의 스위치는 그 항목의 스태거만 끄고, 실제 서비스에 적용할 때는 matchMedia('(prefers-reduced-motion: reduce)') 를 아홉 항목 각각의 애니메이션 값에 따로 걸어야 합니다.

더 읽을거리

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.