VibeTimes

TIPTAP v2 커스텀 노드로 HTML 렌더링 구현 방법 총정리

송시옥송시옥 기자· 2026. 8. 18. AM 9:11:57· 수정 2026. 8. 18. AM 10:21:06

Tiptap v2 커스텀 노드, 어디서부터 시작해야 할까

헤드리스 에디터라는 출발점

Tiptap v2는 ProseMirror 기반의 "headless" 에디터다. UI를 스스로 그리지 않고 데이터 구조를 JSON 모델로 관리하는 방식이다. 기본 제공 노드인 Paragraph, Heading, Image만으로는 실제 웹 서비스에서 자주 쓰이는 div 기반 박스, iframe, 특정 class가 붙은 컨테이너 같은 HTML 구조를 표현하기 어렵다. 사용자 정의 HTML을 에디터 안에서 삽입하고 편집하고 렌더링하려면 커스텀 노드 확장을 직접 만들어야 한다.

구현 경로는 크게 두 갈래로 나뉜다. 기존 노드의 HTML 태그를 div나 section으로 바꾸는 단순 오버라이딩이 첫 번째다. 복잡한 로직 없이 출력만 바꾸면 될 때 쓴다. 대신 클릭 이벤트나 내부 버튼 같은 상호작용은 구현하기 어렵다. 두 번째는 NodeView를 구현하는 방식이다. 리액트, 뷰(Vue), 스벨트 같은 프레임워크 컴포넌트를 노드로 통합할 수 있어 가장 강력하고 권장되는 접근법이다. Tiptap 노드(데이터)와 NodeView 컨트롤러, 프레임워크 컴포넌트(UI) 세 층으로 구성된다.

Schema 정의가 데이터의 뼈대다

커스텀 노드는 NodeExtension을 상속받아 정의한다. name 속성으로 노드를 식별하고, group을 통해 블록 레벨인지 인라인 레벨인지 지정한다. 이 값에 따라 에디터가 커서 위치와 노드의 행동을 제어한다. addAttributes로 노드가 가질 속성을 정의하는데, 예시 코드에서는 count(기본값 0)와 backgroundColor(기본값 #f0f0f0)를 선언했다.

parseHTML은 외부에서 붙여넣은 HTML을 에디터 데이터 모델로 변환하는 규칙이다. 예를 들어 div[data-type="custom-box"] 셀렉터를 지정하면 해당 속성이 붙은 div를 만났을 때 이 노드로 인식한다. 반대 방향인 renderHTML은 노드를 ['div', { 'data-type': 'custom-box', style: ... }] 형태의 태그 배열로 변환해 에디터 화면에 렌더링한다. 두 규칙이 정확히 대응되지 않으면 저장 후 다시 불러올 때 콘텐츠가 깨지므로 쌍방향 매핑을 신경 써야 한다.

NodeView 아키텍처와 렌더링 전략

정적 HTML은 toDOM, 동적 UI는 NodeView

고정된 형태의 배너나 박스처럼 상호작용이 없는 UI라면 무거운 컴포넌트 구조가 필요 없다. toDOM 메서드에서 ['div', { class: 'my-box' }, 0] 같은 배열로 HTML을 직접 정의하면 된다. 렌더링 성능이 우수하고, Tailwind CSS 유틸리티 클래스나 인라인 스타일로 시각적 스타일을 즉시 적용할 수 있다.

버튼 클릭에 반응하거나 내부 상태를 가져야 하는 요소는 NodeView로 구현한다. v2에서는 프레임워크별 렌더링 방식을 추상화해 제공하므로, 리액트 컴포넌트를 component 속성에 바인딩하면 Tiptap이 이를 마운트하고 상태와 Prop으로 데이터를 주고받는다. 예시 코드를 보면 컴포넌트가 node.attrs.count를 useState로 초기화하고, increment 함수에서 로컬 상태를 갱신한 뒤 updateAttributes({ count: newCount })로 에디터 상태를 동기화한다. 로컬 상태만 바꾸면 문서 데이터에는 반영되지 않으므로 이 동기화 호출이 핵심이다.

성능과 UX를 좌우하는 update 오버라이딩

NodeView의 update 메서드를 어떻게 오버라이딩하느냐가 성능을 좌우한다. 외부 데이터가 변경됐을 때 컴포넌트 전체를 재랜더링할지, 아니면 기존 DOM 상태를 유지하며 차이만 반영할지 결정하는 지점이기 때문이다. 무조건 재랜더링하면 대용량 문서에서 편집이 끊기는 원인이 된다.

contentEditable 속성 조절도 세밀한 사용자 경험의 핵심이다. 특정 영역만 편집 가능하게 열거나, 위젯 전체를 읽기 전용으로 잠그는 식으로 제어한다. 슬라이더, 퀴즈, 유튜브 임베드 같은 대화형 요소를 에디터 안에 넣을 때 커서가 위젯 내부로 잘못 진입하는 문제를 이 속성으로 차단할 수 있다.

직렬화와 데이터 호환성 관리

getHTML 출력을 내 손으로 다듬기

에디터 내용을 데이터베이스에 저장하거나 외부로 내보낼 때 기본 toHTML 로직만으로는 원하는 구조가 나오지 않는 경우가 많다. 커스텀 노드의 renderHTML 메서드를 오버라이드하면 aside나 figure 같은 시맨틱 태그로 감싸거나 자식 요소의 포맷을 강제할 수 있다. 그 결과 에디터 내부 JSON 데이터와 별개로, 웹사이트 프론트엔드에 최적화된 마크업을 깔끔하게 추출한다.

직렬화와 역직렬화의 역할 분리

JSON을 HTML로 바꾸는 직렬화와 그 반대 과정인 역직렬화는 반드시 동기화돼야 한다. 어느 한쪽 규칙이 빠지면 저장된 문서를 다시 열 때 노드가 사라지거나 일반 텍스트로 변질된다. addOptions를 활용해 노드별 렌더링 옵션을 분리하는 전략이 효과적이다. 프론트엔드 출력 모드와 에디터 편집 모드를 나누면 하나의 코드 베이스로 두 렌더링 로직을 통합 관리할 수 있다. 이 구조는 에디터에서 작성한 스타일이 실제 웹사이트에서 깨지는 현상을 원천적으로 막는다.

선택 기준을 요약하면

결국 구현 선택은 요구사항의 복잡도에 달려 있다. 태그와 클래스만 바꾸면 충분한 정적 UI라면 태그 오버라이딩으로 충분하다. 상태와 이벤트가 필요하다면 NodeView와 프레임워크 컴포넌트 조합이 정답이다. 어느 쪽이든 parseHTML과 renderHTML의 쌍방향 대응, 그리고 에디터 상태 동기화 규칙을 지키는 것이 커스텀 HTML 노드 구현의 성패를 가른다고 정리할 수 있다.

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

데일리 브리핑 구독

매일 아침 핵심 뉴스를 이메일로 받아보세요. 무료

관련 기사