guksulog
ํฌ์ŠคํŠธ ๋ชฉ๋ก
#JavaScript

'๐Ÿ„'.length === 2? ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ ์ด๋ชจ์ง€ ๋ฌธ์ž์—ด ๊ธธ์ด์˜ ํ•จ์ •๊ณผ ํ•ด๊ฒฐ๋ฒ•

2025๋…„ 10์›” 8์ผ 4๋ถ„ ์ฝ๊ธฐ

ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ฌธ์ž์—ด ๊ธธ์ด๋ฅผ ๊ฒ€์‚ฌํ•˜๋‹ค ๋ณด๋ฉด ์ด๋Ÿฐ ๊ฒฝํ—˜์ด ์žˆ์„ ๊ฑฐ์˜ˆ์š”.

"์ด๋ชจ์ง€ ํ•˜๋‚˜๋ฅผ ๋„ฃ์—ˆ๋Š”๋ฐ ๊ธธ์ด๊ฐ€ 2๋กœ ๊ณ„์‚ฐ๋ผ?" '๐Ÿ„'.length // 2

์ด๊ฑด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์˜ ๋ฌธ์ž์—ด ๋ชจ๋ธ์ด ์œ ๋‹ˆ์ฝ”๋“œ์™€ ๋งž๋ฌผ๋ฆฌ๋Š” ์ง€์ ์—์„œ ์ƒ๊ธฐ๋Š”, ์•„์ฃผ ํ”ํ•˜์ง€๋งŒ ํ—ท๊ฐˆ๋ฆฌ๋Š” ๋ฌธ์ œ์˜ˆ์š”.

์™œ '๐Ÿ„'.length๋Š” 2์ผ๊นŒ?

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฌธ์ž์—ด์€ UTF-16 ์ฝ”๋“œ ์œ ๋‹› ๋‹จ์œ„๋กœ ์ €์žฅ๋ผ์š”. ์ฆ‰, String.prototype.length๋Š” ์‹ค์ œ "๋ณด์ด๋Š” ๊ธ€์ž ์ˆ˜"๊ฐ€ ์•„๋‹ˆ๋ผ "16๋น„ํŠธ ์กฐ๊ฐ ๊ฐœ์ˆ˜"๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฑฐ์ฃ .

"๐Ÿ„" (U+1F344)์€ BMP(๊ธฐ๋ณธ ๋‹ค๊ตญ์–ด ํ‰๋ฉด) ๋ฐ–์˜ ๋ฌธ์ž๋ผ 16๋น„ํŠธ ๋‘ ์กฐ๊ฐ(์„œ๋กœ๊ฒŒ์ดํŠธ ํŽ˜์–ด)๋กœ ํ‘œํ˜„๋ผ์š”. BMP๋Š” U+0000๋ถ€ํ„ฐ U+FFFF๊นŒ์ง€์˜ ๋ฒ”์œ„์ธ๋ฐ, ์ด ์•ˆ์— ๋“œ๋Š” ๋ฌธ์ž๋Š” ์ฝ”๋“œ ์œ ๋‹› ํ•˜๋‚˜๋กœ ํ‘œํ˜„๋˜์ง€๋งŒ ๊ทธ ๋ฐ–์˜ ๋ฌธ์ž๋Š” ๋‘ ๊ฐœ์˜ ์ฝ”๋“œ ์œ ๋‹›, ์ฆ‰ ์„œ๋กœ๊ฒŒ์ดํŠธ ํŽ˜์–ด๋กœ ํ‘œํ˜„ํ•ด์•ผ ํ•˜๊ฑฐ๋“ ์š”. ์ด๋ชจ์ง€ ๋Œ€๋ถ€๋ถ„์ด ์ด ๋ฒ”์œ„ ๋ฐ–์— ์žˆ์–ด์„œ .length๊ฐ€ 2๊ฐ€ ๋˜๋Š” ๊ฑฐ์˜ˆ์š”.

js
"๐Ÿ„".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 ์‹œํ€€์Šค๊ฐ€ ํ”ํ•ด์š”. ๋ˆˆ์— ๋ณด์ด๋Š” ๊ธ€์ž ํ•˜๋‚˜๊ฐ€ ์—ฌ๋Ÿฌ ์ฝ”๋“œ ์œ ๋‹› ๋˜๋Š” ์—ฌ๋Ÿฌ ์ฝ”๋“œ ํฌ์ธํŠธ๋กœ ์ด๋ฃจ์–ด์งˆ ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”.

๊ทธ๋ž˜์„œ "๋ณด์ด๋Š” ๊ธ€์ž ์ˆ˜"๋ฅผ ์„ธ๋ ค๋ฉด ์•„๋ž˜ ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹ ์ค‘ ํ•˜๋‚˜๋ฅผ ์จ์•ผ ํ•ด์š”.

์ฝ”๋“œ ํฌ์ธํŠธ ๊ธฐ์ค€

js
[..."๐Ÿ„๐Ÿ‘๐Ÿฝ"].length; // 2

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

ES2015 ์ด์ƒ์ด๋ฉด ์–ด๋””์„œ๋‚˜ ์ž‘๋™ํ•œ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ZWJ ์กฐํ•ฉ(๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ)์€ ์—ฌ๋Ÿฌ ๊ธ€์ž๋กœ ์ž˜๋ฆด ์ˆ˜ ์žˆ๋‹ค๋Š” ํ•œ๊ณ„๊ฐ€ ์žˆ์–ด์š”.

๊ทธ๋ž˜ํ•Œ ๊ธฐ์ค€ (์‚ฌ์šฉ์ž ๊ด€์ ์˜ ์ง„์งœ ๊ธ€์ž ์ˆ˜)

js
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 ํด๋ฆฌํ•„์„ ์“ฐ๋ฉด ๋ผ์š”.

๊ณตํ†ต ์œ ํ‹ธ ํ…œํ”Œ๋ฆฟ (ํ”„๋ก ํŠธ/์„œ๋ฒ„ ๊ณต์šฉ)

ts
// 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๋ฅผ ์“ธ ๋•Œ์˜ ์˜ˆ์‹œ๋Š” ์ด๋ž˜์š”.

sql
SELECT LENGTH('๐Ÿ„');  -- 4
SELECT LENGTH('ํ•œ');  -- 3
SELECT LENGTH('A');   -- 1

์ด๋ชจ์ง€๋Š” 4๋ฐ”์ดํŠธ, ํ•œ๊ธ€์€ 3๋ฐ”์ดํŠธ, ASCII ๋ฌธ์ž๋Š” 1๋ฐ”์ดํŠธ๋ฅผ ์ฐจ์ง€ํ•ด์š”. LENGTH()๋Š” ์ปฌ๋Ÿผ ํฌ๊ธฐ(VARCHAR(255) ๋“ฑ)๋ฅผ ๊ณ„์‚ฐํ•˜๊ฑฐ๋‚˜ ์ €์žฅ ์šฉ๋Ÿ‰์„ ํ™•์ธํ•  ๋•Œ๋Š” ์œ ์šฉํ•œ๋ฐ, ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ธธ์ด ์ œํ•œ์ฒ˜๋Ÿผ "๊ธ€์ž ์ˆ˜"๊ฐ€ ์ค‘์š”ํ•œ ๋กœ์ง์—๋Š” ์ ํ•ฉํ•˜์ง€ ์•Š์•„์š”.

CHAR_LENGTH() โ€” ๋ฌธ์ž ์ˆ˜ ๊ธฐ์ค€

CHAR_LENGTH() (๋˜๋Š” CHARACTER_LENGTH())๋Š” ๋ฌธ์ž ๊ฐœ์ˆ˜(์ฝ”๋“œ ํฌ์ธํŠธ ์ˆ˜) ๊ธฐ์ค€์œผ๋กœ ๊ธธ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”.

sql
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) ๋กœ ์—ฐ๊ฒฐํ•œ ์กฐํ•ฉ์ด๊ฑฐ๋“ ์š”.

sql
SELECT CHAR_LENGTH('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ'); -- 7

๋ˆˆ์œผ๋กœ๋Š” "ํ•œ ๊ธ€์ž"์ง€๋งŒ, MySQL ์ž…์žฅ์—์„œ๋Š” 7๊ฐœ์˜ ๋ฌธ์ž๋กœ ๊ณ„์‚ฐ๋ผ์š”. MySQL์€ ๊ทธ๋ž˜ํ•Œ ๋‹จ์œ„(์‹œ๊ฐ์ ์œผ๋กœ ๋ณด์ด๋Š” ๊ธ€์ž ์ˆ˜) ๋ฅผ ์ดํ•ดํ•˜์ง€ ๋ชปํ•˜๋‹ˆ๊นŒ์š”.

๊ฒฐ๊ตญ LENGTH()๋Š” ์ €์žฅ ์šฉ๋Ÿ‰(๋ฐ”์ดํŠธ) ๊ธฐ์ค€์ด๋ผ ์ธ์ฝ”๋”ฉ์— ๋”ฐ๋ผ ๊ฐ’์ด ๋‹ฌ๋ผ์ง€๊ณ , CHAR_LENGTH()๋Š” ์ฝ”๋“œ ํฌ์ธํŠธ ๊ธฐ์ค€์œผ๋กœ ์ผ๋ฐ˜์ ์ธ "๋ฌธ์ž ์ˆ˜" ๊ณ„์‚ฐ์— ์จ์š”. ํ•˜์ง€๋งŒ ๊ทธ๋ž˜ํ•Œ ๊ธฐ์ค€์˜ ๋ณด์ด๋Š” ๊ธ€์ž ์ˆ˜๋Š” DB์—์„œ ์ง€์›ํ•˜์ง€ ์•Š์œผ๋‹ˆ๊นŒ JS๋‚˜ ์„œ๋ฒ„์—์„œ ๊ณ„์‚ฐํ•ด์•ผ ํ•ด์š”. DB์˜ ์ปฌ๋Ÿผ ์ œ์•ฝ(VARCHAR, TEXT)์€ ์ €์žฅ ์šฉ๋Ÿ‰ ๊ธฐ์ค€์œผ๋กœ, ์ž…๋ ฅ ๊ฒ€์ฆ์€ "๋ณด์ด๋Š” ๊ธ€์ž ์ˆ˜" ๊ธฐ์ค€์œผ๋กœ ๋‚˜๋ˆ„์–ด ์„ค๊ณ„ํ•˜๋Š” ๊ฒƒ์ด UX ๊ธฐ์ค€์˜ ๊ธ€์ž ์ˆ˜ ์ œํ•œ์„ ์ •ํ™•ํžˆ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์—์š”.