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

이모지 검색기 9가지 — 값 하나로 여닫는 패턴

이모지 검색기 아홉 가지를 나란히 놓고 보면, 입력창 아래 남은 공간 값이 패널 높이보다 작을 때만 패널이 위로 열리는 자리로 바뀌고, 누르고 있는 시간 값이 정해둔 값을 넘을 때만 같은 이모지의 변형이 부채꼴로 펼쳐지며, 커서 위치 값을 미리 저장해 둔 경우에만 고른 이모지가 그

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

계산해 둔 값 하나가 이모지 피커 아홉 개의 동작을 가릅니다

이모지 피커는 겉보기엔 비슷해 보여도 항목마다 계산해 둔 값 하나로 서로 다른 동작을 결정합니다. 01은 사용 횟수, 02는 스크롤 거리, 03은 유사도 점수, 04는 고른 톤 값, 05는 누른 시간, 06은 방향키와 실제 열 수, 07은 남은 공간, 08은 저장해 둔 커서 위치, 09는 동작 줄임 여부 — 이 값이 바뀌는 순간마다 패널이 다시 정렬되거나 펼쳐지거나 자리를 바꿉니다. 항목마다 그 한 값을 만들어 내는 함수와, 값이 바뀐 뒤 화면 어디에 다시 쓰이는지를 실제 소스 그대로 아래에서 짚습니다.

01사용 빈도 값으로 순서가 바뀌는 최근 사용 탭

이모지를 고를 때마다 그 이모지의 사용 횟수 값이 하나씩 올라가고, 최근 사용 탭은 그 값이 큰 순서대로 다시 정렬되어 자주 쓰는 이모지가 앞쪽에 남습니다. rank()는 사용 횟수 내림차순으로 정렬하되 동률이면 lastPicked 값의 최근 순으로 다시 줄을 세워 RECENT_MAX개까지만 자르고, fillItem()이 그 순위를 aria-label과 배지 숫자로 함께 그립니다.

사용 빈도 값으로 순서가 바뀌는 최근 사용 탭
새 창에서 열기

01 · 사용 빈도 값으로 순서가 바뀌는 최근 사용 탭 — 측정 누적면적 2.785% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RECENT_MAXSEQrenderRecent
  // 계산 겹: 사용 횟수 값 내림차순, 동률이면 가장 최근에 고른 순으로 다시 줄을 세운다
  function rank() {
    return EMOJIS.slice().sort(function (a, b) {
      var d = (counts[b.key] || 0) - (counts[a.key] || 0);
      return d !== 0 ? d : (lastPicked[b.key] || 0) - (lastPicked[a.key] || 0);
    }).slice(0, RECENT_MAX);
  }

  function fillItem(li, e, idx) {
    li.dataset.key = e.key;
    li.setAttribute('role', 'option');
    li.setAttribute('aria-selected', 'false');
    li.setAttribute('aria-label', e.key + ', 사용 ' + (counts[e.key] || 0) + '회, 순위 ' + (idx + 1));
    li.innerHTML = '<b class="ep__rank">' + (idx + 1) + '</b><span class="ep__ch">' + e.ch + '</span><span class="ep__count">' + (counts[e.key] || 0) + '</span>';
  }

02스크롤 위치 값과 탭 번호가 서로 맞춰지는 카테고리 전환

패널을 손으로 내리면 지나간 거리 값을 읽어 지금 보이는 카테고리의 탭이 자동으로 밝아지고, 반대로 탭을 누르면 그 탭의 번호 값만큼 패널이 스크롤되어 둘이 항상 같은 자리를 가리킵니다. onScroll()이 scrollTop 값을 각 섹션의 offsetTop과 비교해 지금 보이는 카테고리 번호를 역산하고, setActive()가 만든 스크롤과 사람이 만든 스크롤을 구분하려고 syncing 플래그로 잠깐 무시 구간을 둡니다.

스크롤 위치 값과 탭 번호가 서로 맞춰지는 카테고리 전환
새 창에서 열기

02 · 스크롤 위치 값과 탭 번호가 서로 맞춰지는 카테고리 전환 — 측정 누적면적 16.651% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CATSsetActivescrollTop
  // 계산 겹: 패널을 손으로 내리면 지나간 거리 값(scrollTop)을 각 섹션의 오프셋과 비교해
  // 지금 보이는 카테고리 번호를 역산하고, 그 번호로 탭 표시를 맞춘다.
  function onScroll() {
    if (syncing) return;
    var dist = panelEl.scrollTop, i = 0;
    for (var k = 0; k < sections.length; k++) {
      if (sections[k].offsetTop - panelEl.clientTop <= dist + 18) i = k;
    }
    if (i !== active) { active = i; setActive(i); live.textContent = CATS[i].name + ' 카테고리 보임'; }
  }
  panelEl.addEventListener('scroll', function () { human(); onScroll(); });

03입력한 글자와 이름 사이 유사도 값으로 줄을 세우는 검색

입력창에 글자를 넣을 때마다 이모지 이름과 그 글자 사이의 유사도 값을 다시 매겨, 완전히 같지 않아도 값이 높은 이모지부터 위에서 보여주는 것이 이 이모지 검색 상자의 핵심입니다. score()는 입력 글자를 이름 글자열 안에서 순서대로 찾아가며 연속으로 이어질수록·앞쪽에서 맞을수록 값을 더 높게 매기고, 입력마다 EMOJI_SEARCH_DEBOUNCE_MS(200ms) 뒤에 한 번만 다시 계산합니다.

입력한 글자와 이름 사이 유사도 값으로 줄을 세우는 검색
새 창에서 열기

03 · 입력한 글자와 이름 사이 유사도 값으로 줄을 세우는 검색 — 측정 누적면적 33.422% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

EMOJI_SEARCH_DEBOUNCE_MSscorerank
  // 계산 겹: 입력 글자를 이름 글자열 안에서 순서대로 찾아가며, 연속으로 이어질수록·앞쪽에서
  // 맞을수록 유사도 값을 더 높게 쳐서 완전히 같지 않아도 값 순서로 줄을 세운다.
  function score(query, name) {
    if (!query) return 1;
    var qi = 0, s = 0, streak = 0;
    for (var ni = 0; ni < name.length && qi < query.length; ni++) {
      if (name[ni] === query[qi]) {
        streak++;
        s += 1 + streak * 2 + (ni === 0 ? 4 : 0);
        qi++;
      } else {
        streak = 0;
      }
    }
    if (qi < query.length) return 0;
    return s;
  }

04고른 톤 값에 따라 해당 이모지 집합이 통째로 바뀌는 톤 전환

톤 선택 칸에서 고른 톤 값을 저장해 두었다가, 사람 형태를 가진 이모지가 화면에 그려질 때마다 그 값을 같이 읽어 손·얼굴이 있는 부분만 고른 톤으로 바뀐 버전으로 대신 그립니다. combine()은 BASES 배열의 기본 코드포인트에 EMOJI_TONE_MODIFIERS(U+1F3FB~U+1F3FF) 중 하나를 이어 붙여 새 문자열을 만들 뿐이라, 미리 그려 둔 이미지를 바꿔 끼우는 것이 아니라 매번 코드포인트를 조합합니다.

고른 톤 값에 따라 해당 이모지 집합이 통째로 바뀌는 톤 전환
새 창에서 열기

04 · 고른 톤 값에 따라 해당 이모지 집합이 통째로 바뀌는 톤 전환 — 측정 누적면적 3.824% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

EMOJI_TONE_MODIFIERScombineBASES
  // 계산 겹: 사람 형태 이모지의 원본 코드포인트에 톤 수정자 코드포인트(U+1F3FB~U+1F3FF)를 이어 붙여
  // 새 문자열을 만든다 — 미리 그려둔 다섯 벌을 갈아 끼우는 게 아니라 매번 코드포인트를 조합한다.
  var EMOJI_TONE_MODIFIERS = ['\u{1F3FB}', '\u{1F3FC}', '\u{1F3FD}', '\u{1F3FE}', '\u{1F3FF}'];
  var TONE_NAMES = ['기본', '톤 1', '톤 2', '톤 3', '톤 4', '톤 5'];
  var BASES = [
    { cp: '\u{1F44B}', name: 'waving hand' }, { cp: '\u{1F44D}', name: 'thumbs up' },
    { cp: '\u{1F44F}', name: 'clapping hands' }, { cp: '\u{270B}', name: 'raised hand' },
    { cp: '\u{1F64F}', name: 'folded hands' }, { cp: '\u{1F4AA}', name: 'flexed bicep' },
    { cp: '\u{1F44C}', name: 'ok hand' }, { cp: '\u{270C}', name: 'victory hand' },
    { cp: '\u{270D}', name: 'writing hand' }, { cp: '\u{1F64B}', name: 'raising hand' }
  ];

  function combine(base, toneIndex) {
    if (toneIndex === 0) return base;
    return base + EMOJI_TONE_MODIFIERS[toneIndex - 1];
  }

05누른 시간 값이 문턱을 넘으면 변형이 펼쳐지는 길게 누르기

손가락을 뗄 때까지 누르고 있는 시간 값을 계속 재다가 정해둔 값을 넘어서는 순간 같은 이모지의 색·표정이 다른 변형들이 부채꼴로 펼쳐지고, 그전에 떼면 원래 하나만 그대로 골라집니다. tick()이 매 프레임 performance.now() 값과 pressStart 값의 차를 EMOJI_LONGPRESS_THRESHOLD_MS(550ms)와 비교해 그 비율을 링 채움으로 보여주고, 값이 문턱을 넘는 순간 openFan()이 변형을 펼칩니다.

누른 시간 값이 문턱을 넘으면 변형이 펼쳐지는 길게 누르기
새 창에서 열기

05 · 누른 시간 값이 문턱을 넘으면 변형이 펼쳐지는 길게 누르기 — 측정 누적면적 5.267% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

EMOJI_LONGPRESS_THRESHOLD_MStickopenFan
  // 계산 겹: pointerdown 시각을 performance.now() 로 저장해 두고 매 프레임 지금 시각과의 차(누른
  // 시간 값)를 문턱과 비교한다 — 그 값을 그대로 링 채움 비율로 써서 문턱까지 남은 거리를 보여주고,
  // 값이 문턱을 넘는 순간에만 변형이 펼쳐진다.
  var pressing = false, pressStart = 0, fanOpened = false, rafId = 0, hoverIndex = -1;

  function tick() {
    if (!pressing) return;
    var elapsed = performance.now() - pressStart;
    var pct = Math.min(1, elapsed / EMOJI_LONGPRESS_THRESHOLD_MS);
    ringFill.style.transform = 'scaleX(' + pct + ')';
    if (pct >= 1 && !fanOpened) openFan();
    rafId = requestAnimationFrame(tick);
  }

  function openFan() {
    fanOpened = true;
    fan.hidden = false;
    requestAnimationFrame(function () { fan.classList.add('lp__fan--open'); });
    live.textContent = '변형 ' + VARIANTS.length + '개 펼쳐짐';
  }

06방향키 값으로 격자 안 초점이 옮겨 다니는 키보드 탐색

방향키를 누른 값을 격자의 가로·세로 칸 수와 맞춰 계산해, 화살표 하나에 초점이 옆 칸이 아니라 줄이 바뀌는 위치까지 정확히 건너뛰도록 만듭니다. measureCols()가 첫 줄 칸들의 getBoundingClientRect().top 값이 서로 같다는 사실로 지금 렌더된 실제 열 수를 재고, move()는 그 값을 방향키에 맞춰 ±1 또는 ±열 수만큼 focusIndex를 옮깁니다.

방향키 값으로 격자 안 초점이 옮겨 다니는 키보드 탐색
새 창에서 열기

06 · 방향키 값으로 격자 안 초점이 옮겨 다니는 키보드 탐색 — 측정 누적면적 6.242% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

measureColsmovesetFocus
  function move(key) {
    var cols = measureCols();
    var next = focusIndex;
    if (key === 'ArrowRight') next = focusIndex + 1;
    else if (key === 'ArrowLeft') next = focusIndex - 1;
    else if (key === 'ArrowDown') next = focusIndex + cols;
    else if (key === 'ArrowUp') next = focusIndex - cols;
    else if (key === 'Home') next = 0;
    else if (key === 'End') next = cells.length - 1;
    if (next < 0 || next >= cells.length) return;
    setFocus(next, true);
  }

  var NAV_KEYS = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
  grid.addEventListener('keydown', function (ev) {
    if (NAV_KEYS.indexOf(ev.key) === -1) return;
    ev.preventDefault();
    human();
    move(ev.key);
  });

07남은 공간 값을 계산해 여는 방향이 바뀌는 앵커 배치

입력창 위·아래로 남은 화면 공간 값을 각각 재서 더 넓은 쪽을 고르고, 아래쪽 공간 값이 패널 높이보다 작을 때만 패널이 입력창 위로 열리도록 자리를 바꿉니다. measureAndPlace()는 패널 높이를 짐작하지 않고 화면 밖에서 한 번 펼쳐 실제 렌더 높이를 재며, 아래쪽 공간 값이 패널 높이보다 EMOJI_PANEL_FLIP_MARGIN_PX(8px)만큼도 안 남을 때만 위로 뒤집습니다.

남은 공간 값을 계산해 여는 방향이 바뀌는 앵커 배치
새 창에서 열기

07 · 남은 공간 값을 계산해 여는 방향이 바뀌는 앵커 배치 — 측정 누적면적 27.115% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

EMOJI_PANEL_FLIP_MARGIN_PXmeasureAndPlacemovePosition
  function measureAndPlace() {
    var vpRect = viewport.getBoundingClientRect();
    var trRect = trigger.getBoundingClientRect();
    var wasHidden = panel.hidden;
    panel.style.visibility = 'hidden';
    panel.hidden = false;
    var panelH = panel.offsetHeight;
    panel.hidden = wasHidden;
    panel.style.visibility = '';
    var spaceBelow = vpRect.bottom - trRect.bottom;
    var spaceAbove = trRect.top - vpRect.top;
    var openAbove = (spaceBelow - EMOJI_PANEL_FLIP_MARGIN_PX) < panelH && spaceAbove > spaceBelow;
    panel.classList.toggle('af__panel--above', openAbove);
    panel.classList.toggle('af__panel--below', !openAbove);
    if (openAbove) {
      panel.style.bottom = (vpRect.bottom - trRect.top + EMOJI_PANEL_FLIP_MARGIN_PX) + 'px';
      panel.style.top = 'auto';
    } else {
      panel.style.top = (trRect.bottom - vpRect.top + EMOJI_PANEL_FLIP_MARGIN_PX) + 'px';
      panel.style.bottom = 'auto';
    }
    panel.style.left = (trRect.left + trRect.width / 2 - vpRect.left) + 'px';
    live.textContent = '남은 공간 아래 ' + Math.round(spaceBelow) + 'px · 위 ' + Math.round(spaceAbove) + 'px, ' + (openAbove ? '위로 열림' : '아래로 열림');
    return openAbove;
  }

08저장해 둔 커서 위치 값에 정확히 끼워 넣는 삽입

입력창을 벗어나기 직전의 커서 위치 값을 미리 저장해 두었다가, 이모지를 고른 순간 그 값이 가리키던 글자 사이로 되돌아가 끼워 넣어 커서가 늘 맨 끝으로 밀리지 않게 합니다. field가 blur될 때 selectionStart·selectionEnd 값을 savedStart·savedEnd에 저장해 두고, 이모지를 고르면 그 값이 가리키던 글자 사이에 문자열을 끼워 넣은 뒤 setSelectionRange()로 커서를 그 자리로 되돌립니다.

저장해 둔 커서 위치 값에 정확히 끼워 넣는 삽입
새 창에서 열기

08 · 저장해 둔 커서 위치 값에 정확히 끼워 넣는 삽입 — 측정 누적면적 2.754% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

savedStartselectionStartsetSelectionRange
  // 계산 겹: 입력창이 초점을 잃기 직전(blur)에 selectionStart/selectionEnd 값을 저장해 두고,
  // 이모지를 고르는 순간 그 값이 가리키던 글자 사이에 정확히 끼워 넣어 커서를 그 자리로 되돌린다.
  // (끝에 이어붙이면 값을 안 쓴 것과 같다 — 저장된 위치 값을 실제로 읽어 쓰는 것이 계산 겹이다.)
  var savedStart = 0;
  var savedEnd = 0;
  field.addEventListener('blur', function () {
    savedStart = field.selectionStart;
    savedEnd = field.selectionEnd;
  });

  field.addEventListener('input', function () {
    live.textContent = '글자 ' + field.value.length + '개, 커서 ' + field.selectionStart + '번째';
  });

09동작 줄임 값을 감지해 펼침 없이 바로 뜨는 패널

시스템의 동작 줄임 설정 값을 감지해 그 값이 켜져 있으면 패널이 펼쳐지는 과정 없이 바로 다 켜진 채로 나타나고, 꺼져 있을 때만 앞의 여덟 패턴이 원래 속도로 열립니다. applyMotionPref()가 reduceMQ.matches와 데모용 강제 값 forcedReduce를 합쳐 REDUCE_MOTION_DURATION_MS(0ms)와 NORMAL_MOTION_DURATION_MS(240ms) 중 하나를 고르고, 그 값을 패널 transitionDuration에 직접 놓습니다.

동작 줄임 값을 감지해 펼침 없이 바로 뜨는 패널
새 창에서 열기

09 · 동작 줄임 값을 감지해 펼침 없이 바로 뜨는 패널 — 측정 누적면적 10.636% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

REDUCE_MOTION_DURATION_MSapplyMotionPrefforcedReduce
  var REDUCE_MOTION_DURATION_MS = 0;
  var NORMAL_MOTION_DURATION_MS = 240;
  var reduceMQ = window.matchMedia('(prefers-reduced-motion: reduce)');
  var forcedReduce = false;

  // 계산 겹: 시스템의 동작 줄임 값(matchMedia.matches)과 데모용 강제 값(forcedReduce)을 합쳐
  // 실제로 쓸 지속시간 값을 계산해 패널에 직접 놓는다 — 미디어쿼리 블록이 있고 없고가 아니라
  // 그 값 자체를 읽어 duration 숫자를 정하고, 그 숫자를 배지·안내문에도 그대로 노출한다.
  function applyMotionPref() {
    var reduce = reduceMQ.matches || forcedReduce;
    var ms = reduce ? REDUCE_MOTION_DURATION_MS : NORMAL_MOTION_DURATION_MS;
    panel.style.transitionDuration = ms + 'ms';
    badge.textContent = '동작 줄임 ' + (reduce ? '켜짐' : '꺼짐');
    return { reduce: reduce, ms: ms };
  }
  applyMotionPref();

격자 방향키와 aria-live, reduce 값이 지키는 접근성

01·02·04는 role="option" 또는 목록 구조 위에서 aria-selected·aria-label로 지금 상태를 읽어 주고, 06은 격자 전체에 role="grid"를 주고 칸마다 role="gridcell"과 tabIndex를 굴려 방향키 한 번에 초점이 하나만 움직이는 로빙 탭인덱스로 동작합니다. 09를 뺀 여덟 항목은 각자 role="status" aria-live="polite"인 안내문을 두어 값이 바뀔 때마다 순위·카테고리·남은 공간 같은 수치를 문장으로 다시 읽어 줍니다.

아홉 항목 전부 window.matchMedia('(prefers-reduced-motion: reduce)')를 직접 읽어, 값이 참이면 자동 재생 루프를 시작하지 않고 is-demo 클래스를 곧바로 지워 정지 상태로 둡니다. 09는 한 걸음 더 나아가 이 값 자체를 실제 상호작용에도 반영해, reduced-motion 이 켜진 사용자에게는 펼침 애니메이션 없이 다 켜진 패널을 즉시 보여줍니다 — 나머지 여덟 항목은 정지만 시킬 뿐 값을 바꾸지 않지만, 09는 그 값을 applyMotionPref()의 지속시간 계산에 직접 넣는다는 점이 다릅니다.

이모지 피커를 실무 화면 어디에 붙이나?

채팅 앱의 반응 선택 화면에 이 이모지 피커를 그대로 붙인다면 01(최근 사용)과 03(유사도 검색)을 함께 쓰는 조합이 자연스럽습니다. 자주 쓰는 반응은 검색 없이 최근 탭에서 바로 고르고, 처음 찾는 이모지만 검색창에 이름을 쳐서 유사도 순으로 좁히는 흐름이 실제 메신저 반응 UI와 같습니다.

댓글창이나 긴 메시지 입력창처럼 본문 중간에 이모지를 끼워 넣어야 하는 화면에서는 08(커서 위치 보존)이 핵심입니다. 02(카테고리 스크롤 동기화)·06(키보드 격자 탐색)은 이모지 수가 수백 개로 많아 한 화면에 다 못 보여주는 커스텀 이모지 보드에서, 마우스로 스크롤하거나 키보드만으로 훑는 두 가지 탐색 방식을 함께 제공할 때 붙입니다.

화면 아래쪽에 고정된 입력창처럼 패널 아래 공간이 늘 넉넉하지 않은 레이아웃에서는 07(앵커 배치 뒤집기)을 반드시 함께 써야 패널이 화면 밖으로 잘리지 않습니다. 04(톤 전환)·05(길게 누르기 변형)는 손·얼굴 이모지가 많은 반응 보드에서, 이모지 사용법을 따로 안내하지 않아도 누르는 시간만으로 변형을 발견하게 만드는 조합입니다.

아홉 항목의 실측값 — 누적 면적·강도·움직인 프레임

렌더 화면을 24장 찍어 바로 앞 장과 견준 수치입니다. 움직인프레임 칸은 그 24장을 잇는 23번의 비교 가운데 실제로 화면이 달라진 횟수를 셉니다.

항목 누적면적% 강도평균 움직인프레임
01 최근 사용 순위 2.785 93.4 10/23
02 카테고리 스크롤 동기화 16.651 64.8 9/23
03 유사도 검색 33.422 137.6 16/23
04 톤 전환 3.824 58.4 11/23
05 길게 누르기 변형 5.267 116.4 20/23
06 키보드 격자 탐색 6.242 63.1 12/23
07 앵커 배치 뒤집기 27.115 63.8 12/23
08 커서 위치 보존 삽입 2.754 93.9 8/23
09 동작 줄임 즉시 패널 10.636 157.4 10/23

08처럼 누적면적이 2~3%대로 낮은 항목은 커서만 옮기고 배지 하나만 반짝이는 짧은 동작이라 면적이 작은 것이 정상이고, 03·07처럼 검색 결과 목록이나 패널 전체가 다시 그려지는 항목은 면적이 크게 잡힙니다. 항목마다 값이 다른 이유는 입력 순간에만 계산이 일어나는 컴포넌트라서 그렇습니다 — 배경에서 쉬지 않고 도는 장면이 아니라 사용자가 누르거나 스크롤하거나 타이핑하는 그 순간에만 값이 갱신됩니다.

EMOJI_SEARCH_DEBOUNCE_MS·EMOJI_LONGPRESS_THRESHOLD_MS·EMOJI_PANEL_FLIP_MARGIN_PX, 값 하나씩 바꾼 결과

03의 EMOJI_SEARCH_DEBOUNCE_MS는 입력마다 유사도 값을 언제 다시 계산할지 정하는 지연 값입니다 — 200ms를 400ms로 늘리면 한 글자 칠 때마다 거의 바로 다시 계산되던 것이 타이핑이 멈춘 뒤에야 한 번에 다시 계산돼 목록이 자주 깜빡이지 않고 차분하게 바뀝니다. 05의 EMOJI_LONGPRESS_THRESHOLD_MS는 변형이 펼쳐지는 누른 시간 문턱 값으로, 550ms를 900ms로 올리면 살짝만 눌러도 열리던 것과 달리 꽤 오래 눌러야 펼쳐져 짧게 누르는 선택과 뚜렷이 구분됩니다. 07의 EMOJI_PANEL_FLIP_MARGIN_PX는 패널이 위로 뒤집히는 여유 공간 값으로, 8px을 24px로 늘리면 아래 공간이 넉넉해 보여도 패널이 더 일찍 위쪽으로 돌아 열립니다.

항목 상수 전 → 후
03 EMOJI_SEARCH_DEBOUNCE_MS 200ms → 400ms
05 EMOJI_LONGPRESS_THRESHOLD_MS 550ms → 900ms
07 EMOJI_PANEL_FLIP_MARGIN_PX 8px → 24px

톤 값을 미리 안 재면 깨지는 함정

04는 사람 형태 이모지의 기본 코드포인트에 톤 수정자 코드포인트를 이어 붙여 매번 새 문자열을 계산합니다. 이 조합 순서를 건너뛰고 톤별로 미리 그려 둔 이미지를 갈아 끼우는 방식으로 바꾸면, 새 이모지가 추가될 때마다 다섯 벌의 이미지를 새로 준비해야 하고 코드포인트 자체가 바뀌는 유니코드 갱신을 따라가지 못해 깨집니다. 07도 같은 종류의 함정이 있습니다 — 패널 높이를 상수로 미리 박아 두면 내용에 따라 실제 높이가 달라지는 순간 남은 공간 계산이 틀어져 패널이 화면 밖으로 잘립니다. measureAndPlace()처럼 화면 밖에서 한 번 실측한 뒤에 자리를 정해야 이런 어긋남이 생기지 않습니다. 전체 동작을 코드 그대로 확인하려면 8wfz3py4으로 아홉 개 데모의 HTML·SCSS·JS 원본을 열어 이 계산 순서를 직접 비교해 보는 편이 빠릅니다.

컬러피커 342편, 태그 입력 397편과 무엇이 다른가

컬러피커 UI 디자인 9종 — 휠부터 최근 색상까지의 최근 색상 칸은 342편 01처럼 고른 시각 값으로 순서를 매긴다는 점에서 이 편 01의 사용 횟수 순위와 결이 닮았지만, 342편은 색상값 자체를 저장하고 402편은 몇 번 골랐는지 횟수만 저장한다는 점이 다릅니다. 태그 입력 ui 9가지 — 칩 UI 코드로 만들기는 입력창 안에서 값이 개별 칩으로 쪼개지고 합쳐지는 흐름을 다루지만, 이 편은 이미 정해진 문자 하나(이모지)를 고르는 흐름이라 칩을 새로 만들거나 지우는 로직이 없고, 대신 그 문자를 어떤 기준(빈도·유사도·톤)으로 고르게 하느냐가 항목마다 다릅니다. 세그먼트 컨트롤 선택 UI 9종 — 복붙하면 되는은 몇 개 안 되는 고정 옵션 중 하나를 스위치처럼 고르는 흐름이 중심이라 검색·스크롤 같은 탐색 로직이 필요 없지만, 이 편은 수백 개 이모지 중 하나를 찾아야 하므로 검색(03)·카테고리 스크롤(02)·키보드 격자 탐색(06)처럼 탐색 자체를 돕는 항목이 절반 가까이 됩니다.

이모지 검색은 어디서 더 볼 수 있나?

이 편의 격자·목록 구조는 W3C ARIA 콤보박스·리스트박스 패턴의 role·상태 속성 구성을 참고했고, 09가 읽는 동작 줄임 값의 판정 기준은 MDN prefers-reduced-motion 문서를 따릅니다. 이 검색 기능 자체를 더 넓혀 보고 싶다면 03의 score()가 비교하는 이름 문자열 범위를 늘리거나, 자모 단위 초성 검색처럼 다른 유사도 규칙을 얹는 방향으로 확장할 수 있습니다.

이모지 피커를 적용하기 전에 자주 나오는 질문

06의 방향키 탐색이 이모지 단축키를 새로 만드는 기능인가요?

아니요. 06은 화면 안 격자에서 키보드로 초점만 옮기는 탐색이고, measureCols()가 잰 실제 열 수만큼 focusIndex가 움직일 뿐입니다. 운영체제나 메신저가 제공하는 이모지 단축키 변경 설정과는 별개의 기능이며, 이 편은 그 설정 자체를 바꾸지 않습니다.

이모지 피커를 만들 때 외부 라이브러리나 스프라이트 이미지가 필요한가요?

필요 없습니다. 아홉 항목 전부 화면에 보이는 이모지는 유니코드 문자 그대로이고 이미지 스프라이트나 CDN 폰트를 쓰지 않습니다. 04의 톤 전환도 이미지를 갈아 끼우는 게 아니라 코드포인트를 그때그때 조합해서 만듭니다.

01의 최근 사용 기록은 새로고침해도 남나요?

남지 않습니다. counts·lastPicked 값은 자바스크립트 메모리 안 객체에만 쌓이고 localStorage나 서버에 저장하지 않으므로, 페이지를 새로고침하면 최근 사용 순위가 초기 상태로 돌아갑니다.

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

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