내가 ProseMirror를 물리친 방법: 리치 텍스트 에디터에 프로그래밍적으로 텍스트를 삽입하는 유일한 방법

발행: (2026년 3월 14일 오후 04:23 GMT+9)
6 분 소요
원문: Dev.to

I’m happy to translate the text for you, but I don’t see any content beyond the source line you provided. Could you please paste the article (or the specific portion) you’d like translated? I’ll keep the source link exactly as‑is and preserve all formatting, code blocks, URLs, and technical terms while translating the rest into Korean.

소개

현대 웹 앱에서 폼 자동 입력을 시도해 본 적이 있다면, 아마도 이 장벽에 부딪혔을 것입니다: 입력을 무시하는 리치‑텍스트 편집기. 저는 Playwright를 사용해 GumroadProseMirror 편집기에 내용을 입력하려고 몇 시간을 보냈습니다. 제가 배운 점은 다음과 같습니다.

Why innerHTML and dispatchEvent Don’t Work

editor.innerHTML = '
My description
';
editor.dispatchEvent(new Event('input', { bubbles: true }));

ProseMirror는 자체 내부 문서 모델을 유지합니다. innerHTML을 변경하면 ProseMirror는 이를 알지 못하므로 다음 렌더링 시 변경 사항이 버려집니다. input 이벤트를 디스패치해도 도움이 되지 않습니다—ProseMirror는 자신이 시작하지 않은 DOM 변형을 무시합니다.

Playwright의 fill()contenteditable에 사용하기

await page.locator('[contenteditable]').fill('text')

fill()<input><textarea> 요소를 위해 설계되었으며, contenteditable 요소에서는 작동하지 않습니다.

키보드로 타이핑하기

await editor.click()
await page.keyboard.type('My text')

이 방법은 실제로 편집기에 텍스트를 입력하고 ProseMirror가 이를 감지하지만, 긴 텍스트를 입력할 때 (문자 단위로) 매우 느리고, 타이핑 중에 포커스가 사라질 수 있습니다.

document.execCommand('insertText') 사용하기

execCommand('insertText')는 브라우저가 기본 제공하는 명령으로, ProseMirror(및 TipTap, Slate, Quill, 대부분의 리치 텍스트 에디터)에서 감지합니다. 브라우저가 이 명령을 처리하면 실제 키 입력과 동일한 내부 이벤트 파이프라인이 작동하여 에디터의 트랜잭션 시스템이 이를 포착하고, 문서 모델을 업데이트하며, 올바르게 렌더링됩니다.

왜 작동하는가

MethodWhat Happens
innerHTML편집기를 완전히 우회합니다
dispatchEventProseMirror는 이벤트가 신뢰할 수 있는 출처에서 왔는지 확인합니다
keyboard.type()작동하지만 느립니다 (문자 단위로 입력)
execCommand('insertText')편집기의 내부 트랜잭션 파이프라인을 트리거합니다

execCommand는 기술적으로는 폐기 예정이지만, 모든 브라우저가 여전히 지원하고 있으며 현재 contenteditable 에디터에 프로그래밍 방식으로 텍스트를 입력할 수 있는 유일하고 신뢰할 수 있는 방법입니다.

ProseMirror/TipTap 편집기를 채우기 위한 검증된 함수

async def fill_prosemirror(page, text):
    # Find the editor (skip small contenteditable elements)
    editors = page.locator('[contenteditable="true"]')
    count = await editors.count()
    editor = None
    for i in range(count):
        el = editors.nth(i)
        box = await el.bounding_box()
        if box and box['height'] > 80:  # Skip URL slugs, etc.
            editor = el
            break

    if not editor:
        return False

    # Focus
    await editor.click(force=True)
    await page.wait_for_timeout(300)

    # Clear existing content
    await page.keyboard.press('Meta+a')  # Cmd+A on Mac
    await page.keyboard.press('Backspace')
    await page.wait_for_timeout(200)

    # Insert line by line
    lines = text.split('\n')
    for i, line in enumerate(lines):
        if line.strip():
            await page.evaluate(
                '(t) => document.execCommand("insertText", false, t)',
                line
            )

편집기에 툴바가 포함되어 있다면, execCommand가 필요할 가능성이 높습니다.

Save 버튼 트리거하기

리치 텍스트 편집기는 일반 클릭을 가로채는 경우가 많습니다. 직접적인 JS 클릭으로 Playwright의 액션 가능성 검사를 우회합니다:

await page.evaluate(`() => {
    const buttons = document.querySelectorAll('button');
    for (const btn of buttons) {
        if (btn.textContent.includes('Save')) {
            btn.click();
            return true;
        }
    }
    return false;
}`);

보조 도구: SessionKeeper

저는 SessionKeeper를 만들어 웹 자동화의 인증 부분(CAPTCHA, 로그인 벽)을 처리했습니다. 위의 ProseMirror 트릭은 양식 입력 부분을 담당합니다. 이 둘을 함께 사용하면 브라우저 자동화에서 “마지막 단계” 문제 대부분을 해결할 수 있습니다.


다른 리치 텍스트 에디터와 싸워본 적 있나요? 전쟁 이야기를 댓글로 남겨 주세요.

0 조회
Back to Blog

관련 글

더 보기 »

JavaScript 소개

소개 오늘 수업에서 짧게 JavaScript에 대해 배웠으므로, 이 블로그에서 JavaScript에 관한 몇 가지 사실을 공유하려 합니다. JavaScript란? JavaScript…

당신의 디자인 시스템에 결합 문제가 있습니다

소개 나는 직설적으로 씁니다, 나는 당신의 시간을 소중히 여깁니다—불필요한 말은 줄이고, 가치 있는 내용에 집중합니다. 인기 있는 컴포넌트 라이브러리를 선택하고 Button 컴포넌트를 찾아보세요. 그러면 다음을 보게 됩니다: 구조...