TRENDSHIFT DAILY #4 · 2026-07-24 · HARPER · 오프라인 문법 검사기

Automattic/harper 딥다이브
— Grammarly가 서버로 빼가던 당신의 문장을, Rust가 10밀리초 만에 내 컴퓨터에서 고친다

harper오프라인·프라이버시 우선(offline, privacy-first) 영어 문법 검사기다. 한 문장으로 말하면 "인터넷 연결 없이, 당신의 글을 단 한 글자도 서버로 보내지 않고, 내 컴퓨터 안에서 10밀리초 만에 문법·철자·문체 오류를 잡아 고쳐준다". 전부 Rust로 쓰였다.

이 프로젝트의 급소는 이것이다: 문법 검사기 하나를 통째로 "내장(embed)"할 수 있을 만큼 가볍게 만들었다. 경쟁자인 LanguageTool은 정확도를 위해 ~16GB짜리 n-그램 데이터셋을 지고 다니지만, harper는 FST(유한상태 변환기)로 압축한 54,800단어 사전을 실행 파일 안에 넣어버렸다. 그래서 다운로드도, 디스크 I/O도, 네트워크 왕복도 없이 즉시 동작한다. 같은 코어를 WASM으로 컴파일해 브라우저에서도, LSP 서버로 VS Code·Neovim·Zed에서도 똑같이 쓴다.

(저장소 Automattic/harper · 언어 Rust 79% · 버전 v2.6.0 · 21개 크레이트 워크스페이스·Rust 12만 줄+ · 문법 규칙 284개 · ★ 10.5k · 라이선스 Apache-2.0 · 원저자 Elijah Potter / 현재 Automattic(WordPress.com 모회사) 관리 · shallow clone 소스 직접 분석)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 주목받는가 — 10ms의 비밀
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트
  7. 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

Grammarly의 편리함은 원하지만, 내 글을 남의 서버에 맡기긴 싫은 사람을 위한 도구

글을 쓰는 도구에 문법 검사기를 붙이는 건 이제 당연한 일이 됐다. 문제는 그 대부분이 당신이 입력하는 모든 문장을 클라우드로 전송한다는 점이다. 이메일 초안, 일기, 회사 기밀 문서까지 — 검사받는 순간 남의 서버를 거친다. harper의 원저자 Elijah Potter는 이걸 "프라이버시 악몽(privacy nightmare)"이라 불렀고, 그 반대편에 서는 도구를 만들었다.

한 줄 비유

번역기를 쓸 때마다 원고를 우편으로 부치는 대신, 사전 한 권을 통째로 주머니에 넣고 다니는 것

Grammarly는 "원고를 본사로 보내면 첨삭해 돌려주는 우편 서비스"다 — 빠르지만 남이 내 글을 다 본다. harper는 "사전과 문법책을 통째로 압축해 주머니에 넣은 것"이다. 인터넷이 끊겨도, 비행기 안에서도, 회사 방화벽 안에서도 내 기기만으로 즉시 첨삭된다. 아무것도 밖으로 나가지 않는다.

harper가 하는 일은 단순하다. 텍스트를 받아 → 토큰(단어·기호 조각)으로 쪼개고 → 품사를 붙이고 → 284개의 문법·철자·문체 규칙을 적용해 → 오류 위치와 수정 제안을 내놓는다. 이 코어 엔진(harper-core)을 세 가지 얼굴로 재사용한다: 에디터에 붙는 LSP 서버(harper-ls), 브라우저에서 도는 WASM(harper-wasm), 그리고 명령줄 도구(harper-cli)다.

TERM
오프라인 / 프라이버시 우선 (offline, privacy-first)
"서버에 안 보낸다"가 그냥 부가 기능이 아니라 설계의 제1원칙이라는 뜻. 사전·규칙·품사 모델까지 전부 실행 파일에 내장해서, 네트워크 연결이 아예 필요 없도록 만들었다. 그 대가로 "무겁지 않게" 만드는 게 핵심 기술 과제가 된다.

2왜 주목받는가

"빠르다"가 아니라 "말도 안 되게 빠르다" — 그리고 그게 프라이버시와 한 몸이다

문법 검사기 시장엔 이미 거인들이 있다. Grammarly(상용), LanguageTool(오픈소스), hunspell(철자만). harper가 이들 사이를 비집고 트렌딩에 오른 이유는 딱 하나로 요약된다 — 속도와 프라이버시를 동시에 잡았고, 그 격차가 "조금"이 아니라 "자릿수(order of magnitude)" 수준이라는 것이다.

레포의 COMPARISON.md가 제시하는 정면 비교표다(원문 인용):

제품제안 생성 시간라이선스LSP 지원룰셋
harper10msApache-2.0✅ 네이티브자체 규칙
LanguageTool650msLGPL-2.1🟨 ltex-ls 경유커스텀 + N-그램 + LLM
hunspellLGPL/GPL/MPLhunspell/MySpell
Grammarly4000ms독점(비공개)🟨 서드파티 경유독점

숫자를 곱씹어 보자. harper의 10ms는 LanguageTool(650ms)보다 약 65배, Grammarly(4000ms)보다 약 400배 빠르다. 문법 검사에서 속도는 단순한 편의가 아니다. 타이핑하는 동안 글자마다 실시간으로 검사하려면, 왕복 수백 밀리초는 곧 "느껴지는 렉"이 된다. harper는 그 지연을 사람이 인지하기 어려운 수준으로 떨어뜨렸다.

경쟁자의 함정
LanguageTool은 왜 16GB를 지고 다니나

LanguageTool은 정확도를 위해 n-그램(단어 n개 연쇄) 통계 데이터셋을 쓴다. "이 단어 다음엔 이게 자주 온다"를 방대한 말뭉치로 학습한 표인데, 그 크기가 ~16GB에 달하고 수 기가바이트의 RAM을 요구한다. 강력하지만 무겁고, 로딩도 느리고, 브라우저에 넣는 건 꿈도 못 꾼다.

harper의 해결
FST 사전 + 경량 규칙 = 1/50 메모리

harper는 n-그램 대신 FST(유한상태 변환기)로 압축한 5.4만 단어 사전과 손으로 짠 284개 규칙만 쓴다. README의 주장: "LanguageTool 메모리 사용량의 1/50 미만, 밀리초 단위 린트, 완전 비공개." 사전을 실행 파일에 include_str!로 내장했기 때문에 다운로드도 로딩도 없다. 이 "가벼움"이 곧 WASM 이식과 오프라인 동작을 가능케 한 원천이다.

또 하나 놓치기 쉬운 강점은 침투 범위다. harper는 순수한 산문만 검사하는 게 아니라, harper-comments + tree-sitter로 소스 코드의 주석과 문자열만 뽑아 검사하고, Markdown·LaTeX·Typst·AsciiDoc·Org-mode·깃 커밋 메시지까지 포맷별 파서를 갖췄다. 개발자가 코드를 쓰든 문서를 쓰든 커밋을 쓰든, 같은 엔진 하나가 따라붙는다. 이 "어디에나 붙는 단일 코어" 전략이 harper를 단순 맞춤법 검사기 이상으로 만든다.

3기술 스택 전체 지도

Rust 코어 하나 + 21개 크레이트로 쪼갠 모듈 제국 + 얇은 JS 껍데기

harper는 Rust 에디션 2024로 쓰인 거대한 Cargo 워크스페이스다. 워크스페이스란 여러 개의 크레이트(Rust의 패키지 단위)를 한 저장소에서 함께 빌드·관리하는 구조인데, harper는 무려 21개로 잘게 쪼개 놨다. "왜 이렇게 잘게?"의 답이 곧 이 프로젝트의 설계 철학이다 — 코어는 순수하게, 주변부는 갈아끼울 수 있게.

TERM
크레이트(crate)와 워크스페이스(workspace)
크레이트는 Rust에서 컴파일되는 최소 단위(라이브러리 하나 또는 실행파일 하나). 워크스페이스는 그런 크레이트 여러 개를 묶어 의존성·빌드를 공유하는 상위 컨테이너다. 하나의 거대한 코드 뭉치 대신 21조각으로 나누면, 브라우저용(WASM)은 무거운 파일 파서를 빼고, CLI는 다 넣는 식으로 필요한 조각만 조립할 수 있다.

3-1. 크레이트 역할 지도 (핵심만)

크레이트역할
harper-core엔진 심장. 토큰화·파싱·FST 사전·284개 규칙·Expr DSL이 전부 여기
harper-lsLSP 서버. 에디터(VS Code·Neovim 등)와 표준 프로토콜로 통신 (tower-lsp 기반)
harper-cli개발·디버그용 명령줄 도구 (lint·parse·metadata·train 등 서브커맨드)
harper-wasmWebAssembly 바인딩. 같은 코어를 브라우저에서 구동 (writewithharper.com)
harper-brillBrill 품사 태거 + Burn 신경망 청커(명사구 인식). 사전학습 모델 내장
harper-comments소스 코드에서 주석·문자열만 뽑아 검사 (tree-sitter 활용)
harper-html / typst / tex
/ asciidoc / python …
포맷별 파서. 마크업·문서·커밋 메시지 등 "산문이 아닌 것" 속의 산문을 골라냄
harper.js (npm)WASM을 감싼 JS 패키지. VS Code·Obsidian·Chrome·WordPress 플러그인이 공유
harper-desktopTauri 2 기반 데스크톱 앱 (Svelte 프론트엔드)

3-2. harper-core를 떠받치는 크레이트들

엔진 심장부의 의존성을 보면 harper가 "무엇으로 빠른가"가 드러난다. 무거운 ML 프레임워크가 아니라, 작고 날카로운 자료구조 라이브러리들로 채워져 있다.

크레이트 / 버전역할
fst 0.4.7유한상태 변환기. 5.4만 단어 사전을 압축 저장하고 초고속 조회 — harper 속도의 핵심
levenshtein_automata 0.2.1편집거리 기반 퍼지 매칭. 오타가 나도 비슷한 단어를 찾아 교정 후보로
pulldown-cmark 0.13마크다운 파서 (CommonMark 준수)
smallvec · hashbrown · foldhash할당을 줄이는 고성능 컬렉션·해시 — "무할당" 설계의 도구들
lru · cached반복 조회 캐싱
criterion · quickcheck(개발용) 벤치마크 · 속성 기반 테스트(무작위 입력으로 규칙 검증)

릴리스 빌드 프로파일도 성능에 진심이다. Cargo.tomlopt-level=3(최대 최적화), lto="fat"(링크 시점 전역 최적화), panic="abort"(패닉 처리 코드 제거로 바이너리 축소)로 설정돼 있다. "빠른 언어를 골랐으니 끝"이 아니라, 컴파일러가 낼 수 있는 최대치까지 짜냈다는 뜻이다.

3-3. 언어별 구성

GitHub 기준 언어 비율은 Rust 79.1% · TypeScript 11.6% · Svelte 6.0% 순이다. 로컬 집계로는 Rust가 590개 파일 12만 줄+, TS가 154개 파일 1.8만 줄. 즉 두뇌는 전부 Rust이고, JS/TS/Svelte는 그 두뇌를 브라우저·에디터·웹사이트에 연결하는 얇은 배선이다. 프론트엔드 툴체인은 Node 22+·pnpm 10·포매터는 Biome를 쓴다.

4아키텍처 심화 분석

먼저 텍스트 한 줄이 등급을 받기까지의 전체 흐름 → 그다음 규칙 엔진과 사전의 속을 연다

harper를 이해하는 가장 빠른 길은 "텍스트 한 줄이 어떤 컨베이어 벨트를 지나 '여기 오타요'가 되는지"를 따라가는 것이다. 부품 이름부터 외우면 길을 잃는다. 흐름부터 보자.

4-1. 독수리 시점 — 검사 파이프라인 전체

텍스트 "&str" │ ├─▶ ① 문자 배열화 Vec<char> → Lrc<[char]> 로 공유 (복사 안 함) │ ├─▶ ② 파싱 Parser.parse() [PlainEnglish | Markdown | 코드주석 …] │ └ 결과: Vec<Token> (각 Token = span 범위 + kind 종류) │ ├─▶ ③ 픽스업 공백·약어·생략부호·URL·파일명 조각을 하나로 병합 │ └ match_quotes() 따옴표 짝 맞추기 │ ├─▶ ④ 품사 태깅 Brill 태거 → 각 토큰에 품사(pos_tag) 부여 │ └ Burn 신경망 청커 → 명사구(NP) 묶기 │ ├─▶ ⑤ 사전 조회 FST 사전에서 단어 메타데이터(굴절·품사) 주입 │ ├─▶ ⑥ 규칙 적용 LintGroup(284개 Linter).lint(document) │ └ 결과: Vec<Lint> (오류 위치 + 수정제안 + 메시지 + 우선순위) │ └─▶ ⑦ 겹침 제거 remove_overlaps() → 에디터/CLI로 출력

이 벨트에서 가장 영리한 결정이 ①번에 숨어 있다. harper는 문자열을 잘라 복사하지 않는다. 원문을 Lrc<[char]>(참조 카운트 공유 배열)로 딱 한 번 만들어 두고, 모든 토큰은 "원문의 몇 번째부터 몇 번째까지"라는 범위(span)만 들고 다닌다. 이게 무할당(zero-copy) 설계의 핵심이다 — 수천 개 토큰을 만들어도 문자열 복사가 0번이니, 그만큼 빠르고 메모리를 덜 쓴다.

4-2. 규칙 엔진 — 트레잇 하나로 284개를 꿴다

harper의 문법 규칙 284개는 어떻게 관리될까? 답은 Rust의 트레잇(trait)이다. 모든 규칙은 Linter라는 공통 인터페이스를 구현하는 "무상태 부품"이다.

pub trait Linter: LSend {
    fn lint(&mut self, document: &Document) -> Vec<Lint>;
    fn description(&self) -> &str;
}

그런데 규칙 284개를 매번 이 저수준 트레잇으로 손수 짜면 지친다. 그래서 harper는 대부분의 규칙이 쓰는 고수준 트레잇 ExprLinter를 얹었다. "찾을 패턴(expr)"과 "매치되면 어떻게 고칠지(match_to_lint)"만 선언하면, Linter 구현은 자동으로 딸려 온다(블랭킷 impl).

pub trait ExprLinter: LSend {
    type Unit: DocumentIterator;   // Chunk(절) | Sentence(문장) 단위
    fn expr(&self) -> &dyn Expr;              // 찾을 패턴
    fn match_to_lint(&self, matched: &[Token], src: &[char]) -> Option<Lint>;
    fn description(&self) -> &str;
}

Expr가 바로 harper가 과거 의존하던 nlprule을 걷어내고 자체 개발한 선언적 패턴 언어다. 소스 주석은 그 동기를 이렇게 적는다 — "규칙을 쓰기 쉽게, 그리고 읽기 쉽게. 남의 Rust 코드 로직을 손으로 파싱하던 시절은 끝." SequenceExpr 빌더에 40개+ 콤비네이터를 체이닝해 "비교급 형용사 → 공백 → that → 단어" 같은 패턴을 문장처럼 선언한다.

비유

Expr DSL은 정규식(regex)의 문법판이라고 보면 된다. 정규식이 "글자 패턴"을 [a-z]+처럼 선언하듯, Expr는 "품사·단어·공백의 시퀀스"를 .then_kind(비교급).t_ws().t_aco("that")처럼 선언한다. 규칙 하나 추가가 "복잡한 로직 짜기"에서 "패턴 한 줄 쓰기"로 바뀐다.

이 284개는 LintGroup이라는 중앙 레지스트리가 매크로로 등록·실행한다. 내역을 세어 보면 Expr 기반 224개 + 커스텀 구조체 35개 + 사전/방언 필요 25개다. 규칙의 종류(LintKind)도 21가지로 세분화돼 있는데, 흔한 오타·철자뿐 아니라 Eggcorn(민간어원 오류, 예: "for all intensive purposes"), Malapropism(비슷한 소리 오용), 중복 표현, 지역 방언까지 잡는다.

4-3. 사전의 속 — FST + 레벤슈타인 오토마톤

"오프라인인데 어떻게 오타 교정까지?"의 답이 여기 있다. harper의 사전은 dictionary.dict(hunspell 스타일, 54,719줄)를 FST(Finite State Transducer)로 컴파일해 실행 파일 안에 넣는다(include_str!). FST는 수만 단어를 글자 공유 그래프로 압축해, 조회를 "단어 길이에 비례하는" 속도로 처리하는 자료구조다.

TERM
FST (유한상태 변환기)
단어들을 글자 단위 상태 그래프로 겹쳐 저장하는 자료구조. "apple"과 "apply"가 "appl"까지 길을 공유하는 식이라, 5만 단어를 훨씬 작은 용량에 담고 초고속으로 찾는다. 검색엔진(Lucene)·형태소 분석기가 즐겨 쓰는 기법으로, harper가 "가벼운 사전"을 만든 비결이다.

오타 교정은 여기에 레벤슈타인 오토마톤(Levenshtein automaton)을 교차시킨다. "편집거리 3 이내의 모든 단어"를 받아들이는 오토마톤을 즉석에서 만들고, 그걸 FST와 교차(intersection)해 비슷한 단어 후보를 한 번에 스트리밍으로 뽑는다. 후보 랭킹도 정교하다 — 첫 글자가 같으면 가점, 흔한 단어면 가점, 방언 변형(color/colour, -ize/-ise)을 인지해 감점하는 휴리스틱이 붙어 있다. 접사(affix) 확장도 있어서, annotations.json의 접두/접미사 규칙(un-, -ment, -ly …)이 어간을 곱해 실효 어휘를 5.4만보다 훨씬 크게 파생시킨다.

4-4. LSP — 에디터와 harper가 대화하는 법

harper-ls언어 서버 프로토콜(LSP)을 구현한다. LSP는 마이크로소프트가 표준화한 "에디터↔언어도구" 공용 규약으로, 이걸 구현하면 VS Code·Neovim·Helix·Emacs·Zed에 한 번에 붙는다. 통신 방식은 기본 TCP(127.0.0.1:4000) 또는 --stdio 표준입출력 모드다.

에디터 harper-ls │ initialize → │ "나는 code_action(퀵픽스) 제공 가능" │ did_open / did_change → │ 문서 변경 감지 │ │ → 재파싱 + 284규칙 재적용 │ ← publish_diagnostics │ 오류 목록을 밑줄로 push │ code_action → │ │ ← 수정 제안(퀵픽스) │ "than 으로 교체" / "사전에 추가"

즉 당신이 에디터에서 타이핑하면 did_change가 날아가고, harper-ls가 즉시 재검사해 밑줄(publish_diagnostics)을 그린다. 밑줄에 커서를 올려 퀵픽스를 부르면 code_action으로 수정 제안이 온다. 이 왕복이 전부 로컬 프로세스 간 통신이라 네트워크가 개입하지 않는다 — 2번 섹션의 "10ms"가 실제 편집 경험으로 체감되는 지점이다.

5디렉토리 구조 해부

"코어는 harper-core 하나, 나머지는 그걸 감싸는 껍데기" — 폴더가 곧 설계도

워크스페이스 최상위를 열면 harper-* 크레이트들이 나란히 있고, 그 무게중심은 압도적으로 harper-core에 쏠려 있다. 아래는 핵심만 추린 트리다.

harper/ ├─ Cargo.toml 워크스페이스 정의(21개 멤버) · 릴리스 프로파일 ├─ justfile (33KB) 빌드 레시피 총집합(just build-wasm, just test …) ├─ harper-core/ ◀ 엔진 심장 (전체의 무게중심) │ ├─ dictionary.dict 787KB · 54,719줄 hunspell 스타일 사전 │ ├─ annotations.json 접사(affix) 규칙 (un-, -ment, -ly …) │ └─ src/ │ ├─ document.rs (45KB) 토큰화→파싱→품사→규칙 파이프라인 │ ├─ token.rs / token_kind.rs Token, TokenKind 정의(무할당 핵심) │ ├─ expr/ Expr DSL (SequenceExpr 등 22개 파일) │ ├─ linting/ 284개 규칙 + 프레임워크 (~300 .rs) │ │ ├─ mod.rs Linter 트레잇 │ │ ├─ expr_linter.rs ExprLinter 트레잇 │ │ ├─ lint_group/mod.rs 284개 규칙 레지스트리(48KB) │ │ └─ that_than.rs, oxford_comma.rs, spell_check.rs … │ ├─ spell/ FstDictionary, MutableDictionary, rune/ … │ └─ parsers/ PlainEnglish, Markdown, Mask … ├─ harper-ls/src/ backend.rs(30KB) · config.rs · main.rs (LSP 서버) ├─ harper-cli/src/ main.rs (lint/parse/metadata/train 서브커맨드) ├─ harper-brill/ trained_tagger_model.json · finished_chunker/ (품사 모델) ├─ harper-wasm/src/ WASM 바인딩 ├─ packages/ harper.js · vscode-plugin · obsidian-plugin │ · chrome-plugin · wordpress-plugin · web(Svelte) └─ flake.nix · Dockerfile Nix 개발환경 · 웹서비스용 이미지
디렉토리무슨 일을 하나
harper-core/src/linting/규칙의 몸통. 규칙 하나 = .rs 파일 하나(테스트 포함). 새 규칙은 여기 추가
harper-core/src/expr/규칙을 "패턴"으로 선언하는 DSL. 콤비네이터(All·Optional·Repeating …)의 집합
harper-core/src/spell/FST 사전·가변 사전·병합 사전. 철자 교정 로직의 본진
harper-core/src/parsers/입력 포맷별 파서. "Markdown 속 산문만" 같은 선별을 담당
packages/Rust 코어를 각 플랫폼(에디터·브라우저·WP)에 연결하는 JS 껍데기

이 구조의 교훈: "확장 지점을 파일 규칙으로 못박아 두면 기여가 쉬워진다." 새 문법 규칙을 넣고 싶으면 linting/.rs 하나(로직 + #[test])를 만들어 LintGroup에 매크로로 등록하면 끝이다. 코어의 나머지는 손대지 않는다. iFixAi가 "검사 하나 = 폴더 하나"였다면, harper는 "규칙 하나 = 파일 하나 + 등록 한 줄"이다.

6학습 포인트

이 레포에서 훔쳐 배울 Rust 실전 설계 5가지 — 코드와 함께
학습 1 · 트레잇 기반 플러그인

인터페이스 하나로 284개 규칙을 균일하게 다룬다

모든 규칙은 Linter 트레잇을 구현하는 무상태 부품이다. 그래서 엔진은 규칙이 몇 개든 Vec<Box<dyn Linter>>로 담아 똑같이 순회하면 된다. 대표 규칙 ThatThan("비교급 that → than" 오타)을 보면, 로직이 아니라 패턴 선언임을 알 수 있다.

impl Default for ThatThan {
    fn default() -> Self {
        let expr = SequenceExpr::default()
            .then_kind_except(TokenKind::is_comparative_adjective, &["better","later"])
            .t_ws().t_aco("that").t_ws()      // 공백-that-공백
            .then_word_except(&["way"]);
        Self { expr }
    }
}

배울 점: "블랭킷 impl"로 고수준 트레잇(ExprLinter)만 구현하면 저수준 트레잇(Linter)이 공짜로 딸려오게 설계하면, 기여자는 쉬운 쪽만 보면 된다.

학습 2 · 무할당(zero-copy) 토큰

문자열을 복사하지 말고 "범위"만 들고 다녀라

파싱에서 가장 흔한 낭비가 부분 문자열 복사다. harper의 Token은 원문을 복사하지 않고 span(범위 정수 2개)만 보유한다. 원문은 Lrc<[char]>로 공유된다.

pub struct Token {
    pub span: Span<char>,              // 원문의 [start, end) 범위
    pub kind: TokenKind,
}
pub enum TokenKind {
    Word(Option<DictWordMetadata>),   // None = 사전에 없는 단어
    Punctuation(Punctuation), Number(Number),
    Space(usize), Url, EmailAddress, Unlintable, …
}

배울 점: 수천 개 토큰을 만들어도 힙 할당이 거의 없다. "복사 대신 참조 범위"는 Rust 파서·렉서의 기본 성능 공식이다.

학습 3 · FST + 오토마톤 철자검사

큰 사전을 작게, 오타 교정을 빠르게

5.4만 단어를 fst::Map으로 압축해 바이너리에 내장하고, 오타가 나면 레벤슈타인 오토마톤을 FST와 교차해 편집거리 이내 후보를 스트리밍으로 뽑는다. "사전을 자료구조로 이긴다"의 교과서적 예시.

배울 점: 맞춤법 검사·자동완성·퍼지 검색을 만들 때, 해시맵을 순회하며 일일이 편집거리를 재는 대신 FST 교차를 쓰면 자릿수 빠르다. Lucene·Elasticsearch가 같은 원리를 쓴다.

학습 4 · 규칙을 데이터처럼 (내부 DSL)

로직 대신 "선언"으로 규칙을 쓴다

harper는 남의 라이브러리(nlprule)를 걷어내고 자체 Expr DSL을 만들었다. 규칙 저자는 절차적 코드가 아니라 SequenceExpr 체이닝으로 "찾을 패턴"을 읽히는 선언으로 표현한다. 규칙이 많아질수록 이 "쓰기 쉬움 + 읽기 쉬움"이 복리로 이득이 된다.

배울 점: 같은 형태의 규칙이 수백 개 생길 프로젝트라면, 규칙마다 로직을 짜지 말고 작은 내부 DSL을 먼저 설계하라. 유지보수 비용이 급감한다.

학습 5 · 하나의 코어, 세 개의 얼굴 (WASM)

같은 Rust를 CLI·에디터·브라우저에 재사용

harper-core 하나를 harper-cli(네이티브), harper-ls(LSP), harper-wasm(브라우저)이 공유한다. 로직을 한 곳에만 두고 얇은 바인딩만 갈아끼우니, 웹 데모(writewithharper.com)와 VS Code 확장이 정확히 같은 규칙으로 동작한다.

배울 점: 핵심 로직을 플랫폼 독립적인 순수 크레이트로 격리하면, WASM 컴파일 한 번으로 브라우저까지 무료로 확장된다. "코어를 순수하게" 지킨 21-크레이트 분할이 이 순간 보상받는다.

7시스템 요구사항

GPU도 클라우드도 필요 없음 — 그게 이 프로젝트의 존재 이유다
항목요구사항 / 참고
실행 환경네이티브 바이너리(Windows·macOS·Linux) 또는 WASM(브라우저). 사전이 내장돼 추가 다운로드 0
하드웨어GPU 불필요. LLM을 돌리지 않고 규칙·FST·경량 품사 모델만 사용
메모리README 주장: LanguageTool의 1/50 미만. n-그램 16GB 같은 부담 없음
네트워크불필요. 오프라인 완전 동작 — 프라이버시 우선 설계의 귀결
지원 언어(검사 대상)현재 영어 전용(미국·영국·캐나다·호주 방언 구분). 구조는 타 언어로 확장 가능하나 미지원
빌드 툴체인Rust stable + wasm32-unknown-unknown 타깃 · Node 22+ · pnpm 10 · just. Nix flake 제공
에디터 통합VS Code(전용 확장) · Neovim · Helix · Emacs · Zed · Obsidian · Chrome/Firefox · WordPress
라이선스Apache-2.0 (상업적 사용·수정·재배포 자유)
TERM
Brill 태거 + Burn 청커
harper의 "경량 품사 인식" 두 축. Brill 태거는 규칙 기반 품사 태거(거대 신경망 없이 변환 규칙으로 품사 판정)이고, Burn 청커는 Rust 딥러닝 프레임워크 Burn으로 만든 작은 신경망(명사구 묶기용, 모델 806KB). LLM 없이도 문맥을 어느 정도 읽어 규칙 정확도를 높이는 "딱 필요한 만큼의 ML"이다.

8직접 해볼 수 있는 실습 과제

설치 0분 웹 체험부터 새 문법 규칙 작성까지 — 난이도별

1. 웹 데모로 규칙 감각 잡기난이도 ★☆☆ 입문

설치 없이 writewithharper.com에 접속해 일부러 오타·중복 표현·비교급 실수를 넣어 본다. WASM으로 브라우저 안에서만 도는 harper가 어떤 오류를 어떻게 잡는지 눈으로 익히는 게 목적. 개발자도구 네트워크 탭을 열어 "입력이 서버로 안 나간다"를 직접 확인해 보면 프라이버시 우선 설계가 체감된다.

2. 에디터에 붙여 실사용난이도 ★☆☆ 입문

VS Code에서 "Harper" 확장(publisher: elijah-potter)을 설치하거나, Neovim/Zed의 LSP 설정에 harper-ls를 등록한다. 마크다운 문서를 열어 실시간 밑줄과 퀵픽스를 써 보고, "사전에 단어 추가" 액션으로 가변 사전이 어떻게 동작하는지 관찰한다.

3. CLI로 엔진 내부 들여다보기난이도 ★★☆ 중급

레포를 클론하고 harper-cli를 빌드해 파이프라인의 중간 산물을 직접 본다. cargo run --bin harper-cli -- parse <파일>로 토큰화 결과를, spans로 span 시각화를, metadata <단어>로 사전이 그 단어에 붙인 품사·굴절 정보를 출력해 본다. 4번 섹션의 ASCII 흐름이 실제 데이터로 보인다.

4. 새 lint 규칙 하나 작성난이도 ★★☆ 중급

harper-core/src/linting/에서 that_than.rs를 복사해 나만의 규칙(예: 자주 틀리는 특정 표현 교정)을 만든다. ExprLinterexpr()SequenceExpr 패턴을 선언하고 match_to_lint()에서 Suggestion::replace_with_match_case_str(...)로 제안을 만든 뒤, 같은 파일에 #[test]assert_suggestion_result를 붙여 검증한다. 마지막으로 LintGroup에 등록. "패턴 선언 → 테스트"의 개발 사이클을 체험하는 핵심 실습.

5. WASM으로 빌드해 내 웹앱에 이식난이도 ★★★ 고급

just build-wasmjust build-harperjsharper.js를 빌드하고, 간단한 HTML 페이지에서 import해 텍스트 입력을 실시간 검사하는 미니 에디터를 만든다. "순수 코어를 WASM 한 번으로 브라우저까지" 확장하는 크로스 컴파일 파이프라인을 손으로 굴려 보는 과제. 번들 크기와 초기 로딩 시간을 재 보면 "가벼움"의 설계 가치가 숫자로 다가온다.

9관련 기술 심화 학습 로드맵

"고성능 텍스트 처리 도구를 Rust로 만들 수 있게 되는" 6주 코스
주차주제학습 자료 / 실습
1주차Rust 소유권·트레잇·제네릭The Rust Book 4·10·17장 · harper의 Linter/ExprLinter 트레잇 읽기
2주차렉싱/파싱 · 무할당 설계token.rs·document.rs 정독 · span 기반 토큰을 직접 구현
3주차FST · 레벤슈타인 오토마톤fst 크레이트 문서 · spell/fst_dictionary.rs · 퍼지 검색 미니 프로젝트
4주차내부 DSL(패턴 언어) 설계expr/ 디렉토리 · 콤비네이터 패턴 · 작은 매처 DSL 만들어 보기
5주차LSP 서버 구현harper-ls/src/backend.rs · tower-lsp 튜토리얼 · 미니 언어서버 작성
6주차WASM 크로스 컴파일 · 배포harper-wasm · wasm-pack · just 빌드 시스템 · npm 배포 흐름

10핵심 키워드 사전

이 레포를 읽을 때 걸려 넘어지는 용어를 한 번에
용어의미
FST유한상태 변환기. 단어 사전을 글자 공유 그래프로 압축해 초고속 조회 — harper 속도의 핵심
레벤슈타인 오토마톤"편집거리 N 이내 단어"를 받아들이는 오토마톤. FST와 교차해 오타 교정 후보를 뽑음
LSP언어 서버 프로토콜. 에디터↔언어도구 공용 규약 — 한 번 구현하면 여러 에디터에 붙음
Linter / ExprLinter모든 규칙의 트레잇(인터페이스). ExprLinter는 "패턴 + 변환"만 선언하는 고수준 버전
블랭킷 impl어떤 조건을 만족하는 모든 타입에 트레잇을 한꺼번에 구현. ExprLinter→Linter 자동 연결에 사용
Expr DSLharper 자체 패턴 선언 언어. nlprule을 대체 — "정규식의 문법판"
무할당(zero-copy)문자열을 복사하지 않고 원문의 범위(span)만 참조하는 설계. 파서 성능의 기본기
span원문에서 "몇 번째~몇 번째" 범위를 가리키는 정수 쌍. 토큰이 문자열 대신 이것만 보유
LintGroup284개 규칙을 매크로로 등록·실행하는 중앙 레지스트리
hunspell affix어간 + 접사(un-, -ment …) 규칙으로 어휘를 대량 파생하는 사전 포맷. harper가 채용
Brill 태거규칙 기반 품사 태거. 거대 모델 없이 변환 규칙으로 품사를 판정
BurnRust 딥러닝 프레임워크. harper의 경량 명사구 청커(신경망) 구동에 사용
tree-sitter증분 파서 생성기. harper가 소스 코드에서 주석·문자열만 추출하는 데 활용
크레이트 / 워크스페이스Rust의 패키지 단위 / 그런 패키지 여럿을 묶는 상위 컨테이너(harper는 21개)
Eggcorn / Malapropism민간어원 오류 / 비슷한 소리 단어 오용. harper가 잡는 고급 오류 종류

11참고 링크

원문으로 더 파고들기