Skip to content

Task #1211: rhwp-studio 입력 편집 재렌더 비용 축소#1212

Merged
edwardkim merged 3 commits into
edwardkim:develfrom
postmelee:task-1211
Jun 1, 2026
Merged

Task #1211: rhwp-studio 입력 편집 재렌더 비용 축소#1212
edwardkim merged 3 commits into
edwardkim:develfrom
postmelee:task-1211

Conversation

@postmelee

@postmelee postmelee commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Related: #1211

rhwp-studio에서 표 셀 내부 텍스트 입력이 매 키 입력마다 document-changed -> CanvasView.refreshPages() full refresh를 타며 visible page 전체 재렌더와 image retry state reset을 반복하던 비용을 줄입니다.

이번 PR은 samples/exam_social.hwp성명 칸처럼 표 셀 내부에서 발생하는 단일 insertText / deleteText 편집과 한글 IME 조합 중 raw 텍스트 편집을 좁은 invalidation 경로로 보냅니다. 본문 문단 입력, 붙여넣기, 문단 분할/병합, 객체/표/페이지 구조 변경은 기존 full refresh 경로를 유지합니다.

범위

이 PR은 모든 텍스트 입력 경로를 최적화하지 않습니다. narrow invalidation 경로는 보수적으로 page-local 편집이라고 판단할 수 있는 경우에만 사용합니다.

  • 같은 표 셀 / 같은 cellPath 안에 머무르는 편집
  • 단일 insertText / deleteText
  • command를 거치지 않는 한글 IME/iOS raw 텍스트 입력 중, 편집 전/후 위치가 같은 셀에 남아 있는 경우

본문 문단 입력, 붙여넣기, 문단 분할/병합, 표/객체/페이지 구조 변경, header/footer, footnote 편집은 기존 full refresh 경로를 유지합니다.

선택안: 후보 A + C

이슈 #1211의 개선 후보 중 A + C를 선택했습니다.

  • 후보 A: 단순 텍스트 입력용 document-page-invalidated 이벤트를 추가해 구조 변경용 document-changed와 의미를 분리했습니다.
  • 후보 C: 이 narrow invalidation 경로에서는 refreshPages()를 호출하지 않으므로, full refresh에 포함된 resetImageRetryState()도 매 입력마다 반복하지 않습니다.

후보 A를 선택한 이유는 document-changed가 현재 구조 변경과 단순 입력 편집을 모두 대표하면서 full refresh를 강제하기 때문입니다. 이벤트 의미를 분리하면 구조 변경성 작업은 기존 안전한 경로에 남겨두고, 입력 편집만 더 좁은 repaint로 처리할 수 있습니다.

후보 C를 함께 적용한 이유는 단순 텍스트 입력이 이미지 payload/topology를 바꾸지 않기 때문입니다. 기존 full refresh는 입력마다 image retry state를 초기화해 이미지가 있는 페이지에서 재시도 예약과 prefetch 비용이 반복될 수 있었습니다.

후보 B처럼 document-changed에 reason을 붙여 내부 분기하는 방식은 기존 이벤트 계약이 계속 모호해질 수 있어 제외했습니다. 후보 D의 flow image 전용 WASM API는 API 표면을 늘리는 작업이므로, A+C 적용 후에도 병목이 남을 때 후속 이슈로 다루는 편이 낫다고 판단했습니다.

#865와의 관계

PR #865의 getPageOverlayImages는 overlay 이미지 목록과 imageCount만 필요할 때 대형 PageLayerTree JSON을 받지 않도록 하는 최적화입니다. 현재 PageRenderer.getOverlayImages()도 해당 API를 우선 사용하므로 overlay 추출 비용은 이미 줄어든 상태입니다.

이번 PR은 그 최적화와 별개로 남아 있던 비용을 줄입니다. 즉, 문제의 본질은 getPageOverlayImages 부재가 아니라 일반 텍스트 입력까지 document-changed -> refreshPages() -> releaseAllRenderedPages() -> resetImageRetryState() full refresh를 매번 타는 점입니다.

Changes

  • CanvasView

    • document-page-invalidated 이벤트 처리 추가
    • 기존 canvas를 유지한 채 해당 page만 다시 그리는 renderCanvas() 분리
    • page-local refresh에서는 page info 재수집, layout 재계산, 전체 canvas release, image retry reset을 생략
    • page count 변경 또는 invalid page index는 기존 full refresh로 fallback
  • InputHandler

    • command 실행 전/후 DocumentPosition을 비교
    • 표 셀 내부의 같은 cellPath에 남아 있는 단일 insertText / deleteText만 page-local refresh로 라우팅
    • command를 거치지 않는 IME/iOS raw 텍스트 입력도 같은 page-local 라우터로 연결
    • header/footer, footnote, snapshot/paste, 문단/표/객체 구조 변경은 기존 full refresh 유지
  • InputHandlerText

    • 한글 IME 조합 중 insertTextAtRaw() / deleteTextAt()afterEdit() full refresh를 직접 호출하지 않고 afterTextInputEdit()를 사용
    • iOS composition fallback의 debounce 렌더도 같은 라우터를 사용
  • Tests

    • input-edit-invalidation helper 단위 테스트 추가

Validation

cd rhwp-studio && npm test
cd rhwp-studio && npm run build

수동 확인:

http://127.0.0.1:7700/?url=/samples/exam_social.hwp&filename=exam_social.hwp
  • 1쪽 상단 성명 칸에 테스트 입력 확인
  • Stage 5 보완 후 한글 입력 경로에서 browser console error 없음
  • 브라우저 console error 없음

Measurement

임시 계측 코드를 로컬 worktree에만 넣어 비교했습니다. 계측 코드는 PR 소스에 포함하지 않았습니다.

환경:

  • OS: macOS 26.3.1 (25D771280a), arm64
  • Node.js: v24.15.0
  • Browser: Google Chrome 148.0.7778.215, headless mode
  • Runner: puppeteer-core
  • before: upstream/devel (c884205d)
  • after: task-1211 (b47f4f4)
  • sample: samples/exam_social.hwp
  • 위치: 1쪽 상단 성명
  • viewport: 1280x900, UI zoom 140%
  • input: 가나다라마바사아자차 10자
  • trials: 5
  • 측정 대상: EventBus.emit, CanvasView.refreshPages(), CanvasView.refreshInvalidatedPage(), PageRenderer.renderPage(), PageRenderer.resetImageRetryState()

측정 절차:

  • /private/tmp/rhwp-measure-before worktree에서 upstream/devel을 Vite dev server로 실행 (127.0.0.1:7711)
  • /private/tmp/rhwp-measure-after worktree에서 이 PR branch를 Vite dev server로 실행 (127.0.0.1:7712)
  • 두 서버 모두 실제 rhwp-studio 화면으로 /?url=/samples/exam_social.hwp&filename=exam_social.hwp를 로드
  • headless Chrome에서 문서 로드 완료 후 page canvas의 상대 좌표로 1쪽 상단 성명 셀을 클릭
  • page.keyboard.type()로 10자 문자열을 입력하고 같은 과정을 5회 반복
  • 임시 계측으로 이벤트 emit 횟수, refresh entrypoint 호출 횟수, PageRenderer.renderPage() 호출 수와 누적 시간을 수집
Metric (10 chars / trial) before after change
document-changed events 10 0 -100%
document-page-invalidated events 0 10 +10
CanvasView.refreshPages() 10 0 -100%
CanvasView.refreshInvalidatedPage() 0 10 +10
resetImageRetryState() 10 0 -100%
PageRenderer.renderPage() calls 20 10 -50%
PageRenderer.renderPage() total avg 188.28 ms 65.42 ms -65.3%
PageRenderer.renderPage() total p95 193.30 ms 67.10 ms -65.3%
scripted input elapsed avg 1830.60 ms 1383.00 ms -24.5%

scripted input elapsed는 puppeteer typing delay와 fixed wait를 포함하므로 보조 지표입니다. PR의 핵심 개선값은 full refresh / image retry reset 제거와 renderPage 누적 시간 감소입니다.

주의: 이 측정은 함수만 직접 호출한 microbenchmark가 아니라 실제 dev server와 browser input을 사용한 end-to-end 경로 측정입니다. 다만 headless Chrome의 scripted keyboard input은 macOS 네이티브 한글 IME 조합 입력과 완전히 동일하지 않을 수 있으므로, 사용자 체감 시간 자체보다 render/invalidation 경로 변화와 렌더 누적 시간 감소를 주 근거로 봅니다.

@edwardkim edwardkim self-requested a review June 1, 2026 07:40
@edwardkim edwardkim added the enhancement New feature or request label Jun 1, 2026
@edwardkim edwardkim added this to the v1.0.0 milestone Jun 1, 2026
@edwardkim edwardkim merged commit 3ee6306 into edwardkim:devel Jun 1, 2026
7 checks passed
edwardkim added a commit that referenced this pull request Jun 1, 2026
@postmelee. npm test 52 passed, Rust 무변경(WASM 불요), narrow invalidation 보수적 화이트리스트.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@edwardkim

Copy link
Copy Markdown
Owner

머지했습니다(devel 3ee63060). 감사합니다.

document-page-invalidated 이벤트로 단순 입력과 구조 변경의 의미를 분리하고(후보 A), narrow 경로에서 image retry reset 을 생략한(후보 C) 설계가 깔끔했습니다. 특히 isPageLocalTextEditCommand 가 insert/delete + 동일 cellPath/cell/cellPara 를 모두 만족할 때만 page-local 로 보내고 그 외에는 기존 full refresh 로 안전하게 fallback 하는 보수적 화이트리스트라, 잘못 판정해도 위험이 '최적화 미적용' 방향이라는 점이 좋았습니다. B/D 제외 근거도 명확했습니다.

검증: rhwp-studio npm test 52 passed(input-edit-invalidation 신규 포함), npm run build 통과, Rust 무변경이라 코어 회귀 0(cargo test --tests 1924 passed), CI green.

#1207 로 커서가 중첩 셀에 정확히 남으면서 드러난 입력 비용을 이 PR 이 이어서 줄여준 흐름도 좋았습니다. 감사합니다.

@postmelee postmelee deleted the task-1211 branch June 3, 2026 03:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants