wing
기본 서체높이


NABI NOTE

오픈소스 WYSIWYG 에디터


나비 노트는 서식, 정렬, 표, 업로드 같은 기능을 코어에서 분리해 독립 모듈인 ‘날개’로 구성한 오픈소스 WYSIWYG 에디터입니다. 필요한 날개만 골라 편집기를 구성할 수 있고, 직접 만든 날개도 연결할 수 있습니다. 순수 JavaScript로 작성되어 React나 Vue 같은 프레임워크에서도 사용할 수 있습니다. 빌드 환경이 없는 페이지에서는 CDN 라이브러리를 이용해 바로 시작할 수도 있습니다. 자체 문서 형식인 NABI TREE는 브라우저 DOM이 없는 Node.js 환경에서도 HTML로 변환할 수 있습니다. 입력된 문서는 등록된 규칙에 따라 정리되며, HTML을 만들 때는 텍스트와 속성을 이스케이프하고 링크 주소를 검사합니다. 다만 직접 만든 날개와 서버에서 받은 데이터는 애플리케이션의 보안 정책에 맞게 별도로 검토해야 합니다. CSS 변수와 rem 기반 레이아웃으로 테마와 크기를 조절할 수 있으며, 다크 모드와 여러 언어에 맞는 서체 설정도 제공합니다. 표 정렬과 로컬 히스토리, AI를 활용한 문서 작업도 하나의 문서 모델 위에서 이어집니다.


서체

산세리프(기본), 세리프, 고정폭, 필기체마다 언어별 대체 글꼴을 마련해 두었습니다. 선택한 서체에 맞는 글꼴이 없으면 브라우저의 기본 글꼴로 표시됩니다. 편집기의 기본 서체는 호스트 페이지에서 정합니다.


각 서체가 여러 언어로 어떻게 보이는지 아래에서 확인할 수 있습니다.


산세리프 · ゴシック体 · 无衬线 · Serifenlos · Sans empattement · Sin serifa · Sem serifa · Без засечек · بلا زخارف · सैन्स सेरिफ़ · স্যান্স সেরিফ · سانس سیرف · Tanpa serif · بدون سریف · सॅन्स सेरिफ · Không chân · శాన్స్ సెరిఫ్ · Mara seref · Sans serif · Bila serif · சான்ஸ் செரிஃப் · ไร้เชิง · Senza grazie


세리프 · 明朝体 · 衬线 · Serif · Avec empattement · Con serifa · Com serifa · С засечками · بزخارف · सेरिफ़ · সেরিফ · سیرف · Berserif · سریف · सेरिफ · Có chân · సెరిఫ్ · Mai seref · Yenye serif · செரிஃப் · มีเชิง · Con grazie


고정폭 · 等幅 · 等宽 · Dicktengleich · Chasse fixe · Monoespaciada · Monoespaçada · Моноширинный · ثابت العرض · मोनोस्पेस · মনোস্পেস · یکساں چوڑائی · Lebar tetap · تک‌فاصله · Đơn cách · మోనోస్పేస్ · Tazara ɗaya · Eş aralıklı · Nafasi moja · ஒற்றையகலம் · ความกว้างคงที่ · Monospaziato · Monospace


필기체 · 筆記体 · 手写体 · Schreibschrift · Cursive · Cursiva · Рукописный · خط اليد · हस्तलिपि · হস্তলিপি · رواں خط · Tulisan tangan · دست‌نویس · हस्ताक्षर · Chữ viết tay · చేతిరాత · Rubutun hannu · El yazısı · Mwandiko · கையெழுத்து · ลายมือ · Corsivo



글자 크기

아주 작게

작게

크게

아주 크게



제목

빈 줄에 #을 입력하고 스페이스를 누르면 제목으로 바뀝니다.

H1

H2

H3

H4

H5
H6



굵게 · 기울임 · 밑줄 · 취소선

굵게 기울임 밑줄 취소선을 한 글자에 함께 적용할 수 있습니다.

겹쳐서도 사용할 수 있습니다.

첨자

넓이는 3.5m2이고, 각주는 이렇게 답니다.1

물은 H2O입니다.



글자색 · 형광펜

라이트 모드와 다크 모드에서 모두 읽기 쉬운 색으로 구성했습니다.

글자색은 초록 · 코랄 · 보라 · 호박 · 파랑

형광펜 노랑 · 연두 · 하늘 · 분홍 · 보라 · 주황



링크

주소를 입력하면 링크가 됩니다.

http:// 또는 https:// 주소뿐 아니라 .이나 /로 시작하는 같은 사이트의 경로도 링크에 사용할 수 있습니다. javascript: 같은 주소는 허용하지 않습니다.

예를 들어 https://nabi.saro.me를 입력하고 스페이스나 Enter를 누르면 자동으로 링크로 바뀝니다.

링크가 열리는 방식

문서에는 링크 주소만 저장되며, 링크를 여는 방식은 문서를 표시하는 페이지에서 정합니다.

첨부 링크

이미지가 아닌 파일은 아래와 같은 첨부 링크로 남습니다.

첨부 파일 링크가 문서에 저장됩니다.



정렬

왼쪽 정렬

가운데 정렬

오른쪽 정렬

제목에는 정렬을 적용할 수 없습니다.



목록

글머리 목록

빈 줄에 -를 입력하고 스페이스를 누르면 글머리 목록이 됩니다.

  • 글머리 목록입니다.

    • Tab / Shift+Tab으로 들여쓰기와 내어쓰기를 할 수 있습니다.

번호 목록

빈 줄에 1.을 입력하고 스페이스를 누르면 번호 목록이 됩니다.

  1. 첫째

  2. 둘째

  3. 셋째

체크리스트

빈 줄에 [ ] 또는 [x]를 입력하고 스페이스를 누르면 체크리스트가 됩니다.

  • 체크된 항목입니다.

  • 아직 체크되지 않은 항목입니다.



표

셀 안에 여러 문단과 블록을 담고, 배경색으로 내용을 구분할 수 있습니다. 아래 표의 글과 이미지를 직접 편집해 보세요.

자료

작업 메모

이미지와 설명을 한 셀에 담았습니다.

문단도 자유롭게

Enter로 문단을 나누고, Shift+Enter로 같은 문단 안에서 줄을 바꿔 보세요.

  • 셀 배경색 바꾸기

  • 목록 안에서 Tab으로 들여쓰기

  • 여러 셀을 골라 같은 색 적용하기

눌러서 편집 팁 보기

셀 안에도 접기·인용·코드·동영상을 넣을 수 있습니다.

셀을 병합해도 각 셀의 내용이 순서대로 남습니다.

셀 안을 누르면 셀 배경색과 행·열 도구가 나타납니다. 여러 셀을 선택하면 색과 정렬을 한 번에 바꿀 수 있습니다. 표 전체의 위치는 ‘표 전체 선택’을 누른 뒤 정렬 도구로 바꿔 보세요.

색이 있거나 여러 블록이 담긴 표는 Markdown으로 저장할 때도 HTML로 보존됩니다.

셀 배경색

지원하는 셀 배경색을 모두 모았습니다. 각 셀을 눌러 색을 바꾸거나, 속성 도구의 ‘기본’을 골라 배경색을 지워 보세요.

노랑

yellow

연두

green

하늘

cyan

분홍

pink

보라

purple

주황

orange

표 정렬

미리보기를 누르고 재고와 가격 머리글 칸을 차례로 눌러 보세요.

모델

재고

가격

무게

NB-7

1,200

349

1.2 kg

NB-9

20,000

99

0.9 kg

NB-12

3,500

1,299

1.4 kg

NB-80

900

8,900

2.1 kg

NB-100

미정

12,999

2.4 kg

가격 열은 값이 모두 숫자이므로 숫자를 기준으로 정렬됩니다.

재고 열에는 숫자와 글자가 섞여 있어 문자열을 기준으로 정렬됩니다.



구분선

---를 입력하고 Enter를 누르면 구분선으로 바뀝니다.




이미지

이미지 크기는 30%에서 100%까지 조절할 수 있으며, 왼쪽·가운데·오른쪽으로 정렬할 수 있습니다.



플로트 영역

한눈에 보기

글·이미지·표를 담는 작은 보조 영역입니다.

배치

오른쪽

최대 너비

40%

본문 옆에 보충 설명이나 작은 자료를 두고 싶을 때 플로트 영역을 사용해 보세요. 넓은 본문에서는 이 글이 영역 옆으로 흐릅니다. 영역 안을 누르면 같은 편집기에서 글을 고치고 이미지나 표도 넣을 수 있습니다.

영역 안에 커서를 두면 속성 도구가 열립니다. 왼쪽·오른쪽 배치를 바꾸고, 최대 너비를 20·30·40·50% 중에서 골라 본문과 어울리는 비율을 찾아보세요. 표나 이미지를 붙여 넣어도 영역 안에 들어갑니다.

선택한 문단을 영역으로 묶을 수도 있습니다. 너비 옆의 해제 버튼을 누르면 영역 안의 내용은 본문에 남습니다. 미리보기에서는 편집용 점선이 사라지고, 좁은 본문이나 인쇄에서는 한 열로 이어집니다.


유튜브



업로드

이미지나 파일을 편집기로 끌어다 놓아 보세요.

이 데모에서는 파일을 실제 서버로 전송하지 않습니다. 애플리케이션에 업로더를 연결하면 서버에 저장할 수 있습니다.

이미지 선택창이나 업로드를 연 뒤 커서만 옮겨도 처음 위치에 삽입됩니다. 기다리는 동안 문서를 편집하면 삽입을 취소하고 안내합니다. 셀 안에서 시작한 업로드의 진행 표시와 결과도 그 셀 안에 남습니다.



인용

빈 줄에 >를 입력하고 스페이스를 누르면 인용문이 됩니다.

여러 줄을 사용할 수 있습니다.



코드

빈 줄에 ```를 입력하고 스페이스나 Enter를 누르면 코드 상자가 됩니다.

```java처럼 언어 이름까지 입력하고 스페이스나 Enter를 누르면 해당 언어가 지정된 코드 상자로 바뀝니다.

import { createNabiWith, defaultWings } from 'nabi-note'

const { nabi } = createNabiWith(defaultWings)
const html = nabi.getHtml()



접기

접기 블록은 제목과 내용으로 이루어집니다.

접힌 상태와 펼친 상태 중 원하는 상태로 저장할 수 있습니다.



툴바

제목·글자 서식·정렬·목록·삽입·문서 도구를 그룹별로 모았습니다. PC에서는 도구가 펼쳐지고, 모바일에서는 빠른 도구와 전체 도구판을 사용할 수 있습니다. 선택한 표나 플로트의 속성은 툴바 아래에 나타납니다.


단축키

Shift를 빠르게 두 번 누르면 키보드로 도구를 고를 수 있습니다. PC에서는 펼쳐진 툴바를, 모바일에서는 전체 도구판을 탐색합니다. Tab으로 그룹을 바꾸고 방향키로 도구를 고른 뒤 Enter로 실행해 보세요.

인용·코드·접기 같은 블록 끝에서 Enter를 빠르게 두 번 누르면 밖으로 나옵니다. 기본 연타 간격은 180ms입니다.



자동 서식

예시

액션 키

설명

#

공백

제목

-

공백

글머리 목록

1.

공백

번호 목록

[ ] · [x]

공백

체크리스트

>

공백

인용

``` · ```ts

공백 · 엔터

코드 상자

---

엔터

구분선

https://…

공백 · 엔터

링크



출력 함수

함수

결과

getHtml()

HTML

getJson()

JSON



DOM 없는 환경 지원

NABI TREE를 HTML로 변환할 때는 DOM이 필요하지 않습니다.

서버(Node.js)에서도 저장해 둔 NABI TREE를 읽어 같은 규칙으로 HTML을 만들 수 있습니다.


모바일 환경

  • 모바일 UI — 화면 폭에 맞춰 편집 도구를 배치합니다.

  • 모바일 키보드 보정 — 키보드가 열릴 때 편집 화면의 높이를 조절합니다.

  • 유연한 크기 — 주요 레이아웃에 rem 단위를 사용해 확대와 축소에 맞춥니다.

  • 다국어 — 언어와 글의 방향에 맞는 로케일과 서체를 설정할 수 있습니다.


사용자 정의

  • 사용자 날개 — 필요한 기능이 있다면 직접 만들어 연결할 수 있습니다.

  • 사용자 CSS — 주요 색상과 모서리, 간격을 --nabi-* 토큰으로 조절해 라이트 모드와 다크 모드에 맞출 수 있습니다.

  • 오픈소스 — GitHub에서 소스 코드를 공개하고 함께 개발합니다.


문서 보기 → nabi.saro.me

HTML에디터를 불러오는 중…
JSON나비트리에디터를 불러오는 중…
설치
npm install nabi-note
코드
import {
  createLocale, createNabiWith, mountSurface, mountToolbar,
  mountContextToolbar, mountHints, watchSettle, makeTranslator,
  defaultWings, mountUpload, mountUploadView, browserFileStore,
  mountFile, mountViewTools, browserHistoryStorage, mountLocalHistory,
  openHistoryPanel,
} from 'nabi-note'
import { mountDiffWing } from 'nabi-note/diff'
import { mountOutline } from 'nabi-note/viewer'

// 묻는 상자 — 안 주면 코어는 "아니오"로 답한다(제 상자를 물려도 된다)
const ask = { message: (t: string) => alert(t), confirm: (t: string) => confirm(t) }

const selected = [
  ...defaultWings,
]
const locale = createLocale('ko')
const { nabi, registry } = createNabiWith(selected, { ask, locale })

// 색칠은 호스트의 하이라이터가 한다 (Prism·highlight.js·Shiki…) — wing 은 그대로 두고
// 붙는 일(attach)만 갈아 낀다 — `makeCodeAttach` 도 같은 문에서 나온다
// { ...codeWing, attach: makeCodeAttach({ highlight: (source, lang) => [{ text: source }] }) }

const root = document.querySelector('.nabi')!
const content = document.querySelector('.nabi-content')!

const view = mountUploadView({ nabi, surface: content, locale })
const upload = mountUpload({
  nabi, root: content, locale,
  // 여기에 서버로 올리는 코드 — 진행률은 task.onProgress(0~100)
  uploader: async (task) => ({ uri: 'https://example.com/uploaded-file' }),
  extensions: ['png', 'jpg', 'pdf'], maxFileSize: 10 * 1024 * 1024,
  onStart: (tasks) => view.start(tasks),
  onProgress: (id, percent) => view.progress(id, percent),
  onSettle: () => view.settle(),
  onDone: () => view.done(),
})
const file = mountFile({ nabi, registry, store: browserFileStore(document), locale, name: () => 'note' })
const history = mountLocalHistory({ nabi, storage: browserHistoryStorage(window) })
const diff = mountDiffWing({ nabi, registry, surface: content, locale })

// locale 이 글의 방향도 정한다 — 아랍어·우르두면 오른쪽에서 왼쪽으로 선다
mountSurface({ nabi, registry, root: content, locale, fileSink: upload.take })

const trigger = document.createElement('button')
trigger.type = 'button'
trigger.textContent = makeTranslator(locale).t('outline')
trigger.style.cssText = 'position:fixed;right:1rem;bottom:1rem;z-index:1000'
root.append(trigger)
const outline = mountOutline(content, {
  trigger, locale,
  scrollContainer: () => root.classList.contains('is-fullscreen') ? root : window,
  onChange: ({ items, activeIndex, available, open }) => {
    trigger.hidden = !available
    trigger.setAttribute("aria-expanded", String(open))
  },
})
trigger.onclick = () => outline.toggle()
// 편집기를 제거할 때 outline.unmount() 호출

const settle = watchSettle(document, { surface: content })
const shared = { nabi, registry, surface: content, settle, locale }
// 판이 필요한 도구는 호스트로 돌아온다 — 이 줄이 없으면 단추가 무반응이다
const toolbar = mountToolbar({
  ...shared, root: document.querySelector('#toolbar')!,
  file,
  onFiles: upload.take,
  onHost: (w) => {
    if (w === 'diff') { diff.open(); return }
    if (w !== 'localHistory') return
    openHistoryPanel({
      history, surface: content, locale, sessionId: history.sessionId,
      render: (record) => createNabiWith(selected, { doc: JSON.parse(record.body) }).nabi.getHtml(),
    })
  },
})
const context = mountContextToolbar({ ...shared, root: document.querySelector('#context')! })
mountHints({ toolbar, context, root, surface: content })
// 미리보기·전체화면 두 단추 — 툴바 줄의 끝에 제 상자를 세워 앉는다
mountViewTools({ ...shared, root, container: document.querySelector('#toolbar')! })

// 값이 바뀔 때마다 — 여기에 당신의 코드를 건다
// nabi.onChange(() => user_callback(nabi.getHtml()))

// 언어 선택 이벤트에서 호출한다. 내용과 편집 이력은 유지된다.
// locale.setLocale('ko')