Ace 편집기 자동화
Ace는 화면에 보이는 행만 렌더링하고, 숨겨진 textarea를 통해 키 입력을 받으며 따옴표/괄호를 자동으로 닫고 새 줄을 자동으로 들여씁니다. 기본 Ace JSON 편집기에 {, Enter, "a": [1], Enter, }를 입력하면 다음과 같이 출력됩니다:
{
"a": [1]
}
}
따라서:
- 페이지에서 JavaScript를 실행하여 편집기 API를 통해 읽고 쓰세요.
.ace_line요소나 스크린샷을 스크래핑하지 말고, 코드를 키 하나씩 입력하지 마세요. - 페이지의 메인 세계에서 실행하세요. 페이지가 요소에 설정한 속성(예:
el.env)은 격리된 세계(예: 브라우저 확장 프로그램 콘텐츠 스크립트)에서 보이지 않습니다. 도구의 메인 세계 옵션을 사용하거나 5단계의 대체 방법을 사용하세요. - 읽기 전용 편집기를 존중하세요. API로 여전히 변경할 수 있지만, 앱은 이를 예상하지 않습니다 — 사용자가 요청할 때만 변경하세요.
- 편집기를 변경하는 것은 저장이 아닙니다. 저장/제출/적용을 클릭하기 전에 사용자에게 확인하세요.
아래 코드 조각은 일반 페이지 JavaScript이며, 마지막 표현식이 결과입니다. 7단계는 await를 사용합니다. 도구에 최상위 await가 없으면 코드 조각을 async 함수로 감싸세요(예: Playwright page.evaluate(async () => { ... })).
1. 편집기 찾기
Ace의 렌더러는 모든 버전에서 모든 편집기 컨테이너에 ace_editor 클래스를 추가하므로 선택자는 안정적입니다. 차이는 편집기가 어디에 있는지(메인 문서, 열린 섀도우 루트, 동일 출처 iframe), 어떤 요소가 실제 편집기인지(자동 완성 팝업도 Ace 편집기임), 인스턴스에 접근 가능한지입니다. 동적으로 검색하세요:
(() => {
const hits = [];
const walk = (root, where) => {
for (const el of root.querySelectorAll('.ace_editor')) hits.push({ el, where });
for (const node of root.querySelectorAll('*')) {
if (node.shadowRoot) walk(node.shadowRoot, `${where} > ${node.localName}::shadow`);
}
for (const frame of root.querySelectorAll('iframe, frame')) {
let doc = null;
try { doc = frame.contentDocument; } catch {}
if (doc) walk(doc, `${where} > iframe${frame.id ? '#' + frame.id : ''}`);
else hits.push({ el: null, where: `${where} > 교차 출처 iframe ${frame.src}` });
}
};
walk(document, 'document');
// 자동 완성 팝업도 Ace 편집기이므로 건너뜁니다
const found = hits.filter(h => !h.el?.classList.contains('ace_autocomplete'));
window.__aceEditors = found.map(h => h.el);
return found.map(({ el, where }, i) => {
if (!el) return { i, where, note: '이 페이지에서 접근 불가: iframe URL을 직접 열어주세요' };
const ed = el.env?.editor;
return {
i,
where,
id: el.id || undefined,
label: el.closest('[aria-label]')?.getAttribute('aria-label') || undefined,
visible: el.getClientRects().length > 0,
handle: !!ed,
mode: ed?.session.getMode().$id,
readOnly: ed?.getReadOnly(),
lines: ed?.session.getLength(),
firstLine: (ed ? ed.session.getLine(0) : el.querySelector('.ace_line')?.textContent ?? '').slice(0, 60),
};
});
})()
label,id,mode,firstLine,visible로 대상을 선택하고, 다음 단계에서 해당i를 사용하세요(예제는0을 사용).- 탐색 또는 다시 렌더링 후 찾기를 다시 실행하세요. 단일 페이지 앱에서 빈 목록은 일반적으로 편집기가 아직 마운트되지 않았음을 의미합니다: 기다렸다가 다시 시도하세요.
handle: false→ 페이지가ace.edit()없이 편집기를 생성했거나(el.env없음) 격리된 세계에 있는 경우: 5단계를 사용하세요.el.env가 없는 요소에서ace.edit(el)을 호출하여 핸들을 얻지 마세요 — 페이지의 편집기 위에 두 번째 편집기를 생성합니다.
2. 검사 및 읽기
const editor = window.__aceEditors[0].env.editor; // 찾기에서 인덱스
({
mode: editor.session.getMode().$id, // 예: ace/mode/json (모드 로딩 중에는 ace/mode/text)
readOnly: editor.getReadOnly(),
lines: editor.session.getLength(),
indent: JSON.stringify(editor.session.getTabString()),
cursor: editor.getCursorPosition(), // { row, column }, 0-based
selection: editor.getSelectedText(),
text: editor.getValue(),
})
매우 큰 문서의 경우 editor.session.getLines(firstRow, lastRow)로 조각을 읽으세요(0-based, 포함).
3. 전체 문서 바꾸기
const editor = window.__aceEditors[0].env.editor;
const newText = '...'; // 전체 새 내용
editor.session.getUndoManager().startNewGroup?.(); // 이 변경을 자체 실행 취소 단계로 유지
editor.setValue(newText, -1); // -1은 커서를 시작 위치에 둠; 'change' 이벤트를 발생시켜 앱 상태를 업데이트
editor.getValue() === newText
데이터에서 직접 구조화된 콘텐츠를 생성하세요: JSON.stringify(data, null, editor.session.getTabString())은 편집기의 들여쓰기와 일치합니다.
4. 대상 편집
const editor = window.__aceEditors[0].env.editor;
// 첫 번째 일치; 옵션: wrap, caseSensitive, wholeWord, regExp, backwards, range
if (editor.find('oldValue', { wrap: true, caseSensitive: true })) editor.replace('newValue');
// 모든 일치, 개수 반환; regExp 사용 시 $1 역참조 가능
const replaced = editor.replaceAll('version-$1', { needle: 'v(\\d+)', regExp: true });
// 위치 기준: 행과 열은 0-based, 열이 줄 끝을 넘으면 잘림
editor.session.replace({ start: { row: 2, column: 0 }, end: { row: 2, column: Infinity } }, 'whole new line 3');
editor.session.insert({ row: editor.session.getLength(), column: 0 }, '\\nappended line');
({ replaced, text: editor.getValue() })
더 많은 내용 — 커서, 선택, 접기, 스크롤, 명령, 이벤트: references/ace-api.md.
5. 핸들이 없나요? 숨겨진 textarea에 붙여넣기
const el = window.__aceEditors[0]; // handle: false에서도 작동
const newText = '...';
const input = el.querySelector('textarea.ace_text-input');
const mac = /Mac|iPhone|iPad/.test(navigator.platform);
input.focus();
// 모두 선택: Ace는 Ctrl+A 또는 Apple 플랫폼에서 Cmd+A 바인딩
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', code: 'KeyA', keyCode: 65, which: 65, ctrlKey: !mac, metaKey: mac, bubbles: true, cancelable: true }));
const data = new DataTransfer();
data.setData('text/plain', newText);
input.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }));
'pasted'
- 붙여넣은 텍스트는 그대로 삽입됩니다: 자동 페어링이나 자동 들여쓰기 없음. 읽기 전용 편집기는 무시합니다.
- textarea에 포커스가 있고 모두 선택된 상태의 대안:
document.execCommand('insertText', false, newText). - JavaScript 없이: 텍스트를 OS 클립보드에 복사하고, 편집기 내부를 클릭한 다음
Ctrl+A(Cmd+AmacOS)를 누르고, 이어서Ctrl+V(Cmd+VmacOS)를 누르세요. - 핸들 없이 렌더링된 행만 읽을 수 있습니다(
.ace_text-layer .ace_line, 다음 프레임 이후). 전체 텍스트를 보려면 모두 선택하고 OS 클립보드를 통해 복사하세요.
6. 정말 입력해야 한다면
const editor = window.__aceEditors[0].env.editor;
const assists = ['behavioursEnabled', 'wrapBehavioursEnabled', 'enableAutoIndent', 'enableLiveAutocompletion', 'enableBasicAutocompletion', 'enableSnippets'];
window.__aceSavedOptions = Object.fromEntries(assists.filter(name => editor.$options[name]).map(name => [name, editor.getOption(name)]));
editor.setOptions(Object.fromEntries(Object.keys(window.__aceSavedOptions).map(name => [name, false])));
editor.focus();
window.__aceSavedOptions // 입력 후 옵션 복원: editor.setOptions(window.__aceSavedOptions)
입력한 후 옵션을 복원하세요. 한 줄 콘텐츠를 선호하고, 입력하기 전에 Escape를 누르세요.