WASM 바인딩
MeCab-Ko는 WebAssembly(WASM)를 통해 브라우저와 Deno 환경에서 실행할 수 있습니다.
설치
npm/yarn/pnpm
npm install @mecab-ko/wasm
# or
yarn add @mecab-ko/wasm
# or
pnpm add @mecab-ko/wasm
CDN
<script type="module">
import init, { Tagger } from 'https://cdn.jsdelivr.net/npm/@mecab-ko/wasm/mecab_ko_wasm.js';
await init();
const tagger = new Tagger();
console.log(tagger.parse('안녕하세요'));
</script>
빠른 시작
브라우저 (ES Modules)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MeCab-Ko WASM Demo</title>
</head>
<body>
<textarea id="input" placeholder="분석할 텍스트 입력"></textarea>
<button id="analyze">분석</button>
<pre id="output"></pre>
<script type="module">
import init, { Tagger } from './node_modules/@mecab-ko/wasm/mecab_ko_wasm.js';
// WASM 초기화
await init();
// Tagger 생성
const tagger = new Tagger();
// 분석 버튼 이벤트
document.getElementById('analyze').addEventListener('click', () => {
const text = document.getElementById('input').value;
const result = tagger.parse(text);
document.getElementById('output').textContent = result;
});
</script>
</body>
</html>
Webpack/Vite
import init, { Tagger } from '@mecab-ko/wasm';
// WASM 초기화
await init();
// Tagger 생성 및 사용
const tagger = new Tagger();
const result = tagger.parse('형태소 분석');
console.log(result);
Deno
import init, { Tagger } from 'https://deno.land/x/mecab_ko_wasm/mod.ts';
await init();
const tagger = new Tagger();
console.log(tagger.parse('안녕하세요'));
API 레퍼런스
init() 함수
WASM 모듈을 초기화합니다. 반드시 Tagger를 사용하기 전에 호출해야 합니다.
/**
* WASM 모듈을 초기화합니다.
* @param module_or_path - WASM 모듈 또는 경로 (선택)
* @returns Promise<void>
*/
async function init(module_or_path?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module): Promise<void>;
Tagger 클래스
class Tagger {
/**
* 새로운 Tagger를 생성합니다.
* @param config - Tagger 설정 (선택)
*/
constructor(config?: TaggerConfig);
/**
* 텍스트를 분석하고 문자열로 반환합니다.
* @param text - 분석할 텍스트
* @returns 분석 결과 문자열
*/
parse(text: string): string;
/**
* 텍스트를 분석하고 Node 배열로 반환합니다.
* @param text - 분석할 텍스트
* @returns Node 객체 배열
*/
parseToNodes(text: string): Node[];
/**
* 텍스트를 분석하고 객체로 반환합니다.
* @param text - 분석할 텍스트
* @returns 분석 결과 객체
*/
parseToObject(text: string): ParseResult;
/**
* 리소스를 해제합니다.
*/
free(): void;
}
TaggerConfig 인터페이스
interface TaggerConfig {
/** 출력 포맷 ("mecab" | "wakati" | "json" | "csv") */
outputFormat?: string;
/** 띄어쓰기 패널티 (기본값: -1000) */
spacePenalty?: number;
/** 부분 처리 활성화 */
partial?: boolean;
/** 전부 출력 활성화 */
allMorphs?: boolean;
}
Node 인터페이스
interface Node {
/** 표면형 (실제 텍스트) */
surface: string;
/** 품사 및 의미 정보 */
feature: string;
/** 품사 태그 */
pos: string;
/** 시작 위치 (바이트 단위) */
start: number;
/** 길이 (바이트 단위) */
length: number;
/** 비용 */
cost: number;
}
사용 예제
React
import React, { useEffect, useState } from 'react';
import init, { Tagger } from '@mecab-ko/wasm';
function MecabAnalyzer() {
const [tagger, setTagger] = useState<Tagger | null>(null);
const [input, setInput] = useState('');
const [result, setResult] = useState('');
useEffect(() => {
// WASM 초기화
init().then(() => {
setTagger(new Tagger());
});
// 클린업
return () => {
if (tagger) {
tagger.free();
}
};
}, []);
const handleAnalyze = () => {
if (tagger && input) {
const parsed = tagger.parse(input);
setResult(parsed);
}
};
return (
<div>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="분석할 텍스트 입력"
/>
<button onClick={handleAnalyze} disabled={!tagger}>
분석
</button>
<pre>{result}</pre>
</div>
);
}
export default MecabAnalyzer;
Vue 3
<template>
<div>
<textarea v-model="input" placeholder="분석할 텍스트 입력"></textarea>
<button @click="analyze" :disabled="!tagger">분석</button>
<pre>{{ result }}</pre>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import init, { Tagger } from '@mecab-ko/wasm';
const tagger = ref<Tagger | null>(null);
const input = ref('');
const result = ref('');
onMounted(async () => {
await init();
tagger.value = new Tagger();
});
onUnmounted(() => {
if (tagger.value) {
tagger.value.free();
}
});
const analyze = () => {
if (tagger.value && input.value) {
result.value = tagger.value.parse(input.value);
}
};
</script>
Svelte
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import init, { Tagger } from '@mecab-ko/wasm';
let tagger: Tagger | null = null;
let input = '';
let result = '';
onMount(async () => {
await init();
tagger = new Tagger();
});
onDestroy(() => {
if (tagger) {
tagger.free();
}
});
function analyze() {
if (tagger && input) {
result = tagger.parse(input);
}
}
</script>
<textarea bind:value={input} placeholder="분석할 텍스트 입력" />
<button on:click={analyze} disabled={!tagger}>분석</button>
<pre>{result}</pre>
Web Worker
// worker.js
import init, { Tagger } from '@mecab-ko/wasm';
let tagger = null;
self.onmessage = async (event) => {
const { type, text } = event.data;
if (type === 'init') {
await init();
tagger = new Tagger();
self.postMessage({ type: 'ready' });
} else if (type === 'parse') {
if (tagger) {
const result = tagger.parse(text);
self.postMessage({ type: 'result', result });
}
}
};
// main.js
const worker = new Worker('worker.js', { type: 'module' });
worker.onmessage = (event) => {
const { type, result } = event.data;
if (type === 'ready') {
console.log('Worker ready');
worker.postMessage({ type: 'parse', text: '안녕하세요' });
} else if (type === 'result') {
console.log('결과:', result);
}
};
worker.postMessage({ type: 'init' });
Node 단위 처리
import init, { Tagger } from '@mecab-ko/wasm';
await init();
const tagger = new Tagger();
const nodes = tagger.parseToNodes('형태소 분석을 시작합니다');
nodes.forEach(node => {
console.log(`표면형: ${node.surface}`);
console.log(`품사: ${node.pos}`);
console.log(`특성: ${node.feature}`);
console.log('---');
});
tagger.free();
JSON 출력
import init, { Tagger } from '@mecab-ko/wasm';
await init();
const tagger = new Tagger({ outputFormat: 'json' });
const result = tagger.parseToObject('JSON 형식으로 출력');
console.log('원본 텍스트:', result.text);
console.log('노드 개수:', result.nodes.length);
result.nodes.forEach((node, index) => {
console.log(`${index + 1}. ${node.surface} (${node.pos})`);
});
tagger.free();
명사 추출
import init, { Tagger } from '@mecab-ko/wasm';
await init();
const tagger = new Tagger();
const text = '저는 오늘 학교에 가서 공부를 했습니다.';
const nodes = tagger.parseToNodes(text);
const nouns = nodes
.filter(node => ['NNG', 'NNP'].includes(node.pos))
.map(node => node.surface);
console.log('명사:', nouns);
// 출력: ['오늘', '학교', '공부']
tagger.free();
실시간 입력 분석
import init, { Tagger } from '@mecab-ko/wasm';
await init();
const tagger = new Tagger();
const input = document.getElementById('input') as HTMLTextAreaElement;
const output = document.getElementById('output') as HTMLPreElement;
let debounceTimer: number;
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const text = input.value;
if (text) {
const result = tagger.parse(text);
output.textContent = result;
} else {
output.textContent = '';
}
}, 300);
});
// 페이지 언로드 시 정리
window.addEventListener('beforeunload', () => {
tagger.free();
});
IndexedDB에 결과 저장
import init, { Tagger } from '@mecab-ko/wasm';
await init();
const tagger = new Tagger();
// IndexedDB 열기
const dbPromise = indexedDB.open('MecabDB', 1);
dbPromise.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('analyses')) {
db.createObjectStore('analyses', { keyPath: 'id', autoIncrement: true });
}
};
dbPromise.onsuccess = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
function saveAnalysis(text: string) {
const result = tagger.parseToObject(text);
const transaction = db.transaction(['analyses'], 'readwrite');
const store = transaction.objectStore('analyses');
store.add({
text: result.text,
nodes: result.nodes,
timestamp: Date.now(),
});
}
saveAnalysis('저장할 텍스트');
};
Service Worker 캐싱
// service-worker.js
import init, { Tagger } from '@mecab-ko/wasm';
const CACHE_NAME = 'mecab-cache-v1';
let tagger = null;
self.addEventListener('install', async (event) => {
event.waitUntil(
(async () => {
await init();
tagger = new Tagger();
})()
);
});
self.addEventListener('message', (event) => {
const { type, text, id } = event.data;
if (type === 'parse' && tagger) {
const result = tagger.parse(text);
event.ports[0].postMessage({ id, result });
}
});
번들 크기 최적화
Tree Shaking
// 필요한 것만 import
import init, { Tagger } from '@mecab-ko/wasm';
// Tagger만 사용
await init();
const tagger = new Tagger();
코드 스플리팅 (Webpack)
// 동적 import로 lazy loading
const loadMecab = async () => {
const { default: init, Tagger } = await import('@mecab-ko/wasm');
await init();
return new Tagger();
};
// 필요할 때만 로드
button.addEventListener('click', async () => {
const tagger = await loadMecab();
const result = tagger.parse(input.value);
console.log(result);
});
Vite 최적화
// vite.config.js
export default {
optimizeDeps: {
exclude: ['@mecab-ko/wasm'],
},
build: {
target: 'esnext',
},
};
메모리 관리
WASM 메모리는 자동으로 관리되지만, 명시적으로 해제할 수 있습니다:
const tagger = new Tagger();
// 사용
const result = tagger.parse('텍스트');
// 사용 완료 후 메모리 해제
tagger.free();
React/Vue 등에서 클린업:
useEffect(() => {
let tagger: Tagger | null = null;
init().then(() => {
tagger = new Tagger();
});
return () => {
if (tagger) {
tagger.free();
}
};
}, []);
성능 고려사항
초기화 비용
WASM 초기화는 비용이 높으므로 앱 시작 시 한 번만 수행:
// Good - 앱 시작 시 한 번
const tagger = await initTagger();
// Bad - 사용할 때마다
button.addEventListener('click', async () => {
const tagger = await initTagger(); // 비효율적
});
Tagger 재사용
// Good - 싱글톤 패턴
let globalTagger: Tagger | null = null;
async function getTagger() {
if (!globalTagger) {
await init();
globalTagger = new Tagger();
}
return globalTagger;
}
// Bad - 매번 생성
async function analyze(text: string) {
const tagger = new Tagger(); // 비효율적
return tagger.parse(text);
}
Web Worker 활용
무거운 작업은 Web Worker에서 처리:
// 메인 스레드를 블록하지 않음
const worker = new Worker('mecab-worker.js', { type: 'module' });
worker.postMessage({ text: largeText });
브라우저 호환성
- Chrome 57+
- Firefox 52+
- Safari 11+
- Edge 16+