ProseMirror를 사용하여 텍스트 에디팅 / ProseMirror에서의 Node / ProseMirror 메서드

jini.choi·2023년 8월 1일

ProseMirror

목록 보기
1/2

main.js

import "./style.css";
import { EditorView } from "prosemirror-view";
import { EditorState } from "prosemirror-state";
import { schema } from "./schema";

const holder = document.querySelector(".holder");

const state = EditorState.create({ schema });

const view = new EditorView(holder, {
  state, 
  attributes: {
    class: "editor",
  },

  dispatchTransaction(transaction) {
    const newState = view.state.apply(transaction);
    view.updateState(newState);
  },
});

/**
 * 선택된 텍스트에 <strong> 태그를 적용하는 기능을 구현해 봅니다.
 * 미리 만들어둔 strongButton 을 사용하세요.
 *
 * 1. 트랜잭션 객체를 생성한다.
 * 2. 에디터 상태를 구성하는 스키마를 가져와 strong 마크 를 생성한다.
 * 3. 텍스트 선택의 시작점과 끝점을 가져온다.
 * 4. 시작점과 끝점 사이에 strong 마크를 입힌다는 내용을 트랜잭션 객체에 담는다.
 * 5. 트랜잭션 객체를 에디터에 전달한다.
 *
 */

const strongButton = document.querySelector(".stronger");

strongButton.addEventListener("click", (event) => {
  // 1. 트랜잭션 객체를 생성한다.
  const transaction = view.state.tr;

  // 2. 에디터 상태를 구성하는 스키마를 가져와 strong 마크 를 생성한다.
  const mySchema = view.state.schema;
  const strongMark = mySchema.marks.strong.create();

  // 3. 텍스트 선택의 시작점과 끝점을 가져온다.
  const selection = view.state.selection;
  const from = selection.from;
  const to = selection.to;

  // 4. 시작점과 끝점 사이에 strong 마크를 입힌다는 내용을 트랜잭션 객체에 담는다.
  transaction.addMark(from, to, strongMark);

  // 5. 트랜잭션 객체를 에디터에 전달한다.
  view.dispatch(transaction);
});

설명

ProseMirror의 View 모듈에서 EditorView 클래스를 불러옴.
EditorView는 DOM 요소를 받아서 텍스트 에디터를 렌더링하는 역할.

import { EditorView } from "prosemirror-view";

ProseMirror의 State 모듈에서 EditorState 클래스를 불러옴.
EditorState는 현재 문서의 상태를 나타냄.

import { EditorState } from "prosemirror-state";

schema라는 이름으로 정의된 스키마를 불러옴.
스키마는 문서의 모델 구조를 정의하며, 에디터에서 허용되는 노드 유형과 서식을 제어.

import { schema } from "./schema";

클래스가 "holder"인 DOM 요소를 선택.
이 요소가 ProseMirror 에디터가 삽입될 위치.

const holder = document.querySelector(".holder");

EditorState를 생성.
스키마는 EditorState를 생성할 때 필수적인 요소.

const state = EditorState.create({ schema });

EditorView를 생성.
이때, holder는 에디터가 삽입될 DOM의 요소이고, state는 이전에 생성한 EditorState.
이외에도 attributes 객체를 통해 에디터의 뷰에 추가할 속성을 정의,
dispatchTransaction 콜백 함수를 제공하여 에디터에서 발생하는 트랜잭션(편집 동작)을 처리할 수 있다.

const view = new EditorView(holder, {
  state, 
  attributes: {
    class: "editor",
  },

  dispatchTransaction(transaction) {
    const newState = view.state.apply(transaction);
    view.updateState(newState);
  },
});

view.state 객체에 트랜잭션을 적용(apply)하여 새로운 상태(newState)를 반환하는 메서드 호출.
이 메서드는 에디터의 현재 상태를 변경하는 데 사용.

const newState = view.state.apply(transaction);

에디터 뷰(View)를 업데이트하는 메서드 호출.
이 메서드는 newState를 사용하여 에디터 뷰를 업데이트.
사용자 입력에 따라 에디터의 동작을 관리하는 데 사용됨.

view.updateState(newState);

schema.js

import { Schema } from "prosemirror-model";

/**
 * 스키마를 직접 정의합니다.
 *
 * 1. 새로운 Node 의 스펙을 만들어 봅니다.
 *    <blockquote> 노드의 스펙:
 *    - content: "(paragraph)+"               (paragraph 를 1개 이상 가질 수 있습니다.)
 *    - group: "block"                        (block 으로 분류됩니다.)
 *    - parseDOM: [{ tag: "blockquote" }]     (HTML 태그로부터 노드를 만들 수 있습니다.)
 *    - toDOM: ['blockquote', 0]              (노드를 HTML 태그로 표현할 때 사용됩니다.)
 *
 *
 * 2. 새로운 Mark 의 스펙을 만들어 봅니다.
 *    <strong> 마크의 스펙:
 *    - parseDOM: [{ tag: "strong" }]         (HTML 태그로부터 노드를 만들 수 있습니다.)
 *    - toDOM: ["strong", 0]                  (노드를 HTML 태그로 표현할 때 사용됩니다.)
 *
 *
 */
export const spec = {
  nodes: {
    text: {
      group: "inline",
    },

    doc: {
      content: "block+",
    },

    paragraph: {
      content: "inline*",
      group: "block",
      parseDOM: [{ tag: "p" }],
      toDOM() {
        return ["p", 0];
      },
    },

    blockquote: {
      content: "(paragraph)+",
      group: "block",
      defining: true,
      parseDOM: [{ tag: "blockquote" }],
      toDOM() {
        return blockquoteDOM;
      },
    },
  },

  marks: {
    strong: {
      toDOM() {
        return ["strong", 0];
      },
      parseDOM: [{ tag: "strong" }],
    },
  },
};

export const schema = new Schema(spec);

설명

spec 객체에 스키마를 정의.
스키마는 ProseMirror에서 문서 모델 구조와 서식을 제어하는데 사용됨.

export const spec = {...};

노드 유형을 정의.
text, doc, paragraph, blockquote과 같은 노드 유형들이 있다.
각 노드 유형은 내용(content), 그룹(group), parseDOM, toDOM 등의 속성으로 구성됨.

nodes: {...},

서식 유형을 정의.
여기서는 strong이라는 강조 서식을 정의한다.
strong은 parseDOM과 toDOM 속성을 가지며, <strong> 태그로 표시되도록 설정됨.

marks: {
    strong: {
      toDOM() {
        return ["strong", 0];
      },
      parseDOM: [{ tag: "strong" }],
    },
  },

spec 객체를 사용하여 새로운 스키마를 생성.

export const schema = new Schema(spec);

strongButton 클릭 시 수행되는 과정

main.js 파일에서 "stronger" 클래스를 가진 버튼이 클릭되었을 때,
선택된 텍스트에 <strong> 태그를 적용하는 기능을 구현한 것을 볼 수 있다. strongButton 버튼을 클릭하면 다음과 같은 과정이 수행된다.

  1. 새로운 transaction 객체를 생성.
  2. mySchema에서 strong 마크를 생성.
  3. view.state.selection을 통해 텍스트 선택의 시작점과 끝점을 가져옴.
  4. 시작점과 끝점 사이에 strong 마크를 입힌 transaction 객체를 생성.
  5. 생성된 transaction 객체를 에디터에 전달하여 에디터의 상태를 업데이트.
//main.js
.
.
.
strongButton.addEventListener("click", (event) => {
  // 1. 트랜잭션 객체를 생성한다.
  const transaction = view.state.tr;

  // 2. 에디터 상태를 구성하는 스키마를 가져와 strong 마크 를 생성한다.
  const mySchema = view.state.schema;
  const strongMark = mySchema.marks.strong.create();

  // 3. 텍스트 선택의 시작점과 끝점을 가져온다.
  const selection = view.state.selection;
  const from = selection.from;
  const to = selection.to;

  // 4. 시작점과 끝점 사이에 strong 마크를 입힌다는 내용을 트랜잭션 객체에 담는다.
  transaction.addMark(from, to, strongMark);

  // 5. 트랜잭션 객체를 에디터에 전달한다.
  view.dispatch(transaction);
});

노드 유형이란?

ProseMirror에서 텍스트 문서를 구성하는 기본 단위.
문서는 노드들의 트리로 구성되며, 각 노드 유형은 문서의 특정 부분을 나타낸다.

예를 들어, 문단(paragraph), 제목(heading), 목록(list) 등은 모두 노드 유형에 해당된다.

ProseMirror의 노드 유형은 다음과 같이 정의된다.

  • text: 텍스트 노드를 나타냄. 즉, 텍스트 내용이 들어가는 부분을 의미.

  • doc: 문서 노드를 나타냅니다. 문서의 최상위 노드이며, 보통 모든 내용은 doc 노드 안에 포함된다.

  • 기본 블록 노드(Block Nodes): 문서의 블록 레벨에 위치하는 노드들을 나타낸다. 보통 하나의 블록 노드는 하나 이상의 인라인 노드를 포함할 수 있다.

    예시: paragraph, heading, blockquote, list_item, ordered_list, bullet_list

  • 기본 인라인 노드(Inline Nodes): 문서의 인라인 레벨에 위치하는 노드들을 나타낸다. 보통 하나의 인라인 노드는 텍스트나 다른 인라인 노드를 포함할 수 있다.

    예시: text, em, strong, code, link


노드 유형(Node Type)을 정의할 때 사용되는 속성

group 속성

group은 해당 노드 유형이 어떤 그룹에 속하는지를 나타내는 속성.

노드 그룹은 노드의 역할과 구조를 정의하며,
ProseMirror에서는 크게 두 가지 그룹으로 구분된다.

  • block: 블록 노드 그룹은 문서의 블록 레벨 요소를 나타내며, 보통 하나의 블록 노드는 하나 이상의 인라인 노드를 포함할 수 있다.

  • inline: 인라인 노드 그룹은 문서의 인라인 레벨 요소를 나타내며, 보통 하나의 인라인 노드는 텍스트나 다른 인라인 노드를 포함할 수 있다.

예를 들어, 문단(paragraph) 노드는 group: "block"으로 정의되어 블록 노드 그룹에 속하며, 강조(em) 노드는 group: "inline"으로 정의되어 인라인 노드 그룹에 속한다.

content 속성

content는 노드 유형의 내용(content)을 나타내는 속성.

해당 노드 유형이 어떤 다른 노드 유형들을 포함할 수 있는지를 정의하며,
다양한 방식으로 정의할 수 있다.

  • "inline*": 0개 이상의 인라인 노드를 가진다.

  • "block+": 1개 이상의 블록 노드를 가진다.

  • "(paragraph)+": 1개 이상의 paragraph 노드를 가진다.

다른 노드 유형을 배열로 나열하여 특정 순서로 노드들을 포함할 수도 있다.

예를 들어, 문단(paragraph) 노드는 "inline*"으로 정의되어 텍스트와 인라인 노드들을 내용으로 포함할 수 있다.

paragraph: {
      content: "inline*",
      group: "block",
      parseDOM: [{ tag: "p" }],
      toDOM() {
        return ["p", 0];
      },
    },

parseDOM 속성

parseDOM은 HTML DOM을 ProseMirror 노드로 변환하는데 사용되는 규칙을 정의하는 속성.

배열 형태로 여러 개의 객체를 포함할 수 있으며,
각 객체는 특정 HTML 태그를 어떤 ProseMirror 노드로 변환할지를 정의.

객체의 구조는 다음과 같다.

  • tag: 변환 대상이 되는 HTML 태그를 나타낸다.

  • getAttrs("p"): 해당 태그로부터 노드의 속성을 추출하는 함수를 정의할 수 있다. 이 함수를 사용하여 노드의 속성을 설정할 수 있다.

예를 들어, <p> 태그를 paragraph 노드로 변환하고 싶다면 { tag: "p" }와 같이 설정.

toDOM 속성

toDOM은 ProseMirror 노드를 HTML DOM으로 변환하는데 사용되는 규칙을 정의하는 속성.

해당 노드를 어떤 HTML 태그로 표현할지를 정의.

배열 형태의 값을 반환하는 함수를 정의하여 여러 개의 HTML 태그를 사용할 수도 있다.
예를 들어, paragraph 노드를 <p> 태그로 표현하고 싶다면 ["p", 0]과 같이 설정.


노드 트리란?

노드트리(Node Tree)는 ProseMirror에서 텍스트 문서를 구성하는 노드들이 계층 구조로 연결된 모델이다.

텍스트 에디터에서 작성된 문서는 노드들의 트리 형태로 표현된다.

트리 구조에서 노드들은 부모-자식 관계를 갖는다.
가장 최상위 노드는 문서 전체를 나타내며, 이 노드는 다른 모든 노드들의 부모 노드가 된다.
이 하위에 다른 노드들이 계층적으로 연결되며, 노드들의 트리 구조를 따라가면 문서의 구조와 내용을 파악할 수 있다.

예를 들어, 다음과 같은 문단을 포함한 문서를 생각해보자.

This is a paragraph.

이 문서를 ProseMirror에서 표현한 노드 트리는 다음과 같을 수 있다.

Document (최상위 노드)
└─── Paragraph (문단 노드)
     └─── Text (텍스트 노드)
  • Document: 문서의 최상위 노드이며, 모든 내용을 포함한다.
  • Paragraph: 문단을 나타내며, Text 노드를 자식으로 갖는다.
  • Text: 실제 텍스트를 나타내며, "This is a paragraph."라는 텍스트를 포함한다.

에디터에서 사용자가 문서를 편집하면, 노드 트리가 적절하게 변경되고 업데이트된다.
사용자 입력에 따라 노드들이 추가, 삭제, 수정되어 에디터가 항상 최신 상태의 문서를 표시할 수 있도록 관리된다.


현재 사용된 ProseMirror 메서드

create 메서드

EditorStateTransaction 클래스에서 호출되며, 해당 클래스의 새로운 인스턴스를 생성.

예를 들어, EditorState.create({ schema })는 주어진 schema를 사용하여 새로운 EditorState 인스턴스를 생성.

schema: 에디터에서 사용하는 스키마(schema)를 전달. schema는 에디터의 문서 모델을 정의하는데 사용. 이를 통해 에디터의 동작과 허용되는 노드 유형, 서식 등을 제어할 수 있다.

addMark(from, to, mark)

Transaction 클래스에서 호출되며, 주어진 시작 지점 from과 끝 지점 to 사이에 특정 마크(mark:서식을 나타내는 객체)를 추가한다.

TransactionProseMirror의 편집 동작을 표현하며, Transaction을 사용하여 문서의 상태를 변경할 수 있다.
이 메서드를 사용하면 Transactionmark를 추가하여 문서의 텍스트에 특정 서식을 적용할 수 있다.

from: 마크를 추가할 텍스트의 시작 위치를 나타내는 오프셋(offset)
to: 마크를 추가할 텍스트의 끝 위치를 나타내는 오프셋(offset)
mark: 추가할 마크 객체를 전달. 마크는 서식을 나타내는 객체로, 예를 들어 강조(italic)나 굵게(bold) 등의 서식을 나타낼 수 있다.

offset : 텍스트에서 특정 위치를 가리키는 값

dispatch(transaction)

EditorView 클래스에서 호출되며, EditorViewtransaction을 적용하여 편집 동작을 수행한다.

사용자가 에디터에서 텍스트를 입력하거나 서식을 변경하면, 해당 동작은 transaction으로 표현되어 dispatch() 메서드를 통해 View에 적용되며, 문서의 state가 업데이트된다.

transaction: 적용할 트랜잭션 객체를 전달

updateState(newState)

updateState() 메서드는 EditorView 클래스에서 호출되며, 새로운 EditorState 객체(newState)를 사용하여 에디터 뷰(View)를 업데이트한다.

즉, 새로운 상태를 기반으로 에디터의 텍스트 내용이 변경되거나 화면에 다시 렌더링된다.

apply(transaction)

EditorState 클래스에서 호출되며, 주어진 transaction을 현재 상태에 적용하여 새로운 상태를 반환한다.

이 메서드는 transaction을 사용하여 현재 문서 상태를 변경하는 데 사용된다.

예를 들어, view.state.apply(transaction)view.state 객체에 transaction을 적용하여 새로운 상태를 반환한다.

transaction: 적용할 트랜잭션 객체를 전달


js에서 class란?
https://velog.io/@jjinichoi/javascript-class

profile
개발짜🏃‍♀️

0개의 댓글