Posts

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

2025-10-08

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

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

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

μ™œ 'πŸ„'.lengthλŠ” 2일까?

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

"πŸ„" (U+1F344)은 BMP(κΈ°λ³Έ λ‹€κ΅­μ–΄ 평면) λ°–μ˜ 문자라 16λΉ„νŠΈ 두 쑰각(μ„œλ‘œκ²Œμ΄νŠΈ νŽ˜μ–΄)둜 ν‘œν˜„λΌμš”. BMPλŠ” U+0000λΆ€ν„° U+FFFFκΉŒμ§€μ˜ λ²”μœ„μΈλ°, 이 μ•ˆμ— λ“œλŠ” λ¬ΈμžλŠ” μ½”λ“œ μœ λ‹› ν•˜λ‚˜λ‘œ ν‘œν˜„λ˜μ§€λ§Œ κ·Έ λ°–μ˜ λ¬ΈμžλŠ” 두 개의 μ½”λ“œ μœ λ‹›, 즉 μ„œλ‘œκ²Œμ΄νŠΈ νŽ˜μ–΄λ‘œ ν‘œν˜„ν•΄μ•Ό ν•˜κ±°λ“ μš”. 이λͺ¨μ§€ λŒ€λΆ€λΆ„μ΄ 이 λ²”μœ„ 밖에 μžˆμ–΄μ„œ .lengthκ°€ 2κ°€ λ˜λŠ” κ±°μ˜ˆμš”.

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

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

"길이"λŠ” 무엇을 μ„ΈλŠλƒμ— 따라 달라진닀

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

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

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

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

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

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

μ‹€λ¬΄μ—μ„œ "μ§„μ§œ κΈ€μž 수" μ„ΈλŠ” 방법

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

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

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

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

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

ES2015 이상이면 μ–΄λ””μ„œλ‚˜ μž‘λ™ν•œλ‹€λŠ” μž₯점이 μžˆμ§€λ§Œ, ZWJ μ‘°ν•©(πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦)은 μ—¬λŸ¬ κΈ€μžλ‘œ 잘릴 수 μžˆλ‹€λŠ” ν•œκ³„κ°€ μžˆμ–΄μš”.

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

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 폴리필을 μ“°λ©΄ λΌμš”.

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

// 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);
    }
  }
}

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

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

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

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

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

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

MySQL이 utf8mb4λ₯Ό μ“Έ λ•Œμ˜ μ˜ˆμ‹œλŠ” μ΄λž˜μš”.

SELECT LENGTH('πŸ„');  -- 4
SELECT LENGTH('ν•œ');  -- 3
SELECT LENGTH('A');   -- 1

이λͺ¨μ§€λŠ” 4λ°”μ΄νŠΈ, ν•œκΈ€μ€ 3λ°”μ΄νŠΈ, ASCII λ¬ΈμžλŠ” 1λ°”μ΄νŠΈλ₯Ό μ°¨μ§€ν•΄μš”. LENGTH()λŠ” 컬럼 크기(VARCHAR(255) λ“±)λ₯Ό κ³„μ‚°ν•˜κ±°λ‚˜ μ €μž₯ μš©λŸ‰μ„ 확인할 λ•ŒλŠ” μœ μš©ν•œλ°, μ‚¬μš©μž μž…λ ₯ 길이 μ œν•œμ²˜λŸΌ "κΈ€μž 수"κ°€ μ€‘μš”ν•œ λ‘œμ§μ—λŠ” μ ν•©ν•˜μ§€ μ•Šμ•„μš”.

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()λ₯Ό μ“°λŠ” 게 μ˜¬λ°”λ₯Έ μ„ νƒμ΄μ—μš”.

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

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

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

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

κ²°κ΅­ LENGTH()λŠ” μ €μž₯ μš©λŸ‰(λ°”μ΄νŠΈ) 기쀀이라 인코딩에 따라 값이 달라지고, CHAR_LENGTH()λŠ” μ½”λ“œ 포인트 κΈ°μ€€μœΌλ‘œ 일반적인 "문자 수" 계산에 μ¨μš”. ν•˜μ§€λ§Œ κ·Έλž˜ν•Œ κΈ°μ€€μ˜ λ³΄μ΄λŠ” κΈ€μž μˆ˜λŠ” DBμ—μ„œ μ§€μ›ν•˜μ§€ μ•ŠμœΌλ‹ˆκΉŒ JSλ‚˜ μ„œλ²„μ—μ„œ 계산해야 ν•΄μš”. DB의 컬럼 μ œμ•½(VARCHAR, TEXT)은 μ €μž₯ μš©λŸ‰ κΈ°μ€€μœΌλ‘œ, μž…λ ₯ 검증은 "λ³΄μ΄λŠ” κΈ€μž 수" κΈ°μ€€μœΌλ‘œ λ‚˜λˆ„μ–΄ μ„€κ³„ν•˜λŠ” 것이 UX κΈ°μ€€μ˜ κΈ€μž 수 μ œν•œμ„ μ •ν™•νžˆ μ μš©ν•˜λŠ” λ°©λ²•μ΄μ—μš”.