JavaScript에서 텍스트를 단어로 분리하는 방법은?
Intl.Segmenter를 사용하여 단어 사이에 공백이 없는 언어를 포함한 모든 언어의 텍스트에서 단어를 추출할 수 있습니다.
소개
텍스트에서 단어를 추출해야 할 때, 일반적인 접근 방식은 split(" ")을 사용하여 공백을 기준으로 분할하는 것입니다. 이 방법은 영어에서는 잘 작동하지만, 단어 사이에 공백을 사용하지 않는 언어에서는 완전히 실패합니다. 중국어, 일본어, 태국어 및 기타 언어는 단어 구분자 없이 연속적으로 텍스트를 작성하지만, 사용자는 그 텍스트에서 구분된 단어를 인식합니다.
Intl.Segmenter API는 이 문제를 해결합니다. 이 API는 유니코드 표준과 각 언어의 언어학적 규칙에 따라 단어 경계를 식별합니다. 언어가 공백을 사용하는지 여부에 관계없이 텍스트에서 단어를 추출할 수 있으며, 세그먼터는 단어의 시작과 끝을 결정하는 복잡성을 처리합니다.
이 글에서는 기본 문자열 분할이 국제 텍스트에서 실패하는 이유, 다양한 문자 체계에서 단어 경계가 어떻게 작동하는지, 그리고 모든 언어에서 텍스트를 올바르게 단어로 분할하기 위해 Intl.Segmenter를 어떻게 사용하는지 설명합니다.
공백으로 분할하는 방식이 실패하는 이유
split() 메서드는 구분자가 나타날 때마다 문자열을 분할합니다. 영어 텍스트의 경우, 공백으로 분할하면 단어를 추출할 수 있습니다.
const text = "Hello world";
const words = text.split(" ");
console.log(words);
// ["Hello", "world"]
이 접근 방식은 단어가 공백으로 구분된다고 가정합니다. 많은 언어들이 이 패턴을 따르지 않습니다.
중국어 텍스트는 단어 사이에 공백을 포함하지 않습니다.
const text = "你好世界";
const words = text.split(" ");
console.log(words);
// ["你好世界"]
사용자는 두 개의 구분된 단어를 보지만, 분할할 공백이 없기 때문에 split()은 전체 문자열을 단일 요소로 반환합니다.
일본어 텍스트는 여러 스크립트를 혼합하고 단어 사이에 공백을 사용하지 않습니다.
const text = "今日は良い天気です";
const words = text.split(" ");
console.log(words);
// ["今日は良い天気です"]
이 문장은 여러 단어를 포함하지만, 공백으로 분할하면 하나의 요소만 생성됩니다.
태국어 텍스트도 공백 없이 단어를 연속적으로 작성합니다.
const text = "สวัสดีครับ";
const words = text.split(" ");
console.log(words);
// ["สวัสดีครับ"]
이 텍스트는 두 단어를 포함하지만, split()은 하나의 요소를 반환합니다.
이러한 언어들의 경우, 단어 경계를 식별하기 위해 다른 접근 방식이 필요합니다.
정규 표현식이 단어 경계에 실패하는 이유
정규 표현식 단어 경계는 \b 패턴을 사용하여 단어와 비단어 문자 사이의 위치를 매칭합니다. 이는 영어에서는 잘 작동합니다.
const text = "Hello world!";
const words = text.match(/\b\w+\b/g);
console.log(words);
// ["Hello", "world"]
이 패턴은 공백이 없는 언어에서는 실패합니다. 정규 표현식 엔진이 중국어, 일본어 또는 태국어와 같은 스크립트에서 단어 경계를 인식하지 못하기 때문입니다.
const text = "你好世界";
const words = text.match(/\b\w+\b/g);
console.log(words);
// ["你好世界"]
정규 표현식은 중국어 단어 경계를 이해하지 못하기 때문에 전체 문자열을 하나의 단어로 취급합니다.
영어에서도 정규 표현식 패턴은 구두점, 축약형 또는 특수 문자가 있을 때 잘못된 결과를 생성할 수 있습니다. 정규 표현식은 모든 문자 체계에서 언어적 단어 분할을 처리하도록 설계되지 않았습니다.
다양한 언어에서의 단어 경계
단어 경계는 한 단어가 끝나고 다른 단어가 시작되는 텍스트 내의 위치입니다. 다양한 문자 체계는 단어 경계에 대해 서로 다른 규칙을 사용합니다.
영어, 스페인어, 프랑스어, 독일어와 같은 공백으로 구분된 언어는 단어 경계를 표시하기 위해 공백을 사용합니다. "hello"라는 단어는 "world"와 공백으로 구분됩니다.
중국어, 일본어, 태국어와 같은 연속 문자 언어(scriptio continua)는 단어 사이에 공백을 사용하지 않습니다. 단어 경계는 의미론적 및 형태론적 규칙에 기반하여 존재하지만, 이러한 경계는 텍스트에서 시각적으로 표시되지 않습니다. 중국어 독자는 시각적 구분자가 아닌 언어에 대한 친숙함을 통해 한 단어가 끝나고 다른 단어가 시작되는 위치를 인식합니다.
일부 언어는 혼합된 규칙을 사용합니다. 일본어는 한자, 히라가나, 가타카나 문자를 결합하며, 단어 경계는 문자 유형 간의 전환 또는 문법 구조에 기반하여 발생합니다.
유니코드 표준은 UAX 29에서 단어 경계 규칙을 정의합니다. 이 규칙은 모든 스크립트에 대한 단어 경계를 식별하는 방법을 지정합니다. 이 규칙은 단어의 시작과 끝을 결정하기 위해 문자 속성, 스크립트 유형 및 언어적 패턴을 고려합니다.
Intl.Segmenter를 사용하여 텍스트를 단어로 분할하기
Intl.Segmenter 생성자는 유니코드 규칙에 따라 텍스트를 분할하는 세그먼터 객체를 생성합니다. 로케일과 세분성을 지정합니다.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world!";
const segments = segmenter.segment(text);
첫 번째 인수는 로케일 식별자입니다. 두 번째 인수는 granularity: "word"가 세그먼터에게 단어 경계에서 분할하도록 지시하는 옵션 객체입니다.
segment() 메서드는 세그먼트를 포함하는 반복 가능한 객체를 반환합니다. for...of를 사용하여 세그먼트를 반복할 수 있습니다.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world!";
for (const segment of segmenter.segment(text)) {
console.log(segment);
}
// { segment: "Hello", index: 0, input: "Hello world!", isWordLike: true }
// { segment: " ", index: 5, input: "Hello world!", isWordLike: false }
// { segment: "world", index: 6, input: "Hello world!", isWordLike: true }
// { segment: "!", index: 11, input: "Hello world!", isWordLike: false }
각 세그먼트 객체에는 다음 속성이 포함됩니다:
segment: 이 세그먼트의 텍스트index: 이 세그먼트가 시작되는 원본 문자열의 위치input: 분할되는 원본 문자열isWordLike: 이 세그먼트가 단어인지 비단어 콘텐츠인지 여부
isWordLike 속성 이해하기
단어별로 텍스트를 분할할 때, 세그먼터는 단어 세그먼트와 비단어 세그먼트를 모두 반환합니다. 단어에는 문자, 숫자 및 표의문자가 포함됩니다. 비단어 세그먼트에는 공백, 구두점 및 기타 구분자가 포함됩니다.
isWordLike 속성은 세그먼트가 단어인지 여부를 나타냅니다. 이 속성은 단어 문자를 포함하는 세그먼트의 경우 true이고, 공백, 구두점 또는 기타 비단어 문자만 포함하는 세그먼트의 경우 false입니다.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello, world!";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "Hello" true
// "," false
// " " false
// "world" true
// "!" false
isWordLike 속성을 사용하여 구두점과 공백에서 단어 세그먼트를 필터링합니다. 이렇게 하면 구분자 없이 단어만 얻을 수 있습니다.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello, world!";
const segments = segmenter.segment(text);
const words = Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment);
console.log(words);
// ["Hello", "world"]
이 패턴은 공백이 없는 언어를 포함한 모든 언어에서 작동합니다.
공백 없는 텍스트에서 단어 추출하기
세그먼터는 공백을 사용하지 않는 언어에서도 단어 경계를 정확하게 식별합니다. 중국어 텍스트의 경우, 세그먼터는 유니코드 규칙과 언어적 패턴을 기반으로 단어 경계에서 분할합니다.
const segmenter = new Intl.Segmenter("zh", { granularity: "word" });
const text = "你好世界";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "你好" true
// "世界" true
세그먼터는 이 텍스트에서 두 개의 단어를 식별합니다. 공백이 없지만, 세그먼터는 중국어 단어 경계를 이해하고 텍스트를 적절하게 분할합니다.
일본어 텍스트의 경우, 세그먼터는 혼합 스크립트의 복잡성을 처리하고 단어 경계를 식별합니다.
const segmenter = new Intl.Segmenter("ja", { granularity: "word" });
const text = "今日は良い天気です";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "今日" true
// "は" true
// "良い" true
// "天気" true
// "です" true
세그먼터는 이 문장을 다섯 개의 단어 세그먼트로 분할합니다. "は"와 같은 조사가 별도의 단어이고 "天気"와 같은 복합어가 단일 단위를 형성한다는 것을 인식합니다.
태국어 텍스트의 경우, 세그먼터는 공백 없이 단어 경계를 식별합니다.
const segmenter = new Intl.Segmenter("th", { granularity: "word" });
const text = "สวัสดีครับ";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "สวัสดี" true
// "ครับ" true
세그먼터는 이 인사말에서 두 개의 단어를 정확하게 식별합니다.
단어 추출 함수 구축하기
모든 언어의 텍스트에서 단어를 추출하는 함수를 만들어 보겠습니다.
function getWords(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
return Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment);
}
이 함수는 공백으로 구분된 언어와 공백 없는 언어 모두에서 작동합니다.
getWords("Hello, world!", "en");
// ["Hello", "world"]
getWords("你好世界", "zh");
// ["你好", "世界"]
getWords("今日は良い天気です", "ja");
// ["今日", "は", "良い", "天気", "です"]
getWords("Bonjour le monde!", "fr");
// ["Bonjour", "le", "monde"]
getWords("สวัสดีครับ", "th");
// ["สวัสดี", "ครับ"]
이 함수는 언어나 문자 체계에 관계없이 단어 배열을 반환합니다.
다양한 언어에서 단어 정확하게 세기
모든 언어에서 작동하는 단어 카운터를 만들기 위해 단어와 유사한 세그먼트를 세는 방법을 구현합니다.
function countWords(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
return Array.from(segments).filter(s => s.isWordLike).length;
}
이 함수는 모든 언어의 텍스트에 대해 정확한 단어 수를 제공합니다.
countWords("Hello world", "en");
// 2
countWords("你好世界", "zh");
// 2
countWords("今日は良い天気です", "ja");
// 5
countWords("Bonjour le monde", "fr");
// 3
countWords("สวัสดีครับ", "th");
// 2
이 카운트는 각 언어에서 사용자가 인식하는 단어 경계와 일치합니다.
특정 위치를 포함하는 단어 찾기
'containing()' 메서드는 문자열 내 특정 인덱스를 포함하는 세그먼트를 찾습니다. 이는 커서가 어떤 단어 안에 있는지 또는 어떤 단어가 클릭되었는지 확인하는 데 유용합니다.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world";
const segments = segmenter.segment(text);
const segment = segments.containing(7);
console.log(segment);
// { segment: "world", index: 6, input: "Hello world", isWordLike: true }
인덱스 7은 인덱스 6에서 시작하는 "world"라는 단어 내에 있습니다. 이 메서드는 해당 단어에 대한 세그먼트 객체를 반환합니다.
인덱스가 공백이나 구두점 내에 있는 경우, 메서드는 isWordLike: false와 함께 해당 세그먼트를 반환합니다.
const segment = segments.containing(5);
console.log(segment);
// { segment: " ", index: 5, input: "Hello world", isWordLike: false }
이 기능은 더블 클릭 단어 선택, 커서 위치 기반 컨텍스트 메뉴 또는 현재 단어 강조 표시와 같은 텍스트 편집기 기능에 활용할 수 있습니다.
구두점 및 축약형 처리
세그먼터는 구두점을 별도의 세그먼트로 처리합니다. 영어의 축약형은 일반적으로 여러 세그먼트로 분할됩니다.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "I can't do it.";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "I" true
// " " false
// "can" true
// "'" false
// "t" true
// " " false
// "do" true
// " " false
// "it" true
// "." false
축약형 "can't"는 "can", "'", "t"로 분할됩니다. 축약형을 단일 단어로 유지해야 하는 경우, 아포스트로피를 기준으로 세그먼트를 병합하는 추가 로직이 필요합니다.
대부분의 사용 사례에서는 축약형이 분할되더라도 단어와 유사한 세그먼트를 세는 것이 의미 있는 단어 수를 제공합니다.
로케일이 단어 분할에 미치는 영향
분할기에 전달하는 로케일은 단어 경계가 결정되는 방식에 영향을 미칩니다. 동일한 텍스트에 대해 다른 로케일은 서로 다른 규칙을 가질 수 있습니다.
명확하게 정의된 단어 경계 규칙이 있는 언어의 경우, 로케일은 올바른 규칙이 적용되도록 보장합니다.
const segmenterEn = new Intl.Segmenter("en", { granularity: "word" });
const segmenterZh = new Intl.Segmenter("zh", { granularity: "word" });
const text = "你好世界";
const wordsEn = Array.from(segmenterEn.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
const wordsZh = Array.from(segmenterZh.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
console.log(wordsEn);
// ["你好世界"]
console.log(wordsZh);
// ["你好", "世界"]
영어 로케일은 중국어 단어 경계를 인식하지 못하고 전체 문자열을 하나의 단어로 취급합니다. 중국어 로케일은 중국어 단어 경계 규칙을 적용하여 두 개의 단어를 올바르게 식별합니다.
분할되는 텍스트의 언어에 적합한 로케일을 항상 사용하십시오.
성능을 위한 재사용 가능한 분할기 생성
분할기를 생성하는 것은 비용이 많이 들지 않지만, 더 나은 성능을 위해 여러 문자열에 걸쳐 분할기를 재사용할 수 있습니다.
const enSegmenter = new Intl.Segmenter("en", { granularity: "word" });
const zhSegmenter = new Intl.Segmenter("zh", { granularity: "word" });
const jaSegmenter = new Intl.Segmenter("ja", { granularity: "word" });
function getWords(text, locale) {
const segmenter = locale === "zh" ? zhSegmenter
: locale === "ja" ? jaSegmenter
: enSegmenter;
return Array.from(segmenter.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
}
이 접근 방식은 분할기를 한 번 생성하고 getWords() 호출에 재사용합니다. 분할기는 로케일 데이터를 캐시하므로, 인스턴스를 재사용하면 반복적인 초기화를 피할 수 있습니다.
실용적인 예: 단어 빈도 분석기 구축
단어 분할과 카운팅을 결합하여 텍스트의 단어 빈도를 분석합니다.
function getWordFrequency(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
const words = Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment.toLowerCase());
const frequency = {};
for (const word of words) {
frequency[word] = (frequency[word] || 0) + 1;
}
return frequency;
}
const text = "Hello world! Hello everyone in this world.";
const frequency = getWordFrequency(text, "en");
console.log(frequency);
// { hello: 2, world: 2, everyone: 1, in: 1, this: 1 }
이 함수는 텍스트를 단어로 분할하고, 소문자로 정규화한 후 발생 빈도를 계산합니다. 모든 언어에서 작동합니다.
const textZh = "你好世界!你好大家!";
const frequencyZh = getWordFrequency(textZh, "zh");
console.log(frequencyZh);
// { "你好": 2, "世界": 1, "大家": 1 }
동일한 로직이 수정 없이 중국어 텍스트도 처리합니다.
브라우저 지원 확인
Intl.Segmenter API는 2024년 4월에 기준선 상태에 도달했습니다. 현재 버전의 Chrome, Firefox, Safari 및 Edge에서 작동합니다. 이전 브라우저는 지원하지 않습니다.
API를 사용하기 전에 지원 여부를 확인하세요.
if (typeof Intl.Segmenter !== "undefined") {
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
// segmenter 사용
} else {
// 이전 브라우저를 위한 대체 방안
}
이전 브라우저를 대상으로 하는 프로덕션 애플리케이션의 경우 대체 구현을 제공하세요. 간단한 대체 방안은 영어 텍스트에는 split()을 사용하고 다른 언어에는 전체 문자열을 반환하는 것입니다.
function getWords(text, locale) {
if (typeof Intl.Segmenter !== "undefined") {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
return Array.from(segmenter.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
}
// 대체 방안: 공백으로 구분된 언어에서만 작동
return text.split(/\s+/).filter(word => word.length > 0);
}
이렇게 하면 코드가 이전 브라우저에서도 실행되지만, 공백으로 구분되지 않는 언어에 대해서는 기능이 제한됩니다.
피해야 할 일반적인 실수
다국어 텍스트에 공백이나 정규식 패턴으로 분할하지 마십시오. 이러한 접근 방식은 일부 언어에서만 작동하며 중국어, 일본어, 태국어 및 공백이 없는 다른 언어에서는 실패합니다.
단어를 추출할 때 isWordLike로 필터링하는 것을 잊지 마십시오. 이 필터 없이는 결과에 공백, 구두점 및 기타 단어가 아닌 세그먼트가 포함됩니다.
텍스트를 분할할 때 잘못된 로케일을 사용하지 마십시오. 로케일은 어떤 단어 경계 규칙이 적용되는지 결정합니다. 중국어 텍스트에 영어 로케일을 사용하면 잘못된 결과가 나옵니다.
모든 언어가 단어를 동일한 방식으로 정의한다고 가정하지 마십시오. 단어 경계는 문자 체계와 언어적 관습에 따라 다릅니다. 이러한 차이를 처리하려면 로케일을 인식하는 분할을 사용하십시오.
국제 텍스트에 대해 split(" ").length를 사용하여 단어를 계산하지 마십시오. 이는 공백으로 구분된 언어에서만 작동하며 다른 언어에서는 잘못된 계산을 생성합니다.
단어 분할을 사용해야 하는 경우
다음과 같은 경우에 단어 분할을 사용하십시오:
- 여러 언어에 걸쳐 사용자 생성 콘텐츠의 단어 수 계산
- 모든 문자 체계에서 작동하는 검색 및 강조 기능 구현
- 국제 텍스트를 처리하는 텍스트 분석 도구 구축
- 텍스트 편집기에서 단어 기반 탐색 또는 편집 기능 생성
- 다국어 문서에서 키워드 또는 용어 추출
- 모든 언어를 허용하는 양식에서 단어 수 제한 검증
문자 수만 필요한 경우에는 단어 분할을 사용하지 마십시오. 문자 수준 작업에는 자소 분할을 사용하십시오.
문장 분할에는 단어 분할을 사용하지 마십시오. 이 목적에는 문장 단위를 사용하십시오.
단어 분할이 국제화에 어떻게 적합한지
Intl.Segmenter API는 ECMAScript 국제화 API의 일부입니다. 이 계열의 다른 API는 국제화의 다른 측면을 처리합니다:
Intl.DateTimeFormat: 로케일에 따라 날짜와 시간 형식 지정Intl.NumberFormat: 로케일에 따라 숫자, 통화 및 단위 형식 지정Intl.Collator: 로케일에 따라 문자열 정렬 및 비교Intl.PluralRules: 다양한 언어에서 숫자의 복수 형태 결정
이러한 API들은 함께 전 세계 사용자에게 올바르게 작동하는 애플리케이션을 구축하는 데 필요한 도구를 제공합니다. 단어 경계를 식별해야 할 때는 단어 단위로 Intl.Segmenter를 사용하고, 형식 지정 및 비교에는 다른 Intl API를 사용하십시오.