back

Posts

β€˜πŸ„β€™.length === 2? μžλ°”μŠ€ν¬λ¦½νŠΈμ—μ„œ 이λͺ¨μ§€ λ¬Έμžμ—΄ 길이의 함정과 해결법

2025-10-08

ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ λ¬Έμžμ—΄ 길이λ₯Ό κ²€μ‚¬ν•˜λ‹€ 보면 이런 κ²½ν—˜μ΄ μžˆμŠ΅λ‹ˆλ‹€.

β€œμ΄λͺ¨μ§€ ν•˜λ‚˜λ₯Ό λ„£μ—ˆλŠ”λ° 길이가 2둜 계산돼?”
'πŸ„'.length // 2

이건 μžλ°”μŠ€ν¬λ¦½νŠΈμ˜ λ¬Έμžμ—΄ λͺ¨λΈμ΄ μœ λ‹ˆμ½”λ“œμ™€ λ§žλ¬Όλ¦¬λŠ” μ§€μ μ—μ„œ μƒκΈ°λŠ”, μ•„μ£Ό ν”ν•˜μ§€λ§Œ ν—·κ°ˆλ¦¬λŠ” λ¬Έμ œμž…λ‹ˆλ‹€.

1. μ™œ β€˜πŸ„β€™.lengthλŠ” 2일까?

μžλ°”μŠ€ν¬λ¦½νŠΈ λ¬Έμžμ—΄μ€ UTF-16 μ½”λ“œ μœ λ‹› λ‹¨μœ„λ‘œ μ €μž₯λ©λ‹ˆλ‹€.
즉, String.prototype.lengthλŠ” μ‹€μ œ β€œλ³΄μ΄λŠ” κΈ€μž μˆ˜β€κ°€ μ•„λ‹ˆλΌ β€œ16λΉ„νŠΈ 쑰각 κ°œμˆ˜β€λ₯Ό λ°˜ν™˜ν•˜μ£ .

"πŸ„" (U+1F344)은 BMP(κΈ°λ³Έ λ‹€κ΅­μ–΄ 평면) λ°–μ˜ 문자라 16λΉ„νŠΈ 두 쑰각(μ„œλ‘œκ²Œμ΄νŠΈ νŽ˜μ–΄)둜 ν‘œν˜„λ©λ‹ˆλ‹€.

"πŸ„".length; // 2
"πŸ„".charCodeAt(0).toString(16); // "d83c"
"πŸ„".charCodeAt(1).toString(16); // "df44"
"πŸ„".codePointAt(0).toString(16); // "1f344"

κ²°κ΅­ 'πŸ„'은 μ½”λ“œ μœ λ‹› 2개둜 κ΅¬μ„±λ˜λ―€λ‘œ .length === 2κ°€ λ©λ‹ˆλ‹€.


μ’‹μ•„μš” πŸ‘ ν‘œ λŒ€μ‹  μžμ—°μŠ€λŸ¬μš΄ 문단 ν˜•νƒœλ‘œ μ •λ¦¬ν•˜λ©΄ μ΄λ ‡κ²Œ ν‘œν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€ πŸ‘‡


2. β€œκΈΈμ΄β€λŠ” 무엇을 μ„ΈλŠλƒμ— 따라 달라진닀

μžλ°”μŠ€ν¬λ¦½νŠΈμ—μ„œ λ¬Έμžμ—΄μ˜ β€œκΈΈμ΄β€λŠ” 생각보닀 μ—¬λŸ¬ μΈ΅μœ„λ‘œ λ‚˜λˆ μ§‘λ‹ˆλ‹€. 같은 πŸ„ λ¬ΈμžλΌλ„ 무엇을 κΈ°μ€€μœΌλ‘œ μ„ΈλŠλƒμ— 따라 κ²°κ³Όκ°€ λ‹¬λΌμ§‘λ‹ˆλ‹€.

λ¨Όμ € λ°”μ΄νŠΈ 수λ₯Ό κΈ°μ€€μœΌλ‘œ 보면, πŸ„μ€ UTF-8μ—μ„œ 4λ°”μ΄νŠΈλ₯Ό μ°¨μ§€ν•©λ‹ˆλ‹€. 이건 μ €μž₯ κ³΅κ°„μ΄λ‚˜ λ„€νŠΈμ›Œν¬ 전솑 μ‹œ μš©λŸ‰μ„ 계산할 λ•Œμ˜ 관점이죠.

μ½”λ“œ μœ λ‹› 수 κΈ°μ€€μœΌλ‘œ 보면 2μž…λ‹ˆλ‹€. μ΄λŠ” UTF-16의 16λΉ„νŠΈ λ‹¨μœ„ 쑰각을 μ„ΈλŠ” κ²ƒμœΌλ‘œ, μžλ°”μŠ€ν¬λ¦½νŠΈμ˜ String.prototype.lengthκ°€ λ°”λ‘œ 이 값을 λ°˜ν™˜ν•©λ‹ˆλ‹€.

μ½”λ“œ 포인트 수 κΈ°μ€€μ—μ„œλŠ” 1이 λ©λ‹ˆλ‹€. μœ λ‹ˆμ½”λ“œμ—μ„œ πŸ„μ€ ν•˜λ‚˜μ˜ κ³ μœ ν•œ μ½”λ“œ 포인트(U+1F344)둜 μ‹λ³„λ˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

λ§ˆμ§€λ§‰μœΌλ‘œ κ·Έλž˜ν•Œ 수, 즉 ν™”λ©΄μ—μ„œ μ‚¬μš©μžκ°€ μΈμ‹ν•˜λŠ” β€œλ³΄μ΄λŠ” κΈ€μž μˆ˜β€λ‘œ 보면 μ—­μ‹œ 1μž…λ‹ˆλ‹€. 이게 μš°λ¦¬κ°€ 일반적으둜 β€œλ¬Έμž ν•˜λ‚˜β€λΌκ³  λŠλΌλŠ” UX κΈ°μ€€μ˜ λ‹¨μœ„μž…λ‹ˆλ‹€.

μ •λ¦¬ν•˜μžλ©΄, πŸ„ ν•˜λ‚˜λŠ” μ €μž₯ μ‹œ 4λ°”μ΄νŠΈ, JS의 .lengthλ‘œλŠ” 2, μœ λ‹ˆμ½”λ“œ 문자 κΈ°μ€€μœΌλ‘œλŠ” 1, 그리고 μ‹œκ°μ μœΌλ‘œλ„ 1κΈ€μžμž…λ‹ˆλ‹€. 즉, β€œκΈΈμ΄β€λŠ” 기술적 관점에 따라 μ „ν˜€ λ‹€λ₯΄κ²Œ μ •μ˜λ  수 μžˆμŠ΅λ‹ˆλ‹€.

3. μ‹€λ¬΄μ—μ„œ β€œμ§„μ§œ κΈ€μž μˆ˜β€ μ„ΈλŠ” 방법

μš”μ¦˜ 이λͺ¨μ§€λ“€μ€ μŠ€ν‚¨ν†€, κ°€μ‘±, 성별 μ‘°ν•© 같은 ZWJ μ‹œν€€μŠ€κ°€ ν”ν•©λ‹ˆλ‹€. λˆˆμ— λ³΄μ΄λŠ” κΈ€μž ν•˜λ‚˜κ°€ μ—¬λŸ¬ μ½”λ“œ μœ λ‹› λ˜λŠ” μ—¬λŸ¬ μ½”λ“œ 포인트둜 μ΄λ£¨μ–΄μ§ˆ 수 있죠.

κ·Έλž˜μ„œ β€œλ³΄μ΄λŠ” κΈ€μž μˆ˜β€λ₯Ό μ„Έλ €λ©΄ μ•„λž˜ 두 κ°€μ§€ 방식 쀑 ν•˜λ‚˜λ₯Ό 써야 ν•©λ‹ˆλ‹€.

1) μ½”λ“œ 포인트 κΈ°μ€€

[..."πŸ„πŸ‘πŸ½"].length; // 2

const sliceByCodePoints = (str, n) => Array.from(str).slice(0, n).join("");

μž₯점: ES2015 이상이면 μ–΄λ””μ„œλ‚˜ μž‘λ™

단점: ZWJ μ‘°ν•©(πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦)은 μ—¬λŸ¬ κΈ€μžλ‘œ 잘릴 수 있음


2) κ·Έλž˜ν•Œ κΈ°μ€€ (μ‚¬μš©μž κ΄€μ μ˜ μ§„μ§œ κΈ€μž 수)

const countGraphemes = (str) => {
  const seg = new Intl.Segmenter("ko", { granularity: "grapheme" });
  return [...seg.segment(str)].length;
};

const sliceByGraphemes = (str, n) => {
  const seg = new Intl.Segmenter("ko", { granularity: "grapheme" });
  return [...seg.segment(str)]
    .slice(0, n)
    .map((s) => s.segment)
    .join("");
};

countGraphemes("πŸ‘πŸ½"); // 1

μ΅œμ‹  λΈŒλΌμš°μ €Β·Node(18+) λŒ€λΆ€λΆ„ 지원.

미지원 ν™˜κ²½μ—μ„  grapheme-splitter 폴리필을 μ‚¬μš©ν•˜μ„Έμš”.


4. 곡톡 μœ ν‹Έ ν…œν”Œλ¦Ώ (ν”„λ‘ νŠΈ/μ„œλ²„ 곡용)

// utils/charLength.ts
export type LengthMode = "grapheme" | "codePoint" | "codeUnit";

export function lengthOf(str: string, mode: LengthMode = "grapheme"): number {
  switch (mode) {
    case "codeUnit":
      return str.length;
    case "codePoint":
      return Array.from(str).length;
    case "grapheme": {
      const AnyIntl: any = Intl as any;
      if (AnyIntl?.Segmenter) {
        const seg = new AnyIntl.Segmenter("ko", { granularity: "grapheme" });
        return [...seg.segment(str)].length;
      }
      const { default: GraphemeSplitter } = require("grapheme-splitter");
      return new GraphemeSplitter().countGraphemes(str);
    }
  }
}

ν”„λ‘ νŠΈμ™€ μ„œλ²„κ°€ 같은 μœ ν‹Έμ„ μ‚¬μš©ν•˜λ©΄ 길이 계산 뢈일치 문제λ₯Ό μ›μ²œ 차단할 수 μžˆμŠ΅λ‹ˆλ‹€.


5. MySQLμ—μ„œ λ¬Έμžμ—΄ 길이λ₯Ό μ…€ λ•Œμ˜ 함정과 λŒ€μ²˜λ²•

μžλ°”μŠ€ν¬λ¦½νŠΈμ˜ λ¬Έμžμ—΄ κΈΈμ΄λŠ” UTF-16 μ½”λ“œ μœ λ‹› μˆ˜μ§€λ§Œ, MySQL은 λ¬Έμžμ—΄μ„ λ°”μ΄νŠΈ(byte) λ˜λŠ” 문자(character) κΈ°μ€€μœΌλ‘œ κ³„μ‚°ν•©λ‹ˆλ‹€.

이 차이λ₯Ό ν˜Όλ™ν•˜λ©΄, 특히 이λͺ¨μ§€Β·ν•œκΈ€Β·νŠΉμˆ˜λ¬Έμž μž…λ ₯ μ‹œ μ œμ•½ 쑰건 였λ₯˜λ‚˜ 데이터 잘림 같은 λ¬Έμ œκ°€ μ‰½κ²Œ λ°œμƒν•©λ‹ˆλ‹€.

1) LENGTH() β€” λ°”μ΄νŠΈ 수 κΈ°μ€€

LENGTH()λŠ” λ¬Έμžμ—΄μ΄ μ‹€μ œλ‘œ μ €μž₯μ†Œμ—μ„œ μ°¨μ§€ν•˜λŠ” λ°”μ΄νŠΈ 크기λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. 즉, 같은 λ¬Έμžμ—΄μ΄λΌλ„ 인코딩에 따라 κ²°κ³Όκ°€ λ‹¬λΌμ§‘λ‹ˆλ‹€.

MySQL이 utf8mb4λ₯Ό μ‚¬μš©ν•  λ•Œμ˜ μ˜ˆμ‹œλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

SELECT LENGTH('πŸ„');  -- 4
SELECT LENGTH('ν•œ');  -- 3
SELECT LENGTH('A');   -- 1
  • πŸ„(이λͺ¨μ§€): 4λ°”μ΄νŠΈ
  • ν•œκΈ€: 3λ°”μ΄νŠΈ
  • ASCII 문자: 1λ°”μ΄νŠΈ

LENGTH()λŠ” 컬럼 크기(VARCHAR(255) λ“±)λ₯Ό κ³„μ‚°ν•˜κ±°λ‚˜ μ €μž₯ μš©λŸ‰μ„ 확인할 λ•ŒλŠ” μœ μš©ν•˜μ§€λ§Œ, μ‚¬μš©μž μž…λ ₯ 길이 μ œν•œμ²˜λŸΌ β€œκΈ€μž μˆ˜β€κ°€ μ€‘μš”ν•œ λ‘œμ§μ—λŠ” μ ν•©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.


2) CHAR_LENGTH() β€” 문자 수 κΈ°μ€€

CHAR_LENGTH() (λ˜λŠ” CHARACTER_LENGTH())λŠ” 문자 개수(μ½”λ“œ 포인트 수) κΈ°μ€€μœΌλ‘œ 길이λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.

SELECT CHAR_LENGTH('πŸ„');  -- 1
SELECT CHAR_LENGTH('ν•œ');  -- 1
SELECT CHAR_LENGTH('A');   -- 1

이 ν•¨μˆ˜λŠ” 인코딩 방식(UTF-8, UTF-16, UTF-32)에 상관없이 β€œμœ λ‹ˆμ½”λ“œ 문자 ν•˜λ‚˜β€λ₯Ό 1둜 κ³„μ‚°ν•©λ‹ˆλ‹€.

λ”°λΌμ„œ λ‹‰λ„€μž„, λŒ“κΈ€, μ‚¬μš©μž μž…λ ₯ ν•„λ“œ λ“± UX κΈ°μ€€μ˜ κΈ€μž 수 κ²€μ¦μ—λŠ” CHAR_LENGTH()λ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μ˜¬λ°”λ₯Έ μ„ νƒμž…λ‹ˆλ‹€.


3) κ·Έλž˜ν•Œ λ‹¨μœ„λŠ” μ—¬μ „νžˆ λ³„κ°œλ‹€

μ£Όμ˜ν•  점은, λ³΄μ΄λŠ” κΈ€μž(κ·Έλž˜ν•Œ) 와 μœ λ‹ˆμ½”λ“œ 문자(μ½”λ“œ 포인트) κ°€ 항상 μΌμΉ˜ν•˜μ§„ μ•ŠλŠ”λ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄ κ°€μ‘± 이λͺ¨μ§€ πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ λŠ” μ—¬λŸ¬ μ½”λ“œ 포인트λ₯Ό ZWJ(Zero Width Joiner) 둜 μ—°κ²°ν•œ μ‘°ν•©μž…λ‹ˆλ‹€.

SELECT CHAR_LENGTH('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'); -- 7

λˆˆμœΌλ‘œλŠ” β€œν•œ κΈ€μžβ€μ§€λ§Œ, MySQL μž…μž₯μ—μ„œλŠ” 7개의 문자둜 κ³„μ‚°λ©λ‹ˆλ‹€. 즉, MySQL은 κ·Έλž˜ν•Œ λ‹¨μœ„(μ‹œκ°μ μœΌλ‘œ λ³΄μ΄λŠ” κΈ€μž 수) λ₯Ό μ΄ν•΄ν•˜μ§€ λͺ»ν•©λ‹ˆλ‹€.

UX κΈ°μ€€μ˜ β€œκΈ€μž 수 μ œν•œβ€μ„ μ •ν™•νžˆ μ μš©ν•˜λ €λ©΄ DBκ°€ μ•„λ‹Œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ ˆλ²¨μ—μ„œ μ²˜λ¦¬ν•΄μ•Ό ν•©λ‹ˆλ‹€. (Intl.Segmenter λ˜λŠ” grapheme-splitterλ₯Ό μ‚¬μš©)


4) 정리

  • LENGTH() β†’ μ €μž₯ μš©λŸ‰(λ°”μ΄νŠΈ) κΈ°μ€€. 인코딩에 따라 값이 달라짐.
  • CHAR_LENGTH() β†’ μ½”λ“œ 포인트 κΈ°μ€€. 일반적인 β€œλ¬Έμž μˆ˜β€ 계산에 μ‚¬μš©.
  • κ·Έλž˜ν•Œ κΈ°μ€€(λ³΄μ΄λŠ” κΈ€μž 수) 은 DBμ—μ„œ μ§€μ›ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ JS/μ„œλ²„μ—μ„œ 계산.
  • DB의 컬럼 μ œμ•½(VARCHAR, TEXT)은 μ €μž₯ μš©λŸ‰ κΈ°μ€€μœΌλ‘œ, μž…λ ₯ 검증은 β€œλ³΄μ΄λŠ” κΈ€μž μˆ˜β€ κΈ°μ€€μœΌλ‘œ λ‚˜λˆ„μ–΄ μ„€κ³„ν•˜μž.

μš”μ•½ν•˜μžλ©΄,

MySQL은 β€˜λ³΄μ΄λŠ” κΈ€μž μˆ˜β€™λ₯Ό λͺ¨λ₯Έλ‹€. > LENGTH()λŠ” μ €μž₯ 곡간, CHAR_LENGTH()λŠ” 문자 개수.

UX κΈ°μ€€μ˜ 길이 μ œν•œμ€ κ²°κ΅­ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ λͺ«μ΄λ‹€.