-
Notifications
You must be signed in to change notification settings - Fork 5
에디터 저장 및 고려 사항들
백엔드 기술적 도전으로 가져갈 수 있을 것들
- 실시간 편집의 race condition 해결을 위한.. 버전 관리..?
- 그래프는 조금 더 생각해봐야할 것 같긴 하지만
Graph Data Structure in Relational Database.pdf
→ 확실히 노드의 타입이 여러 개가 되면 더 고려해볼 수 있는 것들이 생길 것 같음
블록단위로 그래프를 만들게 한다는 방법이 있긴 할텐데.. 될 지 모르겠어서 프로토타입을 만들어봐야할 것 같다.. 일단 생각나는 방법은.. 그래프의 각 노드 자체를 novel 에디터로 만들어버리는 거..? 근데 한 블록만 쓰게 강제시키면.. 블록 단위 그래프처럼 보이게 할 수는.. 있..나..? 사실 눈속임이긴 할텐데..
이런 식으로..?
그래프 뷰는 다담주 일정일 것 같으니까 천천히 생각해보자..
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[]
이라는 점…
- 즉 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
으로 바꿔서 저장해도 되돌릴 수만 있으면 괜찮을 듯.
- 즉, 페이지에 필요한 정보만.
@Entity()
export class Content {
@PrimaryGeneratedColumn()
id: number;
@Column('text')
content: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
문제는 어디까지 저장할 수 있는지 모르겠다..
- 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 보는 방식
novel.sh, 예시에서 로컬 스토리지에 저장되는 값 확인.
최적화
예시에서도 debounce 사용 중. 진짜 모든 변화를 다 저장하고 싶으면 api request를 클라이언트에서 배치 처리 하겠지만, 사실 그럴 필요가 없긴 함..!
⚓️ 사용자 피드백과 버그 기록
👷🏻 기술적 도전
📖 위키와 학습정리
✏️ 에디터
Novel이란?
Novel 스타일링 문제
에디터 저장 및 고려 사항들
📠 실시간 협업, 통신
Yorkie와 Novel editor 연동
YJS, Websocket, React-Flow
YJS, Socket.io
WebSocket과 Socket.io에 대해 간단히 알아보기
YJS 가이드 근데 이제 Socket.io를 곁들인
🏗️ 인프라와 CI/CD
NCloud CI CD 구축
BE 개발 스택과 기술적 고민
private key로 원격 서버 접근
nCloud 서버, VPC 만들고 설정
monorepo로 변경
⌛ 캐시, 최적화
rabbit mq 사용법
🔑 인증, 인가, 보안
passport로 oAuth 로그인 회원가입 구현
FE 로그인 기능 구현
JWT로 인증 인가 구현
JWT 쿠키로 사용하기
refresh token 보완하기
🧸 팀원 소개
⛺️ 그라운드 룰
🍞 커밋 컨벤션
🧈 이슈, PR 컨벤션
🥞 브랜치 전략
🌤️ 데일리 스크럼
📑 회의록
1️⃣ 1주차
킥오프(10/25)
2일차(10/29)
3일차(10/30)
4일차(10/31)
2️⃣ 2주차
8일차(11/04)
9일차(11/05)
11일차(11/07)
13일차(11/09)
3️⃣ 3주차
3주차 주간계획(11/11)
16일차(11/12)
18일차(11/14)
4️⃣ 4주차
4주차 주간계획(11/18)
23일차(11/19)
24일차(11/20)
25일차(11/21)
5️⃣ 5주차
5주차 주간계획(11/25)
29일차(11/25)
32일차(11/28)
34일차(11/30)
6️⃣ 6주차
6주차 주간계획(12/2)
37일차(12/3)