Skip to content

에디터 저장 및 고려 사항들

Hyunjun KIM edited this page Nov 16, 2024 · 1 revision

고민

백엔드 기술적 도전으로 가져갈 수 있을 것들

  1. 실시간 편집의 race condition 해결을 위한.. 버전 관리..?
  2. 그래프는 조금 더 생각해봐야할 것 같긴 하지만

Graph Data Structure in Relational Database.pdf

원본 https://levelup.gitconnected.com/designing-a-graph-data-structure-in-a-relational-database-e13ffb857ce2

→ 확실히 노드의 타입이 여러 개가 되면 더 고려해볼 수 있는 것들이 생길 것 같음

블록단위로 그래프를 만들게 한다는 방법이 있긴 할텐데.. 될 지 모르겠어서 프로토타입을 만들어봐야할 것 같다.. 일단 생각나는 방법은.. 그래프의 각 노드 자체를 novel 에디터로 만들어버리는 거..? 근데 한 블록만 쓰게 강제시키면.. 블록 단위 그래프처럼 보이게 할 수는.. 있..나..? 사실 눈속임이긴 할텐데..

image

이런 식으로..?

그래프 뷰는 다담주 일정일 것 같으니까 천천히 생각해보자..

에디터 데이터 타입

import/export할 수 있는 데이터는 다음과 같이 nested된 형태임. (novel이든 tiptap이든)

const defaultValue = {
  type: 'doc',
  content: [
    {
      type: 'paragraph',
      content: []
    }
  ]
}
  • 긴 버전 예시

    {
      "type": "doc",
      "content": [
        {
          "type": "paragraph",
          "content": [
            {
              "type": "text",
              "text": "/"
            }
          ]
        },
        {
          "type": "codeBlock",
          "attrs": {
            "language": null
          },
          "content": [
            {
              "type": "text",
              "text": "export default function RootLayout({\n children,\n}: Readonly<{\n children: React.ReactNode;\n}>) {sssssssssssssssss\nconst b = True;\n return (\n <html lang=\"en\">xss\n <body\ntype={true}\n className={`${geistSans.variable} ${geistMono.variable} antialiased`}\n >\n {children}\n </body>\n </html>\n );\n}"
            }
          ]
        },
        {
          "type": "blockquote",
          "content": [
            {
              "type": "paragraph",
              "content": [
                {
                  "type": "text",
                  "text": "예시."
                }
              ]
            }
          ]
        },
        {
          "type": "paragraph"
        },
        {
          "type": "paragraph"
        },
        {
          "type": "image",
          "attrs": {
            "alt": null,
            "height": null,
            "src": "https://pjrjxbdononaezaz.public.blob.vercel-storage.com/zustand-logo-IQDtwK7U9LPf59HKnIT12XSCWXtTtN.jpeg",
            "title": null,
            "width": null
          }
        },
        {
          "type": "orderedList",
          "attrs": {
            "tight": true,
            "start": 1
          }
        },
        {
          "type": "horizontalRule"
        },
        {
          "type": "heading",
          "attrs": {
            "level": 3
          },
          "content": [
            {
              "type": "text",
              "text": "Learn more"
            }
          ]
        },
        {
          "type": "taskList",
          "content": [
            {
              "type": "taskItem",
              "attrs": {
                "checked": false
              }
            }
          ]
        },
        {
          "type": "taskList",
          "content": [
            {
              "type": "taskItem",
              "attrs": {
                "checked": false
              }
            }
          ]
        },
        {
          "type": "orderedList",
          "attrs": {
            "tight": true,
            "start": 1
          }
        }
      ]
    }

타입은

(alias) type JSONContent = {
    [key: string]: any;
    type?: string;
    attrs?: Record<string, any>;
    content?: JSONContent[];
    marks?: {
        type: string;
        attrs?: Record<string, any>;
        [key: string]: any;
    }[];
    text?: string;
}

중요한 건 content: JSONContent[]이라는 점…

  1. 즉 mongodb를 쓴다했을때는
@Schema()
export class Content {
  @Prop()
  type?: string;

  @Prop({ type: Object })
  attrs?: Record<string, any>;

  @Prop({ type: [{ type: Object }] })
  content?: JSONContent[];

  @Prop({ type: [{ 
    type: { type: String },
    attrs: { type: Object }
  }] })
  marks?: Array<{
    type: string;
    attrs?: Record<string, any>;
  }>;

  @Prop()
  text?: string;
}

사실 JSONContent가,, 재활용 될일이 없음. 즉, 그냥 그대로 string으로 바꿔서 저장해도 되돌릴 수만 있으면 괜찮을 듯.

  1. 즉, 페이지에 필요한 정보만.
@Entity()
export class Content {
  @PrimaryGeneratedColumn()
  id: number;

  @Column('text')
  content: string;  
  
  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

문제는 어디까지 저장할 수 있는지 모르겠다..

  1. PostgreSQL에 JSONB 있던뎅
type JSONContent = {
  [key: string]: any;
  type?: string;
  attrs?: Record<string, any>;
  content?: JSONContent[];  
  marks?: {
    type: string;
    attrs?: Record<string, any>;
    [key: string]: any;
  }[];
  text?: string;
}

@Entity()
export class Document {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'jsonb' })
  content: JSONContent;
}

일케..?

JSON 형식 유지해서 저장할 때 장점 - 쿼리할 때 편하다

우리가 할 쿼리 - search → ElasticSearch..?

결국 과제는 JSON 형태의 nested 된 테이터를 DB에 저장하고 text 매칭 쿼리하기.

어느 정도의 난이도인 건지 잘 모르겠따..

저장 방법

버튼으로 저장

const [content, setContent] = useState<string>('')

// ...

<Editor initialValue={defaultValue} onChange={setContent} />
      <Button onClick={handleSubmit} disabled={pending}>
        {pending ? 'Submitting...' : 'Create'}
      </Button>
</Editor>
  • tailwind example에서는 JSONContent으로 함.

    const [value, setValue] = useState<JSONContent>(defaultValue);

자동으로 저장

<EditorContent
          onUpdate={({ editor }) => {
            debouncedUpdates(editor);
            setSaveStatus("Unsaved");
          }}
/>

이렇게 onUpdate으로 editor 전체를 저장할 수 있음.

이것도 실시간 기능 추가할 때 충돌 방지.. 어떻게 할지.. 생각해야함.. 일단 다 보내야할지..

이미지 저장

content에서 이미지는 이런 식으로 표현

{
  "5": {
    "type": "image",
    "attrs": {
      "alt": null,
      "height": null,
      "src": "https://pjrjxbdononaezaz.public.blob.vercel-storage.com/zustand-logo-IQDtwK7U9LPf59HKnIT12XSCWXtTtN.jpeg",
      "title": null,
      "width": null
    }
  },
  "type": "image"
}

그니까 이미지 업로드는

const { url } = (await res.json()) as { url: string };

const image = new Image();
image.src = url;
image.onload = () => {
  resolve(url);
};

이미지를 따로 관리해야될 필요가 없다면 s3나 uploadthing에 바로 올리고 주소 받아오면 될 듯..?

/upload api 만들어서 url을 돌려주게 하면 됨..!

나머지는 그냥 비슷한 방식.. 아마 코드블럭이 제일 궁금할텐데

코드블럭

{
  "1": {
    "type": "codeBlock",
    "attrs": {
      "language": null
    },
    "content": [
      {
        "type": "text",
        "text": "export default function RootLayout({\n children,\n}: Readonly<{\n children: React.ReactNode;\n}>) {const b = True;\n return (\n <html lang=\"en\">xss\n <body\ntype={true}\n className={`${geistSans.variable} ${geistMono.variable} antialiased`}\n >\n {children}\n </body>\n </html>\n );\n}"
      }
    ]
  },
  "type": "codeBlock"
}

이건 진짜 text.

저장되는 JSONContent 보는 방식

image

novel.sh, 예시에서 로컬 스토리지에 저장되는 값 확인.

최적화

예시에서도 debounce 사용 중. 진짜 모든 변화를 다 저장하고 싶으면 api request를 클라이언트에서 배치 처리 하겠지만, 사실 그럴 필요가 없긴 함..!

개발 문서

⚓️ 사용자 피드백과 버그 기록
👷🏻 기술적 도전
📖 위키와 학습정리
🚧 트러블슈팅

팀 문화

🧸 팀원 소개
⛺️ 그라운드 룰
🍞 커밋 컨벤션
🧈 이슈, PR 컨벤션
🥞 브랜치 전략

그룹 기록

📢 발표 자료
🌤️ 데일리 스크럼
📑 회의록
🏖️ 그룹 회고
🚸 멘토링 일지
Clone this wiki locally