Skip to Content
ํ™•์žฅ

ํ™•์žฅ

Inkio ํ™•์žฅ์€ ๋‘ ์ธต์œผ๋กœ ๋‚˜๋‰ฉ๋‹ˆ๋‹ค.

  • @inkio/core
    • callout
    • details/toggle
    • table
    • keyboard shortcuts
  • @inkio/advanced
    • slash command
    • block handle
    • mention
    • hashtag
    • wiki link
    • bookmark
    • comment

๊ถŒ์žฅ ์‹œ์ž‘์ 

  • classic editor๊ฐ€ ํ•„์š”ํ•˜๋ฉด @inkio/simple
  • notion-like editor๊ฐ€ ํ•„์š”ํ•˜๋ฉด @inkio/editor
  • low-level ์กฐํ•ฉ์ด ํ•„์š”ํ•˜๋ฉด @inkio/core ๋˜๋Š” @inkio/advanced

์กฐํ•ฉ ์˜ˆ์‹œ

import { Editor } from '@inkio/core'; import { getDefaultExtensions } from '@inkio/advanced'; import '@inkio/core/minimal.css'; import '@inkio/advanced/style.css'; const extensions = getDefaultExtensions({ hashtagItems: ({ query }) => [{ id: query || 'inkio', label: `#${query || 'inkio'}` }], }); export function Page() { return <Editor extensions={extensions} initialContent="<p>Hello Inkio</p>" />; }

๋ฉ˜์…˜ (Mention)

@ ํŠธ๋ฆฌ๊ฑฐ๋กœ ์‚ฌ์šฉ์ž ๋“ฑ์„ ๋ฉ˜์…˜ํ•ฉ๋‹ˆ๋‹ค. mentionItems๋Š” ({ query }) => MentionItem[] ํ˜•ํƒœ์˜ ํ•จ์ˆ˜๋กœ, ์‚ฌ์šฉ์ž๊ฐ€ @ ๋’ค์— ์ž…๋ ฅํ•œ query์— ๋งž๋Š” ํ•ญ๋ชฉ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋™๊ธฐ/๋น„๋™๊ธฐ ๋ชจ๋‘ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

interface MentionItem { id: string; label: string; [key: string]: unknown; }
import { Editor } from '@inkio/editor'; <Editor mentionItems={async ({ query }) => { const users = await searchUsers(query); return users.map((user) => ({ id: user.id, label: user.name })); }} />

ํ•ด์‹œํƒœ๊ทธ๋„ ๋™์ผํ•œ ํŒจํ„ด์ด๋ฉฐ, # ํŠธ๋ฆฌ๊ฑฐ์™€ hashtagItems๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

<Editor hashtagItems={({ query }) => [ { id: query || 'inkio', label: `#${query || 'inkio'}` }, ]} />

@inkio/core + @inkio/advanced ์ง์ ‘ ์กฐํ•ฉ ์‹œ์—๋Š” getDefaultExtensions์— ๋™์ผํ•œ ์˜ต์…˜์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

const extensions = getDefaultExtensions({ mentionItems: async ({ query }) => searchUsers(query), });

์Šฌ๋ž˜์‹œ ์ปค๋งจ๋“œ (Slash Command)

/๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๋ช…๋ น ๋ฉ”๋‰ด๊ฐ€ ์—ด๋ฆฝ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ๋ช…๋ น์—๋Š” ํ—ค๋”ฉ, ๋ฆฌ์ŠคํŠธ, ํ‘œ, ์ฝ”๋“œ ๋ธ”๋ก, ์ฝœ์•„์›ƒ, ํ† ๊ธ€ ๋ฆฌ์ŠคํŠธ, ๋ชฉ์ฐจ, ์ด๋ฏธ์ง€ ๋“ฑ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์œผ๋กœ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

transformSlashCommands๋Š” ๊ธฐ๋ณธ ๋ช…๋ น ๋ฐฐ์—ด์„ ๋ฐ›์•„ ๊ฐ€๊ณตํ•œ ๋’ค ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ถ€ ๋ช…๋ น๋งŒ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ œ๊ฑฐํ•  ๋•Œ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜ํ™˜๋œ ํ•ญ๋ชฉ์€ ์ดํ›„ query๋กœ ํ•„ํ„ฐ๋ง๋ฉ๋‹ˆ๋‹ค.

import { Editor } from '@inkio/editor'; import type { SlashCommandItem } from '@inkio/advanced'; <Editor transformSlashCommands={(defaults, { editor }) => { const custom: SlashCommandItem = { id: 'today', label: 'Today', description: '์˜ค๋Š˜ ๋‚ ์งœ ์‚ฝ์ž…', command: ({ editor, range }) => { editor .chain() .focus() .deleteRange(range) .insertContent(new Date().toLocaleDateString()) .run(); }, }; return [custom, ...defaults]; }} />

slashCommands๋Š” ๊ธฐ๋ณธ ๋ช…๋ น์„ ๋ฌด์‹œํ•˜๊ณ  ๋ช…๋ น ๋ชฉ๋ก ์ „์ฒด๋ฅผ ์ง์ ‘ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์‹œ๊ทธ๋‹ˆ์ฒ˜๋Š” (query: string) => SlashCommandItem[] | Promise<SlashCommandItem[]>์ด๋ฉฐ, ์ด ๊ฒฝ์šฐ ํ•„ํ„ฐ๋ง๋„ ์ง์ ‘ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

<Editor slashCommands={(query) => myCommands.filter((item) => item.label.toLowerCase().includes(query.toLowerCase()), ) } />

SlashCommandItem์˜ isAvailable?: (editor) => boolean์œผ๋กœ ํŠน์ • ๋ช…๋ น์˜ ๋…ธ์ถœ ์กฐ๊ฑด์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

[[ํŽ˜์ด์ง€ ์ด๋ฆ„]] ๋ฌธ๋ฒ•์œผ๋กœ ์ธ๋ผ์ธ ์œ„ํ‚ค ๋งํฌ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๋‹ซ๋Š” ]]๋ฅผ ์ž…๋ ฅํ•˜๋Š” ์ˆœ๊ฐ„ ์ž…๋ ฅ ๊ทœ์น™(input rule)์ด ๋ฐœ๋™ํ•ด ํ…์ŠคํŠธ๊ฐ€ ์œ„ํ‚ค ๋งํฌ ๋…ธ๋“œ๋กœ ์น˜ํ™˜๋˜๋ฉฐ, [[...]]๊ฐ€ ํฌํ•จ๋œ ํ…์ŠคํŠธ๋ฅผ ๋ถ™์—ฌ๋„ฃ์–ด๋„ ๋™์ผํ•˜๊ฒŒ ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค.

@inkio/editor์—์„œ๋Š” wikiLink prop์œผ๋กœ ์ผœ๊ณ  ๋„๋ฉฐ ๊ธฐ๋ณธ๊ฐ’์€ true์ž…๋‹ˆ๋‹ค. ๋งํฌ ํด๋ฆญ ๋™์ž‘์€ onWikiLinkClick ์ฝœ๋ฐฑ์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ฝœ๋ฐฑ์„ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๋งํฌ๋Š” ํด๋ฆญํ•  ์ˆ˜ ์—†๋Š” ํ…์ŠคํŠธ๋กœ๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

import { Editor } from '@inkio/editor'; <Editor wikiLink onWikiLinkClick={(href) => { router.push(`/wiki/${encodeURIComponent(href)}`); }} />

href๋Š” [[...]] ์•ˆ์— ์ž…๋ ฅํ•œ ํŽ˜์ด์ง€ ์ด๋ฆ„ ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค. ์•ˆ์ „ํ•˜์ง€ ์•Š์€ URL์€ ํด๋ฆญ์ด ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค.

๋ถ๋งˆํฌ (Bookmark)

URL์„ ๋งํฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์นด๋“œ๋กœ ๋ Œ๋”๋งํ•˜๋Š” ๋ธ”๋ก ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค. @inkio/editor์—์„œ๋Š” bookmark prop์œผ๋กœ ์„ค์ •ํ•˜๋ฉฐ, false๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.

bookmark?: false | { onResolveBookmark?: (url: string) => Promise<BookmarkPreview> };

๋ถ๋งˆํฌ๋Š” setBookmark ์ปค๋งจ๋“œ๋กœ ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค.

editor.commands.setBookmark({ url: 'https://example.com' });

onResolveBookmark๋Š” URL์„ ๋ฐ›์•„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐ์ดํ„ฐ(BookmarkPreview)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋ถ๋งˆํฌ ๋…ธ๋“œ๋Š” ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„์ง ์—†์„ ๋•Œ ํ•œ ๋ฒˆ ์ด ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ๋ฐ˜ํ™˜๋œ ๊ฐ’์œผ๋กœ ์นด๋“œ๋ฅผ ์ฑ„์›๋‹ˆ๋‹ค. ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๋ถ๋งˆํฌ๋Š” ๋‹จ์ˆœ ๋งํฌ๋กœ๋งŒ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

interface BookmarkPreview { title?: string; description?: string; image?: string; favicon?: string; }
<Editor bookmark={{ onResolveBookmark: async (url) => { const meta = await fetchOpenGraph(url); // ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ return { title: meta.title, description: meta.description, image: meta.ogImage, favicon: meta.favicon, }; }, }} />

onResolveBookmark๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง๋งŒ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. Open Graph ์Šคํฌ๋ž˜ํ•‘ ๊ฐ™์€ ์‹ค์ œ ์กฐํšŒ๋Š” ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค(๋ณดํ†ต ์„œ๋ฒ„ ์ธก ์—”๋“œํฌ์ธํŠธ).

Image editor

import { Editor } from '@inkio/editor'; import { ImageEditorModal } from '@inkio/image-editor'; import '@inkio/image-editor/style.css'; export function Page() { return ( <Editor onImageUpload={async (file) => URL.createObjectURL(file)} imageBlock={{ imageEditor: ImageEditorModal }} /> ); }

์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ด๋ฏธ์ง€ ์—๋””ํ„ฐ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.

TocBlock (์ธ๋ผ์ธ ๋ชฉ์ฐจ)

@inkio/core์— ๊ธฐ๋ณธ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ๋ฌธ์„œ ์•ˆ์— Notion ์Šคํƒ€์ผ์˜ ์ธ๋ผ์ธ ๋ชฉ์ฐจ ๋ธ”๋ก์„ ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ : ์—๋””ํ„ฐ ์™ธ๋ถ€์— ์˜ค๋ฒ„๋ ˆ์ด ๋ฏธ๋‹ˆ๋งต ํ˜•ํƒœ๋กœ ํ‘œ์‹œํ•˜๋ ค๋ฉด <ToC> ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”. ์ปดํฌ๋„ŒํŠธ ๋ฌธ์„œ ์ฐธ๊ณ 

  • /toc ์Šฌ๋ž˜์‹œ ์ปค๋งจ๋“œ๋กœ ์‚ฝ์ž… (@inkio/advanced SlashCommand ํ•„์š”)
  • ํ—ค๋”ฉ์ด ์ถ”๊ฐ€/์ˆ˜์ •/์‚ญ์ œ๋˜๋ฉด ์ž๋™์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค
  • ํ•ญ๋ชฉ ํด๋ฆญ ์‹œ ํ•ด๋‹น ํ—ค๋”ฉ์œผ๋กœ ์Šคํฌ๋กค๋ฉ๋‹ˆ๋‹ค
  • ๋ฐฐ๊ฒฝ/ํ…Œ๋‘๋ฆฌ ์—†๋Š” Notion ์Šคํƒ€์ผ ๋ฏธ๋‹ˆ๋ฉ€ ๋””์ž์ธ (ํ…์ŠคํŠธ ๋งํฌ๋งŒ)
  • ํ—ค๋”ฉ์ด ์—†์œผ๋ฉด muted ํ”Œ๋ ˆ์ด์Šคํ™€๋” ํ‘œ์‹œ: โ€œAdd headings to see the table of contents.โ€

๋น„ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด getExtensions์— tocBlock: false๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค:

import { getExtensions } from '@inkio/core'; const extensions = getExtensions({ tocBlock: false });

CodeBlock (์ฝ”๋“œ ๋ธ”๋ก)

@inkio/core์— ๊ธฐ๋ณธ ํฌํ•จ๋˜๋ฉฐ, lowlight ๊ธฐ๋ฐ˜ ๊ตฌ๋ฌธ ๊ฐ•์กฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. 37๊ฐœ์˜ ์ผ๋ฐ˜์ ์ธ ์–ธ์–ด๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

  • ์—๋””ํ„ฐ ๋ชจ๋“œ: ํ”Œ๋กœํŒ… ์–ธ์–ด ์„ ํƒ๊ธฐ(๋“œ๋กญ๋‹ค์šด)๋กœ ์–ธ์–ด ๋ณ€๊ฒฝ
  • ๋ทฐ์–ด ๋ชจ๋“œ: ๋ณต์‚ฌ ๋ฒ„ํŠผ + ์–ธ์–ด ๋ฐฐ์ง€ ํ‘œ์‹œ
  • tsx / jsx๋Š” ๋ชจ๋‘ typescript์˜ ๋ณ„์นญ์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค

์ฝ”๋“œ ๋ธ”๋ก์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด getExtensions์— codeBlock: false๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

์ปค์Šคํ…€ ํ™•์žฅ

extensions prop์œผ๋กœ Tiptap ํ™•์žฅ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ๊ต์ฒดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํƒ€์ž…

type ExtensionsInput = | Extension[] | { items: Extension[]; replace?: boolean };
  • ๋ฐฐ์—ด ์ „๋‹ฌ: ๊ธฐ๋ณธ ํ™•์žฅ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋™์ผํ•œ ์ด๋ฆ„์˜ ํ™•์žฅ์ด ์žˆ์œผ๋ฉด ๊ต์ฒด๋ฉ๋‹ˆ๋‹ค.
  • { items, replace: true } ์ „๋‹ฌ: ๊ธฐ๋ณธ ํ™•์žฅ์„ ์ „๋ถ€ ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์‹œ: ํ™•์žฅ ์ถ”๊ฐ€

import { Editor } from '@inkio/editor'; import CharacterCount from '@tiptap/extension-character-count'; <Editor extensions={[CharacterCount.configure({ limit: 1000 })]} />

์˜ˆ์‹œ: ๊ธฐ๋ณธ ํ™•์žฅ ์ „์ฒด ๊ต์ฒด

import { Editor } from '@inkio/editor'; import { Document, Paragraph, Text } from '@tiptap/starter-kit'; <Editor extensions={{ items: [Document, Paragraph, Text], replace: true }} />

๊ธฐ๋Šฅ ํ† ๊ธ€ (@inkio/editor ์ „์šฉ)

@inkio/editor์˜ Editor ์ปดํฌ๋„ŒํŠธ๋Š” ์ฃผ์š” ํ™•์žฅ์„ prop์œผ๋กœ ์ผœ๊ณ  ๋Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Prop๊ธฐ๋ณธ๊ฐ’์„ค๋ช…
blockHandletrue๋ธ”๋ก ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค
wikiLinktrue[[์œ„ํ‚ค๋งํฌ]] ๋ฌธ๋ฒ•
tabletrueํ‘œ
callouttrue์ฝœ์•„์›ƒ ๋ธ”๋ก
toggleListtrueํ† ๊ธ€(์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ) ๋ฆฌ์ŠคํŠธ
<Editor blockHandle={false} wikiLink={false} />

CSS

  • @inkio/simple/minimal.css
  • @inkio/editor/minimal.css
  • @inkio/advanced/style.css โ€” @inkio/core + @inkio/advanced ์ง์ ‘ ์กฐํ•ฉ ์‹œ
  • @inkio/image-editor/style.css
Last updated on