@aeriz/wysiwyg

WYSIWYG rich text editor
426
Install
npm install @aeriz/wysiwyg
Latest Version:1.0.5
Vue:2 · 3
License:Apache-2.0
Last Updated:Aug 9, 2026
Links: npm
Maintainer: aeriz

@aeriz/wysiwyg

한국어 친화 contentEditable WYSIWYG 에디터.

자세한 동작 사양은 SPEC.md 를 참고하세요.

지원 환경

대상 진입점 방식
Vanilla JS @aeriz/wysiwyg 클래스 직접 사용
Web Component @aeriz/wysiwyg/wc <aeriz-editor> 커스텀 엘리먼트
jQuery @aeriz/wysiwyg/jquery $.fn.aerizEditor 플러그인
React 16.8+ / 17 / 18 / 19 @aeriz/wysiwyg/react <AerizEditor /> 컴포넌트
Vue 3 @aeriz/wysiwyg/vue v-model 컴포넌트
Vue 2.6 / 2.7 @aeriz/wysiwyg/vue2 v-model 컴포넌트
Svelte 3 / 4 / 5 @aeriz/wysiwyg/svelte use:aerizEditor 액션
Angular 14+ @aeriz/wysiwyg/angular [aerizEditor] 디렉티브 (ngModel 지원)
Node / Deno / Bun / Cloudflare Workers @aeriz/wysiwyg/server DOM 없는 렌더러 · 변환기

빌드 도구가 없는 페이지도 지원합니다 — 코어뿐 아니라 어댑터도 스크립트 태그로 로드할 수 있습니다 (CDN 절 참고).

프레임워크 패키지는 모두 optional peerDependency 입니다. React 프로젝트에 Vue 가 설치될 일은 없습니다.

설치

npm install @aeriz/wysiwyg
# yarn add @aeriz/wysiwyg
# pnpm add @aeriz/wysiwyg

스타일시트는 어느 프레임워크를 쓰든 한 번 로드해야 합니다.

import '@aeriz/wysiwyg/style.css';

Vanilla JS

번들러 사용

import { AerizEditor, AerizViewer } from '@aeriz/wysiwyg';
import '@aeriz/wysiwyg/style.css';

const editor = new AerizEditor({
  element: document.querySelector('#editor')!,
  initialHTML: '<p>안녕하세요</p>',
  placeholder: '내용을 입력하세요',
  theme: 'dark',
  onChange: (html) => localStorage.setItem('draft', html),
});

const viewer = new AerizViewer({
  element: document.querySelector('#preview')!,
});
editor.onStateChange(() => viewer.setBlocks(editor.serialize()));

CDN — 스크립트 태그

빌드 단계 없이 바로 사용. 라이브러리는 글로벌 window.AerizWysiwyg 와 단축 별칭 window.AerizEditor / window.AerizViewer 로 노출됩니다.

<!-- ✓ 프로덕션 — 특정 버전 고정 (immutable, 가장 긴 캐시) -->
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.css"
/>
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>

<!-- 1.x 트랙 — 패치만 자동 수신 -->
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@^1.0.5/dist/aeriz-wysiwyg.min.js"></script>

<!-- 항상 최신 — 프로토타이핑용 (프로덕션 권장 X) -->
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@latest/dist/aeriz-wysiwyg.min.js"></script>

<!-- unpkg 미러 -->
<script src="https://unpkg.com/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>

<div id="editor"></div>
<script>
  const editor = new AerizEditor({
    element: document.querySelector('#editor'),
    initialHTML: '<p>안녕하세요</p>',
    onChange: (html) => console.log(html),
  });
</script>

CDN — ES Module

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.css"
/>
<script type="module">
  import {
    AerizEditor,
    AerizViewer,
  } from 'https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.esm.min.js';

  new AerizEditor({ element: document.querySelector('#editor') });
</script>

CDN — 어댑터도 스크립트 태그로

dist/adapters/*.js 는 ESM 이라 <script type="module"> 로 CDN 에서 바로 부르면 bare import 를 해석하지 못합니다. 그래서 프레임워크 의존이 없는 어댑터는 IIFE 로도 배포합니다. 코어를 번들에 넣지 않고 전역 AerizWysiwyg 를 참조하므로, 코어를 먼저 부르기만 하면 됩니다 (코어가 두 벌 로드되지 않습니다).

파일 노출 전역 역할
dist/aeriz-wysiwyg-wc.min.js AerizWysiwygWC <aeriz-editor> 자동 등록
dist/aeriz-wysiwyg-jquery.min.js AerizWysiwygJQuery $.fn.aerizEditor 자동 등록
dist/aeriz-wysiwyg-adapter.min.js AerizWysiwygAdapter createEditorController 등
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.css"
/>
<!-- 1) 코어 먼저 -->
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>
<!-- 2) 필요한 어댑터 -->
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg-wc.min.js"></script>

<aeriz-editor theme="dark" placeholder="내용을 입력하세요"></aeriz-editor>

jQuery 는 jQuery 자체를 먼저 로드해야 자동 등록됩니다.

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg-jquery.min.js"></script>
<script>
  $('#editor').aerizEditor({ value: '<p>안녕하세요</p>' });
</script>

React · Vue 를 CDN 으로 쓸 때 — import map

React / Vue 어댑터는 IIFE 로 제공하지 않습니다. 소비자 앱의 프레임워크 인스턴스를 그대로 써야 하는데 전역 하나로 묶으면 인스턴스가 갈라져 훅이 깨지기 때문입니다. CDN 에서는 import map 이 정답입니다.

<script type="importmap">
  {
    "imports": {
      "vue": "https://esm.sh/vue@3",
      "@aeriz/wysiwyg": "https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/aeriz-wysiwyg.esm.min.js",
      "@aeriz/wysiwyg/vue": "https://cdn.jsdelivr.net/npm/@aeriz/wysiwyg@1.0.5/dist/adapters/vue.js"
    }
  }
</script>
<script type="module">
  import { createApp, h, ref } from 'vue';
  import { AerizEditor } from '@aeriz/wysiwyg/vue';

  const html = ref('<p>안녕하세요</p>');
  createApp({
    render: () =>
      h(AerizEditor, {
        modelValue: html.value,
        'onUpdate:modelValue': (v) => (html.value = v),
      }),
  }).mount('#app');
</script>

@aeriz/wysiwyg 항목이 반드시 있어야 합니다 — 어댑터가 그 이름으로 코어를 가져오기 때문입니다. 빠뜨리면 브라우저가 bare specifier 해석에 실패합니다.


Web Component

프레임워크 의존이 전혀 없는 최소 공통 분모입니다. Shadow DOM 을 쓰지 않으므로 전역 스타일시트가 그대로 적용됩니다.

<link rel="stylesheet" href="/aeriz-wysiwyg.min.css" />
<script type="module">
  import '@aeriz/wysiwyg/wc'; // import 만으로 자동 등록
</script>

<form action="/posts" method="post">
  <!-- name 을 주면 폼 전송에 본문 HTML 이 함께 실립니다 (폼 연동 지원 브라우저) -->
  <aeriz-editor name="body" theme="dark" placeholder="내용을 입력하세요">
  </aeriz-editor>
  <button type="submit">저장</button>
</form>

<script type="module">
  const el = document.querySelector('aeriz-editor');
  el.addEventListener('change', (e) => console.log(e.detail.html));
  el.value = '<p>안녕하세요</p>';
  el.onImageUpload = async (file) => uploadAndGetUrl(file);
</script>
속성 값
value 본문 HTML (프로퍼티로도 읽고 쓸 수 있음)
placeholder 빈 본문 안내 문구
theme light / dark
locale en / ko / ja / zh
toolbar false · 항목 목록("bold italic") · JSON 객체
image-upload-url, file-upload-url 업로드 API URL

프로퍼티: value · editor · getBlocks() · focus() · onImageUpload · onFileUpload. 이벤트: input · change (detail.html) · aeriz-ready (detail.editor).

태그 이름이 충돌하면 접두사를 바꿀 수 있습니다.

import { defineAerizElements } from '@aeriz/wysiwyg/wc';
defineAerizElements('my'); // <my-editor> / <my-viewer>

jQuery

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="/aeriz-wysiwyg.min.css" />
<script type="module">
  import '@aeriz/wysiwyg/jquery'; // 전역 jQuery 가 있으면 자동 등록

  $('#editor').aerizEditor({
    value: '<p>안녕하세요</p>',
    theme: 'dark',
    onChange: (html) => console.log(html),
  });

  const html = $('#editor').aerizEditor('getHTML');
  $('#editor').aerizEditor('setHTML', '<p>바꿈</p>');
  $('#editor').aerizEditor('destroy');
</script>

번들러 환경에서 전역 jQuery 를 쓰지 않는다면 명시적으로 등록합니다.

import $ from 'jquery';
import { registerAerizJQuery } from '@aeriz/wysiwyg/jquery';

registerAerizJQuery($);

명령: getHTML · getBlocks · setHTML · update · focus · instance · destroy. aerizViewer 도 같은 방식으로 동작합니다.

같은 노드에 두 번 초기화하면 새 인스턴스를 만들지 않고 갱신으로 처리합니다.


React

import { useState } from 'react';
import { AerizEditor, AerizViewer } from '@aeriz/wysiwyg/react';
import '@aeriz/wysiwyg/style.css';

export function Compose() {
  const [html, setHtml] = useState('<p>안녕하세요</p>');

  return (
    <>
      <AerizEditor
        value={html}
        onChange={setHtml}
        theme="dark"
        placeholder="내용을 입력하세요"
        className="my-editor"
        imageUploadUrl="/api/upload"
      />
      <AerizViewer html={html} />
    </>
  );
}

value 를 그대로 되돌려줘도 캐럿이 튀지 않습니다 — 들어온 값이 에디터의 현재 출력과 같으면 본문을 교체하지 않기 때문입니다.

ref 로 명령형 API 에 접근합니다.

const ref = useRef<AerizEditorHandle>(null);
// ref.current.editor    — 코어 인스턴스 (전체 API)
// ref.current.getHTML() / setHTML(html) / focus()

Vue 3

<script setup>
import { ref } from 'vue';
import { AerizEditor, AerizViewer } from '@aeriz/wysiwyg/vue';
import '@aeriz/wysiwyg/style.css';

const html = ref('<p>안녕하세요</p>');
const editor = ref(null);
</script>

<template>
  <AerizEditor
    ref="editor"
    v-model="html"
    theme="dark"
    placeholder="내용을 입력하세요"
    @state-change="onStateChange"
    @ready="onReady"
  />
  <AerizViewer :html="html" />
</template>

전역 등록도 가능합니다.

import { AerizWysiwygPlugin } from '@aeriz/wysiwyg/vue';
app.use(AerizWysiwygPlugin);

이벤트: update:modelValue · change · state-change · ready. ref 로 노출되는 것: editor · getHTML() · setHTML() · focus().

Vue 2 (2.6 / 2.7)

Vue 2 관례에 따라 v-model 은 value prop + input 이벤트를 씁니다.

import { AerizEditor } from '@aeriz/wysiwyg/vue2';
import '@aeriz/wysiwyg/style.css';

export default {
  components: { AerizEditor },
  data: () => ({ html: '<p>안녕하세요</p>' }),
  template: `
    <AerizEditor ref="editor" v-model="html" theme="dark" />
  `,
  methods: {
    focusEditor() {
      this.$refs.editor.focus();
    },
  },
};

이 어댑터는 vue 를 import 하지 않습니다 (Vue 2 컴포넌트는 순수 옵션 객체). 따라서 소비자 앱의 Vue 인스턴스와 이중화될 여지가 없습니다.


Svelte (3 / 4 / 5)

컴파일된 .svelte 컴포넌트가 아니라 액션으로 제공합니다. 액션은 순수 함수 계약이라 Svelte 메이저 버전 세 개를 배포본 하나로 만족시킵니다.

<script>
  import { aerizEditor, aerizViewer } from '@aeriz/wysiwyg/svelte';
  import '@aeriz/wysiwyg/style.css';

  let html = '<p>안녕하세요</p>';
</script>

<div
  use:aerizEditor={{
    value: html,
    onChange: (next) => (html = next),
    theme: 'dark',
    placeholder: '내용을 입력하세요',
  }}
/>

<div use:aerizViewer={{ html }} />

Angular (14+)

Angular 연동은 TypeScript 소스로 배포됩니다 — 소비자의 Angular 컴파일러가 직접 컴파일하므로 Angular 14 이상 어느 버전에서도 같은 파일이 동작합니다. 설정 방법(택 1)과 상세 문서는 angular/README.md 를 참고하세요.

tsconfig.app.json 의 include 에 한 줄 추가하면 끝입니다.

"include": ["src/**/*.ts", "node_modules/@aeriz/wysiwyg/angular/*.ts"]
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AerizEditorDirective } from '@aeriz/wysiwyg/angular';

@Component({
  standalone: true,
  imports: [FormsModule, AerizEditorDirective],
  template: `
    <div aerizEditor [(ngModel)]="html" theme="dark"></div>
    <div aerizViewer [html]="html"></div>
  `,
})
export class ComposeComponent {
  html = '<p>안녕하세요</p>';
}

[(value)] · [(ngModel)] · formControlName 세 가지 바인딩을 모두 지원합니다. 디렉티브는 에디터를 NgZone 밖에서 생성하므로 캐럿을 움직일 때마다 변경 감지가 돌지 않습니다.


서버 / 엣지 런타임 (DOM 없음)

저장된 BlockSnapshot[] 을 HTML · 마크다운 · 평문으로 바꿉니다. CSS 를 import 하지 않고, import 그래프 전체가 document / window 를 참조하지 않습니다 — Cloudflare Workers(workerd) · Node · Deno · Bun 에서 그대로 동작합니다.

import {
  renderBlocksToHTML,
  blocksToMarkdown,
  blocksToPlainText,
} from '@aeriz/wysiwyg/server';

// 발행용 HTML — 같은 입력이면 항상 같은 출력 (결정적 · 멱등)
const html = renderBlocksToHTML(post.blocks, { wrapper: 'content' });

// 포맷 종속을 푸는 탈출구
const markdown = blocksToMarkdown(post.blocks);

// 전문 검색 색인용
const text = blocksToPlainText(post.blocks, { blockSeparator: ' ' });
// Cloudflare Worker
export default {
  async fetch(req, env) {
    const post = await env.DB.get('post:1', 'json');
    return new Response(renderBlocksToHTML(post.blocks, { wrapper: 'content' }), {
      headers: { 'content-type': 'text/html; charset=utf-8' },
    });
  },
};

키보드 단축키

macOS 는 Ctrl 대신 Cmd. 전체 목록과 근거는 SPEC.md §7 에 있습니다.

동작 단축키
되돌리기 / 다시 실행 Ctrl+Z / Ctrl+Shift+Z · Ctrl+Y
굵게 · 기울임 · 밑줄 · 취소선 Ctrl+B · Ctrl+I · Ctrl+U · Ctrl+Shift+X
본문으로 Ctrl+Alt+0
제목 1~6 Ctrl+Alt+1 ~ Ctrl+Alt+6
번호 목록 · 글머리 목록 · 체크리스트 Ctrl+Alt+7 · Ctrl+Alt+8 · Ctrl+Alt+T
인용구 · 코드 블록 Ctrl+Alt+9 · Ctrl+Alt+C
블록 이동 / 삭제 Ctrl+Shift+↑ Ctrl+Shift+↓ / Ctrl+Shift+D
표: 다음 / 이전 셀 Tab / Shift+Tab (마지막 셀에서 Tab → 새 행)
목록: 들여쓰기 / 내어쓰기 Tab / Shift+Tab

마크다운처럼 입력해도 됩니다. 블록 맨 앞에서 # - 1. > ``` --- - [ ] 를 치면 그 자리에서 유형이 바뀝니다.

새로 만들어지는 블록의 기본 유형은 언제나 본문입니다. 제목 끝에서 Enter 를 눌러도 제목이 복제되지 않고 본문이 이어지며, 빈 제목·인용구· 코드 블록에서 Backspace 를 누르면 본문으로 돌아옵니다.

서버 sanitizer 와 함께 쓰기

getHTML() 은 기본적으로 테마 색을 인라인 style 에 베이크합니다 — 스타일시트 없이 어디에 붙여도 같은 시각으로 보이지만, style 속성을 제거하는 sanitizer 를 통과하면 서식이 통째로 사라집니다.

두 가지 대응이 있습니다.

1. 클래스 기반 출력으로 전환 — style 프로퍼티가 40여 개에서 10개로 줄고, 정렬은 클래스로 표현됩니다.

new AerizEditor({ element, htmlStyles: 'class' });  // getHTML/onChange 기본값
editor.getHTML({ styles: 'class' });                // 호출 단위 재정의

출력은 aeriz-wysiwyg.min.css 를 로드한 페이지의 .aeriz-wysiwyg > .aw-content 안에 넣어야 기본 서식이 적용됩니다. renderBlocksToHTML(blocks, { wrapper: 'content' }) 가 그 컨테이너를 만들어 줍니다.

2. 허용목록을 출력에 맞추기 — 등장할 수 있는 태그 · 속성 · style 프로퍼티의 확정 목록이 SPEC.md §9 출력 HTML 허용목록 에 있습니다. 추측이 아니라 실측이며 npm run allowlist 로 재생성할 수 있습니다.

⚠️ HTML 가져오기 기능은 sanitizer 가 아닙니다. 원본 스크립트가 실행되고 이벤트 핸들러 속성이 제거되지 않습니다. 저장 전 서버 정화가 필수입니다 — SPEC.md §11 참고.

스냅샷 스키마 버전

editor.serialize() 는 각 블록에 version: 1 을 기록합니다. 저장된 데이터를 읽는 모든 경로가 upgradeSnapshots() 를 통과하므로, 버전이 올라가도 예전 데이터가 그대로 열립니다. 변경 정책은 SPEC.md §10 에 있습니다.

직접 어댑터 만들기

목록에 없는 프레임워크(Solid, Qwik, Lit …) 는 프레임워크 중립 컨트롤러 위에 얇게 올리면 됩니다. 값 바인딩 · 캐럿 보존 · 재생성 판단이 이미 들어 있습니다.

import { createEditorController } from '@aeriz/wysiwyg/adapter';

const controller = createEditorController(hostElement, {
  value: '<p>안녕하세요</p>',
  onChange: (html) => setState(html),
});

controller.update({ value: nextHtml });  // 바뀐 것만 반영
controller.destroy();                    // 언마운트 시

빌드

npm install
npm run build        # dist/ 에 IIFE / ESM / CJS / 어댑터 / CSS / .d.ts 생성
npm test             # 서버 렌더러 · getHTML · 어댑터 계약 테스트 (jsdom)
npm run typecheck
npm run allowlist    # 출력 HTML 허용목록 재생성
npm run version:sync # 문서·데모의 CDN 버전 표기를 package.json 에 맞춤
npm run version:check # 뒤처졌으면 실패 (CI 용)
npm run dev          # 데모 페이지 (http://localhost:5173)

npm run release:stage  # npm 업로드용 release/ 조립 + exports 경로 검증
npm run release:verify # release/ 에서 npm pack --dry-run

dist/ 는 데모 서버의 publicDir 을 겸하고 개발용 비압축본·CDN 번들까지 담고 있습니다. 실제로 npm 에 올라가는 것은 release:stage 가 "files" 만 골라 조립한 release/ 입니다. 배포 절차는 RELEASE.md (붙여넣어 실행하는 런북), 계정·권한·회수 등 배경은 PUBLISHING.md 를 참고하세요.

데모는 세 페이지입니다.

  • /index.html — 에디터 기능 데모
  • /docs.html — 연동 · 직렬화 레퍼런스. 이 README 의 내용을 사이드바 · 검색 가능한 API 표 · 라이브 플레이그라운드로 정리한 단일 페이지 문서입니다. 플레이그라운드에서 같은 문서가 inline / class HTML · 마크다운 · 평문 · 스냅샷 · 서버 렌더 여섯 가지로 어떻게 나오는지 실시간 비교할 수 있습니다.
  • /frameworks.html — 프레임워크 어댑터 데모. React · Vue 3 · Web Component · Svelte 액션 · jQuery 를 한 페이지에 올리고, 로드 직후 자동 점검을 실행합니다.

문서 사이트 배포 — Cloudflare Workers

/docs.html 을 그대로 정적 사이트로 올릴 수 있습니다. docs:build 가 examples/docs.html 과 라이브러리 산출물을 docs/ 에 모으고, 코드가 없는 정적 자산 전용 Worker (wrangler.toml) 가 그 폴더를 서빙합니다. 별도 Pages 프로젝트가 필요 없습니다.

npx wrangler login    # 처음 한 번만

npm run docs:build    # docs/ 조립
npm run docs:preview  # 로컬 Workers 런타임으로 확인 (wrangler dev)
npm run docs:deploy   # 배포

페이지 HTML 은 데모와 배포본이 같은 파일입니다 — examples/docs.html 이 라이브러리를 루트 절대 경로로 부르고, 배포 시에도 산출물을 사이트 루트에 같이 두기 때문에 경로 분기가 없습니다. 캐시·보안 헤더는 docs/_headers 로 함께 생성됩니다. docs/ 는 파생물이라 git 에 추적하지 않습니다.

오프라인 단일 파일

문서 페이지를 외부에 공유하거나 오프라인으로 열어야 하면 라이브러리를 인라인한 단일 파일로 만들 수 있습니다 (외부 요청 0건).

node scripts/build-docs-artifact.mjs docs-standalone.html

설치된 Chrome 으로 그 페이지를 실제 검증할 수 있습니다 (playwright-core 는 브라우저를 내려받지 않고 시스템 Chrome 을 그대로 씁니다).

npm run dev                                    # 다른 터미널에서
npm run test:browser                           # 헤드리스 검증 + 스크린샷
node scripts/browser-check.mjs --headed        # 창을 띄워서 보기

5173 이 이미 쓰이는 중이면 데모를 다른 포트로 띄우고 주소를 넘기면 됩니다.

npx vite --config vite.demo.config.ts --port 5347
node scripts/browser-check.mjs http://localhost:5347/frameworks.html

라이선스

Apache-2.0

Related Packages

@lilac-wysiwyg/core

Lilac - a smooth, modern WYSIWYG text editor built with pure TypeScript. Zero de...

1,851 15
aurebesh

A lightweight, dependency-free WYSIWYG editor for the web. Rich text, tables, co...

590 1
eddy-editor

A lightweight WYSIWYG text editor for Vue and React

3,184 3
@codemerge/integrate

Universal host adapters for on-codemerge (UI frameworks, Web Component)

199 17
@aspect-ui-01/toast

Lightweight framework-agnostic toast notifications (~2KB)

3,926

Version History

Version Released Vue License
1.0.5 ^2.6.0 || >=3.0.0 Apache-2.0
1.0.4 ^2.6.0 || >=3.0.0 Apache-2.0
1.0.3 ^2.6.0 || >=3.0.0 Apache-2.0
1.0.2 Apache-2.0
1.0.1 Apache-2.0