따꿍의 프로젝트

[2026.02.09] 에디터 TF 배경, TipTap 선택 이유, 라이브러리 세팅하기 본문

웹프로젝트/스노로즈

[2026.02.09] 에디터 TF 배경, TipTap 선택 이유, 라이브러리 세팅하기

공장 주인 따꿍 2026. 2. 9. 16:13

에디터 TF팀 배경

구 스노로즈 데이터를 신규 스노로즈로 옮길려니, 

예전에는 에디터가 있어서 색깔/기울기/볼드 등등 이런 추가적 기능들이 글에 첨가되어 있다. 

이런 글들을 신규 스노로즈에 에디터를 추가하지 않고 마이그레이션하려니까

해당 글들이 깨질것 같았다. 

이런 기능들을 쓰고 있는 글들이 버릴 수 있는 사소한 글도 아니었기에 (정보성 글도 꽤 많았음)
에디터를 추가하기로 결정되었다. 

TipTap 선택 이유

React 전용 에디터 라이브러리
Pro (장점)

- 툴바에 대한 UI를 직접 선택해서 (어떤 기능을 넣고 뺄지 선택 가능) 버튼과 기능 연결만 하면 됨

- React 친화적

- 확장성 - 그림 삽입과 주석 기능이 구현하기 쉬움

가벼움 -> 필요한 기능만 골라서 설치 가능해서, 필요없는 기타 기능들 때문에 느려지는 것 방지 가능하다. 

 

TipTap에서 구현하고 싶은 기능

구 스노로즈 에디터 기능 
=> 최소한으로 추가해야하는 에디터 기능들이다. 
- 글꾸: 폰트종류 / 크기 / 굵기 / 기울이기 / 밑줄 / 취소 / 색(배경,글자)
서식: 정렬 / 줄간격 / 목록
그림: 그림 추가 (글사이), 주석
+ (확장하고 싶은 기능: 투표, 지도, 링크 등)

 

허나 프런트엔드 쪽에서 이런 의견이 나왔다. 

- 사용자마다 서식(크기, 색상 등)을 다르게 사용할 경우,
전체적인 피드 및 게시글 상세 화면의 시각적 일관성이 떨어질 우려가 있습니다.

- 모바일 환경 특성상 좁은 화면 내에 다수의 에디터 도구 모음이 노출될 경우 사용성이 저하될 우려가 있습니다. 
- 커뮤니티는 방대한 정보를 기록하는 블로그와 달리, 빠른 정보 소비와 활발한 소통이 주 목적이라고 생각해서 간결한 인터페이스를 추구하는 것이 좋을 것 같습니다.

- 리자들이 작성하는 공지글에 먼저 일부 에디터 기능 (주요 색상 지원, 굵기 등)을 도입해보는게 어떤지? 반면 일반 유저는 링크삽입, 기울림, 취소선, 굵기 정도의 기능만 순차적으로 도입하기

=> 가독성을 해치지 않는 선에서 링크 삽입, 기울임(Italic), 취소선 3가지 기능만 제공하는게 어떤지

 

운기 팀에서는 반면 이러한 의견이 나왔다.

- 학우들이 에브리타임 말고 우리 페이지를 사용하는 데에는 블로그성이 강한 특징 때문도 있다고 생각했다
(글꾸가 가능한 점이 스노로즈만의 강점이라고 생각했음)

- 그래서 에디터 기능 최대한 쓰고 싶긴 하다

 

<결론>

 

TipTap 세팅 방법

https://tiptap.dev/docs/editor/getting-started/install/react

 

React | Tiptap Editor Docs

Learn how to integrate the Tiptap Editor with a React app and develop your custom editor experience.

tiptap.dev

1. 라이브러리 설치하기

npm install @tiptap/react @tiptap/pm @tiptap/starter-kit

 

2. TipTap 컴포넌트 생성하기

Hook-based Manual Approach

import { useMemo } from 'react';
import {
  useEditor,
  EditorContent,
  EditorContext,
  useCurrentEditor,
} from '@tiptap/react';
import { FloatingMenu, BubbleMenu } from '@tiptap/react/menus';
import StarterKit from '@tiptap/starter-kit';

import styles from './TipTap.module.css';

export default function TipTap() {
  const editor = useEditor({
    extensions: [StarterKit], // define your extension array
    content: '<p>Hello World!</p>', // initial content
  });

  // Memoize the provider value to avoid unnecessary re-renders
  const providerValue = useMemo(() => ({ editor }), [editor]);

  return (
    <EditorContext.Provider value={providerValue}>
      <EditorContent editor={editor} />
      <FloatingMenu editor={editor}>This is the floating menu</FloatingMenu>
      <BubbleMenu editor={editor}>This is the bubble menu</BubbleMenu>
    </EditorContext.Provider>
  );
}

//editor의 상태 정보를 JSON 형태로 꺼냄
//EditorContext의 자식 컴포넌트들이 import해서 사용할 수 있음
export function EditorJSONPreview() {
  const { editor } = useCurrentEditor();

  return editor.getJSON();
}

 

 

1. useEditor

  • Tiptap 에디터 생성 및 관리
  • 기본 설정 값들 설정
    • extensions: 에디터에 어떤 기능을 넣을지 정하는 설정 ex) 굵기, 기울림, 리스트, 등 (StarterKit는 기본 기능 묶음)
    • content: 에디터 열었을 때 처음 써 있는 글 (placeholder 같은 느낌)
    • immediatelyRender: 에디터를 바로 화면에 그릴지 말지 정하는 옵션 false면 React 렌더 후에 에디터를 생성한다 보통 SSR을 적용하기 위해, 또는 hydration 에러를 피하려고 false로 세팅한다

2. useMemo

  • 부모가 리렌더링 되어도 editor을 담은 객체 참조를 Memoize해서 context 소비자들이 쓸데없이 리렌더링되지 않게 막는다.
  • 없을 시, EditorContext를 쓰는 모든 자식 컴포넌트가 불필요하게 리렌더링 된다.
    • 타이핑 중 UI가 미세하게 버벅
    • extension이 많을수록 더 체감됨

3. EditorContext.Provider

  • TipTap 구성 요소들을 EditorContext로 둘러싸서 에디터 안의 상태들과 글씨들을 Context 안의 자식들도 자유로이 꺼낼 수 있게 해준다.
  • useCurrentEditor() internally does useContext(EditorContext), so it always gets the editor instance from the nearest EditorContext.Provider above it in the React tree.
  • ⭐ Context의 자식 컴포넌트에서 context를 꺼내는 방식은 다음과 같다. Json 말고도 받아올 수 있는건 다음 표에 정리되어 있다.
export function EditorJSONPreview() {
  const { editor } = useCurrentEditor();

  return editor.getJSON();
}

 

4. Context 안의 컴포넌트들

더보기

+ useContext Reminder

In modern React (React 19 / latest docs), you can now use the context object itself as the Provider:

Context.Provider still exists, but you don’t have to write .Provider anymore.

 

tiptap은 그냥 .Provider 쓰던 시절에 만들어져서 쓰는듯

import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext value={theme}>
      <Form />
      <Button onClick={() => {
        setTheme('light');
      }}>
        Switch to light theme
      </Button>
    </ThemeContext>
  )
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

 

(+) 잘 쓰일지 모르겠는 기능: Reacting to Editor State Changes
To react to editor state changes, you can use the useEditorState hook from @tiptap/react. This hook can be used to fetch information from the editor state without causing re-renders on the editor component or it's children.

import { useEditorState } from '@tiptap/react'

function MyEditorComponent() {
  // ... your editor setup code

  const editorState = useEditorState({
    editor,

    // the selector function is used to select the state you want to react to
    selector: ({ editor }) => {
      if (!editor) return null;

      return {
        isEditable: editor.isEditable,
        currentSelection: editor.state.selection,
        currentContent: editor.getJSON(),
        // you can add more state properties here e.g.:
        // isBold: editor.isActive('bold'),
        // isItalic: editor.isActive('italic'),
      };
    },
  });
}

TipTap 세팅하는 다른 간편한 방식이 있긴 한데, 

아직 정식 배포된건 아니다.

 

PS) Composable API 방식

Is ideal when you 

  • Want a more declarative, component-based approach
  • Need to access the editor from multiple child components
  • Prefer automatic context management over manual prop passing
  • Want built-in loading states and SSR-friendly patterns

우리 프로젝트는 복잡하기도 하고, 그렇게 direct control이 필요한 경우도 없을 것 같아서
이렇게 간단한 방식을 사용하는게 좋을것 같다. 

-> 사용해봤는데 future v3 syntax에 존재할 거란다 (아직 stable한 상태로 npm에 제공하지 않음)..... 못씀..

// src/Editor.tsx
import { Tiptap, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function Editor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World!</p>',
  })

  return (
    <Tiptap instance={editor}>
      <Tiptap.Loading>Loading editor...</Tiptap.Loading>
      <MenuBar />
      <Tiptap.Content />
      <Tiptap.BubbleMenu>
        <button>Bold</button>
        <button>Italic</button>
      </Tiptap.BubbleMenu>
      <Tiptap.FloatingMenu>
        <button>Add heading</button>
      </Tiptap.FloatingMenu>
    </Tiptap>
  )
}

export default Editor

 

1번 방식과 다르게 Composable API를 사용하면 에디터의 상태를 미리 제공된 hook으로 확인 가능하다.

useTiptap hook

- 에디터 상태 확인할 수 있게 해주는 

- The useTiptap hook returns the editor instance and an isReady flag that indicates whether the editor has finished initializing.

- useCurrentEditor이 EditorContext.Provider 안의 자식들에게만 사용됐듯이, 
    useTiptap hook도 <Tiptap>컴포넌트 안의 자식들에게만 사용 가능하다

import { useTiptap } from '@tiptap/react'

function MenuBar() {
  const { editor, isReady } = useTiptap()

  if (!isReady || !editor) {
    return null
  }

  return (
    <div className="menu-bar">
      <button
        onClick={() => editor.chain().focus().toggleBold().run()}
        className={editor.isActive('bold') ? 'is-active' : ''}
      >
        Bold
      </button>
    </div>
  )
}

 

useTiptapState

- For performance-sensitive components, use useTiptapState to subscribe to specific parts of the editor state.
- Prevents unnecessary re-renders when unrelated state changes.

- Only use useTiptapState when the editor is ready. Check isReady from useTiptap() before rendering components that use this hook.

import { useTiptap, useTiptapState } from '@tiptap/react'

function WordCount() {
  const { isReady } = useTiptap()

  const wordCount = useTiptapState((state) => {
    const text = state.editor.state.doc.textContent
    return text.split(/\s+/).filter(Boolean).length
  })

  if (!isReady) {
    return null
  }

  return <span>{wordCount} words</span>
}

Without this,

  • The component re-renders on every editor transaction
  • Cursor move → re-render
  • Selection change → re-render
  • Undo history update → re-render
  • Plugin metadata change → re-render

원리는 다음과 같다:

  1. Tiptap subscribes to editor transactions
  2. After each transaction, it runs your selector function
  3. It compares the previous return value with the new return value
  4. Only if they are different, React re-renders the component

So the subscription is not to the editor, but to the result of your selector.


팀 노션 페이지에 문서화 하기

귀찮아도 개인적으로 정리한거 팀 노션 페이지에도 정리하자

팀원들이 보면 좋을거 아니야

https://www.notion.so/snorose/TF-1e87ef0aa3bf8097953ac6a583543426?source=copy_link