가루 (Garu)
세계에서 가장 가벼운(1.26MB) 브라우저 네이티브 한국어 형태소 분석기.
기존 한국어 형태소 분석기는 대부분 수십 MB의 모델을 서버나 네이티브 런타임으로 구동하는 것을 전제로 합니다. Garu는 1.26MB 모델과 425KB WASM 엔진만으로 브라우저에서 직접 실행되며, 서버 통신 없이 완전한 오프라인 형태소 분석을 수행합니다.
특징
- 초경량 브라우저 실행 -- 1.26MB 모델로 브라우저에서 바로 동작하는 유일한 한국어 형태소 분석기 (수십 MB 모델 불필요)
- 초경량 -- 1.26MB 코드북 모델, 425KB WASM (npm 패키지에 포함)
- 높은 정확도 -- F1 96.05% (9,000문장 v15k 골드 테스트셋, ep_norm 정규화), 2025 구어 고정 평가 F1 91.23%
- 서버 불필요 -- WebAssembly로 클라이언트에서 직접 실행
- 결정적 -- 코드북 + 문장 N-best Trigram Viterbi + 컨텍스트 규칙 POS 보정 + 선형 재순위 perceptron (신경망 없음)
- 오프라인 지원 -- 네트워크 없이도 완전한 형태소 분석 가능
- 라이브 데모 -- 브라우저에서 바로 체험
왜 Garu인가?
기존 형태소 분석기는 수십 MB의 모델을 서버·네이티브 환경에서 구동하는 경우가 많습니다. Garu는 1.26MB 모델을 npm에 번들해 별도 다운로드 없이 브라우저에서 바로 실행되는 초경량 한국어 형태소 분석기입니다.
| | 서버 기반 분석기 | Garu | |---|---|---| | 모델 크기 | 수십 MB | 1.26MB (+425KB WASM) | | 실행 환경 | 서버/네이티브 | 브라우저 (WASM) | | 모델 번들 | 별도 로드 | npm에 포함 | | 네트워크 | 필요 (API 호출) | 불필요 (오프라인) | | 모바일 웹 | 비실용적 | 지원 |
정확도: 9,000문장 v15k 골드 테스트셋 F1 96.05% (ep_norm 정규화 기준), 2025 구어 고정 평가 F1 91.23%. 서버 기반 분석기와 다른 설계 목표(경량·브라우저)를 가지므로 단순 수치 비교보다는 사용 환경에 맞는 선택을 권장합니다.
Quick Start
npm install garu-ko
import { Garu } from 'garu-ko';
// 모델 로드 (npm 패키지에 포함되어 별도 다운로드 불필요)
const garu = await Garu.load();
// 형태소 분석
const result = garu.analyze('배가 아파서 약을 먹었다');
console.log(result.tokens);
// [
// { text: '배', pos: 'NNG', start: 0, end: 2 },
// { text: '가', pos: 'JKS', start: 0, end: 2 },
// { text: '아프', pos: 'VA', start: 3, end: 6 },
// { text: '어서', pos: 'EC', start: 3, end: 6 },
// { text: '약', pos: 'NNG', start: 7, end: 9 },
// { text: '을', pos: 'JKO', start: 7, end: 9 },
// { text: '먹', pos: 'VV', start: 10, end: 13 },
// { text: '었', pos: 'EP', start: 10, end: 13 },
// { text: '다', pos: 'EF', start: 10, end: 13 },
// ]
// 간단 토큰화 (표면형만)
const tokens = garu.tokenize('나는 학교에 간다');
console.log(tokens);
// ['나', '는', '학교', '에', '간다']
// 리소스 해제
garu.destroy();
커스텀 모델 로드
// 커스텀 URL에서 모델 로드
const garu = await Garu.load({ modelUrl: '/my-models/custom.gmdl' });
// ArrayBuffer로 직접 전달
const response = await fetch('/my-models/custom.gmdl');
const modelData = await response.arrayBuffer();
const garu = await Garu.load({ modelData });
공백 없는 한국어 처리
garu-ko/text는 빠진 공백과 문장 경계를 추정하는 선택 진입점이다. 포함된 JavaScript 가중치로 동기 실행하며, Garu.load()나 추가 WASM 로드, CDN·서버 통신이 필요 없다. 경계 가중치는 이 진입점에서만 가져오며 기존 형태소 분석에 복원을 자동 적용하지 않는다.
import { restoreSpacing, splitUnspaced } from 'garu-ko/text';
restoreSpacing('나는밥을먹었다.');
// '나는 밥을 먹었다.'
restoreSpacing('나는밥을먹었다.', { mode: 'balanced' });
splitUnspaced('안녕하세요반갑습니다');
// [
// { text: '안녕하세요', offset: 0 },
// { text: '반갑습니다', offset: 5 },
// ]
restoreSpacing(text, { mode? })는 원래 문자와 기존 공백을 보존하고 공백만 삽입한다. 기본conservative는 불확실한 공백을 일부 놓칠 수 있고,balanced는 더 많은 경계를 복원하지만 단어를 지나치게 나눌 수 있다.splitUnspaced(text)는 공백을 삽입하지 않고 원문의 조각과 시작offset을 반환한다. 조각 양끝 공백은 제외하며, offset은 원문의 Unicode 코드포인트 단위다. JavaScript의 UTF-16 인덱스와는 다를 수 있다.
문맥 검증을 추가한 문장 분리
garu-ko/sentences는 짝이 맞는 인용문을 하나의 단위로 보존하고, 경계 후보를 작은 문자 문맥 모델로 재검증합니다. 인용문 안에 여러 문장이 있어도 그 내부를 자르지 않는 바깥 문장 분리 기능입니다.
import { splitSentences } from 'garu-ko/sentences';
splitSentences('안녕하세요반갑습니다');
// [{ text: '안녕하세요', offset: 0 }, { text: '반갑습니다', offset: 5 }]
splitSentences('“안녕하세요반갑습니다”');
// [{ text: '“안녕하세요반갑습니다”', offset: 0 }]
형태소 분석기를 불러올 필요 없이 패키지에 포함된 Q4 모델로 오프라인 실행합니다. 기존 splitSentences(text, garu) 호출은 더 느린 형태소 검증 경로를 선택합니다. 원문과 코드포인트 offset을 보존하고 조각 양끝의 공백만 제외합니다. 기존 splitUnspaced, 기본 진입점의 구두점 기반 splitSentences, 형태소 분석 동작은 유지됩니다.
기본 호출의 고정 NIKL2025 뉴스 9,355문단 경계 F1은 공백 삭제 95.91%, 공백·종결부호 삭제 96.28%입니다. Apple M4에서 로드 후 평균은 Node 0.020ms, Chrome 0.018ms였습니다(1,250입력, 평균82.5문자, 7회 측정의 중앙값). Node P95는0.061ms로 모든 문단의45µs 보장은 아닙니다. 초기 import와 첫 호출은 별도 비용입니다. 인용문 보호는 고정셋 오류를 확인한 뒤 추가한 정책이며, 인용 내부도 나누는 다른 참조 기준에서는 점수가 내려갑니다. 측정 조건과 한계를 참고하세요.
아키텍처
코드북 + 어절 캐시 + 문장 수준 N-best Trigram Viterbi 구조에 결정적 후처리 규칙과 재순위 perceptron을 결합한 경량 분석기입니다.
전체 문장에 대해 래티스를 구축하고, 어절 캐시 항목과 등록된 사용자 단어를 저비용 후보 아크로 주입한 뒤 문장 수준 Viterbi 디코딩(top-10)을 수행합니다. 각 후보에 결정적 후처리 규칙(fix_*)을 적용한 다음 컨텍스트 보너스로 최적 후보를 고르고, 재순위 perceptron이 확신 마진을 넘을 때만 그 선택을 교체합니다. 마지막으로 POS 보정 규칙(시간 부사 "오늘/지금"→MAG, 의문대명사 "뭐+VV"→NP 등)과 NNG-hint 사전(NIKL gold에서 NNG 비율 ≥ 95% 단어 225개)을 적용합니다.
코드북 데이터는 모두의 말뭉치(NIKL MP) 골드 어노테이션과 Kiwi 분석기 출력의 하이브리드로 추출합니다. 결정 규칙과 어절 캐시 확장 패턴은 5K 골드/NIKL 코퍼스에서 마이닝된 사례를 기반으로 합니다. 자세한 연구 경과는 기술 논문과 CHANGELOG를 참고하세요.
API Reference
Garu.load(options?): Promise
WASM 모듈을 초기화하고 모델을 로드합니다.
| Option | Type | Description |
|---|---|---|
| modelData | ArrayBuffer | 모델 바이트를 직접 전달 |
| modelUrl | string | 커스텀 URL에서 모델 로드 |
옵션을 지정하지 않으면 npm 패키지에 포함된 기본 모델을 로드합니다.
garu.analyze(text, options?): AnalyzeResult | AnalyzeResult[]
형태소 분석을 수행합니다.
text-- 분석할 한국어 텍스트options.topN-- 1보다 크면 N-best 결과를 배열로 반환 (아직 완전히 지원되지 않으며, 결과가 더 적을 수 있음)
interface AnalyzeResult {
tokens: Token[]; // 형태소 토큰 배열
score: number; // 디코더 경로 점수
elapsed: number; // 처리 시간 (밀리초)
}
interface Token {
text: string; // 표면형
pos: POS; // 품사 태그 (세종 태그셋)
start: number; // 어절 시작 오프셋 (글자 단위)
end: number; // 어절 끝 오프셋 (글자 단위)
}
garu.nouns(text, options?): string[]
텍스트에서 명사(NNG, NNP)만 추출합니다. options.includeSL을 켜면 "AI", "BM25" 같은 외래어 토큰(SL)도 함께 포함합니다.
garu.nouns('인공지능 기술이 발전했다');
// ["인공", "지능", "기술", "발전"]
garu.nouns('AI 기술이 발전했다', { includeSL: true });
// ["AI", "기술", "발전"]
garu.tokenize(text): string[]
표면형 문자열 배열을 반환합니다. 분절된 텍스트만 필요할 때 analyze()의 경량 대안으로 사용합니다.
garu.addUserWord(surface, pos, freq?): void
도메인 어휘를 런타임에 등록합니다. 모델 리빌드가 필요 없습니다. 등록한 단어는 래티스에 통짜 후보로 들어가 내장 사전과 같은 비용 척도로 경쟁합니다.
garu.nouns('인공지능 기술이 발전했다');
// ["인공", "지능", "기술", "발전"]
garu.addUserWord('인공지능', 'NNG');
garu.nouns('인공지능 기술이 발전했다');
// ["인공지능", "기술", "발전"]
freq는 내장 사전의 빈도와 같은 척도이며 클수록 분해 후보를 이길 확률이 높습니다(기본값 5000). 사용자 단어는 어절 경계를 넘지 않으므로 공백을 포함한 표면은 매칭되지 않습니다.
제품명·기관명·신조어처럼 일반 코퍼스에 없는 복합명사를 검색 색인에 통짜로 넣고 싶을 때 특히 유용합니다.
garu.addUserWords(words): void
여러 단어를 한 번에 등록합니다.
garu.addUserWords([
{ surface: '전세사기', pos: 'NNG' },
{ surface: '탄소중립', pos: 'NNG', freq: 20000 },
]);
garu.clearUserWords(): void / garu.userWordCount(): number
등록한 사용자 단어를 모두 제거하거나 개수를 확인합니다. 등록된 단어가 없으면 분석 결과는 내장 사전만 쓸 때와 완전히 동일합니다.
garu.isLoaded(): boolean
WASM 분석기가 초기화되어 사용 가능한 상태이면 true를 반환합니다.
garu.modelInfo(): ModelInfo
로드된 모델의 메타데이터를 반환합니다.
interface ModelInfo {
version: string; // 모델 버전
size: number; // 모델 크기 (바이트)
accuracy: number; // 보고된 정확도 (0.9605)
}
garu.destroy(): void
WASM 인스턴스를 해제하고 메모리를 반환합니다. 호출 후 인스턴스를 재사용할 수 없습니다.
통합 패키지
기존 JS 검색 라이브러리에 가루의 한국어 형태소 토큰화를 바로 끼울 수 있는 어댑터입니다.
garu-orama-tokenizer— Orama 검색 엔진용 한국어 토크나이저garu-minisearch-tokenizer— MiniSearch 검색 엔진용 한국어 토크나이저
"먹었다"가 "먹는다"와 매치되지 않고, "학교" 검색이 "학교에/를/가"를 놓치는 문제를 해결합니다. 조사/어미를 떼고 어간만 색인하므로 활용형이 달라도 같은 단어로 잡힙니다.
// Orama 예시
import { create, insert, search } from '@orama/orama'
import { createTokenizer } from 'garu-orama-tokenizer'
const db = await create({
schema: { title: 'string' },
components: { tokenizer: await createTokenizer() }
})
await insert(db, { title: '학교에서 점심을 먹었다' })
await search(db, { term: '먹다' }) // ← 매치됨
자주 묻는 질문 (FAQ)
Garu(가루)는 무엇인가요? Garu는 1.26MB 모델과 425KB WASM 엔진만으로 브라우저에서 직접 실행되는 한국어 형태소 분석기입니다. 서버나 네트워크 없이 클라이언트에서 완전 오프라인으로 동작합니다.
모델과 엔진 크기는 얼마인가요? 모델은 brotli 압축 기준 1.26MB, WASM 엔진은 raw 425KB(gzip 182KB)입니다. 모델이 npm 패키지에 포함되어 있어 별도 CDN이나 다운로드가 필요 없습니다.
정확도는 어느 정도인가요? 9,000문장 v15k 골드 테스트셋에서 F1 96.05%(ep_norm 정규화 기준), 2025 구어 고정 평가 평가셋에서 F1 91.23%입니다.
서버가 필요한가요? 아니요. WebAssembly로 클라이언트에서 직접 실행되므로 API 서버나 네트워크 통신이 필요 없습니다.
어떤 환경에서 동작하나요? WebAssembly를 지원하는 모던 브라우저와 Node.js 18 이상에서 동작합니다. 모델이 가벼워 모바일 웹에서도 실용적입니다.
어떻게 설치하고 사용하나요?
npm install garu-ko 후 await Garu.load()로 모델을 로드하고 analyze()로 형태소 분석을 수행합니다. 자세한 예시는 위의 Quick Start를 참고하세요.
신경망(딥러닝)을 사용하나요? 아니요. 코드북 + 어절 캐시 + 문장 수준 N-best Trigram Viterbi + 결정적 후처리 규칙 + 선형 재순위 perceptron으로 동작합니다. perceptron은 신경망이 아닌 선형 모델이며, 행렬 연산이나 확률적 요소가 없어 결과가 결정적이고 재현 가능합니다.
어떤 품사 태그셋을 사용하나요? 세종 태그셋(42개 품사 태그)을 사용합니다.
검색 엔진에 한국어 토큰화를 붙일 수 있나요?
네. Orama용 garu-orama-tokenizer, MiniSearch용 garu-minisearch-tokenizer 어댑터를 제공합니다. 조사·어미를 떼고 어간만 색인해 활용형이 달라도 같은 단어로 검색됩니다.
개발 가이드
요구 사항
- Rust toolchain (stable)
wasm-pack- Node.js >= 18
- Python >= 3.10 (학습 파이프라인용)
프로젝트 구조
crates/
garu-core/ # Rust 코어: Codebook 분석기, FST 사전, 모델 로더
garu-wasm/ # WASM 바인딩 (wasm-bindgen)
garu-tools/ # CLI 도구 (FST 빌더)
js/ # TypeScript API 래퍼 (npm 패키지)
models/ # 번들된 모델 파일 (base.gmdl)
pkg/ # WASM 빌드 출력
training/ # 코드북 추출/빌드 파이프라인 (Python)
models/ # 컴파일된 모델 파일 (.gmdl)
docs/ # 기술 문서
빌드
# Rust 코어 라이브러리 빌드
cargo build --release
WASM 패키지 빌드
wasm-pack build crates/garu-wasm --target web --out-dir ../../js/pkg
TypeScript 래퍼 빌드
cd js && npx tsc
전체 빌드
cd js && npm run build
테스트
# Rust 테스트
cargo test
JS/TS 테스트
cd js && npm test
기술 문서
아키텍처와 설계 결정에 대한 상세한 내용은 기술 논문을 참고하세요.
데이터 출처
이 프로젝트의 형태소 분석 모델은 다음 데이터를 기반으로 학습되었습니다:
- 모두의 말뭉치 형태 분석 말뭉치(v1.1) — 국립국어원(National Institute of Korean Language) 제공. 모델에는 원본 텍스트가 포함되지 않으며, 빈도 통계 및 패턴만 사용됩니다.
- 세종 태그셋 — 42개 품사 태그 체계
라이선스
분석기 코드는 MIT입니다. 번들 사전에 가져온 데이터에는 Apache-2.0 및 CC BY-SA 2.0 KR 조건이 적용됩니다. 출처·변경 내역과 재배포 조건은 사전 데이터 고지에 있습니다.
Star History