InfoGrab DocsInfoGrab Docs

리치 텍스트 편집기 개발 가이드라인

요약

리치 텍스트 편집기는 GitLab 애플리케이션에서 GitLab Flavored Markdown을 WYSIWYG 방식으로 편집할 수 있게 해 주는 UI 컴포넌트입니다. 리치 텍스트 편집기는 Tiptap 2.0과 ProseMirror로 만듭니다.

리치 텍스트 편집기는 GitLab 애플리케이션에서 GitLab Flavored Markdown을 WYSIWYG 방식으로 편집할 수 있게 해 주는 UI 컴포넌트입니다. 또한 정적 사이트 생성기 같은 다른 엔진을 대상으로 하는 Markdown 중심 편집기를 구현하는 기반 역할도 합니다.

리치 텍스트 편집기는 Tiptap 2.0과 ProseMirror로 만듭니다. 이 프레임워크들은 네이티브 contenteditable 웹 기술 위에 한 단계의 추상화를 제공합니다.

사용 가이드#

기능에 리치 텍스트 편집기를 포함하려면 다음 절차를 따릅니다.

  1. 리치 텍스트 편집기 컴포넌트 포함하기.
  2. Markdown 설정 및 가져오기.
  3. 변경 사항 수신하기.

리치 텍스트 편집기 컴포넌트 포함하기#

ContentEditor Vue 컴포넌트를 임포트합니다. ContentEditor는 크기가 큰 의존성이므로 캐시를 활용할 수 있도록 비동기 명명 임포트를 사용하기를 권장합니다.

<script>
export default {
  components: {
    ContentEditor: () =>
      import(
        /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
      ),
  },
  // rest of the component definition
}
</script>

리치 텍스트 편집기에는 두 가지 속성이 필요합니다.

  • renderMarkdown은 Markdown API 호출 결과(String)를 반환하는 비동기 함수입니다.
  • uploadsPath는 multipart/form-data를 지원하는 GitLab 업로드 서비스를 가리키는 URL 입니다.

이 두 속성의 실제 사용 예는 WikiForm.vue 컴포넌트를 참고합니다.

Markdown 설정 및 가져오기#

Markdown을 설정하고 가져오는 작업은 비용이 크기 때문에 ContentEditor Vue 컴포넌트는 Vue 데이터 바인딩 흐름(v-model)을 구현하지 않습니다. 데이터 바인딩을 사용하면 사용자가 컴포넌트와 상호작용할 때마다 이 작업이 실행됩니다.

대신 initialized 이벤트를 수신해 ContentEditor 클래스의 인스턴스를 얻습니다.

<script>
import { createAlert } from '~/alert';
import { __ } from '~/locale';

export default {
  methods: {
    async loadInitialContent(contentEditor) {
      this.contentEditor = contentEditor;

      try {
        await this.contentEditor.setSerializedContent(this.content);
      } catch (e) {
        createAlert({ message: __('Could not load initial document') });
      }
    },
    submitChanges() {
      const markdown = this.contentEditor.getSerializedContent();
    },
  },
};
</script>
<template>
  <content-editor
    :render-markdown="renderMarkdown"
    :uploads-path="pageInfo.uploadsPath"
    @initialized="loadInitialContent"
  />
</template>

변경 사항 수신하기#

리치 텍스트 편집기의 변경 사항에는 그대로 반응할 수 있습니다. 이때는 @change 이벤트 핸들러를 사용합니다.

<script>
export default {
  data() {
    return {
      disabled: false,
    };
  },
  methods: {
    handleContentEditorChange({ markdown }) {
      this.disabled = !!/XXX/.exec(markdown);
    }
  },
};
</script>
<template>
  <div>
    <content-editor
      :render-markdown="renderMarkdown"
      :uploads-path="pageInfo.uploadsPath"
      @initialized="loadInitialContent"
      @change="handleContentEditorChange"
    />
    <gl-button :disabled="disabled" @click="submitChanges">
      {{ __('Submit changes') }}
    </gl-button>
  </div>

</template>

구현 가이드#

리치 텍스트 편집기는 세 개의 주요 계층으로 구성됩니다.

  • 편집 도구 UI는 툴바나 표 구조 편집기 같은 요소입니다. 편집기의 상태를 표시하고 커맨드를 디스패치해 상태를 변경합니다.
  • Tiptap Editor 객체는 편집기의 상태를 관리하고, 편집 도구 UI가 실행하는 커맨드 형태로 비즈니스 로직을 노출합니다.
  • Markdown 직렬화기는 Markdown 소스 문자열을 ProseMirror 문서로, 또 그 반대로 변환합니다.

편집 도구 UI#

편집 도구 UI는 편집기의 상태를 표시하고 상태를 변경하는 커맨드를 디스패치하는 Vue 컴포넌트입니다. 이 컴포넌트는 ~/content_editor/components 디렉터리에 있습니다. 예를 들어 Bold 툴바 버튼은 사용자가 굵은 텍스트를 선택하면 활성 상태가 되어 편집기의 상태를 표시합니다. 또한 이 버튼은 텍스트를 굵게 서식 지정하기 위해 toggleBold 커맨드를 디스패치합니다.

Mermaid 다이어그램 (5줄)
소스 코드 보기
sequenceDiagram
    participant A as Editing tools UI
    participant B as Tiptap object
    A->>B: queries state/dispatches commands
    B--)A: notifies state changes

노드 뷰#

표나 이미지 같은 일부 콘텐츠 유형에 인라인 편집 도구를 제공하기 위해 노드 뷰를 구현합니다. 노드 뷰를 사용하면 콘텐츠 유형의 표현을 그 모델에서 분리할 수 있습니다. 표현 계층에 Vue 컴포넌트를 사용하면 리치 텍스트 편집기에서 정교한 편집 경험을 구현할 수 있습니다. 노드 뷰는 ~/content_editor/components/wrappers에 있습니다.

커맨드 디스패치#

Vue 컴포넌트에 Tiptap Editor 객체를 주입해 커맨드를 디스패치할 수 있습니다.

Note

편집기의 상태를 바꾸는 로직은 Vue 컴포넌트에 구현하지 않습니다. 이 로직은 커맨드에 캡슐화하고, 컴포넌트의 메서드에서 해당 커맨드를 디스패치합니다.

<script>
export default {
  inject: ['tiptapEditor'],
  methods: {
    execute() {
      //Incorrect
      const { state, view } = this.tiptapEditor.state;
      const { tr, schema } = state;
      tr.addMark(state.selection.from, state.selection.to, null, null, schema.mark('bold'));

      // Correct
      this.tiptapEditor.chain().toggleBold().focus().run();
    },
  }
};
</script>
<template>

편집기 상태 쿼리#

문서나 선택 영역이 바뀌는 등 편집기 상태의 변화에 반응하려면 렌더리스 컴포넌트인 EditorStateObserver를 사용합니다. 다음 이벤트를 수신할 수 있습니다.

  • doc-update
  • selection-update
  • transaction
  • focus
  • blur
  • error.

이 이벤트에 대한 자세한 내용은 Tiptap 이벤트 가이드에서 확인할 수 있습니다.

<script>
// Parts of the code has been hidden for efficiency
import EditorStateObserver from './editor_state_observer.vue';

export default {
  components: {
    EditorStateObserver,
  },
  data() {
    return {
      error: null,
    };
  },
  methods: {
    displayError({ message }) {
      this.error = message;
    },
    dismissError() {
      this.error = null;
    },
  },
};
</script>
<template>
  <editor-state-observer @error="displayError">
    <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="dismissError">
      {{ error }}
    </gl-alert>
  </editor-state-observer>
</template>

Tiptap 편집기 객체#

Tiptap Editor 클래스는 편집기의 상태를 관리하고 리치 텍스트 편집기를 구동하는 비즈니스 로직 전체를 캡슐화합니다. 리치 텍스트 편집기는 이 클래스의 인스턴스를 새로 생성하고 GitLab Flavored Markdown을 지원하는 데 필요한 확장을 모두 제공합니다.

새 확장 구현#

확장은 리치 텍스트 편집기를 이루는 구성 요소입니다. 새 확장을 구현하는 방법은 Tiptap 가이드에서 확인할 수 있습니다. 새 확장을 처음부터 구현하기 전에 내장 노드와 마크 목록을 먼저 확인하기를 권장합니다.

리치 텍스트 편집기 확장은 ~/content_editor/extensions 디렉터리에 저장합니다. Tiptap 내장 확장을 사용할 때는 이 디렉터리 안에서 ES6 모듈로 감쌉니다.

export { Bold as default } from '@tiptap/extension-bold';

확장의 동작을 커스터마이즈할 때는 extend 메서드를 사용합니다.

import { HardBreak } from '@tiptap/extension-hard-break';

export default HardBreak.extend({
  addKeyboardShortcuts() {
    return {
      'Shift-Enter': () => this.editor.commands.setHardBreak(),
    };
  },
});

확장 등록#

새 확장은 ~/content_editor/services/create_content_editor.js에 등록합니다. 확장 모듈을 임포트한 뒤 builtInContentEditorExtensions 배열에 추가합니다.

import Emoji from '../extensions/emoji';

const builtInContentEditorExtensions = [
  Code,
  CodeBlockHighlight,
  Document,
  Dropcursor,
  Emoji,
  // Other extensions
]

Markdown 직렬화기#

Markdown 직렬화기는 Markdown 문자열을 ProseMirror 문서로, 또 그 반대로 변환합니다.

역직렬화#

역직렬화는 Markdown을 ProseMirror 문서로 변환하는 과정입니다. 먼저 Markdown API 엔드포인트로 Markdown을 HTML로 렌더링해 ProseMirror의 HTML 파싱 및 직렬화 기능을 활용합니다.

Mermaid 다이어그램 (12줄)
소스 코드 보기
sequenceDiagram
    participant A as rich text editor
    participant E as Tiptap object
    participant B as Markdown serializer
    participant C as Markdown API
    participant D as ProseMirror parser
    A->>B: deserialize(markdown)
    B->>C: render(markdown)
    C-->>B: html
    B->>D: to document(html)
    D-->>A: document
    A->>E: setContent(document)

역직렬화기는 확장 모듈 안에 있습니다. 구현 방법은 Tiptap 문서의 parseHTML과 addAttributes를 참고합니다. Tiptap API는 ProseMirror 스키마 스펙 API를 감싼 래퍼입니다.

직렬화#

직렬화는 ProseMirror 문서를 Markdown으로 변환하는 과정입니다. Content Editor는 문서를 직렬화하는 데 prosemirror-markdown을 사용합니다. 직렬화기를 구현하기 전에 MarkdownSerializer와 MarkdownSerializerState 클래스 문서를 읽어 보기를 권장합니다.

Mermaid 다이어그램 (7줄)
소스 코드 보기
sequenceDiagram
    participant A as rich text editor
    participant B as Markdown serializer
    participant C as ProseMirror Markdown
    A->>B: serialize(document)
    B->>C: serialize(document, serializers)
    C-->>A: Markdown string

prosemirror-markdown을 사용하려면 리치 텍스트 편집기가 지원하는 콘텐츠 유형마다 직렬화기 함수를 구현해야 합니다. 직렬화기는 ~/content_editor/services/markdown_serializer.js에 구현합니다.

리치 텍스트 편집기 개발 가이드라인

GitLab v19.4
원문 보기

요약

리치 텍스트 편집기는 GitLab 애플리케이션에서 GitLab Flavored Markdown을 WYSIWYG 방식으로 편집할 수 있게 해 주는 UI 컴포넌트입니다. 리치 텍스트 편집기는 Tiptap 2.0과 ProseMirror로 만듭니다.

리치 텍스트 편집기는 GitLab 애플리케이션에서 GitLab Flavored Markdown을 WYSIWYG 방식으로 편집할 수 있게 해 주는 UI 컴포넌트입니다. 또한 정적 사이트 생성기 같은 다른 엔진을 대상으로 하는 Markdown 중심 편집기를 구현하는 기반 역할도 합니다.

리치 텍스트 편집기는 Tiptap 2.0과 ProseMirror로 만듭니다. 이 프레임워크들은 네이티브 contenteditable 웹 기술 위에 한 단계의 추상화를 제공합니다.

사용 가이드#

기능에 리치 텍스트 편집기를 포함하려면 다음 절차를 따릅니다.

  1. 리치 텍스트 편집기 컴포넌트 포함하기.
  2. Markdown 설정 및 가져오기.
  3. 변경 사항 수신하기.

리치 텍스트 편집기 컴포넌트 포함하기#

ContentEditor Vue 컴포넌트를 임포트합니다. ContentEditor는 크기가 큰 의존성이므로 캐시를 활용할 수 있도록 비동기 명명 임포트를 사용하기를 권장합니다.

<script>
export default {
  components: {
    ContentEditor: () =>
      import(
        /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
      ),
  },
  // rest of the component definition
}
</script>

리치 텍스트 편집기에는 두 가지 속성이 필요합니다.

  • renderMarkdown은 Markdown API 호출 결과(String)를 반환하는 비동기 함수입니다.
  • uploadsPath는 multipart/form-data를 지원하는 GitLab 업로드 서비스를 가리키는 URL 입니다.

이 두 속성의 실제 사용 예는 WikiForm.vue 컴포넌트를 참고합니다.

Markdown 설정 및 가져오기#

Markdown을 설정하고 가져오는 작업은 비용이 크기 때문에 ContentEditor Vue 컴포넌트는 Vue 데이터 바인딩 흐름(v-model)을 구현하지 않습니다. 데이터 바인딩을 사용하면 사용자가 컴포넌트와 상호작용할 때마다 이 작업이 실행됩니다.

대신 initialized 이벤트를 수신해 ContentEditor 클래스의 인스턴스를 얻습니다.

<script>
import { createAlert } from '~/alert';
import { __ } from '~/locale';

export default {
  methods: {
    async loadInitialContent(contentEditor) {
      this.contentEditor = contentEditor;

      try {
        await this.contentEditor.setSerializedContent(this.content);
      } catch (e) {
        createAlert({ message: __('Could not load initial document') });
      }
    },
    submitChanges() {
      const markdown = this.contentEditor.getSerializedContent();
    },
  },
};
</script>
<template>
  <content-editor
    :render-markdown="renderMarkdown"
    :uploads-path="pageInfo.uploadsPath"
    @initialized="loadInitialContent"
  />
</template>

변경 사항 수신하기#

리치 텍스트 편집기의 변경 사항에는 그대로 반응할 수 있습니다. 이때는 @change 이벤트 핸들러를 사용합니다.

<script>
export default {
  data() {
    return {
      disabled: false,
    };
  },
  methods: {
    handleContentEditorChange({ markdown }) {
      this.disabled = !!/XXX/.exec(markdown);
    }
  },
};
</script>
<template>
  <div>
    <content-editor
      :render-markdown="renderMarkdown"
      :uploads-path="pageInfo.uploadsPath"
      @initialized="loadInitialContent"
      @change="handleContentEditorChange"
    />
    <gl-button :disabled="disabled" @click="submitChanges">
      {{ __('Submit changes') }}
    </gl-button>
  </div>

</template>

구현 가이드#

리치 텍스트 편집기는 세 개의 주요 계층으로 구성됩니다.

  • 편집 도구 UI는 툴바나 표 구조 편집기 같은 요소입니다. 편집기의 상태를 표시하고 커맨드를 디스패치해 상태를 변경합니다.
  • Tiptap Editor 객체는 편집기의 상태를 관리하고, 편집 도구 UI가 실행하는 커맨드 형태로 비즈니스 로직을 노출합니다.
  • Markdown 직렬화기는 Markdown 소스 문자열을 ProseMirror 문서로, 또 그 반대로 변환합니다.

편집 도구 UI#

편집 도구 UI는 편집기의 상태를 표시하고 상태를 변경하는 커맨드를 디스패치하는 Vue 컴포넌트입니다. 이 컴포넌트는 ~/content_editor/components 디렉터리에 있습니다. 예를 들어 Bold 툴바 버튼은 사용자가 굵은 텍스트를 선택하면 활성 상태가 되어 편집기의 상태를 표시합니다. 또한 이 버튼은 텍스트를 굵게 서식 지정하기 위해 toggleBold 커맨드를 디스패치합니다.

Mermaid 다이어그램 (5줄)
소스 코드 보기
sequenceDiagram
    participant A as Editing tools UI
    participant B as Tiptap object
    A->>B: queries state/dispatches commands
    B--)A: notifies state changes

노드 뷰#

표나 이미지 같은 일부 콘텐츠 유형에 인라인 편집 도구를 제공하기 위해 노드 뷰를 구현합니다. 노드 뷰를 사용하면 콘텐츠 유형의 표현을 그 모델에서 분리할 수 있습니다. 표현 계층에 Vue 컴포넌트를 사용하면 리치 텍스트 편집기에서 정교한 편집 경험을 구현할 수 있습니다. 노드 뷰는 ~/content_editor/components/wrappers에 있습니다.

커맨드 디스패치#

Vue 컴포넌트에 Tiptap Editor 객체를 주입해 커맨드를 디스패치할 수 있습니다.

Note

편집기의 상태를 바꾸는 로직은 Vue 컴포넌트에 구현하지 않습니다. 이 로직은 커맨드에 캡슐화하고, 컴포넌트의 메서드에서 해당 커맨드를 디스패치합니다.

<script>
export default {
  inject: ['tiptapEditor'],
  methods: {
    execute() {
      //Incorrect
      const { state, view } = this.tiptapEditor.state;
      const { tr, schema } = state;
      tr.addMark(state.selection.from, state.selection.to, null, null, schema.mark('bold'));

      // Correct
      this.tiptapEditor.chain().toggleBold().focus().run();
    },
  }
};
</script>
<template>

편집기 상태 쿼리#

문서나 선택 영역이 바뀌는 등 편집기 상태의 변화에 반응하려면 렌더리스 컴포넌트인 EditorStateObserver를 사용합니다. 다음 이벤트를 수신할 수 있습니다.

  • doc-update
  • selection-update
  • transaction
  • focus
  • blur
  • error.

이 이벤트에 대한 자세한 내용은 Tiptap 이벤트 가이드에서 확인할 수 있습니다.

<script>
// Parts of the code has been hidden for efficiency
import EditorStateObserver from './editor_state_observer.vue';

export default {
  components: {
    EditorStateObserver,
  },
  data() {
    return {
      error: null,
    };
  },
  methods: {
    displayError({ message }) {
      this.error = message;
    },
    dismissError() {
      this.error = null;
    },
  },
};
</script>
<template>
  <editor-state-observer @error="displayError">
    <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="dismissError">
      {{ error }}
    </gl-alert>
  </editor-state-observer>
</template>

Tiptap 편집기 객체#

Tiptap Editor 클래스는 편집기의 상태를 관리하고 리치 텍스트 편집기를 구동하는 비즈니스 로직 전체를 캡슐화합니다. 리치 텍스트 편집기는 이 클래스의 인스턴스를 새로 생성하고 GitLab Flavored Markdown을 지원하는 데 필요한 확장을 모두 제공합니다.

새 확장 구현#

확장은 리치 텍스트 편집기를 이루는 구성 요소입니다. 새 확장을 구현하는 방법은 Tiptap 가이드에서 확인할 수 있습니다. 새 확장을 처음부터 구현하기 전에 내장 노드와 마크 목록을 먼저 확인하기를 권장합니다.

리치 텍스트 편집기 확장은 ~/content_editor/extensions 디렉터리에 저장합니다. Tiptap 내장 확장을 사용할 때는 이 디렉터리 안에서 ES6 모듈로 감쌉니다.

export { Bold as default } from '@tiptap/extension-bold';

확장의 동작을 커스터마이즈할 때는 extend 메서드를 사용합니다.

import { HardBreak } from '@tiptap/extension-hard-break';

export default HardBreak.extend({
  addKeyboardShortcuts() {
    return {
      'Shift-Enter': () => this.editor.commands.setHardBreak(),
    };
  },
});

확장 등록#

새 확장은 ~/content_editor/services/create_content_editor.js에 등록합니다. 확장 모듈을 임포트한 뒤 builtInContentEditorExtensions 배열에 추가합니다.

import Emoji from '../extensions/emoji';

const builtInContentEditorExtensions = [
  Code,
  CodeBlockHighlight,
  Document,
  Dropcursor,
  Emoji,
  // Other extensions
]

Markdown 직렬화기#

Markdown 직렬화기는 Markdown 문자열을 ProseMirror 문서로, 또 그 반대로 변환합니다.

역직렬화#

역직렬화는 Markdown을 ProseMirror 문서로 변환하는 과정입니다. 먼저 Markdown API 엔드포인트로 Markdown을 HTML로 렌더링해 ProseMirror의 HTML 파싱 및 직렬화 기능을 활용합니다.

Mermaid 다이어그램 (12줄)
소스 코드 보기
sequenceDiagram
    participant A as rich text editor
    participant E as Tiptap object
    participant B as Markdown serializer
    participant C as Markdown API
    participant D as ProseMirror parser
    A->>B: deserialize(markdown)
    B->>C: render(markdown)
    C-->>B: html
    B->>D: to document(html)
    D-->>A: document
    A->>E: setContent(document)

역직렬화기는 확장 모듈 안에 있습니다. 구현 방법은 Tiptap 문서의 parseHTML과 addAttributes를 참고합니다. Tiptap API는 ProseMirror 스키마 스펙 API를 감싼 래퍼입니다.

직렬화#

직렬화는 ProseMirror 문서를 Markdown으로 변환하는 과정입니다. Content Editor는 문서를 직렬화하는 데 prosemirror-markdown을 사용합니다. 직렬화기를 구현하기 전에 MarkdownSerializer와 MarkdownSerializerState 클래스 문서를 읽어 보기를 권장합니다.

Mermaid 다이어그램 (7줄)
소스 코드 보기
sequenceDiagram
    participant A as rich text editor
    participant B as Markdown serializer
    participant C as ProseMirror Markdown
    A->>B: serialize(document)
    B->>C: serialize(document, serializers)
    C-->>A: Markdown string

prosemirror-markdown을 사용하려면 리치 텍스트 편집기가 지원하는 콘텐츠 유형마다 직렬화기 함수를 구현해야 합니다. 직렬화기는 ~/content_editor/services/markdown_serializer.js에 구현합니다.