언제든지 도와드립니다
도입 전 기술 검토부터 운영 중 발생하는 이슈까지,
DEXTSOLUTION 기술팀이 빠르게 응답합니다.
문의 채널
아래 채널로 문의해 주시면 영업일 기준 1일 이내에 답변드립니다.
개발자 FAQ
설치부터 고급 기능까지 — 자주 묻는 기술 질문을 카테고리별로 정리했습니다.
최소 필수 파일 2개를
<head>와 <body> 닫는
태그 앞에 각각 로드합니다.
<!-- ① CSS (head 안에) -->
<link rel="stylesheet" href="dist/webeditor.min.css">
<!-- ② JS (body 닫기 전) -->
<script src="dist/webeditor.min.js"></script>
<!-- Word 가져오기가 필요한 경우만 추가 -->
<script src="dist/plugins/wordimport-native.min.js"></script>
HTML에 컨테이너 요소를 추가하고, JS에서
new WebEditor()를 호출합니다.
<!-- HTML -->
<div id="editor"></div>
<!-- JavaScript -->
<script>
const editor = new WebEditor('#editor', {
height: 400,
placeholder: '내용을 입력하세요...'
});
</script>
컨테이너는 div 이외에도 CSS 선택자 형태
'.my-editor' 또는 DOM 요소를 직접 전달할 수
있습니다.
번들러 환경(Webpack, Vite 등)에서는 ESM 번들을 사용합니다.
// ESM (권장)
import WebEditor from './dist/webeditor.esm.js';
// CommonJS
const WebEditor = require('./dist/webeditor.cjs.js');
dist/webeditor.d.ts를 함께 배치하세요.
tsconfig의 typeRoots 또는
/// <reference>로 참조합니다.
-
webeditor.min.css가 정상 로드되었는지 개발자 도구 → Network 탭에서 확인 -
컨테이너 요소(
#editor)가 DOM에 존재하는 시점 이후에 초기화했는지 확인 (DOMContentLoaded이후 권장) -
height옵션이 숫자(px) 또는 문자열('400px')로 전달되었는지 확인 -
부모 요소에
display:none이 적용된 경우 초기화 후 표시해도 높이가 0이 될 수 있습니다 → 표시 후editor.setHeight(400)재호출
destroy()를 호출하면 DOM을 원래 상태로 복원하고
모든 이벤트 리스너를 해제합니다.
editor.destroy();
// 이후 editor 변수를 null로 해제하는 것을 권장
editor = null;
SPA(Single Page Application) 환경에서 컴포넌트 언마운트 시 반드시 호출해 메모리 누수를 방지하세요.
가능합니다. 각 컨테이너마다 독립된 인스턴스를 생성합니다.
const editor1 = new WebEditor('#editor-1', { height: 300 });
const editor2 = new WebEditor('#editor-2', { height: 200, readOnly: true });
// 정적 API로 인스턴스 접근
const model0 = WebEditor.getAPIModelByIndex(0); // 첫 번째 인스턴스
const html = model0.getHTML();
WebEditor.setLicense(key)로 전역
1회만 설정하면 모든 인스턴스에 자동 적용됩니다.
menubar: true로 기본 메뉴바를 활성화하거나,
배열로 표시할 메뉴 항목을 직접 지정합니다.
// 메뉴바 전체 표시
new WebEditor('#editor', { menubar: true });
// 특정 메뉴만 표시
new WebEditor('#editor', {
menubar: ['file', 'edit', 'view', 'insert', 'format', 'table', 'tools']
});
툴바는 showToolbar(false) 메서드로 런타임에
숨기거나 표시할 수 있습니다.
plugins 옵션에 사용할 플러그인 이름 배열을
전달합니다.
// 내장 플러그인 선택 (9종)
new WebEditor('#editor', {
plugins: ['format', 'image', 'table', 'link', 'emoji',
'video', 'sourceview', 'insert', 'find']
});
// Word 가져오기 추가 (외부 번들 필요)
new WebEditor('#editor', {
plugins: ['format', 'image', 'table', 'link',
'emoji', 'video', 'sourceview', 'insert',
'find', 'wordimport']
});
기본값은 <p> 단락 삽입입니다.
<br>로 변경하려면:
new WebEditor('#editor', {
enterKey: 'br' // 기본값: 'p'
});
'p' 모드를
권장합니다. 'br' 모드는 단순 메모 입력 등 단락
구조가 불필요한 경우에 적합합니다.
new WebEditor('#editor', {
pastePlainText: true // 모든 붙여넣기를 plain text로 처리
});
일회성 plain-paste가 필요한 경우
Ctrl+Shift+V 단축키를 사용할 수도 있습니다.
// 초기화 옵션으로 설정
new WebEditor('#editor', {
defaultFont: 'Malgun Gothic', // 기본: 'Malgun Gothic'
defaultFontSize: 14 // 기본: 14 (pt 단위)
});
// 런타임에 변경
editor.setDefaultFont('나눔고딕');
editor.setDefaultFontSize(16);
new WebEditor('#editor', {
hyperLinkDefaultTarget: '_blank' // '_self' | '_blank' | '_top' | '_parent'
});
사용자가 링크 삽입 다이얼로그에서 target을 별도로 변경하지 않으면 이 값이 기본으로 적용됩니다.
document.getElementById('submit-btn').addEventListener('click', () => {
const html = editor.getHTML(); // 본문 HTML 문자열
// hidden input에 담아 전송
document.getElementById('content-field').value = html;
document.getElementById('my-form').submit();
});
// 순수 텍스트만 필요한 경우
const text = editor.getText();
// 초기화 후 setHTML 호출
const editor = new WebEditor('#editor', { height: 400 });
editor.on('ready', () => {
editor.setHTML(savedHtmlFromServer);
editor.resetModified(); // 수정 플래그 초기화 (변경 감지 오작동 방지)
});
resetModified()를 호출하지 않으면 setHTML
직후부터 isModified()가 true를 반환합니다.
if (editor.isEmpty()) {
alert('내용을 입력해 주세요.');
return;
}
// 또는 직접 체크
const html = editor.getHTML();
const text = editor.getText().trim();
if (!text) { /* 빈 상태 */ }
isEmpty()는 공백·줄바꿈·
등을 모두 무시하고 실질적인 콘텐츠가 있는지 판단합니다.
// 기본 사용 (script, on* 이벤트 제거)
const safeHtml = editor.getSafeHTML();
// 세부 옵션 지정
const safeHtml = editor.getSafeHTML({
stripScript: true,
allowedTags: ['p','br','strong','em','u','a','img','table','tr','td'],
allowedAttrs: ['href','src','alt','width','height'],
imageRewriter: (src) => {
// base64 이미지 → CDN 경유 URL
if (src.startsWith('data:')) return '/img/placeholder.png';
return src;
}
});
// 초기화 시 설정
const viewer = new WebEditor('#viewer', {
height: 300,
readOnly: true // 키보드·마우스 입력 완전 차단
});
viewer.setHTML(savedContent);
// 런타임 전환
editor.setReadOnly(true); // 잠금
editor.setReadOnly(false); // 편집 허용
console.log(editor.isReadOnly()); // true/false
setter 계열 메서드는 모두 this를 반환하므로
체이닝이 가능합니다.
editor
.setHTML('<p>안녕하세요.</p>')
.setReadOnly(false)
.setHeight(500)
.setDefaultFont('나눔고딕')
.focus();
// 탭 전환 (0=WYSIWYG, 1=HTML소스, 2=미리보기)
editor.setTab(1); // HTML 소스 편집 탭으로 이동
// 현재 탭 확인
const tabIndex = editor.getActiveTab(); // 0 | 1 | 2
// 탭 변경 이벤트 수신
editor.on('tabChange', (index) => {
console.log('탭 전환:', ['WYSIWYG', 'HTML', '미리보기'][index]);
});
uploadHandler 옵션에 비동기 함수를 등록합니다.
함수가 URL을 반환하면 base64 이미지가 해당 URL로 자동
교체됩니다.
new WebEditor('#editor', {
uploadHandler: async (file, headers) => {
const formData = new FormData();
formData.append('file', file);
const resp = await fetch('/api/upload', {
method: 'POST',
body: formData,
headers: headers || {} // CSRF 헤더 자동 포함 (v2.4)
});
if (!resp.ok) throw new Error('업로드 실패');
const json = await resp.json();
return json.url; // 반환값이 서버 URL로 교체됨
}
});
new WebEditor('#editor', {
// 방법 1: 쿠키에서 자동 추출
csrfCookie: 'csrftoken', // 쿠키명 지정
// 방법 2: 헤더 직접 지정
uploadHeaders: {
'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content,
'X-Custom-Header': 'value'
},
uploadHandler: async (file, headers) => {
// headers 매개변수에 위 헤더가 자동 포함됨
const resp = await fetch('/api/upload', {
method: 'POST',
body: new FormData()...,
headers
});
return (await resp.json()).url;
}
});
// 이미지가 에디터 DOM에 삽입된 직후 (base64 상태)
editor.on('imageInserted', ({ src, file, node }) => {
console.log('삽입된 이미지:', src.substring(0, 30));
});
// uploadHandler 완료 후 URL 교체 직후
editor.on('imageUploaded', ({ src, file, node }) => {
console.log('업로드 완료 URL:', src); // 서버 URL
});
new WebEditor('#editor', {
maxImageWidth: 800 // px 단위, 0이면 무제한 (기본값)
});
이미지 삽입 시 원본이 800px을 초과하면 자동으로
width="800"이 적용됩니다. height는 비율에 맞게
자동 계산됩니다.
이미지를 더블클릭하거나 선택 후 우클릭 메뉴에서 이미지 속성 다이얼로그를 열면 설정 가능합니다.
-
캡션:
<figure><img><figcaption>구조로 생성 -
하이퍼링크:
<a href="..."><img></a>래핑, target 속성 지원
// 코드로 이미지 삽입
editor.insertImage('https://example.com/image.jpg');
// change 이벤트: 내용이 변경될 때마다 발생
editor.on('change', (html) => {
// 디바운스 적용 권장 (예: 1초 지연)
clearTimeout(window._saveTimer);
window._saveTimer = setTimeout(() => {
localStorage.setItem('draft', html);
}, 1000);
});
// 또는 v2.4 내장 AutoSave 사용
new WebEditor('#editor', {
autoSave: {
interval: 30000, // 30초마다
storageKey: 'draft',
unloadWarning: true, // 페이지 이탈 시 경고
onSave: (html) => console.log('자동 저장됨')
}
});
// 리스너 등록
const handler = (html) => console.log(html);
editor.on('change', handler);
// 특정 리스너 해제
editor.off('change', handler);
// 해당 이벤트의 모든 리스너 해제
editor.off('change');
ready— 에디터 초기화 완료change(html)— 콘텐츠 변경-
focus/blur— 포커스 획득/해제 input(e)— 키 입력 직후paste(e)— 붙여넣기selectionchange— 선택 영역 변경tabChange(index)— 탭 전환 (0/1/2)-
resize({width, height})— 에디터 크기 변경 -
undo/redo— 실행 취소/다시 실행 -
imageInserted({src, file, node})— 이미지 DOM 삽입 완료 -
imageUploaded({src, file, node})— 이미지 업로드 URL 교체 완료 zoom({ratio})— 확대/축소 변경-
error({context, error})— 내부 오류 (업로드 실패 등)
editor.on('error', ({ context, error }) => {
console.error(`[${context}] 오류 발생:`, error.message);
if (context === 'upload') {
alert('이미지 업로드에 실패했습니다. 네트워크를 확인해 주세요.');
}
});
context 값: 'upload',
'plugin', 'license',
'autosave' 등
방법 1 — 초기화 옵션 (인스턴스별 적용)
new WebEditor('#editor', {
license: 'WED2-XXXX-XXXX-...',
onLicenseValid: (info) => console.log('유효:', info.customer),
onLicenseInvalid: (state) => console.warn('오류:', state.reason)
});
방법 2 — 전역 설정 (다중 인스턴스에 권장)
// 인스턴스 생성 전에 1회만 호출
await WebEditor.setLicense('WED2-XXXX-XXXX-...');
const editor1 = new WebEditor('#ed1');
const editor2 = new WebEditor('#ed2'); // 자동 적용
'unlicensed'— 키가 없거나 미설정-
'invalid'— 키 형식 오류 또는 서명 불일치 'expired'— 라이선스 만료일 초과-
'mismatch'— 현재 도메인이 라이선스 허용 도메인에 포함되지 않음
const info = await WebEditor.getLicenseInfo();
/*
{
valid: true,
customer: 'ACME-001',
domains: ['example.com', '*.example.com'],
type: 'perm', // 'perm'|'year'|'trial30'|'trial180'|'custom'
expiry: null, // 영구는 null, 기간제는 Unix timestamp
mode: 'ecdsa'
}
*/
// 인스턴스별 상태
const state = editor._licenseState;
// { evaluation: false, devHost: true }
정식 라이선스 키를 발급받아 적용하면 자동으로 사라집니다. 개발/테스트 단계에서 임시로 비활성화하려면:
new WebEditor('#editor', {
evaluationWatermark: false // 워터마크 표시 비활성화 (기본값: true)
});
evaluationWatermark: false를
사용하는 것은 라이선스 계약 위반입니다. 반드시 정식 키를
구매 후 적용하세요.
const editor = new WebEditor('#editor', {
autoSave: {
interval: 30000, // 저장 주기 (ms), 기본 60000
storageKey: 'draft-post-1', // localStorage 키
unloadWarning: true, // 페이지 이탈 시 경고 다이얼로그
unloadMessage: '저장하지 않은 내용이 있습니다.',
onSave: (html) => {
console.log('자동 저장:', new Date().toLocaleTimeString());
}
}
});
// 임시 저장본 복구
const draft = editor.getAutoSaved();
if (draft && confirm('임시 저장된 내용을 복구하시겠습니까?')) {
editor.setHTML(draft);
editor.clearAutoSaved(); // 복구 후 초기화
}
// 수동 즉시 저장
editor.saveNow();
const editor = new WebEditor('#editor', {
// 금칙어 설정
profanity: {
words: ['욕설1', '욕설2'],
maskChar: '*', // 마스킹 문자 (기본 '*')
onDetect: (matches) => {
console.log('금칙어 발견:', matches);
}
},
// 개인정보 검출 패턴
privacy: {
detect: ['ssn', 'phone', 'email', 'credit-card', 'bank-account'],
onDetect: (matches) => {
console.warn('개인정보 검출:', matches);
}
}
});
// 저장 전 일괄 검증
document.getElementById('submit-btn').onclick = async () => {
const result = await editor.validate();
if (result.profanity.length > 0) {
alert(`금칙어 ${result.profanity.length}건이 발견되었습니다.`);
return;
}
if (result.privacy.length > 0) {
if (!confirm('개인정보가 포함되어 있습니다. 계속하시겠습니까?')) return;
}
// 마스킹 처리 후 저장
const maskedHtml = editor.maskSensitiveData();
submitContent(maskedHtml);
};
// 기본 미니툴바 (Bold, Italic, Underline, Link, 색상)
new WebEditor('#editor', {
inlineToolbar: true
});
텍스트를 드래그로 선택하면 선택 영역 위에 미니툴바가 자동으로 표시됩니다. 모바일 터치 환경도 지원합니다.
// 확대/축소 설정 (0.25 ~ 4.0 범위)
editor.setZoom(1.5); // 150%
editor.setZoom(0.75); // 75%
editor.setZoom(1.0); // 100% (원래 크기)
// 현재 배율 조회
const ratio = editor.getZoom(); // 예: 1.5
// 줌 변경 이벤트
editor.on('zoom', ({ ratio }) => {
console.log('현재 배율:', Math.round(ratio * 100) + '%');
});
// 에디터 본문만 브라우저 인쇄 다이얼로그로 PDF 저장
editor.saveToPDF();
// 새 창에서 인쇄 (인쇄 후 창 자동 닫힘)
editor.saveToPDFInWindow();
getHTML()로 콘텐츠를 추출해 Puppeteer,
wkhtmltopdf 등을 활용하세요.
외부 플러그인 번들을 추가로 로드하고,
plugins 옵션에 'wordimport'를
포함시킵니다.
<!-- 외부 번들 추가 로드 -->
<script src="dist/plugins/wordimport-native.min.js"></script>
<script>
new WebEditor('#editor', {
plugins: ['format', 'image', 'table', 'link', 'emoji',
'video', 'sourceview', 'insert', 'find', 'wordimport']
});
</script>
활성화 후 메뉴바의 파일 → Word 가져오기 또는 에디터 영역에 .docx 파일을 드래그하면 자동 변환됩니다.
// File 객체를 직접 전달
document.getElementById('file-input').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file || !file.name.endsWith('.docx')) return;
try {
await editor.importWordFile(file);
console.log('Word 가져오기 완료');
} catch (err) {
console.error('가져오기 실패:', err.message);
}
});
변환 지원 항목:
- 제목(H1~H6), 단락, 줄바꿈
- 굵게·기울임·밑줄·취소선·글자색·형광펜
- 표 (셀 배경색, 테두리색, 열 너비 포함)
- 인라인 이미지 (EMU → px 변환, base64 삽입)
- 하이퍼링크
미지원 항목:
- SmartArt, 차트, OLE 개체
- 복잡한 단 나누기(Multi-column) 레이아웃
- 매크로, VBA 스크립트