[TIL] 에디터 서비스와 웹 워커

2026. 06. 27.Yeji Kim
Frontend

에디터 서비스는 latency에 특히 예민하다. 키를 눌렀는데 글자가 한 박자 늦게 뜨면 그 순간 바로 티가 난다. 그런데 에디터에는 무거운 계산이 은근히 많다. 그래서 이 중 일부를 웹워커로 뺄 수 있지 않을까?를 자료조사 겸 간단히 고민해본다.


왜 에디터에서 특히 중요한가

브라우저의 메인 스레드는 자바스크립트 실행 + 레이아웃 + 페인트 + 입력 처리를 한 줄로 처리한다. 즉 무거운 JS가 도는 동안엔 키 입력 이벤트조차 큐에 밀린다.

일반 화면이면 잠깐 버벅이는 정도지만, 에디터는 다르다. 사용자가 연속으로, 빠르게 입력하기 때문에, 메인 스레드가 50ms만 막혀도 타이핑 중 끊김(input latency)으로 바로 체감된다. 16ms(60fps) 안에 한 프레임을 그려야 하는데, 무거운 계산 하나가 그 예산을 통째로 먹어버린다.. (냠냠)

웹워커의 본질은 별도 스레드에서 JS를 돌려 메인 스레드를 비워두는 것, 에디터에선 이 비워둠이 곧 입력 반응성이다.


핵심 제약 - 워커는 DOM을 못 만진다

가장 먼저 확인해야 할 제약이다. 웹워커에는 document, window, DOM이 없다. 그래서 에디터 그 자체(contentEditable, 커서, 선택 영역, 렌더링)는 워커로 못 옮긴다. > 이건 무조건 메인 스레드다.

워커로 옮길 수 있는 건 DOM이 필요 없는 순수 계산 뿐인데, 이를 활용할 수 있는지 살펴봐야 한다. ProseMirror 같은 에디터의 문서 모델은 JSON으로 직렬화 가능한 순수 데이터라서, 그 데이터만 워커로 넘기면 DOM 없이도 분석·변환을 할 수 있어 효과적일 수 있다.

text
메인 스레드 (DOM 필수)        워커 (순수 계산)
─────────────────────        ─────────────────────
contentEditable 렌더링        문서(JSON) 분석
커서 / 선택 영역                검색 / 정규식 매칭
입력 이벤트 처리                 diff 계산
스크롤 / 레이아웃                직렬화 / 파싱
                             통계 / 맞춤법 / 린트

에디터에서 워커로 뺄 만한 작업들

생각나는 후보들을 적합도와 함께 정리해봤다.

작업워커 적합도메모
맞춤법·문법 검사★★★무겁고, 결과가 약간 늦어도 됨(비동기 친화)
린트 / 규칙 검사★★★마찬가지로 결과 지연 허용
대용량 검색·치환(정규식)★★★긴 문서 전체 스캔은 메인에서 위험
diff / 버전 비교★★★협업·히스토리에서 CPU 많이 먹음
직렬화·역직렬화(Markdown 등)★★큰 문서 export/import 시
문서 통계(글자·단어 수, 가독성)★★자주 갱신되면 누적 비용 큼
CRDT/OT 머지(협업)★★Yjs 등. 단, 동기화 설계가 까다로움
붙여넣기 이미지 압축·변환★★ArrayBuffer라 transfer 효율 좋음
AI 토큰화/임베딩 전처리★★모델 입력 가공 같은 순수 계산
신택스 하이라이팅(토크나이징)가능하지만 결과를 DOM에 빨리 반영해야 해 round-trip이 부담

공통점은 "결과가 몇십 ms 늦어도 사용자 경험이 안 깨지는" 작업이라는 것. 반대로 커서 위치처럼 즉각 반영돼야 하는 건 워커로 보내면 오히려 손해다.


진짜 함정은 통신 비용?

기회비용도 있는데, 워커가 공짜가 아니다. 메인 ↔ 워커는 postMessage로만 대화하고, 넘기는 데이터는 기본적으로 structured clone(복사) 된다. 즉 큰 문서를 통째로 매번 넘기면, 복사 비용이 계산 절약분을 다 까먹는다.

그래서 패턴이 중요할 것 같다!

  • 통째로 보내지 말고 diff/변경분만 - 워커 안에 문서 복제본(replica)을 두고, 매 입력마다 트랜잭션(변경 step)만 전송해 동기화
  • debounce / throttle - 타이핑 중엔 모았다가 멈칫할 때 보내기
  • Transferable 활용 - ArrayBuffer 같은 건 복사 대신 소유권 이전(transfer)이라 거의 공짜. 이미지·바이너리에 유리
  • SharedArrayBuffer - 같은 메모리를 공유. 단, cross-origin isolation(COOP/COEP) 헤더 설정이 필요해 진입장벽 있음
ts
// 큰 ArrayBuffer는 복사 대신 transfer로 — 두 번째 인자가 핵심
worker.postMessage({ type: "compress", buffer }, [buffer]);
// 이 시점부터 메인 스레드에선 buffer를 더 못 씀(소유권이 워커로 넘어감)

또 하나, 워커 통신은 비동기라 결과를 동기적으로 못 읽는다. "검사 결과가 올 때까지 기다렸다가 렌더"가 아니라, "일단 글자는 보여주고, 결과 오면 밑줄 같은 걸 덧붙인다"는 낙관적(optimistic) 흐름으로 설계해야 한다.


예시 사례 - Monaco는 언어 서비스를 워커에서 돌린다

이미 잘 하고 있는 사례가 있다. VS Code의 에디터 엔진인 Monaco는 TypeScript/CSS/JSON/HTML 같은 언어 서비스(자동완성, 진단, 타입 체크)를 웹워커에서 실행한다. 무거운 분석은 워커가 하고, 그 결과(에러 위치, 자동완성 목록)만 메인으로 보내 UI에 그린다. 위에서 정리한 "DOM 작업은 메인 / 순수 분석은 워커" 구도가 그대로 들어맞는다.

Comlink 같은 라이브러리를 쓰면 postMessage 보일러플레이트 없이 워커 함수를 await worker.checkSpelling(doc)처럼 호출할 수 있어, 위 패턴들을 구현할 때 코드가 한결 단순해진다.


나가며

text
워커로 보낸다  → DOM 불필요 + 무겁다 + 결과 지연 허용
                 (맞춤법, 린트, 검색, diff, 통계, 큰 직렬화)
 
메인에 둔다    → DOM 필요 or 즉각 반영 필요 or 데이터가 작다
                 (커서, 선택, 렌더링, 가벼운 계산)

결국 에디터에서 웹워커는 무겁지만 급하지 않은 순수 계산을 메인 스레드에서 떼어내 입력 반응성을 지키는 도구다. 단, 통신·복사 비용과 비동기 동기화라는 새 복잡도를 떠안는 거라서, 먼저 측정해서 메인 스레드가 실제로 막히는지 확인한 다음 도입하는 게 맞겠다는 생각이다. 작은 문서엔 round-trip 비용이 더 클 수 있으니까!


레퍼런스