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

textarea 글자수 카운터 9가지 — 자소 단위로 세고 한도를 링으로 보여 주는 입력창

글자수는 length 한 줄로 끝날 것 같지만, 이모지 하나가 2로 올라가고 한글 조합 중에는 숫자가 깜박인다 — 01번과 04번이 그 두 군데를 하나씩 막는다. textarea 글자수 카운터는 입력이 바뀔 때마다 사람이 보는 한 글자를 세어 남은 한도를 알려 주는 숫자다.

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

textarea 글자수 카운터 9가지 — 자소 단위 계산이 숫자를 바로잡는다

아홉 개는 숫자가 틀리는 순서대로 놓았다. 01 은 이모지를 한 글자로 세는 기본 계산, 02 는 그 숫자를 한도 대비 링으로 바꾸는 장면, 03 은 전각과 반각의 폭이 다를 때의 계산이다. 04 는 조합 중인 입력에서 숫자가 깜박이는 문제를, 05 는 넘친 글자만 칠해 주는 거울 레이어를, 06 은 붙여넣기가 한도를 넘는 순간을 다루고, 07 은 글자·단어·바이트·읽는 시간을 한 번에 재며, 08 은 남은 글자 안내 문구와 색이 구간마다 이어서 바뀌는 방식, 09 는 스크린리더에 읽어 주는 횟수를 줄이는 방식이다. 글쓰기 폼에 처음 붙이는 사람이라면 01·02·08 세 개만 합쳐도 숫자와 링과 안내 문구가 한 입력창 안에서 맞물려 돌아간다.

기법별 코드로 보는 글자수 계산

아홉 항목 모두 textarea 하나와 그 label, 카운트 요소를 가리키는 aria-describedby 로 구성되고, 아래에는 숫자를 계산하는 핵심 구간만 발췌했다. 나머지 마크업과 스타일은 데모 아래 압축 파일 안에 통째로 들어 있고, 시연 문장은 데모용이며 서버와는 통신하지 않는다. 시연 중에 입력칸을 누르거나 키를 치면 시연은 멈추고 직접 쓴 글자가 같은 계산을 지나간다.

01자소 묶음 단위로 세는 실시간 글자수

graphemes 가 Intl.Segmenter 로 입력값을 사람이 보는 한 글자씩 쪼개 큰 숫자로 보여 주고 옆 「length 로는」 숫자에는 ta.value.length 를 놓아, 이모지가 들어가는 순간 두 숫자가 갈리며 포스터 시점에는 묶음 12자 대 length 19 로 벌어져 있다. 글자수 표시를 처음 붙이는 폼이라면 여기서 시작한다. 01 항목은 누적면적 13.4%·강도 평균 64.8·23프레임 중 23프레임에서 움직였다.

자소 묶음 단위로 세는 실시간 글자수
새 창에서 열기

01 · 자소 묶음 단위로 세는 실시간 글자수 — 측정 누적면적 13.419% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STRIP_SLOTSPOP_MSCOUNT_LERP_RATEANNOUNCE_EVERY
  function update(nowMs) {
    var list = graphemes(ta.value);
    var count = list.length;
    var view = count <= STRIP_SLOTS ? list : list.slice(count - STRIP_SLOTS);
    for (var k = 0; k < STRIP_SLOTS; k += 1) {
      var g = view[k] || '';
      tiles[k].textContent = g;
      tiles[k].className = 'cc__tile' + (g ? ' is-on' : '') + (g.length > 1 ? ' is-multi' : '');
    }
    if (count > lastCount && !reduce) born[Math.min(count, STRIP_SLOTS) - 1] = nowMs;
    if (count % ANNOUNCE_EVERY === 0 && count !== lastCount && count > 0) {
      sr.textContent = '현재 ' + count + '자';
    }
    lastCount = count;
    target = count;
    lenTarget = ta.value.length;
    if (reduce) {
      shown = target;
      lenShown = lenTarget;
      paintNumbers();
    }
  }

02남은 글자에 따라 차오르는 스프링 링

사용한 글자를 한도로 나눈 비율이 링의 호 길이가 되고, 그 값이 RING_SPRING_STIFFNESS·RING_SPRING_DAMPING 스프링을 거쳐 목표를 따라가며 80% 부터 민트에서 앰버로, 한도에서 핑크로 색이 이어 바뀐다. 처음 한도를 넘는 순간 감쇠 사인으로 링이 흔들리므로 한도 근처를 숫자보다 먼저 눈에 띄게 하고 싶을 때 쓴다. 02 항목은 누적면적 30.3%·강도 평균 108.5·23프레임 중 23프레임에서 움직였다.

남은 글자에 따라 차오르는 스프링 링
새 창에서 열기

02 · 남은 글자에 따라 차오르는 스프링 링 — 측정 누적면적 30.287% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CHAR_LIMITRING_SPRING_STIFFNESSRING_SPRING_DAMPINGWARN_RATIOSHAKE_AMP
  function paint(nowMs) {
    var ratio = used / CHAR_LIMIT;
    var drawn = clamp(x, 0, 1);
    arc.style.strokeDasharray = (drawn * RING_LENGTH).toFixed(2) + ' ' + RING_LENGTH.toFixed(2);
    arc.style.stroke = colorAt(Math.min(x, 1.12));
    card.style.background = tintAt(Math.min(x, 1.12), 0.16);
    var remain = CHAR_LIMIT - used;
    countEl.textContent = String(remain);
    countEl.style.color = remain < 0 ? 'rgb(255,158,187)' : '';
    hint.textContent = remain >= 0 ? '남은 글자 ' + remain + '자' : '넘친 글자 ' + (-remain) + '자';
    var pulse = 1 + Math.max(0, x - 1) * 0.5;
    var dx = 0;
    if (shakeAt >= 0 && !reduce) {
      var age = nowMs - shakeAt;
      var amp = SHAKE_AMP * Math.exp(-age / SHAKE_DECAY_MS);
      dx = amp < 0.1 ? 0 : amp * Math.sin(age * SHAKE_OMEGA);
    }
    ring.style.transform = 'translateX(' + dx.toFixed(2) + 'px) scale(' + pulse.toFixed(3) + ')';
    return ratio;
  }

03전각 2·반각 1로 세는 폭 가중 카운트

weightOf 는 묶음의 첫 코드 포인트가 0x7e 이하이거나 반각 가타카나 범위면 HALF_WEIGHT 1, 아니면 FULL_WEIGHT 2 를 돌려주고, 이 가중치를 합산해 80폭 한도까지 칸 너비가 두 배씩 차이 나는 막대를 그린다. 일본어 전각과 영문 반각이 섞인 입력, 한도가 글자 수가 아니라 폭으로 정해진 입력칸에 맞다. 03 항목은 누적면적 10.0%·강도 평균 150.6·23프레임 중 23프레임에서 움직였다.

전각 2·반각 1로 세는 폭 가중 카운트
새 창에서 열기

03 · 전각 2·반각 1로 세는 폭 가중 카운트 — 측정 누적면적 9.971% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

WIDTH_LIMITHALF_WEIGHTFULL_WEIGHTPOP_MS
  function graphemes(s) {
    if (!seg) return Array.from(s);
    return Array.from(seg.segment(s), function (x) { return x.segment; });
  }
  function weightOf(g) {
    var cp = g.codePointAt(0);
    if (cp <= 0x7e || (cp >= 0xff61 && cp <= 0xff9f)) return HALF_WEIGHT;
    return FULL_WEIGHT;
  }

04조합 중인 글자까지 흔들리지 않는 IME 대응 카운터

compositionstart 에서 composing 이 켜지면 onInput 은 「바로 세기」 숫자만 갱신하고 「조합 보류」 숫자는 붙잡아 두며, compositionend 에서 확정값을 한 번에 올리고 값이 달라졌을 때만 숫자를 튀게 해 한글·일본어 입력이 많은 폼에서 숫자가 오르내리는 것을 막는다. 시연은 일본어 입력기가 n→に→にほ→にほん 처럼 조합하는 단계를 같은 핸들러로 흘려 보내는 재현이고 실제 입력기 장면은 아니며, 직접 쓸 때는 진짜 조합 이벤트가 같은 경로를 지난다. 04 항목은 누적면적 1.9%·강도 평균 121.1·23프레임 중 23프레임에서 움직였다.

조합 중인 글자까지 흔들리지 않는 IME 대응 카운터
새 창에서 열기

04 · 조합 중인 글자까지 흔들리지 않는 IME 대응 카운터 — 측정 누적면적 1.882% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STEP_MSPAUSE_MSBUMP_MSHOLD_MS
  function onCompStart() { composing = true; }
  function onInput() {
    rawCount = countGraphemes(ta.value);
    if (!composing) safeCount = rawCount;
  }
  function onCompEnd(nowMs) {
    composing = false;
    preEl.textContent = '';
    var next = countGraphemes(ta.value);
    if (next !== safeCount) bumpAt = nowMs;
    safeCount = next;
    rawCount = next;
    sr.textContent = '현재 ' + safeCount + '자';
  }

05한도를 넘은 뒷부분만 표시하는 거울 레이어

입력칸 뒤에 글꼴·줄 높이·패딩·white-space: pre-wrap 이 한 벌인 거울 레이어를 겹쳐 두고, 한도 이후 글자만 <mark> 로 감싸 형광 배경을 칠하며 스크롤을 거울과 맞춘다. 입력칸 배경은 투명하고 거울 글자색도 투명이라 칠한 색만 비치며, 새로 넘친 순간에는 색이 진하게 시작해 가라앉아 어디까지가 넘친 글자인지 바로 고쳐 쓰게 하는 입력에 쓴다. 05 항목은 누적면적 14.4%·강도 평균 117.8·23프레임 중 22프레임에서 움직였다.

한도를 넘은 뒷부분만 표시하는 거울 레이어
새 창에서 열기

05 · 한도를 넘은 뒷부분만 표시하는 거울 레이어 — 측정 누적면적 14.367% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

MIRROR_LIMITFLASH_MSCHUNK_MS
  function update(nowMs) {
    var list = graphemes(ta.value);
    var over = Math.max(0, list.length - MIRROR_LIMIT);
    mirror.textContent = '';
    mirror.appendChild(document.createTextNode(list.slice(0, MIRROR_LIMIT).join('')));
    mark = null;
    if (over > 0) {
      mark = document.createElement('mark');
      mark.className = 'cc__mark';
      mark.textContent = list.slice(MIRROR_LIMIT).join('');
      mirror.appendChild(mark);
    }
    mirror.appendChild(document.createTextNode('​'));
    syncScroll();
    countEl.textContent = String(list.length);
    countEl.style.color = over > 0 ? '#9a2b52' : '';
    overEl.textContent = over > 0 ? '넘침 ' + over + '자' : '';
    overEl.className = 'cc__over' + (over > 0 ? ' is-on' : '');
    if (over > lastOver && !reduce) flashAt = nowMs;
    lastOver = over;
    var next = over > 0 ? 1 : 0;
    if (next !== stage) {
      sr.textContent = next === 1 ? '한도를 넘었어요' : '';
      stage = next;
    }

06붙여넣기가 넘치면 잘라 내고 감쇠 진동으로 알리기

paste 이벤트를 preventDefault 로 가로채 남은 칸 room 을 계산하고, 붙여넣은 글의 앞부분만 받고 나머지는 버린 뒤 감쇠 사인으로 입력칸을 흔들며 「잘림 N자」 토스트를 올린다. 선택 구간이 있으면 그 구간을 대체하는 계산이라 중간에 붙여넣어도 맞고, 긴 글을 붙여넣는 폼에서 조용히 잘리는 것보다 잘렸다고 말해 주는 쪽이 낫다. 06 항목은 누적면적 12.0%·강도 평균 110.3·23프레임 중 22프레임에서 움직였다.

붙여넣기가 넘치면 잘라 내고 감쇠 진동으로 알리기
새 창에서 열기

06 · 붙여넣기가 넘치면 잘라 내고 감쇠 진동으로 알리기 — 측정 누적면적 12.012% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

PASTE_LIMITSHAKE_AMPSHAKE_DECAY_MSSHAKE_OMEGATOAST_MS
  function applyPaste(text, nowMs) {
    var start = ta.selectionStart;
    var end = ta.selectionEnd;
    var before = ta.value.slice(0, start);
    var after = ta.value.slice(end);
    var room = PASTE_LIMIT - graphemes(before + after).length;
    var incoming = graphemes(text);
    var take = incoming.slice(0, Math.max(0, room)).join('');
    ta.value = before + take + after;
    ta.setSelectionRange(start + take.length, start + take.length);
    refresh();
    if (incoming.length > room) signal(incoming.length - Math.max(0, room), nowMs);
  }

07글자·단어·바이트·읽는 시간을 한 번에 재는 계기판

같은 텍스트에서 글자는 자소 묶음, 단어는 isWordLike 조각, 바이트는 TextEncoder 의 UTF-8 길이, 읽는 시간은 글자를 READ_CHARS_PER_MIN 으로 나눈 올림 초로 재서 네 숫자가 동시에 굴러 바뀌고, 타일 배경과 밑줄 막대는 상한 대비 비율만큼 차오른다. 글쓰기 도구의 하단 바에 붙이면 수치를 한 번에 읽을 수 있다. 07 항목은 누적면적 23.0%·강도 평균 127.8·23프레임 중 23프레임에서 움직였다.

글자·단어·바이트·읽는 시간을 한 번에 재는 계기판
새 창에서 열기

07 · 글자·단어·바이트·읽는 시간을 한 번에 재는 계기판 — 측정 누적면적 22.963% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

READ_CHARS_PER_MINNUMBER_LERP_RATEANNOUNCE_EVERYBAR_CAPS
  function measure(text) {
    var chars = gSeg ? Array.from(gSeg.segment(text)).length : Array.from(text).length;
    var words = wSeg
      ? Array.from(wSeg.segment(text)).filter(function (x) { return x.isWordLike; }).length
      : text.split(/\s+/).filter(Boolean).length;
    var bytes = enc.encode(text).length;
    var secs = Math.ceil(chars / READ_CHARS_PER_MIN * 60);
    return [chars, words, bytes, secs];
  }

08임계 구간마다 문구와 색이 이어서 바뀌는 남은 글자 안내

사용 비율이 WARN_RATIO 를 넘으면 민트에서 앰버로, 한도를 넘으면 OVER_SPAN 안에서 핑크로 색을 선형으로 이어 주고, 남은 숫자는 1배에서 MAX_SCALE 인 1.8배까지 커진다. 문구는 여유·주의·위험 구간이 바뀔 때만 페이드로 교체되어 매 글자마다 흔들리지 않으니, 글자수 한도가 엄격한 입력에서 남은 글자를 가장 먼저 읽게 하고 싶을 때 쓴다. 08 항목은 누적면적 20.2%·강도 평균 103.7·23프레임 중 23프레임에서 움직였다.

임계 구간마다 문구와 색이 이어서 바뀌는 남은 글자 안내
새 창에서 열기

08 · 임계 구간마다 문구와 색이 이어서 바뀌는 남은 글자 안내 — 측정 누적면적 20.176% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

HINT_LIMITWARN_RATIOMAX_SCALEOVER_SPANMSG_FADE_MS
  function update(nowMs) {
    used = countGraphemes(ta.value);
    var ratio = used / HINT_LIMIT;
    var next = ratio >= 1 ? 2 : (ratio >= WARN_RATIO ? 1 : 0);
    if (next !== stage) {
      stage = next;
      var text = next === 2 ? '한도를 넘었어요' : (next === 1 ? '곧 한도예요' : '여유 있어요');
      msgEl.textContent = text;
      sr.textContent = next === 0 ? '' : text;
      swapAt = nowMs;
    }
    if (reduce) { shownUsed = used; paint(nowMs); }
  }

09낭독 폭주를 막는 aria-live 스로틀과 움직임 줄이기

숫자가 바뀔 때마다 스크린리더가 읽으면 입력을 방해하므로 ANNOUNCE_RATIOS 의 0.8 과 1 을 처음 넘는 순간에만 sr 문구를 바꾸고, 「움직임 줄이기」 버튼이나 운영체제 설정이 켜지면 보간을 모두 즉시 값으로 바꾼다. 스크린리더 사용자가 있는 폼의 기준이며, 데모의 「매번 낭독」 횟수는 비교를 위한 시연용 숫자이고 실제 aria-live 에는 넣지 않는다. 09 항목은 누적면적 4.7%·강도 평균 138.0·23프레임 중 23프레임에서 움직였다.

낭독 폭주를 막는 aria-live 스로틀과 움직임 줄이기
새 창에서 열기

09 · 낭독 폭주를 막는 aria-live 스로틀과 움직임 줄이기 — 측정 누적면적 4.685% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ANNOUNCE_LIMITANNOUNCE_RATIOSTICK_SLOTSNUMBER_LERP_RATEPOP_MS
    if (len !== lastLen) {
      rawCount += 1;
      addTick(rawList, rawCount, nowMs);
    }
    var ratio = len / ANNOUNCE_LIMIT;
    for (var i = 0; i < ANNOUNCE_RATIOS.length; i += 1) {
      if (!passed[i] && ratio >= ANNOUNCE_RATIOS[i]) {
        passed[i] = true;
        safeCount += 1;
        addTick(safeList, safeCount, nowMs);
        sr.textContent = i === 1 ? '한도를 넘었어요' : '곧 한도예요';
      }
    }
    lastLen = len;

값이 장면을 바꾸는 곳 — 한도·링 스프링·경고 비율

링이 얼마나 빨리 차고 언제 색이 바뀌는지는 항목 개수가 아니라 이 표의 수치와 세 상수가 정한다.

항목 누적면적 % 강도 평균 움직인 프레임
01 자소 묶음 세기 13.4% 64.8 23/23
02 스프링 링 30.3% 108.5 23/23
03 폭 가중 카운트 10.0% 150.6 23/23
04 IME 대응 1.9% 121.1 23/23
05 거울 레이어 14.4% 117.8 22/23
06 붙여넣기 잘림 12.0% 110.3 22/23
07 계기판 23.0% 127.8 23/23
08 안내 문구 20.2% 103.7 23/23
09 낭독 스로틀 4.7% 138.0 23/23

03 은 9.97% 로 10% 바로 아래이고 04·09 는 그보다 훨씬 낮은데, 컴포넌트 편에서는 정상이다 — 04 는 숫자 두 개와 점이 전부라 면적이 작아도 조합 단계마다 값이 바뀌어 23프레임 모두 움직이고, 09 는 눈금 몇 개가 늘어날 뿐이다. 포스터는 타이핑 장면이라 02 의 흔들림이나 04 의 숫자 튐은 움직이는 데모에서 보인다. CHAR_LIMIT 은 링이 가득 차고 흔들리는 글자 수를, RING_SPRING_STIFFNESS 는 링이 목표를 따라가는 빠르기를, WARN_RATIO 는 앰버가 시작되는 비율을 정하는데, 02 의 감쇠비는 14 를 2√220 으로 나눈 약 0.47 이라 계단 응답에서 오버슛이 약 19% 나온다.

어디서 깨지는가 — 이모지·조합 중 입력·붙여넣기 폭주

폴백이 먼저 걸린다. 01 의 graphemes 는 Intl.Segmenter 가 없으면 Array.from 으로 코드 포인트 단위로 쪼개는데, 그러면 가족 이모지는 사람 셋과 이음 기호 둘로 다시 갈라지고 국기는 둘, 결합 문자가 붙은 café 는 다섯이 되어 이 편의 핵심 장면이 사라진다. 03 의 weightOf 는 묶음의 첫 코드 포인트만 보고 0x7e 를 넘으면 전부 폭 2 로 치므로, 한글이나 악센트 붙은 라틴 글자나 이모지도 같은 2로 센다. 폭 기준 한도를 쓰는 서비스는 서비스가 정한 폭 규칙을 이 함수에 그대로 옮겨 넣어야 한다.

06 은 paste 경로와 input 경로가 따로다. 붙여넣기는 setSelectionRange 로 캐럿을 붙여넣은 끝에 두지만, 타이핑처럼 input 이벤트로 한도를 넘는 경우를 받는 경로는 value 를 80묶음까지 잘라 다시 대입할 뿐 캐럿을 되돌리는 줄이 없다. 05 의 거울 레이어는 입력칸과 글꼴·패딩·줄 높이가 한 벌이어야 하고 입력칸 스크롤바를 숨겨 폭을 맞춰 둔 구조이므로, 입력칸의 글꼴이나 패딩만 따로 바꾸면 칠한 자리가 어긋난다. 01 의 글자 띠는 STRIP_SLOTS 16칸까지만 그리고 그 뒤는 마지막 16자만 보여 준다. 이 한계까지 포함한 아홉 개 소스를 그대로 열어 보려면 내려받은 압축 파일을 풀 때 비밀번호 jg6vwkst 을 입력한다.

변형 세 가지와 접근성

항목 상수 값 느낌
02 CHAR_LIMIT 120 → 60 한도를 줄이면 같은 144자 시연에서 링이 절반 길이에서 이미 가득 차고 앰버는 96자째가 아니라 48자째에, 핑크 흔들림은 60자째에 시작된다
02 RING_SPRING_STIFFNESS 220 → 110 감쇠 14 를 그대로 두고 강성을 낮추면 링이 더 느리게 따라가고 감쇠비가 0.47 에서 0.67 로 올라 오버슛이 약 19% 에서 약 6% 로 줄어든다
08 WARN_RATIO 0.8 → 0.9 100자 한도에서 앰버와 「곧 한도예요」 문구가 80자째에서 90자째로 늦어져 여유 구간이 10자 길어지고 주의 구간은 20자에서 10자로 줄어든다

prefers-reduced-motion: reduce 분기는 matchMedia 로 아홉 소스 모두에서 한 번씩 확인하므로, 동작 줄임 설정을 켠 방문자에게는 requestAnimationFrame 루프를 시작하지 않고 시연 텍스트를 즉시 채워 최종 상태를 한 번 그리며, 이후 입력은 보간 없이 바로 그려진다. 09 는 운영체제 설정과 같은 효과를 내는 「움직임 줄이기」 버튼(aria-pressed)도 따로 둔다. 키보드로 닿는 것은 입력칸 하나라 Tab 으로 바로 도달하고, 숫자는 입력마다 바뀌므로 aria-live="polite" 는 숫자가 아니라 숨은 상태 줄 한 곳에만 있으며 구간에 들어서거나 열 글자 단위 같은 드문 순간에만 문구를 넣는다. 입력칸은 label 과 연결되고 aria-describedby 로 카운트 요소를 가리킨다.

글자수는 length 로 세면 왜 틀리는가?

ta.value.length 는 사람이 보는 글자가 아니라 내부 코드 단위의 수라서 이모지나 결합 문자가 들어오면 실제 글자보다 크게 나온다 — 01 이 두 숫자를 나란히 놓은 것이 그래서다. 이 계산 위에 붙는 장치를 고르려면 입력칸 주변 부품으로 이어서 읽으면 된다. 칩 입력은 태그 입력 ui 9가지 — 칩 UI 코드로 만들기, 입력창 자체의 모양은 입력창 디자인 9가지 — 복붙하면 되는 폼 위젯, 붙여넣기 피드백은 클립보드 복사 버튼 UI 9종 — 코드·링크·텍스트 복사 피드백까지, 입력값의 표시 방식은 비밀번호 표시 아이콘 9가지 — 입력창 속 문자가 도트로 바뀌는 계산 에서 다룬다. 검색창 쪽 글자 카운트는 CSS 입력창 검색창 포커스 애니메이션 9종 — 한 줄 복붙으로 와 비교해 볼 만하다.

글자수 카운터를 쓰기 전에 자주 묻는 질문

textarea 글자수 제한은 넘으면 입력을 막아야 하나요?

이 편의 아홉 개는 대부분 막지 않고 알린다 — 02·05·08 은 넘친 글자를 그대로 두고 링·형광칠·색으로 표시만 하고, 06 만 붙여넣기가 한도를 넘을 때 앞부분만 받고 나머지를 버린다. 막을지 표시만 할지는 서비스가 정할 일이라 카운터는 계산과 신호만 맡기고 처리는 폼 쪽에 두는 편이 고치기 쉽다.

javascript textarea 글자수 표시를 length 한 줄로 쓰면 안 되나요?

영문과 숫자만 받는 입력이라면 충분하지만 이모지나 결합 문자가 들어올 수 있다면 01 처럼 Intl.Segmenter 로 묶음 단위를 세는 쪽이 사람이 보는 숫자와 맞는다. 그 객체가 없는 환경을 대비해 Array.from 폴백을 두되, 폴백에서는 이모지가 갈라진다는 한계를 알고 쓴다.

단어 수나 읽는 시간도 같이 보여 줘야 하나요?

글자 한도가 목적이면 02 나 08 한 개로 충분하고, 글쓰기 도구라면 07 처럼 네 숫자를 한 줄에 놓는 것이 자연스럽다. 읽는 시간은 분당 READ_CHARS_PER_MIN 으로 나눈 값일 뿐이라 서비스의 글 성격에 맞게 이 상수 하나만 바꾸면 된다.

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

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