VibeTimes
#기술

TIPTAP v2カスタムノードによるHTMLレンダリング実装方法の総まとめ

송시옥송시옥 기자· 2026/8/18 9:11:57· Updated 2026/8/18 11:23:38

Tiptap v2カスタムノード、どこから始めるべきか

ヘッドレスエディタという出発点

Tiptap v2はProseMirrorベースの「headless」エディタです。UIを自ら描画せず、データ構造をJSONモデルで管理する方式です。基本的な提供ノードであるParagraph、Heading、Imageだけでは、実際のWebサービスで頻繁に使われるdivベースのボックス、iframe、特定のclassが付いたコンテナのようなHTML構造を表現するのは困難です。ユーザー定義HTMLをエディタ内で挿入、編集、レンダリングするには、カスタムノード拡張を直接作成する必要があります。

実装のパスは大きく二手に分かれます。既存ノードのHTMLタグをdivやsectionに変える単純なオーバーライドが一つ目です。複雑なロジックなしに出力だけ変えればよい場合に使います。その代わり、クリックイベントや内部ボタンのようなインタラクションの実装は難しいです。二つ目はNodeViewを実装する方式です。React、Vue、Svelteのようなフレームワークコンポーネントをノードとして統合できるため、最も強力で推奨されるアプローチです。Tiptapノード(データ)とNodeViewコントローラー、フレームワークコンポーネント(UI)の3層で構成されます。

Schema定義がデータの骨格だ

カスタムノードはNodeExtensionを継承して定義します。name属性でノードを識別し、groupを通じてブロックレベルかインラインレベルかを指定します。この値に基づき、エディタがカーソル位置とノードの挙動を制御します。addAttributesでノードが持つ属性を定義しますが、サンプルコードではcount(デフォルト値0)とbackgroundColor(デフォルト値#f0f0f0)を宣言しました。

parseHTMLは、外部から貼り付けられたHTMLをエディタのデータモデルに変換するルールです。例えば、div[data-type="custom-box"]セレクタを指定すると、該当する属性を持つdivに遭遇した際、このノードとして認識します。その逆方向であるrenderHTMLは、ノードを['div', { 'data-type': 'custom-box', style: ... }]形式のタグ配列に変換し、エディタ画面にレンダリングします。この2つのルールが正確に対応していないと、保存後に再読み込みする際にコンテンツが崩れるため、双方向のマッピングに留意する必要があります。

NodeViewアーキテクチャとレンダリング戦略

静的HTMLはtoDOM、動的UIはNodeView

固定された形状のバナーやボックスのように、インタラクションのないUIであれば、重いコンポーネント構造は不要です。toDOMメソッドで['div', { class: 'my-box' }, 0]のような配列でHTMLを直接定義すればよいのです。レンダリング性能に優れ、Tailwind CSSのユーティリティクラスやインラインスタイルで視覚的なスタイルを即座に適用できます。

ボタンクリックに反応したり、内部状態を持つ必要がある要素はNodeViewで実装します。v2ではフレームワークごとのレンダリング方式を抽象化して提供しているため、Reactコンポーネントをcomponent属性にバインドすると、Tiptapがそれをマウントし、状態とPropでデータをやり取りします。サンプルコードを見ると、コンポーネントがnode.attrs.countをuseStateで初期化し、increment関数でローカル状態を更新した後、updateAttributes({ count: newCount })でエディタ状態を同期しています。ローカル状態だけ変更してもドキュメントデータには反映されないため、この同期呼び出しが鍵となります。

性能とUXを左右するupdateのオーバーライド

NodeViewのupdateメソッドをどのようにオーバーライドするかが性能を左右します。外部データが変更された際、コンポーネント全体を再レンダリングするか、あるいは既存のDOM状態を維持したまま差分のみ反映するかを決定するポイントだからです。無条件に再レンダリングすると、大容量文档での編集が途切れる原因となります。

contentEditable属性の調整も、緻密なユーザー体験の核心です。特定領域のみ編集可能にするよう開放したり、ウィジェット全体を読み取り専用にロックするよう制御します。スライダー、クイズ、YouTube埋め込みのような対話型要素をエディタ内に入れる際、カーソルがウィジェット内部に誤って進入する問題を、この属性でブロックできます。

直列化とデータ互換性の管理

getHTML出力を自分で手直しする

エディタの内容をデータベースに保存したり外部にエクスポートする際、デフォルトのtoHTMLロジックだけでは望む通りの構造が出力されないことが多々あります。カスタムノードのrenderHTMLメソッドをオーバーライドすれば、asideやfigureのようなセマンティックタグで囲ったり、子要素のフォーマットを強制できます。その結果、エディタ内部のJSONデータとは別に、Webサイトのフロントエンドに最適化されたマークアップをクリーンに抽出できます。

直列化と逆直列化の役割分担

JSONをHTMLに変える直列化と、その逆工程である逆直列化は必ず同期されていなければなりません。いずれか一方のルールが欠けると、保存された文档を再び開いた際にノードが消えたり、平文に変質したりします。addOptionsを活用してノード別のレンダリングオプションを分離する戦略が効果的です。フロントエンド出力モードとエディタ編集モードを分ければ、一つのコードベースで二つのレンダリングロジックを統合管理できます。この構造は、エディタで作成したスタイルが実際のWebサイトで崩れる現象を根本的に防ぎます。

選択基準をまとめると

結局のところ、実装の選択は要件の複雑度にかかっています。タグとクラスを変えるだけで足りる静的UIなら、タグのオーバーライドで十分です。状態とイベントが必要なら、NodeViewとフレームワークコンポーネントの組み合わせが正解です。いずれにせよ、parseHTMLとrenderHTMLの双方向の対応、そしてエディタ状態の同期ルールを守ることが、カスタムHTMLノード実装の成否を分かつと言えるでしょう。

쿠팡 파트너스 활동의 일환으로 일정 수수료를 제공받습니다

関連記事