Search
๐Ÿฆ‰

Prosemirror Guide: Schema, State, View

subtitle
๊ณต์‹๋ฌธ์„œ ๋ฒˆ์—ญ ๋ฐ ๋‚ด์šฉ ์ถ”๊ฐ€
Tags
์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ
front-end
Created
2025/08/01
2 more properties

Introduction

ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ๋Š” ๋ฆฌ์น˜ํ…์ŠคํŠธ ์—๋””ํ„ฐ๋ฅผ ์œ„ํ•œ ํˆด์„ ์ œ๊ณตํ•œ๋‹ค. WISWIG(what-you-see-is-what-you-get) ์—๋””ํ„ฐ๋กœ๋ถ€ํ„ฐ ์˜๊ฐ์„ ๋ฐ›์€ UI๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ๋„ WISWIG์˜ ํŽธ์ง‘ ์Šคํƒ€์ผ๋กœ ์ธํ•œ ๋‹จ์ ๋“ค์€ ๋ณด์™„ํ•˜๋ ค ๋…ธ๋ ฅํ•œ๋‹ค.
WISWIG์˜ ํŽธ์ง‘ ์Šคํƒ€์ผ๋กœ ์ธํ•œ ๋‹จ์ , ์˜ˆ๋ฅผ ๋“ค์–ด ๋ญ๊ฐ€ ์žˆ์„๊นŒ?
ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ์˜ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์›์น™์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋„ํ๋จผํŠธ์— ๋Œ€ํ•œ ์™„์ „ํ•œ ํ†ต์ œ๊ถŒ์„ ์–ป๊ฒŒํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์—ฌ๊ธฐ์„œ ๋„ํ๋จผํŠธ๋ž€ HTML์˜ blob ๊ฐ์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ, ์‚ฌ์šฉ์ž๊ฐ€ ์ •์˜ํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ๋งํ•œ๋‹ค. ๋„ํ๋จผํŠธ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ •์˜ํ•œ ๊ด€๊ณ„ ๋‚ด์—์„œ ๋ช…์‹œํ•œ ์—˜๋ฆฌ๋จผํŠธ๋งŒ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด์— ๋Œ€ํ•ด์„œ๋Š” ํ›„์— โ€˜์Šคํ‚ค๋งˆโ€™๋ฅผ ๋‹ค๋ฃจ๋ฉฐ ๋” ์ž์„ธํžˆ ์–˜๊ธฐ๊ฐ€ ๋‚˜์˜จ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ๋ชจ๋“  ์—…๋ฐ์ดํŠธ๋Š” ์‹ฑ๊ธ€ ํฌ์ธํŠธ๋ฅผ ํ†ต๊ณผํ•ด์•ผํ•˜๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ํ•œ ๊ณณ์—์„œ ์—…๋ฐ์ดํŠธ๋ฅผ ๊ฒ€์‚ฌํ•˜๊ณ  ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ง€ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ์˜ ์ฝ”์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ํŽธ๋ฆฌํ•œ ๋“œ๋ž์ธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์•„๋‹ˆ๋‹ค. ๋‹จ์ˆœํ•จ๋ณด๋‹ค ๋ชจ๋“ˆ์„ฑ๊ณผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์„ ์šฐ์„ ์‹œํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ๋Š” ์ž๋™์ฐจ ์™„์ œํ’ˆ๋ณด๋‹ค๋Š” ๋ ˆ๊ณ  ์„ธํŠธ์— ๋” ๊ฐ€๊น๋‹ค.
ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ์—๋Š” 4๊ฐ€์ง€ ํ•„์ˆ˜ ๋ชจ๋“ˆ์ด ์žˆ๋‹ค. ๋ฌด์—‡์„ ํŽธ์ง‘ํ•˜๋“  ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ํ•„์š”ํ•˜๊ณ , ์ฝ”์–ดํŒ€์œผ๋กœ๋ถ€ํ„ฐ ์ˆ˜๋งŽ์€ Extension ๋ชจ๋“ˆ๋“ค์ด ๊ด€๋ฆฌ๋œ๋‹ค. ์ด ๋ชจ๋“ˆ๋“ค์€ ์œ ์šฉํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์ง€๋งŒ ํ•„์š”์—†๋Š” ๊ธฐ๋Šฅ์€ ๋นผ๋ฒ„๋ฆฌ๊ฑฐ๋‚˜ ๋น„์Šทํ•œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ๋‹ค.
์—๋””ํ„ฐ์˜ ๋„ํ๋จผํŠธ ๋ชจ๋ธ์— ๋Œ€ํ•ด ์ •์˜ํ•œ๋‹ค. ๋„ํ๋จผํŠธ ๋ชจ๋ธ์€ ์œ„์—์„œ ๋งํ–ˆ๋“ฏ์ด ์—๋””ํ„ฐ์˜ ์ฝ˜ํ…์ธ ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ๋งํ•œ๋‹ค.
์—๋””ํ„ฐ์˜ ๋ชจ๋“  state๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ์—ฌ๊ธฐ์„œ state๋ž€ selection(์‚ฌ์šฉ์ž์˜ ์ปค์„œ๊ฐ€ ์œ„์น˜ํ•˜๋Š” ๋ถ€๋ถ„), ๊ทธ๋ฆฌ๊ณ  transaction(์ผ๋ จ์˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์ž‘์—…๋“ค) ์‹œ์Šคํ…œ๋“ค์„ ํฌํ•จํ•œ๋‹ค.
ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ ๋ทฐ ๋ชจ๋“ˆ์€ DOM์— ์—๋””ํ„ฐ state๋ฅผ ํ‘œ์‹œํ•˜๊ณ , ์œ ์ € ์ด๋ฒคํŠธ๋ฅผ ํ•ธ๋“ค๋งํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
๋„ํ๋จผํŠธ๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค. ์ด ๋ณ€๊ฒฝ์‚ฌํ•ญ๋“ค์€ state ๋ชจ๋“ˆ์˜ ํŠธ๋žœ์žญ์…˜ ์‹œ์Šคํ…œ์„ ํ†ตํ•ด ๊ธฐ๋ก๋˜๊ณ  ์žฌ์ƒ๋  ์ˆ˜ ์žˆ๋‹ค. ์ด ๋•๋ถ„์— ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ ์—๋””ํ„ฐ๋Š” ์ฝœ๋ผ๋ณด๋ ˆ์ดํŒ… ์—๋””ํ„ฐ๋‚˜ ๋˜๋Œ๋ฆฌ๊ธฐ(undo history) ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.
์ด ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๊นƒํ—™์—์„œ ProseMirror Organization์—๋Š” basic editing commands,ย binding keys,ย undo history,ย input macros,ย collaborative editing ๋“ฑ ์—ฌ๋Ÿฌ ๋ชจ๋“ˆ๋“ค์ด ๋” ์กด์žฌํ•œ๋‹ค.

My first editor

import {schema} from "prosemirror-schema-basic" import {EditorState} from "prosemirror-state" import {EditorView} from "prosemirror-view" let state = EditorState.create({schema}) let view = new EditorView(document.body, {state})
JavaScript
๋ณต์‚ฌ
์ฒซ๋ฒˆ์งธ๋กœ ํ•  ์ผ์€ โ€˜์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•˜๋Š” ๊ฒƒโ€™์ด๋‹ค. ๋„ํ๋จผํŠธ๊ฐ€ ๋ฌด์—‡์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด์•ผ ํ•˜๋Š”์ง€ ์•Œ์•„์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
๊ทธ ๋‹ค์Œ ๋‘๋ฒˆ์งธ๋กœ๋Š” ์Šคํ‚ค๋งˆ๋ฅผ ํ†ตํ•ด ์—๋””ํ„ฐ์˜ State๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•œ๋‹ค.
ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ๋Š” ์ด๋ ‡๊ฒŒ ์ƒ์„ฑ๋œ state๋ฅผ ๊ฐ€์ง€๊ณ  view ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋Š” Schema โ†’ State โ†’ View ์ˆœ์„œ๋Œ€๋กœ ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ์— ๋Œ€ํ•ด ์•Œ์•„๊ฐˆ ๊ฒƒ์ด๋‹ค.

1. Schema

ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ์˜ ๊ฐ ๋„ํ๋จผํŠธ๋Š” ๊ฐ์ž์˜ โ€˜์Šคํ‚ค๋งˆโ€™๋ฅผ ๊ฐ–๊ณ  ์žˆ๋‹ค. ์Šคํ‚ค๋งˆ๋Š” ๋„ํ๋จผํŠธ์—์„œ ์–ด๋–ค ์ข…๋ฅ˜์˜ ๋…ธ๋“œ๋“ค์ด ํฌํ•จ๋˜๊ณ , ๋˜ ๊ทธ ๋…ธ๋“œ๋“ค์ด ์–ด๋–ป๊ฒŒ ์ค‘์ฒฉ๋  ์ˆ˜ ์žˆ๋Š”์ง€ ์„ค๋ช…ํ•˜๋Š” ๋ฐ์ดํ„ฐ์ด๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ตœ์ƒ์œ„ ๋…ธ๋“œ๋Š” ํ•˜๋‚˜ ์ด์ƒ์˜ ๋ธ”๋ก์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ๊ณ  paragraph ๋…ธ๋“œ๋Š” mark๊ฐ€ ์ ์šฉ๋œ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ธ๋ผ์ธ ๋…ธ๋“œ๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ์Œ์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค.
import { Schema } from 'prosemirror-model' const nodes = { doc: { content: 'block+', // ํ•˜๋‚˜ ์ด์ƒ์˜ ๋ธ”๋ก ๋…ธ๋“œ๋ฅผ ํฌํ•จ }, paragraph: { content: 'inline*', // 0๊ฐœ ์ด์ƒ์˜ ์ธ๋ผ์ธ ๋…ธ๋“œ(text ๋“ฑ) group: 'block', // paragraph๋Š” block ๊ทธ๋ฃน parseDOM: [{ tag: 'p' }], toDOM() { return ['p', 0] // 0์€ p ํƒœ๊ทธ์˜ ์ฝ˜ํ…์ธ ๊ฐ€ ์‚ฝ์ž…๋  ์œ„์น˜ }, }, text: { group: 'inline', // text๋Š” inline ๊ทธ๋ฃน }, } const marks = { bold: { parseDOM: [{ tag: 'strong' }, { style: 'font-weight=bold' }], toDOM() { return ['strong', 0] }, }, italic: { parseDOM: [{ tag: 'em' }, { style: 'font-style=italic' }], toDOM() { return ['em', 0] }, }, } export const schema = new Schema({ nodes, marks })
JavaScript
๋ณต์‚ฌ

Node Types

๋„ํ๋จผํŠธ์˜ ๋ชจ๋“  ๋…ธ๋“œ๋Š” type์„ ๊ฐ€์ง„๋‹ค. type์€ ๋…ธ๋“œ๊ฐ€ ๊ฐ€์ง„ ์†์„ฑ์ด๋‚˜ ์˜๋ฏธ๋ฅผ ํ‘œํ˜„ํ•˜๋ฉฐ ์—๋””ํ„ฐ์— ๋ Œ๋”๋ง๋œ๋‹ค. ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•  ๋•Œ ๋…ธ๋“œ ํƒ€์ž…์„ ํ†ตํ•ด ์Šคํ‚ค๋งˆ๊ฐ€ ์–ด๋–ค ๋…ธ๋“œ๋“ค์ด ์–ด๋–ค ๊ทœ์น™์œผ๋กœ ๊ตฌ์„ฑ๋˜๋Š”์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ๋ชจ๋“  ์Šคํ‚ค๋งˆ๋Š” ์ตœ์†Œํ•œ ํ•˜๋‚˜์˜ ์ตœ์ƒ์œ„ ๋…ธ๋“œ ํƒ€์ž…์„ ๊ฐ€์ ธ์•ผ ํ•˜๊ณ , ๊ธฐ๋ณธ์ ์œผ๋กœ ์ตœ์ƒ์œ„ ๋…ธ๋“œ ํƒ€์ž…์˜ ์ด๋ฆ„์€ "doc" ์ด๋‹ค. ๋ฌผ๋ก  ์ด ๋˜ํ•œ ์„ค์ •์œผ๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๊ธด ํ•˜๋‹ค. ๋˜ํ•œ, ์ธ๋ผ์ธ ๋…ธ๋“œ๋“ค๋กœ ๊ฐ„์ฃผ๋˜๋Š” ๋…ธ๋“œ ํƒ€์ž…๋“ค์€ inline ์†์„ฑ์œผ๋กœ ์„ ์–ธํ•ด์•ผ ํ•œ๋‹ค. text ํƒ€์ž…์˜ ๊ฒฝ์šฐ ๋‚ด๋ถ€์ ์œผ๋กœ ์ธ๋ผ์ธ ๋…ธ๋“œ๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ๋ช…์‹œํ•ด์ค„ ํ•„์š”๋Š” ์—†๋‹ค.

Content Expressions

์Šคํ‚ค๋งˆ์˜ contentย ํ•„๋“œ๋Š” content expressions(์ฝ˜ํ…์ธ  ํ‘œํ˜„์‹)์œผ๋กœ ํ‘œํ˜„๋œ๋‹ค. ์ด๋Š” ์ž์‹๋…ธ๋“œ์˜ ์ˆœ์„œ๋ฅผ ์ œ์–ดํ•˜๊ณ  ์—๋””ํ„ฐ๊ฐ€ ์œ ํšจํ•œ ๋…ธ๋“œ ํƒ€์ž…์œผ๋กœ ๊ตฌ์„ฑ๋˜์—ˆ๋Š”์ง€ ํŒ๋‹จํ•˜๋Š” ๊ธฐ์ค€์ด ๋œ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด paragraph๋Š” โ€œํ•œ ๋‹จ๋ฝโ€์„ ์˜๋ฏธํ•˜๋ฉฐ, paragraph+๋Š” โ€œํ•œ ๊ฐœ ์ด์ƒ์˜ ๋‹จ๋ฝโ€์„ ํ‘œํ˜„ํ•œ๋‹ค. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ paragraph*๋Š” โ€œ0๊ฐœ ์ด์ƒ์˜ ๋‹จ๋ฝโ€์„ ์˜๋ฏธํ•˜๋ฉฐ, caption?๋Š” โ€œ0๊ฐœ ๋˜๋Š” 1๊ฐœ์˜ ์บก์…˜ ๋…ธ๋“œโ€๋ฅผ ์˜๋ฏธํ•œ๋‹ค.
๋…ธ๋“œ ์ด๋ฆ„ ๋’ค์— ์ •๊ทœ ํ‘œํ˜„์‹ ์œ ์‚ฌ ๋ฒ”์œ„ ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ์˜ˆ๋ฅผ ๋“ค์–ด {2}๋Š” โ€œ์ •ํ™•ํžˆ ๋‘ ๊ฐœโ€, {1, 5}๋Š” โ€œ1๊ฐœ์—์„œ 5๊ฐœ๊นŒ์ง€โ€, {2,}๋Š” โ€œ๋‘ ๊ฐœ ์ด์ƒโ€์„ ์˜๋ฏธํ•œ๋‹ค.
์ด๋Ÿฌํ•œ ํ‘œํ˜„์‹์„ ๊ฒฐํ•ฉํ•˜์—ฌ ์‹œํ€€์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด โ€œheading paragraph+โ€๋Š” โ€œ๋จผ์ € ์ œ๋ชฉ, ๊ทธ ๋‹ค์Œ ํ•˜๋‚˜ ์ด์ƒ์˜ ๋‹จ๋ฝโ€์„ ์˜๋ฏธํ•œ๋‹ค. ๋‘ ํ‘œํ˜„์‹ ์‚ฌ์ด์˜ ์„ ํƒ์„ ํ‘œ์‹œํ•˜๋ ค๋ฉด ํŒŒ์ดํ”„ | ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด โ€œ(paragraph | blockquote)+โ€๋Š” โ€˜ํ•œ ๊ฐœ ์ด์ƒ์˜ ๋‹จ๋ฝ ๋˜๋Š” ์ธ์šฉ๋ฌธโ€™์„ ์˜๋ฏธํ•œ๋‹ค.

group

group์€ ์Šคํ‚ค๋งˆ์—์„œ ์—ฌ๋Ÿฌ ๋ฒˆ ๋‚˜ํƒ€๋‚  ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด โ€˜๋ธ”๋กโ€™ ๋…ธ๋“œ๋ผ๋Š” ๊ฐœ๋…์ด ์ตœ์ƒ์œ„ ์ˆ˜์ค€์— ๋‚˜ํƒ€๋‚  ์ˆ˜ ์žˆ์ง€๋งŒ blockquote ๋‚ด๋ถ€์— ์ค‘์ฒฉ๋˜์–ด ์žˆ์„ ์ˆ˜๋„ ์žˆ๋‹ค. ์ด๋Ÿด ๋•, ๋…ธ๋“œ ์‚ฌ์–‘์— group ์†์„ฑ์„ ์ง€์ •ํ•˜์—ฌ ๋…ธ๋“œ ๊ทธ๋ฃน์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ‘œํ˜„์‹์—์„œ ํ•ด๋‹น ๊ทธ๋ฃน์˜ ์ด๋ฆ„์„ ์ฐธ์กฐํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
const groupSchema = new Schema({ nodes: { doc: {content: "block+"}, paragraph: {group: "block", content: "text*"}, blockquote: {group: "block", content: "block+"}, text: {} } })
JavaScript
๋ณต์‚ฌ
์ด ์˜ˆ์‹œ์—์„œ block+ ๋Š” (paragraph | blockquote)+ ๊ณผ ๊ฐ™์€ ์˜๋ฏธ์ด๋‹ค. paragraph์™€ blcokquote์— ๋ชจ๋‘ group ์†์„ฑ์„ block ์œผ๋กœ ์ง€์ •ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
๋ธ”๋ก ์ฝ˜ํ…์ธ ๋ฅผ ํฌํ•จํ•˜๋Š” ๋…ธ๋“œ(์˜ˆ: ์œ„ ์˜ˆ์‹œ์—์„œ์˜ doc ๋ฐ blockquote)์—์„œ๋Š” ํ•ญ์ƒ ์ตœ์†Œ ํ•œ ๊ฐœ์˜ ์ž์‹ ๋…ธ๋“œ๋ฅผ ํฌํ•จํ•˜๋„๋ก ๊ถŒ์žฅ๋œ๋‹ค. ๋ธŒ๋ผ์šฐ์ €๋Š” ํ•ด๋‹น ๋…ธ๋“œ๊ฐ€ ๋นˆ ์ƒํƒœ์ผ ๊ฒฝ์šฐ ๋…ธ๋“œ๋ฅผ ์™„์ „ํžˆ ์ ‘๊ธฐ(collapse) ๋•Œ๋ฌธ์— ํŽธ์ง‘์ด ์ƒ๋‹นํžˆ ์–ด๋ ค์›Œ์ง€๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์—ฌ๊ธฐ์„œ ๋…ธ๋“œ๋ฅผ collapseํ•œ๋‹ค๋Š” ๊ฒƒ์€ ํ™”๋ฉด์ƒ์—์„œ ์•„์˜ˆ ์•ˆ ๋ณด์ด๊ฒŒ ๋งŒ๋“ค๊ฑฐ๋‚˜, ์ปค์„œ์กฐ์ฐจ ๋†“์„ ์ˆ˜ ์—†๋Š” ์ƒํƒœ๋กœ ๋งŒ๋“ค์–ด๋ฒ„๋ฆฐ๋‹ค๋Š” ๋œป์ด๋‹ค.

์ฝ˜ํ…์ธ  ํ‘œํ˜„์‹์—์„œ ์ˆœ์„œ๊ฐ€ ์ค‘์š”ํ•œ ์ด์œ 

๋˜ํ•œ, ๋…ธ๋“œ๊ฐ€ OR ํ‘œํ˜„์‹ (|)์—์„œ ๋‚˜ํƒ€๋‚˜๋Š” ์ˆœ์„œ๋„ ์ค‘์š”ํ•˜๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, replace ๋‹จ๊ณ„(๋„ํ๋จผํŠธ์˜ ์ผ๋ถ€๋ฅผ ์ƒˆ๋กœ์šด ์ฝ˜ํ…์ธ ๋กœ ๋Œ€์ฒดํ•˜๋Š” ์ž‘์—…) ์ดํ›„์—๋„ ๋„ํ๋จผํŠธ๊ฐ€ ์Šคํ‚ค๋งˆ๋ฅผ ๊ณ„์† ์ง€ํ‚ค๋„๋ก ํ•ด์•ผํ•˜๋Š”๋ฐ, ์ด๋ฅผ ์œ„ํ•ด ์„ ํƒ๋˜์ง€ ์•Š์€ ๋…ธ๋“œ์˜ ๊ธฐ๋ณธ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•  ๋•Œ, ํ‘œํ˜„์‹์˜ ์ฒซ๋ฒˆ์งธ ํƒ€์ž…์ด ๊ธฐ๋ณธ์œผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค. ๋งŒ์•ฝ์— ๋…ธ๋“œ๊ฐ€ ์•„๋‹Œ ๊ทธ๋ฃน์ธ ๊ฒฝ์šฐ๋Š” ์–ด๋–จ๊นŒ? ๊ทธ๋ฃน์—์„œ paragraph์™€ blockquote ์˜ ์ˆœ์„œ๋ฅผ ๋ฐ”๊ฟ” blockquote๊ฐ€ ๋จผ์ € ์˜ค๊ฒŒ ๋œ๋‹ค๋ฉด ์—๋””ํ„ฐ๋Š” ๋ธ”๋ก ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋ ค๊ณ  ์‹œ๋„ํ•  ๋•Œ๋งˆ๋‹ค ์Šคํƒ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋œ๋‹ค. ์™œ ๊ทธ๋Ÿด๊นŒ? blockquote ๋…ธ๋“œ๋Š” block ํƒ€์ž…์˜ ๋…ธ๋“œ๋ฅผ ์ตœ์†Œ ํ•˜๋‚˜ ์ด์ƒ ํ•„์š”๋กœ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฝ˜ํ…์ธ ๋กœ ๋˜ ๋‹ค๋ฅธ block ์„ ์ƒ์„ฑํ•˜๋ ค ํ•˜๊ณ , block ๊ทธ๋ฃน์˜ ๊ธฐ๋ณธ์€ blockquote ์ด๋ฏ€๋กœ ๋‹ค์‹œ blockquote ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜๊ณ , ์ด ๊ณผ์ •์ด ๋ฐ˜๋ณต๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค ใ„ทใ„ท.

์œ ํšจ์„ฑ ๊ฒ€์‚ฌ

ProseMirror ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—๋Š” ๋…ธ๋“œ๋ฅผ ์กฐ์ž‘ํ•˜๋Š” ๋‹ค์–‘ํ•œ ํ•จ์ˆ˜๋“ค์ด ์žˆ์ง€๋งŒ, ๊ทธ ๋ชจ๋“  ํ•จ์ˆ˜๋“ค์ด ์ž…๋ ฅ๊ฐ’์˜ ์œ ํšจ์„ฑ์„ ๋‹ค ๊ฒ€์‚ฌํ•ด์ฃผ๋Š” ๊ฑด ์•„๋‹ˆ๋‹ค. transform ๊ฐ™์€ ๊ณ ์ˆ˜์ค€(high-level) API๋Š” ์ž…๋ ฅ์ด ์œ ํšจํ•œ์ง€ ์ž๋™์œผ๋กœ ๊ฒ€์‚ฌํ•ด์ฃผ์ง€๋งŒ, NodeType.create ๊ฐ™์€ ์ €์ˆ˜์ค€(primitive) ๋…ธ๋“œ ์ƒ์„ฑ ๋ฉ”์„œ๋“œ๋“ค์€ ์ž…๋ ฅ๊ฐ’์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•˜์ง€ ์•Š๊ณ , ๊ทธ ์ฑ…์ž„์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๋งก๊ธด๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, NodeType.create๋ฅผ ์ด์šฉํ•˜๋ฉด ์ž˜๋ชป๋œ ๊ตฌ์กฐ์˜ ๋…ธ๋“œ๋„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  slice์˜ ๊ฒฝ๊ณ„(edge)๊ฐ€ ์—ด๋ฆฐ ์ƒํƒœ์ผ ๋•Œ(์˜ˆ: ๋ณต๋ถ™์„ ์œ„ํ•œ ๋ฌธ์„œ ์กฐ๊ฐ(slice) ๋“ฑ)๋Š” ์ผ์‹œ์ ์œผ๋กœ ์ž˜๋ชป๋œ ๊ตฌ์กฐ์˜ ๋…ธ๋“œ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ํ•ฉ๋ฆฌ์ ์ผ ์ˆ˜๋„ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ •์ƒ์ ์ธ ์ „์ฒด ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ์‚ฌ์šฉํ•  ๋•Œ๋Š”, createChecked()๋ผ๋Š” ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ํฌํ•จํ•œ ์ƒ์„ฑ ๋ฉ”์„œ๋“œ๋‚˜ ์ด๋ฏธ ๋งŒ๋“  ๋…ธ๋“œ์— ๋Œ€ํ•ด check()๋ฅผ ํ˜ธ์ถœํ•ด์„œ ๊ทธ ๋…ธ๋“œ์˜ ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค.

Marks

Marks๋Š” ์ธ๋ผ์ธ ์ฝ˜ํ…์ธ ์— ์ถ”๊ฐ€์ ์ธ ์ •๋ณด๋‚˜ ์Šคํƒ€์ผ์„ ๋”ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. ์Šคํ‚ค๋งˆ๋Š” ๋ฐ˜๋“œ์‹œ ์Šคํ‚ค๋งˆ์—์„œ ํ—ˆ์šฉํ•˜๋Š” ๋งˆํฌ ํƒ€์ž…์„ ๋ชจ๋‘ ์„ ์–ธํ•ด์•ผ ํ•œ๋‹ค. Mark types๋Š” ๋…ธ๋“œ ํƒ€์ž…๊ณผ ๋น„์Šทํ•œ ํ˜•ํƒœ์˜ ๊ฐ์ฒด์ด๋ฉฐ ๋งˆํฌ ๊ฐ์ฒด๋ฅผ ์‹๋ณ„ํ•˜๊ณ  ๊ทธ์— ๋Œ€ํ•œ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•œ๋‹ค.
๊ธฐ๋ณธ์ ์œผ๋กœ ์ธ๋ผ์ธ ์ฝ˜ํ…์ธ ๋ฅผ ํฌํ•จํ•˜๋Š” ๋…ธ๋“œ๋Š” ์Šคํ‚ค๋งˆ์— ์ •์˜๋œ ๋ชจ๋“  ๋งˆํฌ๋ฅผ ์ž์‹ ๋…ธ๋“œ์— ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๋™์ž‘์€ ๊ฐ ๋…ธ๋“œ์˜ marks ์†์„ฑ์„ ํ†ตํ•ด ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ๋‹ค.
์˜ˆ์‹œ
paragraph ๋…ธ๋“œ์˜ ํ…์ŠคํŠธ์— strong ๊ณผ emphasis ๋งˆํฌ๋“ค์„ ์ง€์›ํ•˜๋Š” ๊ฐ„๋‹จํ•œ ์Šคํ‚ค๋งˆ๋‹ค. ํ•˜์ง€๋งŒ heading์—๋Š” ๋งˆํฌ๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค.
const markSchema = new Schema({ nodes: { doc: {content: "block+"}, paragraph: {group: "block", content: "text*", marks: "_"}, heading: {group: "block", content: "text*", marks: ""}, text: {inline: true} }, marks: { strong: {}, em: {} } })
JavaScript
๋ณต์‚ฌ
๋งˆํฌ์˜ ์ง‘ํ•ฉ(marks ์†์„ฑ)์€ ๋งˆํฌ ์ด๋ฆ„์ด๋‚˜ ๋งˆํฌ ๊ทธ๋ฃน์„ ๊ณต๋ฐฑ์œผ๋กœ ๊ตฌ๋ถ„ํ•œ ๋ฌธ์ž์—ด๋กœ ํ•ด์„๋œ๋‹ค. "_"(์–ธ๋”์Šค์ฝ”์–ด)๋Š” ์™€์ผ๋“œ์นด๋“œ๋กœ ๋™์ž‘ํ•˜๋ฉฐ, ์Šคํ‚ค๋งˆ์— ์ •์˜๋œ ๋ชจ๋“  ๋งˆํฌ๋ฅผ ํ—ˆ์šฉํ•œ๋‹ค๋Š” ์˜๋ฏธ๋‹ค. "" (๋นˆ ๋ฌธ์ž์—ด)์€ ๋งˆํฌ๋ฅผ ์ „ํ˜€ ํ—ˆ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์˜๋ฏธ๋‹ค.
์œ„ ์˜ˆ์‹œ์—์„œ paragraph ์˜ mark๋Š” _ ์ด๊ณ  heading ์˜ mark๋Š” ๋นˆ ๋ฌธ์ž์—ด์ด๋ฏ€๋กœ paragraph๋Š” ์Šคํ‚ค๋งˆ ๋‚ด์˜ ๋ชจ๋“  ๋งˆํฌ๋ฅผ ํ—ˆ์šฉํ•˜๊ณ , heading์€ ์•„๋ฌด๊ฒƒ๋„ ํ—ˆ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋งŒ์•ฝ ์ผ๋ถ€๋งŒ ํ—ˆ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด marks: โ€œstrong emโ€ ๊ณผ ๊ฐ™์ด ์“ฐ๋ฉด ๋œ๋‹ค. strong, em์˜ mark๋งŒ ํ—ˆ์šฉํ•œ๋‹ค๋Š” ๋œป์ด๋‹ค.

Attributes

๋ฌธ์„œ ์Šคํ‚ค๋งˆ๋Š” ๊ฐ ๋…ธ๋“œ๋‚˜ ๋งˆํฌ๊ฐ€ ์–ด๋–ค ์†์„ฑ(attributes)์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š”์ง€๋„ ํ•จ๊ป˜ ์ •์˜ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, heading ๋…ธ๋“œ์ฒ˜๋Ÿผ ๋ ˆ๋ฒจ(level) ๊ฐ™์€ ์ถ”๊ฐ€์ ์ธ ์ •๋ณด๋ฅผ ์ €์žฅํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ, ์ด๋Ÿฌํ•œ ๋ฐ์ดํ„ฐ๋Š” attribute๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ ์ ˆํ•˜๋‹ค. ๊ฐ ๋…ธ๋“œ๋‚˜ ๋งˆํฌ์˜ attribute๋Š” JSON์œผ๋กœ ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ, ํ‰๋ฒ”ํ•œ ๊ฐ์ฒด ํ˜•ํƒœ๋กœ ํ‘œํ˜„๋œ๋‹ค.
์–ด๋–ค ์†์„ฑ๋“ค์„ ํ—ˆ์šฉํ• ์ง€ ์ •์˜ํ•˜๋ ค๋ฉด, ํ•ด๋‹น ๋…ธ๋“œ๋‚˜ ๋งˆํฌ์˜ spec์—์„œ attrs ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค. ์ด ํ•„๋“œ๋Š” ์„ ํƒ์‚ฌํ•ญ์ด์ง€๋งŒ, attribute๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋ฐ˜๋“œ์‹œ ๋ช…์‹œ์ ์œผ๋กœ ์ •์˜ํ•ด์•ผ ํ•œ๋‹ค.
heading: { content: "text*", attrs: {level: {default: 1}} }
JavaScript
๋ณต์‚ฌ
์ด ์Šคํ‚ค๋งˆ์—์„œ heading ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด, ๋ชจ๋“  ์ธ์Šคํ„ด์Šค๋Š” .attrs.level ์†์„ฑ์— level์ด๋ผ๋Š” attribute๋ฅผ ๊ฐ€์ง€๊ฒŒ ๋œ๋‹ค. ์ด ์†์„ฑ์ด ๋ช…์‹œ๋˜์ง€ ์•Š์€ ์ฑ„๋กœ ๋…ธ๋“œ๊ฐ€ ์ƒ์„ฑ๋˜๋ฉด, ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ 1์ด ์ž๋™ ํ• ๋‹น๋œ๋‹ค. ๋งŒ์•ฝ attribute์— ๋Œ€ํ•ด ๊ธฐ๋ณธ๊ฐ’์„ ์ง€์ •ํ•˜์ง€ ์•Š์•˜๋Š”๋ฐ, ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ํ•ด๋‹น attribute๋ฅผ ์ƒ๋žตํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋œ๋‹ค.
๋˜ํ•œ ๊ธฐ๋ณธ๊ฐ’์ด ์—†๋Š” attribute๋Š” ProseMirror ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ•ด๋‹น ๋…ธ๋“œ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉํ•ดํ•˜๊ฒŒ ๋œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํŠธ๋žœ์Šคํผ์ด๋‚˜ createAndFill์„ ์‚ฌ์šฉํ•  ๋•Œ, ์Šคํ‚ค๋งˆ ์ œ์•ฝ์„ ๋งŒ์กฑ์‹œํ‚ค๊ธฐ ์œ„ํ•ด ๋น„์–ด ์žˆ๋Š” ๋…ธ๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์ฑ„์›Œ ๋„ฃ์–ด์•ผ ํ•  ๋•Œ, ๊ธฐ๋ณธ๊ฐ’์„ ์•Œ ์ˆ˜ ์—†์–ด์„œ ํ•ด๋‹น ๋…ธ๋“œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์—†๊ฒŒ ๋œ๋‹ค.
์ด๋Ÿฐ ์ด์œ  ๋•Œ๋ฌธ์—, ๊ธฐ๋ณธ๊ฐ’ ์—†๋Š” attribute๋ฅผ ์š”๊ตฌํ•˜๋Š” ๋…ธ๋“œ๋Š” ์Šคํ‚ค๋งˆ ์ƒ์—์„œ ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•ด์•ผ ํ•˜๋Š” ์œ„์น˜(required position)์— ๋‘˜ ์ˆ˜ ์—†๋‹ค. ํŽธ์ง‘๊ธฐ๊ฐ€ ์Šคํ‚ค๋งˆ ์ œ์•ฝ์„ ๊ฐ•์ œํ•˜๋ ค๋ฉด, ์–ธ์ œ๋“ ์ง€ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ฑ„์šด ๋นˆ ๋…ธ๋“œ๋ฅผ ๋งŒ๋“ค์–ด ๋„ฃ์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
์กฐ๊ธˆ ๋” ์„ค๋ช…์„ ๋ถ€์—ฐํ•˜์ž๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์€ ์Šคํ‚ค๋งˆ๊ฐ€ ์žˆ๋‹ค๊ณ  ํ•˜์ž.
doc: { content: "heading paragraph+" }
JavaScript
๋ณต์‚ฌ
์ด๋Ÿฐ ์Šคํ‚ค๋งˆ๊ฐ€ ์žˆ์„ ๋•Œ, ๋ฌธ์„œ๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“ค๋ฉด ProseMirror๋Š” ์ž๋™์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฑ„์šฐ๋ ค๊ณ  ํ•œ๋‹ค.
doc( heading(...?) // ํ•„์ˆ˜๋‹ˆ๊นŒ ๋ญ”๊ฐ€ ๋„ฃ์–ด์ค˜์•ผ ํ•จ paragraph() // ์ตœ์†Œ ํ•˜๋‚˜ ํ•„์š” )
JavaScript
๋ณต์‚ฌ
๊ทธ๋Ÿฐ๋ฐ ํ—ค๋”ฉ ๋…ธ๋“œ๊ฐ€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์˜๋˜์–ด์žˆ๋‹ค๋ฉด ProseMirror๊ฐ€ heading์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๋ ค๊ณ  ํ•  ๋•Œ, level ๊ฐ’์ด ํ•„์ˆ˜์ธ๋ฐ ๊ธฐ๋ณธ๊ฐ’๋„ ์—†๊ณ  ProseMirror๋Š” ์ž๋™์œผ๋กœ level ๊ฐ’์„ ์ถ”์ธกํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ์—๋Ÿฌ๊ฐ€ ๋‚œ๋‹ค.
heading: { attrs: { level: {} // ๊ธฐ๋ณธ๊ฐ’์ด ์—†์Œ }, content: "text*" }
JavaScript
๋ณต์‚ฌ
์ด๋Ÿฐ ๋…ธ๋“œ๋Š” ์œ„์—์„œ ๋งํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ๊ธฐ๋ณธ ๊ฐ’์„ ์ง€์ •ํ•ด์ค˜์•ผ ํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด heading ๊ฐ™์€ ๋…ธ๋“œ๋ฅผ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•œ ์œ„์น˜(์˜ˆ: doc์˜ ์ฒซ ์ž์‹ ๋…ธ๋“œ) ์— ๋‘˜ ์ˆ˜ ์—†๋‹ค.
ํ•˜์ง€๋งŒ prosemirror-model ๋ชจ๋“ˆ์—์„œ Attrs ์— ๋Œ€ํ•œ ํƒ€์ž… ์ •์˜๋ฅผ ๋ณด๋ฉด ์ด๋Ÿฌํ•œ default์— ๋Œ€ํ•œ ๊ฐ•์ œ๋Š” ๋˜์–ด์žˆ์ง€ ์•Š๋‹ค.
type Attrs = { readonly [attr: string]: any; };
TypeScript
๋ณต์‚ฌ
๋•Œ๋ฌธ์— tiptap๊ณผ ๊ฐ™์ด default ํ‚ค๋ฅผ ํฌํ•จํ•œ ํƒ€์ž…์„ ๊ฐ•์ œํ•˜๊ฑฐ๋‚˜ tiptap์˜ Attribute ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.
export declare type Attribute = { default: any; rendered?: boolean; renderHTML?: ((attributes: Record<string, any>) => Record<string, any> | null) | null; parseHTML?: ((element: HTMLElement) => any | null) | null; keepOnSplit: boolean; isRequired?: boolean; };
TypeScript
๋ณต์‚ฌ

Serialization and Parsing

๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฌธ์„œ๋ฅผ ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ์œผ๋ ค๋ฉด, ๋ฌธ์„œ์˜ ๊ฐ ๋…ธ๋“œ๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์˜ DOM ๊ตฌ์กฐ๋กœ ํ‘œํ˜„ ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค.
์ด๋ฅผ ๊ฐ€์žฅ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์€, ๊ฐ ๋…ธ๋“œ์˜ DOM ํ‘œํ˜„ ๋ฐฉ์‹์„ ์Šคํ‚ค๋งˆ ์•ˆ์— ์žˆ๋Š” toDOM ํ•„๋“œ์— ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
toDOM ํ•„๋“œ๋Š” ํ•จ์ˆ˜ ํ˜•ํƒœ๋กœ ์ •์˜๋˜๋ฉฐ, ์ด ํ•จ์ˆ˜๋Š” ๋…ธ๋“œ๋ฅผ ์ธ์ž๋กœ ๋ฐ›์•„, ๊ทธ ๋…ธ๋“œ๋ฅผ ์–ด๋–ป๊ฒŒ DOM์œผ๋กœ ๋‚˜ํƒ€๋‚ผ์ง€๋ฅผ ์„ค๋ช…ํ•˜๋Š” ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•œ๋‹ค. ์ด ๋ฐ˜ํ™˜๊ฐ’์€ ์‹ค์ œ DOM ๋…ธ๋“œ์ผ ์ˆ˜๋„ ์žˆ๊ณ , DOMOutputSpec ํ˜•ํƒœ์˜ ๋ฐฐ์—ด ํ˜•์‹์œผ๋กœ DOM ๊ตฌ์กฐ๋ฅผ ๋ฌ˜์‚ฌํ•˜๋Š” ๊ฐ’์ผ ์ˆ˜๋„ ์žˆ๋‹ค.
์˜ˆ์‹œ
const schema = new Schema({ nodes: { doc: {content: "paragraph+"}, paragraph: { content: "text*", toDOM(node) { return ["p", 0] } }, text: {} } })
JavaScript
๋ณต์‚ฌ
["p", 0]๋ผ๋Š” ํ‘œํ˜„์€ ํ•ด๋‹น ๋…ธ๋“œ๋ฅผ HTML์˜ <p> ํƒœ๊ทธ๋กœ ๋ Œ๋”๋งํ•˜๊ฒ ๋‹ค๋Š” ์˜๋ฏธ๋‹ค. ์—ฌ๊ธฐ์„œ 0์€ ๋…ธ๋“œ์˜ ์ฝ˜ํ…์ธ ๊ฐ€ ์‚ฝ์ž…๋  ์ž๋ฆฌ(hole)๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค. ๋งŒ์•ฝ HTML ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ํƒœ๊ทธ ์ด๋ฆ„ ๋’ค์— ๊ฐ์ฒด ํ˜•ํƒœ๋กœ ๋„ฃ์„ ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ["div", { class: "c" }, 0]์€ <div class="c">๋‚ด์šฉ</div>์„ ๋‚˜ํƒ€๋‚ธ๋‹ค. Leaf ๋…ธ๋“œ(์ž์‹์ด ์—†๋Š” ๋…ธ๋“œ)๋Š” ์ฝ˜ํ…์ธ ๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์—, DOM ๊ตฌ์กฐ ์•ˆ์— ๋ณ„๋„์˜ โ€˜holeโ€™์„ ์ง€์ •ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.
๋งˆํฌ(Mark) ์ŠคํŽ™์—๋„ ๋น„์Šทํ•˜๊ฒŒ toDOM ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ๋งˆํฌ๋Š” ํ•ญ์ƒ ์ฝ˜ํ…์ธ ๋ฅผ ํ•˜๋‚˜์˜ ํƒœ๊ทธ๋กœ ๊ฐ์‹ธ๋Š” ํ˜•ํƒœ๋กœ ๋ Œ๋”๋งํ•ด์•ผ ํ•˜๋ฏ€๋กœ, ๋ฐ˜ํ™˜๋˜๋Š” ๋…ธ๋“œ ์•ˆ์— ์ฝ˜ํ…์ธ ๊ฐ€ ๋ฐ”๋กœ ๋“ค์–ด๊ฐ€๋ฉฐ, ๋ณ„๋„๋กœ 0์œผ๋กœ hole์„ ์ง€์ •ํ•  ํ•„์š”๋Š” ์—†๋‹ค.
๋˜ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๋ฌด์–ธ๊ฐ€๋ฅผ ๋ถ™์—ฌ๋„ฃ๊ฑฐ๋‚˜ ๋“œ๋ž˜๊ทธํ•ด์„œ ์—๋””ํ„ฐ์— ๋„ฃ์„ ๊ฒฝ์šฐ, DOM์œผ๋กœ๋ถ€ํ„ฐ ๋ฌธ์„œ๋ฅผ ํŒŒ์‹ฑํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด ์ž์ฃผ ๋ฐœ์ƒํ•œ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ProseMirror์˜ model ๋ชจ๋“ˆ์—๋Š” DOM ํŒŒ์‹ฑ ๊ธฐ๋Šฅ๋„ ํ•จ๊ป˜ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉฐ, ์Šคํ‚ค๋งˆ ์•ˆ์— parseDOM ์†์„ฑ์„ ์‚ฌ์šฉํ•ด DOM์—์„œ ๋ฌธ์„œ๋ฅผ ์–ด๋–ป๊ฒŒ ์ฝ์–ด์˜ฌ์ง€ ์ •์˜ํ•˜๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋œ๋‹ค.
parseDOM ํ•„๋“œ๋Š” ํ•ด๋‹น ๋…ธ๋“œ๋‚˜ ๋งˆํฌ๊ฐ€ ์–ด๋–ค DOM ๊ตฌ์กฐ๋กœ๋ถ€ํ„ฐ ํŒŒ์‹ฑ๋  ์ˆ˜ ์žˆ๋Š”์ง€๋ฅผ ์„ค๋ช…ํ•˜๋Š” ํŒŒ์‹ฑ ๊ทœ์น™(parse rules)์˜ ๋ฐฐ์—ด์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๊ธฐ๋ณธ ์Šคํ‚ค๋งˆ์—์„œ๋Š” emphasis ๋งˆํฌ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ •์˜ํ•œ๋‹ค.
parseDOM: [ {tag: "em"}, // Match <em> nodes {tag: "i"}, // and <i> nodes {style: "font-style=italic"} // and inline 'font-style: italic' ]
JavaScript
๋ณต์‚ฌ
์ด์ฒ˜๋Ÿผ tag ๊ฐ’์—๋Š” CSS ์„ ํƒ์ž(CSS selector) ๋ฅผ ์“ธ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ์˜ˆ๋ฅผ ๋“ค์–ด "div.myclass" ๊ฐ™์€ ํŠน์ • ํด๋ž˜์Šค๋ฅผ ๊ฐ€์ง„ ์š”์†Œ๋ฅผ ์ง€์ •ํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค. ๋˜ํ•œ style์„ ์‚ฌ์šฉํ•˜๋ฉด ์ธ๋ผ์ธ CSS ์Šคํƒ€์ผ ๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ๋„ ๋งˆํฌ๋ฅผ ์ธ์‹์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.
์Šคํ‚ค๋งˆ์— parseDOM ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๋ฉด, DOMParser.fromSchema ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ์Šคํ‚ค๋งˆ์— ๋งž๋Š” DOMParser ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
ProseMirror ์—๋””ํ„ฐ๋Š” ์ด ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•ด ๊ธฐ๋ณธ ํด๋ฆฝ๋ณด๋“œ ํŒŒ์„œ๋ฅผ ๋งŒ๋“ ๋‹ค. ๋ฌผ๋ก  ํ•„์š”ํ•˜๋‹ค๋ฉด clipboardParser ํ”„๋กœํผํ‹ฐ๋ฅผ ํ†ตํ•ด ์ด ํŒŒ์„œ๋ฅผ ์ง์ ‘ ์˜ค๋ฒ„๋ผ์ด๋“œํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
ProseMirror ๋ฌธ์„œ๋Š” ๋˜ํ•œ ๋‚ด์žฅ๋œ JSON ์ง๋ ฌํ™” ํฌ๋งท์„ ์ง€์›ํ•œ๋‹ค. ๋ฌธ์„œ ๋…ธ๋“œ์— .toJSON()์„ ํ˜ธ์ถœํ•˜๋ฉด, JSON.stringify๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ๋ฌธ์ž์—ดํ™”ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค. ๋ฐ˜๋Œ€๋กœ ์Šคํ‚ค๋งˆ ๊ฐ์ฒด์˜ nodeFromJSON() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ์ด JSON ํ‘œํ˜„์„ ๋‹ค์‹œ ProseMirror ๋ฌธ์„œ ๊ฐ์ฒด๋กœ ๋˜๋Œ๋ฆด ์ˆ˜ ์žˆ๋‹ค.

Extending a schema

Schema ์ƒ์„ฑ์ž์— ์ „๋‹ฌ๋˜๋Š” nodes์™€ marks ์˜ต์…˜์€ ์ผ๋ฐ˜์ ์ธ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๊ฐ์ฒด๋ฟ ์•„๋‹ˆ๋ผ OrderedMap ๊ฐ์ฒด๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•œ ๋ฒˆ ์Šคํ‚ค๋งˆ๊ฐ€ ์ƒ์„ฑ๋˜๋ฉด, schema.spec.nodes์™€ schema.spec.marks๋Š” ํ•ญ์ƒ OrderedMap ํ˜•ํƒœ๋กœ ์ œ๊ณต๋˜๋ฉฐ, ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ค๋ฅธ ์Šคํ‚ค๋งˆ๋ฅผ ํ™•์žฅํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•˜๋Š” ๋ฐ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
OrderedMap์€ ๋…ธ๋“œ ์„ธํŠธ๋ฅผ ์œ ์—ฐํ•˜๊ฒŒ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค์–‘ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, schema.spec.nodes.remove("blockquote")์ฒ˜๋Ÿผ ์ž‘์„ฑํ•˜๋ฉด, ๊ธฐ์กด ์Šคํ‚ค๋งˆ์—์„œ blockquote ๋…ธ๋“œ๋ฅผ ์ œ๊ฑฐํ•œ ์ƒˆ๋กœ์šด ๋…ธ๋“œ ์„ธํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ•œ ๊ฒฐ๊ณผ๋Š” ๋‹ค์‹œ ์ƒˆ๋กœ์šด ์Šคํ‚ค๋งˆ๋ฅผ ๋งŒ๋“ค ๋•Œ nodes ์˜ต์…˜์œผ๋กœ ๋„˜๊ฒจ์ค„ ์ˆ˜ ์žˆ๋‹ค.
์ถ”๊ฐ€๋กœ, schema-list ๋ชจ๋“ˆ์€ ๋ฆฌ์ŠคํŠธ ๊ด€๋ จ ๋…ธ๋“œ๋“ค์„ ๊ธฐ์กด ๋…ธ๋“œ์…‹์— ์‰ฝ๊ฒŒ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” addListNodes ๋ฉ”์„œ๋“œ๋ฅผ ํ•จ๊ป˜ ์ œ๊ณตํ•œ๋‹ค. ์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ ProseMirror ์Šคํ‚ค๋งˆ๋Š” ์ ์ง„์  ํ™•์žฅ๊ณผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
์ฆ‰, ProseMirror์—์„œ ๊ธฐ๋ณธ ์Šคํ‚ค๋งˆ๊ฐ€ ์žˆ๊ณ , ์—ฌ๊ธฐ์— "ํŠน์ • ๋…ธ๋“œ๋ฅผ ๋นผ๊ฑฐ๋‚˜ ์ถ”๊ฐ€ํ•ด์„œ" ์ƒˆ๋กœ์šด ์Šคํ‚ค๋งˆ๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ์„ ๋•Œ, ์˜ˆ๋ฅผ ๋“ค์–ดโ€ฆ
โ€ข
blockquote ๋…ธ๋“œ๋Š” ๋นผ๊ณ  ์‹ถ๊ณ 
โ€ข
bullet_list, ordered_list ๊ฐ™์€ ๋ฆฌ์ŠคํŠธ ๊ด€๋ จ ๋…ธ๋“œ๋Š” ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ๋‹ค
์ด๋Ÿด ๋•Œ ๊ธฐ์กด ์Šคํ‚ค๋งˆ์˜ nodes๋ฅผ ๊ทธ๋ƒฅ ๊ฐ์ฒด์ฒ˜๋Ÿผ ๋‹ค๋ฃจ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, OrderedMap์ด๋ผ๋Š” ํŠน์ˆ˜ํ•œ ์ž๋ฃŒ๊ตฌ์กฐ๋ฅผ ์ด์šฉํ•ด์„œ ์œ ์—ฐํ•˜๊ฒŒ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
์˜ˆ์‹œ
import { Schema } from 'prosemirror-model' import { schema as basicSchema } from 'prosemirror-schema-basic' import { addListNodes } from 'prosemirror-schema-list' // 1. ๊ธฐ๋ณธ ์Šคํ‚ค๋งˆ์˜ nodes๋Š” OrderedMap ํ˜•ํƒœ๋กœ ๋˜์–ด ์žˆ์Œ const originalNodes = basicSchema.spec.nodes // 2. blockquote ๋…ธ๋“œ๋ฅผ ์ œ๊ฑฐ const nodesWithoutBlockquote = originalNodes.remove('blockquote') // 3. ๋ฆฌ์ŠคํŠธ ๋…ธ๋“œ๋ฅผ ์ถ”๊ฐ€ (bullet_list, ordered_list ๋“ฑ) const extendedNodes = addListNodes( nodesWithoutBlockquote, 'paragraph block*', 'block' ) // 4. ์ƒˆ๋กœ์šด ์Šคํ‚ค๋งˆ ์ƒ์„ฑ const mySchema = new Schema({ nodes: extendedNodes, marks: basicSchema.spec.marks, // ๋งˆํฌ๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ })
TypeScript
๋ณต์‚ฌ

2. Editor State

์—๋””ํ„ฐ์˜ ์ƒํƒœ(state)๋Š” ์–ด๋–ค ์š”์†Œ๋“ค๋กœ ๊ตฌ์„ฑ๋ ๊นŒ? ๊ฐ€์žฅ ๊ธฐ๋ณธ์ด ๋˜๋Š” ๊ฒƒ์€ ๋‹น์—ฐํžˆ ๋ฌธ์„œ(document) ์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ปค์„œ๋ฅผ ์–ด๋””์— ๋‘๊ณ  ์žˆ๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์„ ํƒ(selection) ์ •๋ณด๋„ ํ•„์š”ํ•˜๋‹ค.
๋˜ ํ•˜๋‚˜ ์ค‘์š”ํ•œ ๊ฒƒ์€, ์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž๊ฐ€ ์•„์ง ์ž…๋ ฅ์„ ์‹œ์ž‘ํ•˜์ง„ ์•Š์•˜์ง€๋งŒ bold ๊ฐ™์€ ๋งˆํฌ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๊ฑฐ๋‚˜ ๋น„ํ™œ์„ฑํ™”ํ•œ ๊ฒฝ์šฐ์ฒ˜๋Ÿผ, ํ˜„์žฌ ์ ์šฉํ•  ์˜ˆ์ •์ธ ๋งˆํฌ ์ƒํƒœ(storedMarks) ๋„ ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค.
ProseMirror์˜ ์—๋””ํ„ฐ ์ƒํƒœ๋Š” ์ด ์„ธ ๊ฐ€์ง€๋ฅผ ๊ธฐ๋ณธ ๊ตฌ์„ฑ ์š”์†Œ๋กœ ๊ฐ–๋Š”๋‹ค. ์ด๋“ค์€ ๊ฐ๊ฐ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ”„๋กœํผํ‹ฐ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.
โ€ข
doc: ํ˜„์žฌ ๋ฌธ์„œ
โ€ข
selection: ํ˜„์žฌ ์ปค์„œ ๋˜๋Š” ์„ ํƒ ์˜์—ญ
โ€ข
storedMarks: ์•„์ง ์ž…๋ ฅ์€ ์•ˆ ํ–ˆ์ง€๋งŒ ์ ์šฉํ•  ๋งˆํฌ ์ƒํƒœ
์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๋กœ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
import {schema} from "prosemirror-schema-basic" import {EditorState} from "prosemirror-state" let state = EditorState.create({schema}) console.log(state.doc.toString()) // An empty paragraph console.log(state.selection.from) // 1, the start of the paragraph
JavaScript
๋ณต์‚ฌ
์ด ์˜ˆ์‹œ์—์„œ ์—๋””ํ„ฐ์˜ ํ˜„์žฌ ์ƒํƒœ๋Š” ์•„์ง ๋น„์–ด์žˆ๋Š” paragraph์˜ ์ƒํƒœ์ด๊ณ , selection์„ ์กฐํšŒํ•˜๋ฉด ํ˜„์žฌ ์ปค์„œ์˜ ์œ„์น˜๋Š” ๋ฌธ๋‹จ์˜ ๋งจ ์ฒซ๋ฒˆ์งธ์ด๋ฏ€๋กœ 1์ด ์ถœ๋ ฅ๋œ๋‹ค.
ํ•˜์ง€๋งŒ ์—๋””ํ„ฐ์˜ ์ƒํƒœ๋Š” ์ด ๊ธฐ๋ณธ ์š”์†Œ๋“ค๋งŒ์œผ๋กœ ๋๋‚˜์ง€ ์•Š๋Š”๋‹ค.์˜ˆ๋ฅผ ๋“ค์–ด undo/redo ๊ธฐ๋Šฅ์€ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ์ €์žฅํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ด๋Ÿฐ ๊ธฐ๋Šฅ๋“ค์€ ํ”Œ๋Ÿฌ๊ทธ์ธ ํ˜•ํƒœ๋กœ ์ถ”๊ฐ€์ ์ธ ์ƒํƒœ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ProseMirror๋Š” ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ง‘ํ•ฉ๋„ state์— ํฌํ•จ๋˜๋ฉฐ, ๊ฐ ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์ž์‹ ๋งŒ์˜ ์ƒํƒœ ์Šฌ๋กฏ์„ ์ •์˜ํ•˜์—ฌ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

Selection

ProseMirror๋Š” ์—ฌ๋Ÿฌ ์ข…๋ฅ˜์˜ ์„ ํƒ(selection) ์„ ์ง€์›ํ•˜๋ฉฐ, ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ ์‚ฌ์šฉ์ž ์ฝ”๋“œ์—์„œ ์ปค์Šคํ…€ selection ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค. ์„ ํƒ์€ Selection ํด๋ž˜์Šค(๋˜๋Š” ๊ทธ ์„œ๋ธŒํด๋ž˜์Šค)์˜ ์ธ์Šคํ„ด์Šค๋กœ ํ‘œํ˜„๋œ๋‹ค. ๋ฌธ์„œ๋‚˜ ๊ธฐํƒ€ ์ƒํƒœ ๊ด€๋ จ ๊ฐ’๋“ค๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, selection ๊ฐ์ฒด๋„ ๋ถˆ๋ณ€(immutable) ์ด๋‹ค.
์ฆ‰, ์„ ํƒ์„ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด ๊ธฐ์กด ๊ฐ์ฒด๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์ƒˆ๋กœ์šด selection ๊ฐ์ฒด์™€ ์ด๋ฅผ ๋‹ด์„ ์ƒˆ๋กœ์šด state๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค. ๋ชจ๋“  selection์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹ค์Œ ์ •๋ณด๋ฅผ ๊ฐ€์ง„๋‹ค.
โ€ข
from: ์„ ํƒ์˜ ์‹œ์ž‘ ์œ„์น˜
โ€ข
to: ์„ ํƒ์˜ ๋ ์œ„์น˜
์ด ๋‘ ๊ฐ’์€ ํ˜„์žฌ ๋ฌธ์„œ ์•ˆ์˜ ๊ตฌ์ฒด์ ์ธ ์œ„์น˜(position) ๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค. ๋˜ํ•œ ๋งŽ์€ selection ํƒ€์ž…์—์„œ๋Š” anchor์™€ head๋ผ๋Š” ๊ฐœ๋…๋„ ์กด์žฌํ•œ๋‹ค.
โ€ข
anchor: ์ปค์„œ๊ฐ€ ์‹œ์ž‘๋œ ๊ณณ
โ€ข
head: ํ˜„์žฌ ์ปค์„œ๊ฐ€ ๋„์ฐฉํ•œ ์ง€์ 
์˜ˆ๋ฅผ ๋“ค์–ด, Hello, world! ๋ฌธ์„œ์—์„œ "Hello"์˜ H๋Š” 1๋ฒˆ ์œ„์น˜, !์€ 13๋ฒˆ ์œ„์น˜๋ผ๊ณ  ๊ฐ€์ •ํ•œ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋งˆ์šฐ์Šค๋กœ "Hello"์˜ H์—์„œ ์‹œ์ž‘ํ•ด์„œ o๊นŒ์ง€ ๋“œ๋ž˜๊ทธํ•˜๋ฉด anchor๋Š” H ์œ„์น˜ (1๋ฒˆ)๊ฐ€ ๋˜๊ณ , head: o ์œ„์น˜ (6๋ฒˆ)๊ฐ€ ๋œ๋‹ค.
TextSelection์˜ from๊ณผ to๋Š” ํ•ญ์ƒ ์ธ๋ผ์ธ ์ฝ˜ํ…์ธ ๋ฅผ ํ—ˆ์šฉํ•˜๋Š” ๋…ธ๋“œ ๋‚ด๋ถ€๋ฅผ ๊ฐ€๋ฆฌ์ผœ์•ผ ํ•œ๋‹ค. ProseMirror ์ฝ”์–ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” NodeSelection๋„ ์ง€์›ํ•œ๋‹ค. ์ด ํƒ€์ž…์€ ๋ฌธ์„œ์—์„œ ํ•˜๋‚˜์˜ ๋…ธ๋“œ ์ „์ฒด๊ฐ€ ์„ ํƒ๋  ๋•Œ ์‚ฌ์šฉ๋œ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž๊ฐ€ ctrl ๋˜๋Š” cmd ํ‚ค๋ฅผ ๋ˆ„๋ฅธ ์ฑ„ ๋…ธ๋“œ๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ด์— ํ•ด๋‹นํ•œ๋‹ค(Mac์—์„œ๋Š” ctrl). NodeSelection์€ ์„ ํƒ๋œ ๋…ธ๋“œ์˜ ์ง์ „ ์œ„์น˜๋ถ€ํ„ฐ ์งํ›„ ์œ„์น˜๊นŒ์ง€๋ฅผ ๋ฒ”์œ„๋กœ ์‚ผ๋Š”๋‹ค. ์ฆ‰, ํŠน์ • ๋…ธ๋“œ ์ž์ฒด๊ฐ€ ํ†ต์งธ๋กœ ์„ ํƒ๋œ ์ƒํƒœ๊ฐ€ ๋œ๋‹ค.
๋ฐ˜๋Œ€๋กœ, ์ด๋ฒˆ์—” ์‚ฌ์šฉ์ž๊ฐ€ "Hello"์˜ o์—์„œ ์‹œ์ž‘ํ•ด์„œ H๊นŒ์ง€ ๋“œ๋ž˜๊ทธํ•˜๋ฉด ์…€๋ ‰์…˜ ๊ฐ’์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.
selection = { anchor: 6, head: 1, from: 1, to: 6 }
TypeScript
๋ณต์‚ฌ
from, to๋Š” ํ•ญ์ƒ ์ž‘์€ ๊ฐ’๋ถ€ํ„ฐ ํฐ ๊ฐ’๊นŒ์ง€ ์ •๋ ฌ๋˜์ง€๋งŒ, anchor, head๋Š” ์‚ฌ์šฉ์ž์˜ ์‹ค์ œ ๋“œ๋ž˜๊ทธ ๋ฐฉํ–ฅ์„ ๋ณด์กดํ•œ๋‹ค.
๋”ฐ๋ผ์„œ ๋ชจ๋“  selection ๊ฐ์ฒด์—๋Š” from, to๋ฟ๋งŒ ์•„๋‹ˆ๋ผ anchor, head๋„ ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•ด์•ผ ํ•œ๋‹ค. ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” selection ํƒ€์ž…์€ TextSelection์ด๋‹ค. ์ด ํƒ€์ž…์€ ๋‹ค์Œ ๋‘ ๊ฐ€์ง€ ๊ฒฝ์šฐ์— ์‚ฌ์šฉ๋œ๋‹ค.
โ€ข
์ปค์„œ๊ฐ€ ๊นœ๋นก์ด๋Š” ์œ„์น˜ (์ฆ‰, anchor์™€ head๊ฐ€ ๊ฐ™์„ ๋•Œ)
โ€ข
ํ…์ŠคํŠธ ์ผ๋ถ€๊ฐ€ ๋“œ๋ž˜๊ทธ๋กœ ์„ ํƒ๋˜์—ˆ์„ ๋•Œ

Transactions

์ผ๋ฐ˜์ ์ธ ํŽธ์ง‘ ์ƒํ™ฉ์—์„œ๋Š” ProseMirror์˜ ์ƒํƒœ(state)๋Š” ์ด์ „ ์ƒํƒœ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ ์ง„์ ์œผ๋กœ ๊ฐฑ์‹ ๋œ๋‹ค. ํŠน๋ณ„ํ•œ ๊ฒฝ์šฐ(์˜ˆ: ์ƒˆ๋กœ์šด ๋ฌธ์„œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ)๋ฅผ ์ œ์™ธํ•˜๋ฉด, ๊ธฐ์กด ์ƒํƒœ์—์„œ ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ํŒŒ์ƒ์‹œํ‚ค๋Š” ๋ฐฉ์‹์ด ๊ธฐ๋ณธ ํ๋ฆ„์ด๋‹ค.
์ƒํƒœ ์—…๋ฐ์ดํŠธ๋Š” transaction์„ ๊ธฐ์กด ์ƒํƒœ์— applyํ•˜์—ฌ ์ด๋ฃจ์–ด์ง„๋‹ค. ์ด ๊ณผ์ •์„ ํ†ตํ•ด ์ƒˆ๋กœ์šด ์ƒํƒœ๊ฐ€ ์ƒ์„ฑ๋˜๋ฉฐ, ๊ฐœ๋…์ ์œผ๋กœ๋Š” ํ•œ ๋ฒˆ์— "์“ฑ" ์ผ์–ด๋‚˜๋Š” ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ๋œ๋‹ค.
์ฆ‰, ์ด์ „ ์ƒํƒœ์™€ ํŠธ๋žœ์žญ์…˜์ด ์ฃผ์–ด์ง€๋ฉด, ์ƒํƒœ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ฐ ์š”์†Œ(doc, selection, storedMarks ๋“ฑ)๊ฐ€ ์ƒˆ๋กญ๊ฒŒ ๊ณ„์‚ฐ๋˜๊ณ , ์ด ๊ฐ’๋“ค์„ ๋ชจ์•„ ์ƒˆ๋กœ์šด ์ƒํƒœ ๊ฐ์ฒด๊ฐ€ ๋งŒ๋“ค์–ด์ง„๋‹ค.
let tr = state.tr console.log(tr.doc.content.size) // 25 tr.insertText("hello") // Replaces selection with 'hello' let newState = state.apply(tr) console.log(tr.doc.content.size) // 30
JavaScript
๋ณต์‚ฌ
Transaction์€ Transform์˜ ์„œ๋ธŒํด๋ž˜์Šค๋กœ, ์—ฌ๋Ÿฌ ๊ฐœ์˜ step์„ ๋ฌธ์„œ์— ์ฐจ๋ก€๋กœ ์ ์šฉํ•ด ์ƒˆ ๋ฌธ์„œ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ƒ์†๋ฐ›๋Š”๋‹ค. ์—ฌ๊ธฐ์— ๋”ํ•ด, ํŠธ๋žœ์žญ์…˜์€ ํ˜„์žฌ ์„ ํƒ(selection) ์ƒํƒœ ๋“ฑ ์ƒํƒœ ๊ด€๋ จ ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ์ถ”์ ํ•˜๋ฉฐ, replaceSelection ๊ฐ™์€ selection ์กฐ์ž‘์— ํŠนํ™”๋œ ํŽธ์˜ ๋ฉ”์„œ๋“œ๋„ ํ•จ๊ป˜ ์ œ๊ณตํ•œ๋‹ค.
ํŠธ๋žœ์žญ์…˜์„ ๋งŒ๋“œ๋Š” ๊ฐ€์žฅ ์‰ฌ์šด ๋ฐฉ๋ฒ•์€ EditorState ๊ฐ์ฒด์—์„œ ์ œ๊ณตํ•˜๋Š” tr getter๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋Š” ๋น„์–ด ์žˆ๋Š” ์ƒˆ ํŠธ๋žœ์žญ์…˜์ด ๋งŒ๋“ค์–ด์ง€๊ณ , ์—ฌ๊ธฐ์— step์ด๋‚˜ ๊ธฐํƒ€ ์—…๋ฐ์ดํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด๋‚˜๊ฐˆ ์ˆ˜ ์žˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ํŠธ๋žœ์žญ์…˜์ด ์ˆ˜ํ–‰๋˜๋ฉด, ๊ธฐ์กด ์„ ํƒ ์˜์—ญ์€ ๊ฐ step์„ ๊ฑฐ์น˜๋ฉฐ ์ž๋™์œผ๋กœ map๋˜์–ด ์ƒˆ๋กœ์šด ๋ฌธ์„œ ๊ตฌ์กฐ์— ๋งž๊ฒŒ ์œ„์น˜๊ฐ€ ์กฐ์ •๋œ๋‹ค.
ํ•˜์ง€๋งŒ ํ•„์š”ํ•˜๋‹ค๋ฉด setSelection์„ ํ†ตํ•ด ์„ ํƒ ์˜์—ญ์„ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
let tr = state.tr console.log(tr.selection.from) // โ†’ 10 tr.delete(6, 8) console.log(tr.selection.from) // โ†’ 8 (moved back) tr.setSelection(TextSelection.create(tr.doc, 3)) console.log(tr.selection.from) // โ†’ 3
JavaScript
๋ณต์‚ฌ
๋น„์Šทํ•˜๊ฒŒ, ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ๋งˆํฌ ์ง‘ํ•ฉ(storedMarks)๋„ ๋ฌธ์„œ๋‚˜ ์„ ํƒ ์˜์—ญ์ด ๋ฐ”๋€Œ๋ฉด ์ž๋™์œผ๋กœ ์ดˆ๊ธฐํ™”๋œ๋‹ค. ํ•„์š”ํ•  ๊ฒฝ์šฐ setStoredMarks๋‚˜ ensureMarks๋ฅผ ์‚ฌ์šฉํ•ด ์ ์šฉํ•  ๋งˆํฌ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ์ดํ›„์—๋Š” ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐscrollIntoView๋ฅผ ํ˜ธ์ถœํ•ด ์„ ํƒ ์˜์—ญ์ด ์Šคํฌ๋กค๋ทฐ์— ๋‚˜ํƒ€๋‚˜๋„๋ก ๊ฐ•์ œํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.
๋งˆ์ง€๋ง‰์œผ๋กœ, Transform ๊ณ„์—ด์˜ ๋ฉ”์„œ๋“œ๋“ค๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, ๋Œ€๋ถ€๋ถ„์˜ Transaction ๋ฉ”์„œ๋“œ๋Š” ์ž๊ธฐ ์ž์‹ (this) ์„ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ, .insertText(...).setSelection(...).scrollIntoView() ์™€ ๊ฐ™์ด ๋ฉ”์„œ๋“œ ์ฒด์ด๋‹์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

Plugins

์ƒˆ๋กœ์šด EditorState๋ฅผ ์ƒ์„ฑํ•  ๋•Œ, ์‚ฌ์šฉํ•  ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ฐฐ์—ด์„ ํ•จ๊ป˜ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์ง€์ •๋œ ํ”Œ๋Ÿฌ๊ทธ์ธ๋“ค์€ ํ•ด๋‹น ์ƒํƒœ ๊ฐ์ฒด์™€, ๊ทธ๋กœ๋ถ€ํ„ฐ ํŒŒ์ƒ๋˜๋Š” ๋ชจ๋“  ์ƒํƒœ์—์„œ ๊ณต์œ ๋˜๋ฉฐ, ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์ด๋‚˜ ์—๋””ํ„ฐ ๋™์ž‘ ์ „๋ฐ˜์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ๋‹ค.
ํ”Œ๋Ÿฌ๊ทธ์ธ์€ Plugin ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค์ด๋ฉฐ, ๊ฐ„๋‹จํ•œ ์ด๋ฒคํŠธ ์‘๋‹ต๋ถ€ํ„ฐ ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ๊นŒ์ง€ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ๋ชจ๋ธ๋งํ•  ์ˆ˜ ์žˆ๋‹ค.
๊ฐ€์žฅ ๋‹จ์ˆœํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ์€ EditorView์— ํŠน์ • props๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉ๋˜๋Š”๋ฐ, ์˜ˆ๋ฅผ ๋“ค์–ด ํ‚ค๋ณด๋“œ ์ž…๋ ฅ ์ด๋ฒคํŠธ์— ๋ฐ˜์‘ํ•˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ๋‹ค์Œ์ฒ˜๋Ÿผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
let myPlugin = new Plugin({ props: { handleKeyDown(view, event) { console.log("A key was pressed!") return false // We did not handle this } } }) let state = EditorState.create({schema, plugins: [myPlugin]})
JavaScript
๋ณต์‚ฌ
ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ์ž์‹ ๋งŒ์˜ ์ƒํƒœ ๊ฐ’์„ ์ €์žฅํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, state ์†์„ฑ์„ ์‚ฌ์šฉํ•ด ์ดˆ๊ธฐ๊ฐ’๊ณผ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ๋‹จ์ˆœํžˆ ์ง€๊ธˆ๊นŒ์ง€ ์ ์šฉ๋œ ํŠธ๋žœ์žญ์…˜์˜ ์ˆ˜๋ฅผ ์นด์šดํŒ…ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ฐ€์ง„๋‹ค. ํ˜„์žฌ ์นด์šดํŠธ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์‹ถ์„ ๋•Œ๋Š” getState() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.
let transactionCounter = new Plugin({ state: { init() { return 0 }, // ์ดˆ๊ธฐ๊ฐ’ 0 apply(tr, value) { return value + 1 } // ํŠธ๋žœ์žญ์…˜ ๋ฐœ์ƒ ์‹œ ์นด์šดํ„ฐ ์ฆ๊ฐ€ } }) function getTransactionCount(state) { return transactionCounter.getState(state) }
JavaScript
๋ณต์‚ฌ
ProseMirror์˜ ์ƒํƒœ ๊ฐ์ฒด๋Š” ๋ถˆ๋ณ€(immutable) ์ด๊ธฐ ๋•Œ๋ฌธ์—, ํ”Œ๋Ÿฌ๊ทธ์ธ ์ƒํƒœ๋„ ์—ญ์‹œ ๋ถˆ๋ณ€ ๊ฐ์ฒด์—ฌ์•ผ ํ•œ๋‹ค. ์ฆ‰, ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค๋ฉด ๊ธฐ์กด ๊ฐ’์„ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ƒˆ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜๋ฉฐ, ๋‹ค๋ฅธ ์ฝ”๋“œ๊ฐ€ ์ด ๊ฐ’์„ ์ง์ ‘ ๋ณ€๊ฒฝํ•ด์„œ๋Š” ์•ˆ ๋œ๋‹ค.

๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์™€ ํŠธ๋žœ์žญ์…˜ ๋งˆํ‚น

ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํ†ตํ•ด ํŠธ๋žœ์žญ์…˜์— ์ถ”๊ฐ€ ์ •๋ณด(metadata)๋ฅผ ๋ถ™์ด๋Š” ๊ฒƒ๋„ ๋งค์šฐ ์œ ์šฉํ•˜๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด undo ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ์˜ ๊ฒฝ์šฐ, ์ผ๋ฐ˜์ ์ธ ๋ณ€๊ฒฝ ๋‚ด์šฉ์€ undo ์Šคํƒ์— ์ถ”๊ฐ€ํ•˜์ง€๋งŒ, undo๊ฐ€ ์‹ค์ œ ์‹คํ–‰๋˜๋Š” ์ˆœ๊ฐ„์—๋Š” ํŠธ๋žœ์žญ์…˜์— ํ‘œ์‹œ๋ฅผ ๋‚จ๊ฒจ์„œ undo ์Šคํƒ์—์„œ ์ œ๊ฑฐํ•˜๊ณ , redo ์Šคํƒ์— ๋„ฃ๋„๋ก ํŠน๋ณ„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.
์ด๋Ÿฐ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ํŠธ๋žœ์žญ์…˜์—๋Š” setMeta์™€ getMeta ๋ฉ”์„œ๋“œ๊ฐ€ ์กด์žฌํ•œ๋‹ค. ์•„๋ž˜๋Š” ํŠน์ • ํŠธ๋žœ์žญ์…˜์— ์นด์šดํŠธ ์ œ์™ธ ํ‘œ์‹œ๋ฅผ ๋ถ™์ด๊ณ , ๊ทธ ํ‘œ์‹œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋Š” ์นด์šดํ„ฐ ์ฆ๊ฐ€๋ฅผ ์ƒ๋žตํ•˜๋Š” ์˜ˆ์‹œ๋‹ค
let transactionCounter = new Plugin({ state: { init() { return 0 }, apply(tr, value) { if (tr.getMeta(transactionCounter)) return value else return value + 1 } } }) function markAsUncounted(tr) { tr.setMeta(transactionCounter, true) }
JavaScript
๋ณต์‚ฌ
์—ฌ๊ธฐ์„œ getMeta()์™€ setMeta()์— ๋„˜๊ฒจ์ง€๋Š” ํ‚ค๋Š” ๋ฌธ์ž์—ด๋„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์ด๋ฆ„ ์ถฉ๋Œ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ฐ์ฒด ์ž์ฒด๋ฅผ ํ‚ค๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋œ๋‹ค. ProseMirror ๋‚ด๋ถ€์—์„œ๋„ ์˜๋ฏธ ์žˆ๋Š” ๋ฌธ์ž์—ด ํ‚ค๊ฐ€ ๋ช‡ ๊ฐ€์ง€ ์กด์žฌํ•˜๋Š”๋ฐ, ์˜ˆ๋ฅผ ๋“ค๋ฉด,
โ€ข
"addToHistory": false undo ํžˆ์Šคํ† ๋ฆฌ์— ํฌํ•จ๋˜์ง€ ์•Š๋„๋ก ์ง€์ •
โ€ข
"paste": true ๋ถ™์—ฌ๋„ฃ๊ธฐ ์ด๋ฒคํŠธ๋กœ ์ƒ์„ฑ๋œ ํŠธ๋žœ์žญ์…˜์ž„์„ ๋ช…์‹œ
tr.setMeta('addToHistory', false); tr.setMeta('paste', true);
TypeScript
๋ณต์‚ฌ
์ด์ฒ˜๋Ÿผ ํŠธ๋žœ์žญ์…˜์— ๋ถ€๊ฐ€ ์ •๋ณด๋ฅผ ๋ถ€์ฐฉํ•˜๋ฉด, ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ๋ณด๋‹ค ์ •๊ตํ•˜๊ฒŒ ํŠธ๋žœ์žญ์…˜์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

3. View Component

ProseMirror์˜ EditorView๋Š” ์—๋””ํ„ฐ ์ƒํƒœ(editor state)๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ฃผ๊ณ , ํ•ด๋‹น ์ƒํƒœ์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ์ง‘ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” UI ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. ๋‹ค๋งŒ ProseMirror ์ฝ”์–ด ๋ทฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‹ค๋ฃจ๋Š” โ€œํŽธ์ง‘ ์ž‘์—…(editing actions)โ€์˜ ๋ฒ”์œ„๋Š” ๊ฝค ์ œํ•œ์ ์ด๋‹ค. ์—ฌ๊ธฐ์„œ ๋งํ•˜๋Š” ํŽธ์ง‘ ์ž‘์—…์ด๋ž€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ง์ ‘์ ์ธ ์ƒํ˜ธ์ž‘์šฉ๋งŒ์„ ์˜๋ฏธํ•œ๋‹ค.
โ€ข
ํ‚ค๋ณด๋“œ ์ž…๋ ฅ
โ€ข
๋งˆ์šฐ์Šค ํด๋ฆญ
โ€ข
๋ณต์‚ฌ/๋ถ™์—ฌ๋„ฃ๊ธฐ
โ€ข
๋“œ๋ž˜๊ทธ
์ฆ‰, ๋ฉ”๋‰ด๋ฅผ ๋„์šฐ๋Š” UI๋‚˜, ๋ชจ๋“  ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ ๋“ฑ์€ ์ฝ”์–ด ๋ทฐ ์ปดํฌ๋„ŒํŠธ์˜ ์ฑ…์ž„์— ํฌํ•จ๋˜์ง€ ์•Š์œผ๋ฉฐ, ์ด๋Ÿฐ ๊ธฐ๋Šฅ๋“ค์€ ํ”Œ๋Ÿฌ๊ทธ์ธ(plugin) ์„ ํ†ตํ•ด ๋”ฐ๋กœ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.

Editable DOM

๋ธŒ๋ผ์šฐ์ €๋Š” HTML ์š”์†Œ์— contentEditable ์†์„ฑ์„ ์ง€์ •ํ•จ์œผ๋กœ์จ ํ•ด๋‹น ์š”์†Œ๋ฅผ ํŽธ์ง‘ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค. ์ด ์†์„ฑ์ด ํ™œ์„ฑํ™”๋˜๋ฉด, ํ•ด๋‹น ์š”์†Œ๋ฅผ ํฌ์ปค์Šคํ•  ์ˆ˜ ์žˆ๊ณ , ์„ ํƒ(selection)ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์‚ฌ์šฉ์ž๊ฐ€ ํ‚ค๋ณด๋“œ๋กœ ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค.
ProseMirror์˜ EditorView๋Š” ์Šคํ‚ค๋งˆ์— ์ •์˜๋œ toDOM ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•ด ๋ฌธ์„œ(document)์˜ DOM ํ‘œํ˜„์„ ์ƒ์„ฑํ•˜๊ณ , ํ•ด๋‹น DOM ์š”์†Œ๋ฅผ ํŽธ์ง‘ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ๋งŒ๋“ ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉ์ž๊ฐ€ ํ•ด๋‹น ์š”์†Œ์— ํฌ์ปค์Šค๋ฅผ ์ฃผ๋ฉด, ๋ธŒ๋ผ์šฐ์ €์˜ DOM selection ์ƒํƒœ์™€ ์—๋””ํ„ฐ ๋‚ด๋ถ€ selection ์ƒํƒœ๊ฐ€ ์ผ์น˜ํ•˜๋„๋ก ๋™๊ธฐํ™”ํ•ด์ค€๋‹ค.
๋˜ํ•œ ProseMirror๋Š” ์—ฌ๋Ÿฌ ์ข…๋ฅ˜์˜ DOM ์ด๋ฒคํŠธ์— ๋Œ€ํ•œ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋“ฑ๋กํ•˜์—ฌ, ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ๋ฅผ ์ ์ ˆํ•œ transaction์œผ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž๊ฐ€ ๋ถ™์—ฌ๋„ฃ๊ธฐ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฉด, ๋ถ™์—ฌ๋„ฃ์€ ์ฝ˜ํ…์ธ ๋Š” clipboardParser๋ฅผ ํ†ตํ•ด ProseMirror ๋ฌธ์„œ ์กฐ๊ฐ(slice)์œผ๋กœ ํŒŒ์‹ฑ๋˜๊ณ , ํ•ด๋‹น ์œ„์น˜์— ์‚ฝ์ž…๋œ๋‹ค.
ํ•œํŽธ ๋งŽ์€ ์ด๋ฒคํŠธ๋Š” ์ฒ˜์Œ๋ถ€ํ„ฐ ProseMirror๊ฐ€ ์ง์ ‘ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ณ , ์ผ๋‹จ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ธฐ๋ณธ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•œ ๋’ค, ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ProseMirror๊ฐ€ ๋‹ค์‹œ ํ™•์ธํ•˜์—ฌ ๋‚ด๋ถ€ ๋ชจ๋ธ๋กœ ํ•ด์„ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ปค์„œ ์ด๋™์ด๋‚˜ ์„ ํƒ(selection) ์ฒ˜๋ฆฌ๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž˜ ์ฒ˜๋ฆฌํ•œ๋‹ค. ํŠนํžˆ ์–‘๋ฐฉํ–ฅ ํ…์ŠคํŠธ(bidirectional text, ์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์ฝ๋Š” ์–ธ์–ด(LTR) ์™€ ์˜ค๋ฅธ์ชฝ์—์„œ ์™ผ์ชฝ์œผ๋กœ ์ฝ๋Š” ์–ธ์–ด(RTL) ๊ฐ€ ๊ฐ™์€ ๋ฌธ์žฅ์ด๋‚˜ ๋ฌธ์„œ ์•ˆ์— ์„ž์—ฌ ์žˆ๋Š” ๊ฒฝ์šฐ) ๊ฐ™์€ ๋ณต์žกํ•œ ์ƒํ™ฉ๊นŒ์ง€ ๊ณ ๋ คํ•ด์•ผ ํ•˜๋ฏ€๋กœ, ์ด๋Ÿฐ ์ปค์„œ ๊ด€๋ จ ํ‚ค ์ž…๋ ฅ์ด๋‚˜ ๋งˆ์šฐ์Šค ๋™์ž‘์€ ๋ธŒ๋ผ์šฐ์ €์—๊ฒŒ ๋งก๊ธฐ๊ณ , ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋งŒ๋“  DOM selection์ด ์—๋””ํ„ฐ ์ƒํƒœ์™€ ๋‹ค๋ฅด๋‹ค๊ณ  ํŒ๋‹จ๋  ๊ฒฝ์šฐ์—๋งŒ ์„ ํƒ(selection)์„ ๊ฐฑ์‹ ํ•˜๋Š” ํŠธ๋žœ์žญ์…˜์„ ๋””์ŠคํŒจ์น˜(dispatch)ํ•œ๋‹ค.
์‹ฌ์ง€์–ด ํ…์ŠคํŠธ ์ž…๋ ฅ์กฐ์ฐจ๋„ ๋ธŒ๋ผ์šฐ์ €์—๊ฒŒ ์œ„์ž„ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ํ…์ŠคํŠธ ์ž…๋ ฅ ๊ณผ์ •์„ ์ง์ ‘ ๊ฐ€๋กœ์ฑ„๋ฉด
โ€ข
์ž๋™ ์™„์„ฑ
โ€ข
์ž๋™ ๋Œ€๋ฌธ์ž ๋ณ€ํ™˜
โ€ข
๋ชจ๋ฐ”์ผ ์ธํ„ฐํŽ˜์ด์Šค์˜ ๋„ค์ดํ‹ฐ๋ธŒ ๊ธฐ๋Šฅ๋“ค(์˜ˆ: iOS์˜ ์ž๋™ ์ˆ˜์ •)
๊ฐ™์€ ๋ธŒ๋ผ์šฐ์ € ๊ณ ์œ ์˜ ํŽธ์˜ ๊ธฐ๋Šฅ๋“ค์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
๋Œ€์‹  ๋ธŒ๋ผ์šฐ์ €๊ฐ€ DOM์„ ์—…๋ฐ์ดํŠธํ•˜๋ฉด, ProseMirror๋Š” ๊ทธ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๊ฐ์ง€ํ•˜๊ณ , ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„์„ ๋‹ค์‹œ ํŒŒ์‹ฑํ•œ ๋’ค, ๋ฌธ์„œ์˜ ์ฐจ์ด์ ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒˆ๋กœ์šด ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•ด ๋ฌธ์„œ๋ฅผ ๊ฐฑ์‹ ํ•œ๋‹ค.

Data flow

ProseMirror์—์„œ EditorView๋Š” ํŠน์ •ํ•œ EditorState๋ฅผ ํ™”๋ฉด์— ๋ Œ๋”๋งํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์–ด๋–ค ์ผ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ(์˜ˆ: ์ž…๋ ฅ, ํด๋ฆญ ๋“ฑ), ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํŠธ๋žœ์žญ์…˜(transaction) ์„ ์ƒ์„ฑํ•˜๊ณ  ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ(์ „ํŒŒ) ํ•œ๋‹ค.
์ด ํŠธ๋žœ์žญ์…˜์€ ์ผ๋ฐ˜์ ์œผ๋กœ ์ƒˆ๋กœ์šด ์ƒํƒœ(state) ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๊ณ , ๊ทธ ์ƒˆ๋กœ์šด ์ƒํƒœ๋Š” EditorView์˜ updateState ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋ทฐ์— ๋‹ค์‹œ ์ „๋‹ฌ๋œ๋‹ค.
์ด ํ๋ฆ„์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ง๊ด€์ ์ด๊ณ  ์ˆœํ™˜์ ์ธ ๋ฐ์ดํ„ฐ ํ๋ฆ„(cyclic data flow)์„ ๊ตฌ์„ฑํ•œ๋‹ค.
์ถœ์ฒ˜: ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ ๊ฐ€์ด๋“œ ๊ณต์‹๋ฌธ์„œ
์ด ๋ฐฉ์‹์€ JavaScript์—์„œ ํ”ํžˆ ์“ฐ์ด๋Š” ๋ช…๋ นํ˜• ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ฒด๊ณ„๋ณด๋‹ค ํ›จ์”ฌ ๋‹จ์ˆœํ•˜๊ณ  ์˜ˆ์ธก ๊ฐ€๋Šฅํ•˜๋‹ค. ์ „ํ†ต์ ์ธ ๋ฐฉ์‹์€ ๋‹ค์–‘ํ•œ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๊ณณ๊ณณ์—์„œ ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๊ธฐ ๋•Œ๋ฌธ์—, ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด ๋งค์šฐ ๋ณต์žกํ•œ ๊ฑฐ๋ฏธ์ค„์ฒ˜๋Ÿผ ์–ฝํžˆ๊ฒŒ ๋˜๊ธฐ ์‰ฝ๋‹ค. ๋ฐ˜๋ฉด ProseMirror๋Š” ์ด ์ˆœํ™˜ ๊ตฌ์กฐ ๋•๋ถ„์— ์ƒํƒœ ํ๋ฆ„์„ ๊น”๋”ํ•˜๊ฒŒ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์™€ ์—ฐ๊ฒฐํ•˜๊ณ  ์‹ถ์„ ๋•?

ProseMirror์˜ ํŠธ๋žœ์žญ์…˜์„ ๋” ํฐ ์•ฑ ๊ตฌ์กฐ์— ํ†ตํ•ฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ํŠธ๋žœ์žญ์…˜์ด dispatch ๋  ๋•ŒdispatchTransaction์ด๋ผ๋Š” prop์„ ์ด์šฉํ•ด ๊ฐ€๋กœ์ฑ„๋Š”(intercept) ๊ฒƒ์ด ๊ฐ€๋Šฅํ•˜๋‹ค. ์ด ๋ฐฉ๋ฒ•์„ ์“ฐ๋ฉด, ProseMirror์˜ ๋‚ด๋ถ€ ์ƒํƒœ ๊ฐฑ์‹  ํ๋ฆ„์„ Redux์™€ ๊ฐ™์€ ์•„ํ‚คํ…์ฒ˜์˜ ์ „์ฒด ์ƒํƒœ ํ๋ฆ„์— ํ†ตํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค.
์ฆ‰, ProseMirror์˜ ์ƒํƒœ๋ฅผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ „์—ญ ์Šคํ† ์–ด(store) ์—์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ProseMirror๋„ ๋‹ค๋ฅธ UI ์ƒํƒœ์™€ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด, ๋ณต์žกํ•œ ์—๋””ํ„ฐ ๋กœ์ง๋„ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ํ†ต์ œํ•  ์ˆ˜ ์žˆ๋‹ค.
// The app's state let appState = { editor: EditorState.create({schema}), score: 0 } let view = new EditorView(document.body, { state: appState.editor, dispatchTransaction(transaction) { update({type: "EDITOR_TRANSACTION", transaction}) } }) // A crude app state update function, which takes an update object, // updates the `appState`, and then refreshes the UI. function update(event) { if (event.type == "EDITOR_TRANSACTION") appState.editor = appState.editor.apply(event.transaction) else if (event.type == "SCORE_POINT") appState.score++ draw() } // An even cruder drawing function function draw() { document.querySelector("#score").textContent = appState.score view.updateState(appState.editor) }
JavaScript
๋ณต์‚ฌ
appState๋Š” ์—๋””ํ„ฐ ์ƒํƒœ(editor)์™€ ์ ์ˆ˜(score)๋ผ๋Š” ๋‘ ๊ฐ€์ง€ ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ ์ „์—ญ ์ƒํƒœ ๊ฐ์ฒด์ด๋‹ค. ์ฆ‰, ProseMirror์˜ ์ƒํƒœ๋„ ์ด ๊ฐ์ฒด ์•ˆ์—์„œ ๊ฐ™์ด ๊ด€๋ฆฌ๋˜๊ณ  ์žˆ๋‹ค.
์—๋””ํ„ฐ๋Š” appState.editor๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒ์„ฑ๋˜๋Š”๋ฐ, ProseMirror ๋‚ด๋ถ€์—์„œ ์–ด๋–ค ํŠธ๋žœ์žญ์…˜(transaction) ์ด ๋ฐœ์ƒํ•˜๋ฉด dispatchTransaction ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๊ณ  ์ด ํŠธ๋žœ์žญ์…˜์„ update()๋ผ๋Š” ์•ฑ ๋ ˆ๋ฒจ ์ƒํƒœ ๊ฐฑ์‹  ํ•จ์ˆ˜๋กœ ์ „๋‹ฌํ•œ๋‹ค. ์ฆ‰, ProseMirror์˜ ์ƒํƒœ ๋ณ€๊ฒฝ ํ๋ฆ„์ด appState๋ฅผ ํ†ตํ•ด ์ค‘์•™ ์ง‘์ค‘์‹์œผ๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค.
update ํ•จ์ˆ˜๋Š” ๋งˆ์น˜ Redux์˜ reducer์ฒ˜๋Ÿผ event.type์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋™์ž‘ํ•˜๋Š”๋ฐ, "EDITOR_TRANSACTION"์ด๋ฉด โ†’ ํŠธ๋žœ์žญ์…˜์„ ์ ์šฉํ•ด ์ƒˆ๋กœ์šด ์—๋””ํ„ฐ ์ƒํƒœ๋ฅผ ๋งŒ๋“ค๊ณ  "SCORE_POINT"์ด๋ฉด ์ ์ˆ˜๋ฅผ 1 ์˜ฌํžŒ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋งค๋ฒˆ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค draw()๋ฅผ ํ˜ธ์ถœํ•ด UI๋ฅผ ๊ฐฑ์‹ ํ•œ๋‹ค.
๋งˆ์ง€๋ง‰์œผ๋กœ draw ํ•จ์ˆ˜์—์„œ๋Š” #score์— ํ˜„์žฌ ์ ์ˆ˜๋ฅผ ๋ฐ˜์˜ํ•˜๊ณ , view.updateState()๋ฅผ ํ†ตํ•ด ์—๋””ํ„ฐ์— ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ์ ์šฉํ•œ๋‹ค.
ํ•ต์‹ฌ ํ๋ฆ„์„ ์š”์•ฝํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ๋œ๋‹ค.
์‚ฌ์šฉ์ž ํŽธ์ง‘ โ†’ transaction ๋ฐœ์ƒ โ†’ dispatchTransaction์œผ๋กœ app-level update() ํ˜ธ์ถœ โ†’ appState.editor์— ํŠธ๋žœ์žญ์…˜ ์ ์šฉ โ†’ draw() ํ†ตํ•ด UI ์—…๋ฐ์ดํŠธ (DOM + EditorView)
View โ†’ Transaction โ†’ AppState โ†’ View ๋กœ ์ด์–ด์ง€๋Š” ์ˆœํ™˜์  ๋ฐ์ดํ„ฐ ํ๋ฆ„(cyclic data flow) ์„ ๊ตฌ์„ฑํ•œ๋‹ค.

Efficient updating

updateState๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฐ€์žฅ ๋‹จ์ˆœํ•œ ๋ฐฉ๋ฒ•์€, ์—๋””ํ„ฐ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์ „์ฒด ๋ฌธ์„œ๋ฅผ ๋‹ค์‹œ ๊ทธ๋ฆฌ๋Š” ๊ฒƒ์ด๋‹ค. ํ•˜์ง€๋งŒ ๋ฌธ์„œ๊ฐ€ ํด ๊ฒฝ์šฐ, ์ด๋Ÿฐ ๋ฐฉ์‹์€ ์„ฑ๋Šฅ์— ํฐ ๋ถ€๋‹ด์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์— ํ˜„์‹ค์ ์ด์ง€ ์•Š๋‹ค. ๋‹คํ–‰ํžˆ EditorView๋Š” ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๋•Œ, ์ด์ „ ๋ฌธ์„œ์™€ ์ƒˆ ๋ฌธ์„œ๋ฅผ ๋ชจ๋‘ ๊ฐ€์ง€๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๋‘ ๋ฌธ์„œ๋ฅผ ๋น„๊ตํ•ด์„œ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์€ ๋ถ€๋ถ„์€ DOM์„ ๊ทธ๋Œ€๋กœ ๋‘๊ณ , ๋ฐ”๋€ ๋ถ€๋ถ„๋งŒ ๊ฐฑ์‹ ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค. ProseMirror๋Š” ์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ ์ž‘๋™ํ•˜๋ฉฐ, ๋•๋ถ„์— ์ผ๋ฐ˜์ ์ธ ํŽธ์ง‘ ์ƒํ™ฉ์—์„œ๋Š” DOM์— ๋Œ€ํ•ด ์ตœ์†Œํ•œ์˜ ์ž‘์—…๋งŒ ์ˆ˜ํ–‰ํ•ด๋„ ๋œ๋‹ค.
์˜ˆ์‹œ: ํƒ€์ดํ•‘์ฒ˜๋Ÿผ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋จผ์ € DOM์„ ์ˆ˜์ •ํ•œ ๊ฒฝ์šฐ
์‚ฌ์šฉ์ž๊ฐ€ ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด, ๋ธŒ๋ผ์šฐ์ €๋Š” ์ด๋ฏธ DOM์— ํ•ด๋‹น ํ…์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ ์ƒํƒœ๋‹ค. ์ด๋Ÿด ๊ฒฝ์šฐ ProseMirror๋Š” DOM๊ณผ ๋‚ด๋ถ€ ์ƒํƒœ๋ฅผ ๋งž์ถ”๊ธฐ๋งŒ ํ•˜๋ฉด ๋˜๋ฏ€๋กœ, ์ถ”๊ฐ€์ ์ธ DOM ์ˆ˜์ • ์—†์ด ์ƒํƒœ๋งŒ ๋™๊ธฐํ™”ํ•˜๋ฉด ๋œ๋‹ค. ํ•˜์ง€๋งŒ ๋งŒ์•ฝ ํ•ด๋‹น ํŠธ๋žœ์žญ์…˜์ด ๋‚˜์ค‘์— ์ทจ์†Œ๋˜๊ฑฐ๋‚˜ ๋ณ€๊ฒฝ๋œ๋‹ค๋ฉด, ProseMirror๋Š” DOM์„ ๋˜๋Œ๋ ค์„œ ๋‹ค์‹œ ์ƒํƒœ์™€ ์ผ์น˜ํ•˜๋„๋ก ๋งŒ๋“ ๋‹ค. ์ƒํƒœ(state) ์™€ ํ™”๋ฉด(DOM) ์˜ ๋ถˆ์ผ์น˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋‹ค.
ํ•œํŽธ, EditorView๋Š” ์‚ฌ์šฉ์ž์˜ ์„ ํƒ(selection)์ด ์‹ค์ œ๋กœ ์ƒํƒœ์™€ ์–ด๊ธ‹๋‚ฌ์„ ๋•Œ๋งŒ DOM selection์„ ๊ฐฑ์‹ ํ•œ๋‹ค. ๊ทธ ์ด์œ ๋Š”, ๋ธŒ๋ผ์šฐ์ €๊ฐ€ selection๊ณผ ๊ด€๋ จ๋œ ์ˆจ๊ฒจ์ง„ ์ƒํƒœ๋“ค์„ ํ•จ๊ป˜ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋ž˜ ๋ฐฉํ–ฅํ‚ค๋กœ ์งง์€ ์ค„์„ ํ†ต๊ณผํ•ด ๋‹ค์Œ ๊ธด ์ค„๋กœ ์ด๋™ํ•  ๋•Œ, ์ด์ „ ์ค„์—์„œ์˜ ์ˆ˜ํ‰ ์œ„์น˜ ์ •๋ณด๋ฅผ ๊ธฐ์–ตํ•ด๋‘๊ณ , ๋‹ค์Œ ์ค„์—์„œ๋„ ๊ฐ™์€ ๊ฐ€๋กœ ์œ„์น˜๋กœ ์ปค์„œ๋ฅผ ์ž๋™ ์ด๋™์‹œํ‚ค๋Š” ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค. ๋งŒ์•ฝ ProseMirror๊ฐ€ ๋งค๋ฒˆ DOM selection์„ ๋ฎ์–ด์จ๋ฒ„๋ฆฌ๋ฉด, ์ด๋Ÿฐ ๋ฏธ๋ฌ˜ํ•œ ๋ธŒ๋ผ์šฐ์ €์˜ ๋™์ž‘์ด ๊นจ์งˆ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ์ •๋ง ์–ด๊ธ‹๋‚ฌ์„ ๋•Œ๋งŒ ์กฐ์‹ฌ์Šค๋Ÿฝ๊ฒŒ ๊ฐฑ์‹ ํ•œ๋‹ค.

Props

ํ”„๋กœ์Šค๋ฏธ๋Ÿฌ์˜ Props๋ผ๋Š” ์šฉ์–ด๋Š” React์˜ ๊ทธ Props์—์„œ ๊ฐ€์ ธ์˜จ ๊ฐœ๋…์ด ๋งž๋‹ค. Props๋Š” UI ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌ๋˜๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ(์†์„ฑ)์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•˜๋ฉฐ, ์ด์ƒ์ ์œผ๋กœ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ด๋–ค props๋ฅผ ๋ฐ›๋А๋ƒ์— ๋”ฐ๋ฅธ ๋™์ž‘์ด ์™„์ „ํžˆ ์ •์˜๋˜์–ด์•ผ ํ•œ๋‹ค.
let view = new EditorView({ state: myState, editable() { return false }, // Enables read-only behavior handleDoubleClick() { console.log("Double click!") } })
JavaScript
๋ณต์‚ฌ
์ด์ฒ˜๋Ÿผ state ์—ญ์‹œ ํ•˜๋‚˜์˜ prop์ด๋ฉฐ, ์ด ์™ธ์˜ ๋‹ค๋ฅธ prop ๊ฐ’๋“ค๋„ ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด์„œ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ด๋“ค์€ ์ปดํฌ๋„ŒํŠธ ์™ธ๋ถ€์—์„œ๋งŒ ๋ณ€๊ฒฝ๋˜๋ฉฐ, ์ปดํฌ๋„ŒํŠธ ์ž์‹ ์ด ์ง์ ‘ ๋ฐ”๊พธ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— state๋กœ ๊ฐ„์ฃผ๋˜์ง„ ์•Š๋Š”๋‹ค. updateState() ๋ฉ”์„œ๋“œ๋Š” ๊ฒฐ๊ตญ state prop์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋‹จ์ถ• ๋ฉ”์„œ๋“œ์ผ ๋ฟ์ด๋‹ค.

ํ”Œ๋Ÿฌ๊ทธ์ธ๋„ props๋ฅผ ์„ ์–ธํ•  ์ˆ˜ ์žˆ๋‹ค

ํ”Œ๋Ÿฌ๊ทธ์ธ ์—ญ์‹œ props ์†์„ฑ์„ ํ†ตํ•ด ์ž์‹ ๋งŒ์˜ props๋ฅผ ๋ทฐ์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‹จ, state๋‚˜ dispatchTransaction์ฒ˜๋Ÿผ ๋ทฐ์— ์ง์ ‘ ์ง€์ •ํ•ด์•ผ ํ•˜๋Š” ํ•„์ˆ˜ props๋Š” ์ œ์™ธ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ฌธ์„œ ํฌ๊ธฐ์— ๋”ฐ๋ผ ์—๋””ํ„ฐ๋ฅผ ์ž๋™์œผ๋กœ ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ๋งŒ๋“œ๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค
function maxSizePlugin(max) { return new Plugin({ props: { editable(state) { return state.doc.content.size < max } } }) }
JavaScript
๋ณต์‚ฌ

์—ฌ๋Ÿฌ ๊ณณ์—์„œ ๋™์ผํ•œ prop์„ ์„ ์–ธํ•˜๋ฉด?

ํŠน์ • prop์ด ์ง์ ‘ ์ง€์ •๋œ ๊ฒฝ์šฐ์™€ ํ”Œ๋Ÿฌ๊ทธ์ธ ์–‘์ชฝ์—์„œ ์ค‘๋ณต ์„ ์–ธ๋  ์ˆ˜๋„ ์žˆ๋Š”๋ฐ, ์ด๋Ÿฐ ๊ฒฝ์šฐ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ• ์ง€๋Š” prop์˜ ์ข…๋ฅ˜์— ๋”ฐ๋ผ ๋‹ค๋ฅด๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ, ์ง์ ‘ ์ œ๊ณต๋œ prop์ด ์šฐ์„ ๊ถŒ์„ ๊ฐ€์ง„๋‹ค. ๊ทธ๋‹ค์Œ์œผ๋กœ๋Š” ๋“ฑ๋ก๋œ ํ”Œ๋Ÿฌ๊ทธ์ธ๋“ค์ด ์„ ์–ธํ•œ props๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ์ ์šฉ๋œ๋‹ค. props ๋ณ„๋กœ ์ค‘๋ณต ์„ ์–ธ๋œ prop์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด ๋‹ค๋ฅธ๋ฐ, ์˜ˆ๋ฅผ ๋“ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
prop ๋ณ„ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์˜ˆ์‹œ
prop ์ข…๋ฅ˜
๋ณ‘ํ•ฉ ๋ฐฉ์‹
domParser
๊ฐ€์žฅ ๋จผ์ € ๋ฐœ๊ฒฌ๋œ ๊ฐ’ ํ•˜๋‚˜๋งŒ ์‚ฌ์šฉํ•˜๊ณ , ๋‚˜๋จธ์ง€๋Š” ๋ฌด์‹œ
handleKeyDown, handleClick ๋“ฑ ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜
์ฒ˜์Œ์œผ๋กœ true๋ฅผ ๋ฐ˜ํ™˜ํ•œ ํ•ธ๋“ค๋Ÿฌ๋งŒ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌ
attributes, decorations
๋ชจ๋“  ๊ฐ’๋“ค์„ ํ•ฉ์ณ์„œ(union) ์ ์šฉ

Decorations

Decorations(๋ฐ์ฝ”๋ ˆ์ด์…˜) ์€ ProseMirror ์—๋””ํ„ฐ ๋ทฐ๊ฐ€ ๋ฌธ์„œ๋ฅผ ๊ทธ๋ฆด ๋•Œ ์Šคํƒ€์ผ์ด๋‚˜ ์‹œ๊ฐ์  ์š”์†Œ๋ฅผ ๋ง๋ถ™์ผ ์ˆ˜ ์žˆ๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด๋‹ค. ๋ฐ์ฝ”๋ ˆ์ด์…˜์€ decorations prop์„ ํ†ตํ•ด ์ œ๊ณต๋˜๋ฉฐ, ์ด ์„ธ ๊ฐ€์ง€ ์ข…๋ฅ˜๊ฐ€ ์žˆ๋‹ค.
โ€ข
Node (Decoration.node): ํŠน์ • ๋…ธ๋“œ์˜ DOM ํ‘œํ˜„์— ์Šคํƒ€์ผ์ด๋‚˜ ์†์„ฑ์„ ์ถ”๊ฐ€ํ•จ
โ€ข
Widget (Decoration.widget): ๋ฌธ์„œ์— ์‹ค์ œ ์กด์žฌํ•˜์ง€ ์•Š๋Š” DOM ๋…ธ๋“œ๋ฅผ ํŠน์ • ์œ„์น˜์— ์‚ฝ์ž…ํ•จ (์˜ˆ: ์ปค์„œ ์œ„์— ๋ฒ„ํŠผ ํ‘œ์‹œ)
โ€ข
Inline (Decoration.inline): ํŠน์ • ๋ฒ”์œ„ ์•ˆ์˜ ์ธ๋ผ์ธ ์ฝ˜ํ…์ธ ์— ์Šคํƒ€์ผ์ด๋‚˜ ์†์„ฑ์„ ์ ์šฉํ•จ
๋ฐ์ฝ”๋ ˆ์ด์…˜์„ ํšจ์œจ์ ์œผ๋กœ ๋ Œ๋”๋งํ•˜๊ณ  ๋น„๊ตํ•˜๊ธฐ ์œ„ํ•ด, ๋‹จ์ผ ๊ฐ์ฒด๊ฐ€ ์•„๋‹Œ DecorationSet ํ˜•ํƒœ๋กœ ์ œ๊ณต๋˜์–ด์•ผ ํ•œ๋‹ค. DecorationSet์€ ์‹ค์ œ ๋ฌธ์„œ ํŠธ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ํ‰๋‚ด ๋‚ธ ์ž๋ฃŒ๊ตฌ์กฐ๋กœ, create ์ •์  ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
let purplePlugin = new Plugin({ props: { decorations(state) { return DecorationSet.create(state.doc, [ Decoration.inline(0, state.doc.content.size, {style: "color: purple"}) ]) } } })
JavaScript
๋ณต์‚ฌ
์œ„ ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์ „์ฒด ๋ฌธ์„œ ํ…์ŠคํŠธ์— ๋ณด๋ผ์ƒ‰ ์ธ๋ผ์ธ ์Šคํƒ€์ผ์„ ์ ์šฉํ•˜๋Š” ๋ฐ์ฝ”๋ ˆ์ด์…˜์„ ์ƒ์„ฑํ•œ๋‹ค.

๋น„ํšจ์œจ์ ์ธ ๋ฐฉ์‹: ๋งค๋ฒˆ ๋ฐ์ฝ”๋ ˆ์ด์…˜ ์ƒˆ๋กœ ์ƒ์„ฑ

๋ฌธ์„œ๊ฐ€ ํฌ๊ฑฐ๋‚˜ ๋ฐ์ฝ”๋ ˆ์ด์…˜์ด ๋งŽ์„ ๋•Œ๋Š”, ์ƒํƒœ๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค DecorationSet.create๋ฅผ ๋‹ค์‹œ ํ˜ธ์ถœํ•˜๋Š” ๊ฑด ๋น„ํšจ์œจ์ ์ด๋‹ค. ์ด๋Ÿด ๋• ํ”Œ๋Ÿฌ๊ทธ์ธ ๋‚ด๋ถ€ ์ƒํƒœ์— DecorationSet์„ ์ €์žฅํ•ด๋‘๊ณ , ๋ฌธ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋Š” ๊ธฐ์กด ๋ฐ์ฝ”๋ ˆ์ด์…˜์„ map()์œผ๋กœ ๋งคํ•‘ํ•ด์„œ ๋ณ€ํ™”๋œ ๋ฌธ์„œ์— ๋งž๊ฒŒ ์กฐ์ •ํ•˜๋Š” ๋ฐฉ์‹์ด ์ข‹๋‹ค.
let specklePlugin = new Plugin({ state: { init(_, {doc}) { let speckles = [] for (let pos = 1; pos < doc.content.size; pos += 4) speckles.push(Decoration.inline(pos - 1, pos, {style: "background: yellow"})) return DecorationSet.create(doc, speckles) }, apply(tr, set) { return set.map(tr.mapping, tr.doc) } }, props: { decorations(state) { return specklePlugin.getState(state) } } })
JavaScript
๋ณต์‚ฌ
์ด ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ๋ฌธ์„œ์˜ 4๋ฒˆ์งธ ๊ธ€์ž๋งˆ๋‹ค ๋…ธ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ์„ ์ฃผ๋Š” ๋ฐ์ฝ”๋ ˆ์ด์…˜์„ ์ƒ์„ฑํ•œ๋‹ค. ์‹ค์šฉ์ ์ด์ง„ ์•Š์ง€๋งŒ, ์˜ˆ๋ฅผ ๋“ค์–ด ๊ฒ€์ƒ‰์–ด ํ•˜์ด๋ผ์ดํŒ…์ด๋‚˜ ์ฃผ์„ ํ‘œ์‹œ ๊ฐ™์€ ๊ธฐ๋Šฅ์˜ ๊ตฌ์กฐ์™€ ์œ ์‚ฌํ•˜๋‹ค.

.map()์˜ ์—ญํ• 

ํŠธ๋žœ์žญ์…˜์ด ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค apply() ๋ฉ”์„œ๋“œ์—์„œ DecorationSet.map()์„ ํ˜ธ์ถœํ•ด ๊ธฐ์กด ๋ฐ์ฝ”๋ ˆ์ด์…˜์„ ์ƒˆ ๋ฌธ์„œ ๊ตฌ์กฐ์— ๋งž๊ฒŒ ์ž๋™ ์žฌ๋ฐฐ์น˜ํ•œ๋‹ค. map()์€ ๋‚ด๋ถ€์ ์œผ๋กœ ๋ฌธ์„œ ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ผ๊ฐ€๋Š” ํŠธ๋ฆฌ ๊ตฌ์กฐ ๋•๋ถ„์—, ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ๋‹ค์‹œ ๊ณ„์‚ฐํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ์ด ๋งค์šฐ ๋›ฐ์–ด๋‚˜๋‹ค.

์‹ค์ œ ์‚ฌ์šฉ์—์„œ๋Š”?

์‹ค์ œ ํ”Œ๋Ÿฌ๊ทธ์ธ์—์„œ๋Š” ๋‹จ์ˆœํžˆ map()๋งŒ ํ•˜์ง€ ์•Š๊ณ , apply() ์•ˆ์—์„œ ํŠธ๋žœ์žญ์…˜ ์ •๋ณด๋ฅผ ๋ณด๊ณ 
โ€ข
์ƒˆ ๋ฐ์ฝ”๋ ˆ์ด์…˜์„ add() ํ•˜๊ฑฐ๋‚˜
โ€ข
๊ธฐ์กด ๋ฐ์ฝ”๋ ˆ์ด์…˜์„ remove() ํ•˜๋ฉด์„œ
๋ฐ์ฝ”๋ ˆ์ด์…˜ ์ƒํƒœ๋ฅผ ๋™์ ์œผ๋กœ ๊ฐฑ์‹ ํ•œ๋‹ค. ์ด ๋•Œ ํŠธ๋žœ์žญ์…˜์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋‚˜ ๋ณ€๊ฒฝ ๋ฒ”์œ„๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋”์šฑ ์ •๊ตํ•˜๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค.

Node views

ProseMirror์—์„œ ์—๋””ํ„ฐ ๋ทฐ๊ฐ€ ๋ฌธ์„œ๋ฅผ ๊ทธ๋ฆด ๋•Œ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ๋˜ ํ•˜๋‚˜์˜ ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค. ๋ฐ”๋กœ NodeView๋ฅผ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค. NodeView๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฌธ์„œ์˜ ํŠน์ • ๋…ธ๋“œ์— ๋Œ€ํ•ด ์ž‘์€ UI ์ปดํฌ๋„ŒํŠธ์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋Š” ๋ Œ๋”๋ง ๋กœ์ง์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๊ฐ ๋…ธ๋“œ์˜ DOM์„ ์ง์ ‘ ๋ Œ๋”๋งํ•˜๊ณ , ์—…๋ฐ์ดํŠธ ๋ฐฉ์‹์ด๋‚˜ ์ด๋ฒคํŠธ ์‘๋‹ต ๋“ฑ์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ๋‹ค.
let view = new EditorView({ state, nodeViews: { image(node) { return new ImageView(node) } } }) class ImageView { constructor(node) { // The editor will use this as the node's DOM representation this.dom = document.createElement("img") this.dom.src = node.attrs.src this.dom.addEventListener("click", e => { console.log("You clicked me!") e.preventDefault() }) } stopEvent() { return true } }
JavaScript
๋ณต์‚ฌ
์œ„ ์˜ˆ์‹œ์—์„œ image ๋…ธ๋“œ์— ๋Œ€ํ•ด ImageView๋ผ๋Š” NodeView๋ฅผ ์ง€์ •ํ–ˆ์œผ๋ฉฐ, ํ•ด๋‹น ํด๋ž˜์Šค์—์„œ๋Š” img ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ง์ ‘ ๋งŒ๋“ค์–ด ์‚ฌ์šฉ์ž์˜ ํด๋ฆญ ์ด๋ฒคํŠธ์— ๋ฐ˜์‘ํ•˜๋„๋ก ์„ค์ •ํ–ˆ๋‹ค. ๋˜ํ•œ stopEvent()๋ฅผ ์ •์˜ํ•จ์œผ๋กœ์จ, ์ด DOM์—์„œ ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ๋Š” ProseMirror ๊ธฐ๋ณธ ์ฒ˜๋ฆฌ์—์„œ ์ œ์™ธ๋œ๋‹ค.

๋ฌธ์„œ ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๋ ค๋ฉด ๋…ธ๋“œ์˜ ์œ„์น˜(getPos)๊ฐ€ ํ•„์š”ํ•˜๋‹ค

NodeView๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž์™€ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜๋Š” ์žˆ์ง€๋งŒ, ์‹ค์ œ๋กœ ๋ฌธ์„œ ๋‚ด๋ถ€์˜ ๋…ธ๋“œ๋ฅผ ๋ฐ”๊พธ๋ ค๋ฉด ํ•ด๋‹น ๋…ธ๋“œ๊ฐ€ ๋ฌธ์„œ ๋‚ด์—์„œ ์–ด๋”” ์œ„์น˜ํ•˜๋Š”์ง€๋ฅผ ์•Œ์•„์•ผ ํ•œ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ProseMirror๋Š” NodeView ์ƒ์„ฑ์ž์— getPos() ํ•จ์ˆ˜๋ฅผ ํ•จ๊ป˜ ์ „๋‹ฌํ•ด์ค€๋‹ค. ์•„๋ž˜๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฏธ์ง€๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ alt ์†์„ฑ์„ ๋ฐ”๊พธ๋„๋ก ์ˆ˜์ •ํ•œ ์˜ˆ์‹œ๋‹ค.
let view = new EditorView({ state, nodeViews: { image(node, view, getPos) { return new ImageView(node, view, getPos) } } }) class ImageView { constructor(node, view, getPos) { this.dom = document.createElement("img") this.dom.src = node.attrs.src this.dom.alt = node.attrs.alt this.dom.addEventListener("click", e => { e.preventDefault() let alt = prompt("New alt text:", "") if (alt) view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { src: node.attrs.src, alt })) }) } stopEvent() { return true } }
JavaScript
๋ณต์‚ฌ
์—ฌ๊ธฐ์„œ ์‚ฌ์šฉํ•˜๋Š” setNodeMarkup ๋ฉ”์„œ๋“œ๋Š” ํŠน์ • ์œ„์น˜์˜ ๋…ธ๋“œ์— ๋Œ€ํ•ด ๋…ธ๋“œ ํƒ€์ž…์ด๋‚˜ ์†์„ฑ(attribute)์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” ํŠธ๋žœ์žญ์…˜์„ ๋งŒ๋“ ๋‹ค.

์ฝ˜ํ…์ธ ๊ฐ€ ์žˆ๋Š” ๋…ธ๋“œ๋ผ๋ฉด? (contentDOM ์‚ฌ์šฉ)

์•ž์„œ ๋ณธ ์ด๋ฏธ์ง€ ๋…ธ๋“œ๋Š” ์ž์‹ ์ฝ˜ํ…์ธ ๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— contentDOM ๊ฐ™์€ ๊ฑธ ์‹ ๊ฒฝ ์“ฐ์ง€ ์•Š์•„๋„ ๋˜์ง€๋งŒ, ๋‹จ๋ฝ(paragraph)์ฒ˜๋Ÿผ ์ฝ˜ํ…์ธ ๋ฅผ ํฌํ•จํ•˜๋Š” ๋…ธ๋“œ๋Š” ์ƒํ™ฉ์ด ๋‹ฌ๋ผ์ง„๋‹ค.
let view = new EditorView({ state, nodeViews: { paragraph(node) { return new ParagraphView(node) } } }) class ParagraphView { constructor(node) { this.dom = this.contentDOM = document.createElement("p") if (node.content.size == 0) this.dom.classList.add("empty") } update(node) { if (node.content.size > 0) this.dom.classList.remove("empty") else this.dom.classList.add("empty") return true } }
JavaScript
๋ณต์‚ฌ
์ด ์˜ˆ์‹œ์—์„œ ParagraphView๋Š” ๋‹จ๋ฝ์— ํ…์ŠคํŠธ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ "empty" ํด๋ž˜์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๋‚ด์šฉ์ด ์ƒ๊ธฐ๋ฉด ํ•ด๋‹น ํด๋ž˜์Šค๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค. ์—ฌ๊ธฐ์„œ ํ•ต์‹ฌ์€ contentDOM์ด๋ผ๋Š” ์†์„ฑ์ด๋‹ค. ProseMirror๋Š” ์ด ์†์„ฑ์ด ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉด, ๋…ธ๋“œ์˜ ์ฝ˜ํ…์ธ ๋ฅผ ์ž๋™์œผ๋กœ contentDOM ์•ˆ์— ๋ Œ๋”๋งํ•˜๊ณ , ๋‚ด์šฉ ๋ณ€๊ฒฝ๋„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค. ๋งŒ์•ฝ contentDOM์„ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด, ํ•ด๋‹น ๋…ธ๋“œ์˜ ์ž์‹ ์ฝ˜ํ…์ธ ๋Š” ProseMirror๊ฐ€ ์ „ํ˜€ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์œผ๋ฉฐ, ์™„์ „ํžˆ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ๋ Œ๋”๋ง ๋ฐ ์—…๋ฐ์ดํŠธ๋ฅผ ์ฑ…์ž„์ ธ์•ผ ํ•œ๋‹ค.

update(node) ๋ฉ”์„œ๋“œ๋ž€?

NodeView์˜ update() ๋ฉ”์„œ๋“œ๋Š” ์—๋””ํ„ฐ ๋‚ด๋ถ€์—์„œ ํ•ด๋‹น ๋…ธ๋“œ์— ๋ณ€๊ฒฝ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ํ˜ธ์ถœ๋˜๋ฉฐ, ๊ธฐ์กด NodeView๊ฐ€ ์ƒˆ ๋…ธ๋“œ๋ฅผ ๊ณ„์† ์‚ฌ์šฉํ•ด๋„ ๊ดœ์ฐฎ์€์ง€ ํŒ๋‹จํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๊ธฐ์กด NodeView ์ธ์Šคํ„ด์Šค์™€ ์™ธ๋ถ€ DOM์„ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๊ณ  ๊ทธ ๋‚ด๋ถ€์— ์žˆ๋Š” ์ฝ˜ํ…์ธ (contentDOM)๋งŒ ๊ฐฑ์‹ ํ•œ๋‹ค.
๋ฐ˜๋Œ€๋กœ false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ProseMirror๋Š” ์ด NodeView๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค๊ณ  ํŒ๋‹จํ•˜๊ณ , ๊ธฐ์กด DOM๊ณผ ์ธ์Šคํ„ด์Šค๋ฅผ ์™„์ „ํžˆ ํ๊ธฐํ•˜๊ณ , ์ƒˆ๋กœ์šด NodeView๋ฅผ ์ƒ์„ฑํ•ด์„œ ์ƒˆ๋กœ์šด DOM์„ ๋ Œ๋”๋งํ•œ๋‹ค.
๋•Œ๋ฌธ์—, ์—…๋ฐ์ดํŠธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ด๋ฃจ์–ด์กŒ๋‹ค๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•ด ์ƒˆ๋กœ์šด DOM์„ ๋ Œ๋”๋งํ•ด์•ผํ•œ๋‹ค.
โ€ข
๋˜๋Š” ๋…ธ๋“œ์˜ ํƒ€์ž…์ด ๋ฐ”๋€Œ์—ˆ๊ฑฐ๋‚˜, ์ด NodeView๊ฐ€ ๋‹ค๋ฃฐ ์ˆ˜ ์—†๋Š” ๊ตฌ์กฐ๋ผ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜๊ณ 
โ€ข
๊ทธ๋ ‡์ง€ ์•Š๊ณ  DOM์„ ๊ทธ๋Œ€๋กœ ๋‘๊ณ  ์Šคํƒ€์ผ๋งŒ ๋ฐ”๊พผ๋‹ค๋“ ์ง€, ๋‚ด๋ถ€ ์ฝ˜ํ…์ธ ๋งŒ ๋ฐ”๊พธ๋ฉด ๋˜๋Š” ๊ฒฝ์šฐ true๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผํ•œ๋‹ค.
์œ„ ์˜ˆ์‹œ์—์„œ๋Š” update()๋ฅผ ํ™œ์šฉํ•ด ๋‹จ๋ฝ์ด ๋น„์—ˆ๋Š”์ง€ ์—ฌ๋ถ€์— ๋”ฐ๋ผ "empty" ํด๋ž˜์Šค๋ฅผ ๋™์ ์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค. ๊ธฐ์กด DOM ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•œ ์ฑ„, class๋งŒ ๋ฐ”๊พธ๋ฉด ๋˜๋ฏ€๋กœ true๋ฅผ ๋ฐ˜ํ™˜ํ•œ ๊ฒƒ์ด๋‹ค.

References