๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Frontend Dev/Output

[Mini Project] ํ”„๋ก ํŠธ์—”๋“œ ๊ณผ์ œํ…Œ์ŠคํŠธ๋กœ ๋งŒ๋‚œ Note-App ๊ตฌํ˜„ ๊ณผ์ • ์ •๋ฆฌ (2)

๋ฐ˜์‘ํ˜•

[Mini Project] Note-App

๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป Github Repository https://github.com/sw2377/note-app

 

 

์ž‘์—… ๊ธฐ๊ฐ„ : 2024. 01. 09 ~ 2024. 01. 16

ํ”„๋ก ํŠธ์—”๋“œ ์ฑ„์šฉ ๊ณผ์ œ๋กœ ์ˆ˜ํ–‰ํ•œ Note-App ๊ตฌํ˜„ ๊ณผ์ • (๊ตฌํ˜„ ์ค‘ ์—๋Ÿฌ์‚ฌํ•ญ & ๊ณ ๋ฏผํ•œ ๋ถ€๋ถ„)์„ ์ •๋ฆฌํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค.

 

๐Ÿ”— ์‹œ๋ฆฌ์ฆˆ ์ด์–ด๋ณด๊ธฐ

Note-App ๊ตฌํ˜„ ๊ณผ์ • ์ •๋ฆฌ (1)

ํ˜„์žฌ๊ธ€ : Note-App ๊ตฌํ˜„ ๊ณผ์ • ์ •๋ฆฌ (2)

Note-App ๊ตฌํ˜„ ๊ณผ์ • ์ •๋ฆฌ (3)

 

๐Ÿ“– ๋ชฉ์ฐจ

1. lexical text editor ์‚ฌ์šฉํ•˜๊ธฐ
2. ํ—ท๊ฐˆ๋ ธ๋˜ ๋ถ€๋ถ„ 1, props type ์ •์˜ ๋ฐฉ๋ฒ•
3. ํ—ท๊ฐˆ๋ ธ๋˜ ๋ถ€๋ถ„ 2, JSX๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋Š” React component์˜ return null
4. ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ค์› ๋˜ ์ž๋™์ €์žฅ ๊ธฐ๋Šฅ

 

1. lexical text editor ์‚ฌ์šฉํ•˜๊ธฐ

์š”๊ตฌ์‚ฌํ•ญ์— ์‚ฌ์šฉํ•˜๋ผ๊ณ  ์–ธ๊ธ‰๋˜์–ด ์žˆ์—ˆ๋˜ lexical text editor๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ค์› ๋‹ค.

๐Ÿ”— ๊ณต์‹ ํ™ˆํŽ˜์ด์ง€ : https://lexical.dev/

์™ธ๋ถ€ ์ž๋ฃŒ๋„ ๋ณ„๋กœ ์—†๊ณ , ๊ณต์‹ ํ™ˆํŽ˜์ด์ง€๋Š” ์นœ์ ˆํ•˜์ง€ ์•Š๋‹ค๋Š” ๋Š๋‚Œ์„ ๋ฐ›์•˜๋Š”๋ฐ ์•„๋งˆ ์ฒ˜์Œ ์‚ฌ์šฉํ•ด๋ด์„œ ๋”์šฑ ๊ทธ๋Ÿฐ ๋Š๋‚Œ์ด ๋“ค์—ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค. ์‚ฌ์‹ค ์ด๋ ‡๊ฒŒ ๊ณต์‹๋ฌธ์„œ๋งŒ ๋ณด๊ณ  ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ๊ฑฐ์˜ ์ฒ˜์Œ์ด๋ผ ์ต์ˆ™ํ•˜์ง€ ์•Š์•„ ์–ด๋ ต๊ฒŒ ๋Š๊ปด์กŒ๋‹ค. ์—ฌํŠผ lexical ์‚ฌ์šฉ๋ฒ•์„ ์ตํžˆ๊ณ , ์›ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ์‹œ๊ฐ„์„ ์ •๋ง ๋งŽ์ด ๋ณด๋ƒˆ๋‹ค.

 

์ฒซ ์„ธํŒ…์€ ๊ณต์‹๋ฌธ์„œ์˜ ๐Ÿ”— React์—์„œ ์„ธํŒ…ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ฐธ๊ณ ํ•ด์„œ ์ž‘์„ฑํ–ˆ๊ณ , ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•์€ ์˜๋„ํ•œ๋Œ€๋กœ ์ž‘๋™์ด ์ž˜ ๋˜์—ˆ๋‹ค.

import {useState, useEffect} from 'react';

import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';

const theme = {
  // Theme styling goes here
  paragraph: 'editor-paragraph',
}

const OnChangePlugin = ({ onChange }) => {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    return editor.registerUpdateListener(({editorState}) => {
      onChange(editorState);
    });
  }, [editor, onChange]);
}

const Editor = () => {
  const [editorState, setEditorState] = useState(""); // ํ˜„์žฌ ์—๋””ํ„ฐ์˜ ๊ธ€์ด ์ €์žฅ

  function onChange(editorState) {
    const editorStateJSON = editorState.toJSON();
    setEditorState(JSON.stringify(editorStateJSON));
  }

  function onError(error) {
    console.log(error)
  }

  const initialConfig = {
    namespace: 'MyEditor',
    theme,
    onError,
  };
  
  return (
    <div className="editorWrapper">
      <LexicalComposer initialConfig={initialConfig}>
        <PlainTextPlugin
          contentEditable={<ContentEditable className='contentEditable' />}
          placeholder={<div className='placeholder'>Enter some text...</div>}
          ErrorBoundary={LexicalErrorBoundary}
        />
        <HistoryPlugin />
        <OnChangePlugin onChange={onChange} />
    </LexicalComposer>
    </div>
  )
}

export default Editor

 

์ฒ˜์Œ์—๋Š” lexical Editor์˜ ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•์ด ํ—ท๊ฐˆ๋ ธ์—ˆ๋Š”๋ฐ, ๊ณ„์† ๋ณด๋‹ค๋ณด๋‹ˆ ์–ด๋–ค ๋ฐฉ์‹์ธ์ง€ ์ดํ•ด๊ฐ€ ์ข€ ๋˜๋Š”๋“ฏํ–ˆ๋‹ค.

์ปค์Šคํ…€ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๋งŒ๋“ค๋ฉด LexicalComposer ๋‚ด์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ํ•ด๋‹น ํ”Œ๋Ÿฌ๊ทธ์ธ ๋‚ด์—์„œ editor๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

 

export function MyLexicalPlugin() {
  const [editor] = useLexicalComposerContext();
  console.log("editor: ", editor)
}

<LexicalComposer initialConfig={initialConfig}>
  // ...
  <MyLexicalPlugin>
</LexicalComposer>

 

1-1) lexical editor์—์„œ initial state ๊ฐ€์ ธ์˜ค๊ธฐ

// Before
const initialEditorState = note.content

const initialConfig = {
  namespace: "MyEditor",
  theme,
  onError,
  editorState: initialEditorState
};

// โฌ‡๏ธŽ ๋ณ€๊ฒฝ //

// After
// InitPlugin : note์— ์ฒซ ์ง„์ž…์‹œ ์ดˆ๊ธฐ๊ฐ’ ์„ธํŒ…
export const InitPlugin = ({ note }) => {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    const editorState = editor.parseEditorState(note.content);
    editor.setEditorState(editorState);
  }, [note, editor]);

  return null;
};

 

์ฒ˜์Œ์—๋Š” initialConfig์— initialEditorState๋ฅผ ๋„ฃ์œผ๋ฉด ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ๋“ค์–ด์˜จ๋‹ค๊ณ  ํ•ด์„œ ๋„ฃ์—ˆ์—ˆ๋Š”๋ฐ, ์ด๋ ‡๊ฒŒ ํ•˜๋‹ˆ ์„ ํƒํ•œ Note๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ์—๋””ํ„ฐ์˜ ์ดˆ๊ธฐ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค. ๐Ÿ”— https://lexical.dev/docs/concepts/editor-state ์ด ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๋ฉด,

“Note that Lexical uses initialConfig.editorState only once (when it's being initialized) and passing different value later won't be reflected in editor. See "Update state" below for proper ways of updating editor state.”

“Lexical์€ initialConfig.editorState๋ฅผ ํ•œ ๋ฒˆ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค(์ดˆ๊ธฐํ™”๋  ๋•Œ๋งŒ ์‚ฌ์šฉ) ๊ทธ ์ดํ›„์—๋Š” ๋‹ค๋ฅธ ๊ฐ’ ์ „๋‹ฌ์ด ํŽธ์ง‘๊ธฐ์— ๋ฐ˜์˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ ์ ˆํ•œ ๋ฐฉ๋ฒ•์€ ๋‚˜์ค‘์— "์ƒํƒœ ์—…๋ฐ์ดํŠธ" ์„น์…˜์—์„œ ์„ค๋ช…ํ•œ ๋Œ€๋กœ ํŽธ์ง‘๊ธฐ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.”

 

์ด๋Ÿฌํ•œ ๋ถ€๋ถ„์ด ์žˆ๋Š”๋ฐ, ์ด ๋•Œ๋ฌธ์— ์ดˆ๊ธฐ์— ํ•œ๋ฒˆ ์‚ฌ์šฉ๋œ ์ดˆ๊ธฐ๊ฐ’์ด Note๋ฅผ ๋ฐ”๊พธ์–ด๋„ ์ƒˆ๋กœ์šด ๋…ธํŠธ์˜ ๊ฐ’์œผ๋กœ ๋“ค์–ด์˜ค์ง€ ์•Š๋Š”๊ฒƒ ๊ฐ™์•˜๋‹ค. (ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ์„ ํƒํ•œ ๋…ธํŠธ์˜ ๋‚ด์šฉ์ด ๋“ค์–ด์™”๋‹ค.)

๊ทธ๋ž˜์„œ initialEditorState ๋Œ€์‹  InitPlugin์„ ๋งŒ๋“ค์–ด์„œ note์— ์ฒซ ์ง„์ž…์‹œ ๊ธฐ์กด์— ๊ฐ€์ง€๊ณ  ์žˆ๋˜ note.content๋กœ ์ดˆ๊ธฐ๊ฐ’์„ ์„ธํŒ…ํ•ด์ฃผ๋„๋ก ํ•˜์˜€๋”๋‹ˆ ์ œ๋Œ€๋กœ ๋™์ž‘์ด ๋˜์—ˆ๋‹ค.

 

1-2) lexical editor์—์„œ Node ๊ฐ€์ ธ์˜ค๊ธฐ (์ œ๋ชฉ๊ณผ ๋‚ด์šฉ ๊ตฌ๋ถ„)

1. DOM์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ

// DOM์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ 1
const CustomStylePlugin = () => {
  const [editor] = useLexicalComposerContext();

  editor.getEditorState().read(() => {
   editor.getRootElement()?.getElementsByTagName("span")[0].classList.add("editor-title");
   editor.getRootElement()?.getElementsByTagName("span")[1]?.classList.add("editor-desc");
  })
}

 

ํ…์ŠคํŠธ ์—๋””ํ„ฐ์— ๊ธ€์„ ์ž…๋ ฅํ•˜๋ฉด pํƒœ๊ทธ ์•ˆ์˜ span ํƒœ๊ทธ๋กœ ๋“ค์–ด๊ฐ„๋‹ค. ๊ทธ๋ž˜์„œ ์ฒ˜์Œ์—๋Š” ์ด๋ ‡๊ฒŒ editor์—์„œ span ํƒœ๊ทธ๋ฅผ ์ฐพ์•„ ์ฒซ๋ฒˆ์งธ span ์š”์†Œ๋Š” ์ œ๋ชฉ์œผ๋กœ, ๋‘๋ฒˆ์งธ span ์š”์†Œ๋Š” ๋‚ด์šฉ์œผ๋กœ className์„ ์ง€์ •ํ•˜๋Š” ๋ฐฉ์‹์„ ๋– ์˜ฌ๋ ธ์—ˆ๋‹ค. ๋‚ด์šฉ์„ ๊ตฌ๋ถ„ํ•œ ์ด์œ ๋Š” NoteList์— ๋‚ด์šฉ์ด ๋“ค์–ด๊ฐ€์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

 

// DOM์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ 2
const theme = {
  paragraph: 'editor-paragraph',
}

const paragraphElement = document.querySelector(".editor-paragraph");
const title = paragraphElement?.childNodes[0]?.textContent;

 

๋˜๋Š” ์ด๋Ÿฐ ์‹์œผ๋กœ ์ฒ˜์Œ์— theme์—์„œ ์„ค์ •ํ•œ p ํƒœ๊ทธ์˜ className์„ ๊ฐ€์ง€๊ณ  ์˜ค๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์—ˆ๋‹ค.

 

2. textNode๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ → ์ตœ์ข…์ ์œผ๋กœ ์‚ฌ์šฉํ•œ ๋ฐฉ๋ฒ•

ํ•˜์ง€๋งŒ ๊ฒฐ๊ตญ ๋งˆ์ง€๋ง‰์œผ๋กœ ๋‚ด๊ฐ€ ์‚ฌ์šฉํ•œ ๋ฐฉ๋ฒ•์€, editorState์—์„œ textNode๋ฅผ ์ฐพ๋Š” ๋ฐฉ๋ฒ•์ด์—ˆ๋‹ค. Lexical Editor๋Š” Node๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Node๋ฅผ ์ฐพ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•˜๋‹ˆ ์ข€ ๋” ๊ฐ„ํŽธํ•˜๊ฒŒ ์ฒซ ๋ฒˆ์งธ์™€ ๋‘ ๋ฒˆ์งธ textNode๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

// Editor.tsx
// editorState์˜ ์ œ๋ชฉ์€ ์ฒซ๋ฒˆ์งธ textNode
const textNodes = Array.from(editorState._nodeMap.values()).find(
  node => node.__type === "text",
);
const titleNode = textNodes?.__text ?? null;
setTitleNode(titleNode);

// NoteList.tsx
// parsedEditorState์˜ ๋‚ด์šฉ์€ index 1dml textNode
const textNode: string = parsedEditorState.root?.children[0].children.filter(
  (node: any) => node.type === "text",
)[1]?.text;

 

์‹ค์ œ ์‚ฌ์šฉ์€ titleNode๋Š” Editor ์ปดํฌ๋„ŒํŠธ์—์„œ, contentNode๋Š” NoteList ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ฐ๊ฐ ๋”ฐ๋กœ ์‚ฌ์šฉํ•˜์˜€๋‹ค.

 

๐Ÿ“Œ Array Method, filter์™€ find์˜ ์ฐจ์ด

์œ„ textNode๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ์™€ ์ด์–ด์ง€๋Š” ๋‚ด์šฉ์ธ๋ฐ, ์ œ๋ชฉ์„ ๊ฐ€์ ธ์˜ฌ ๋•Œ๋Š” Array ๋ฉ”์†Œ๋“œ ์ค‘ find๋ฅผ, ๋‚ด์šฉ์„ ๊ฐ€์ ธ์˜ฌ ๋•Œ๋Š” filter๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. ํ‰์†Œ ๋ฐฐ์—ด์˜ ์š”์†Œ๋ฅผ ์ฐพ์„ ๋•Œ find๋ณด๋‹ค filter๋ฅผ ๋” ์ž์ฃผ ์‚ฌ์šฉํ–ˆ์—ˆ๋Š”๋ฐ, find๋กœ๋„ ๋ฐฐ์—ด์˜ ์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค.

// filter
// ๋ฐฐ์—ด์˜ ์š”์†Œ ์ค‘์—์„œ ์ฃผ์–ด์ง„ ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜๊ฐ’์ด true์ธ ์š”์†Œ๋“ค๋กœ ์ด๋ฃจ์–ด์ง„ ์ƒˆ๋กœ์šด ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = numbers.filter((number) => number % 2 === 0);
console.log(evenNumbers); // [2, 4, 6, 8, 10]

// find
// ๊ฒ€์‚ฌ๋ฅผ ์œ„ํ•ด ์ „๋‹ฌ๋ฐ›์€ ํ•จ์ˆ˜๋ฅผ ๋งŒ์กฑํ•˜๋Š” ๋ฐฐ์—ด ์š”์†Œ์˜ ์ฒซ๋ฒˆ์งธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. 
// ๋งŒ์กฑํ•˜๋Š” ๊ฐ’์ด ์—†์œผ๋ฉด undefined๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
const numbers = [1, 3, 5, 7, 2, 9, 11];
const firstEvenNumber = numbers.find((number) => number % 2 === 0);
console.log(firstEvenNumber); // 2
  • filter๋Š” ๋ฐฐ์—ด์˜ ์š”์†Œ์— ์ œ๊ณต๋œ ํ•จ์ˆ˜๋ฅผ ์ ์šฉํ•˜์—ฌ, ํ•จ์ˆ˜๊ฐ€ true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ ์ƒˆ ๋ฐฐ์—ด์˜ ์š”์†Œ๋ฅผ ํฌํ•จํ•˜๋ฉฐ, ์ƒˆ๋กœ์šด ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ํŠน์ • ์กฐ๊ฑด์„ ์ถฉ์กฑํ•˜๋Š” ์—ฌ๋Ÿฌ ์š”์†Œ๋ฅผ ๊ฒ€์ƒ‰ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ ์ผ๋ฐ˜์ ์œผ๋กœ filter๊ฐ€ ๋” ์ ํ•ฉํ•˜๋‹ค.
  • find๋Š” ์ผ์น˜ํ•˜๋Š” ์ฒซ ๋ฒˆ์งธ ์š”์†Œ๋ฅผ ์ฐพ์„ ๋•Œ๊นŒ์ง€ ๋ฐฐ์—ด์„ ๋ฐ˜๋ณตํ•œ ๋‹ค์Œ ํ•ด๋‹น ์š”์†Œ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์ผ์น˜ ํ•ญ๋ชฉ์ด ๋ฐœ๊ฒฌ๋˜๋ฉด ๋ฐ˜๋ณต์„ ์ค‘์ง€ํ•˜๋ฏ€๋กœ ๋‹จ์ผ ์š”์†Œ๋ฅผ ๊ฒ€์ƒ‰ํ•  ๋•Œ ์ž ์žฌ์ ์œผ๋กœ ๋” ํšจ์œจ์ ์ด๋‹ค.

Big O ํ‘œ๊ธฐ๋ฒ•์˜ ๊ด€์ ์—์„œ 'filter'์™€ 'find'๋Š” ๋ชจ๋‘ O(n)์˜ ์‹œ๊ฐ„ ๋ณต์žก๋„๋ฅผ ๊ฐ–๋Š”๋‹ค๊ณ  ํ•œ๋‹ค. (n์€ ๋ฐฐ์—ด์˜ ๊ธธ์ด) ๊ทธ๋Ÿฌ๋‚˜ ์ƒ์ˆ˜ ์š”์†Œ์™€ ์‹ค์ œ ์„ฑ๋Šฅ์€ ์‚ฌ์šฉ ์‚ฌ๋ก€์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ๋‹ค.

 

lexical ์—๋””ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ์‹œ๊ฐ„์„ ์ •๋ง ๋งŽ์ด ๋ณด๋ƒˆ๋‹ค. ์ต์ˆ™ํ•˜์ง€ ์•Š๊ณ , ์ž๋ฃŒ๊ฐ€ ๋ณ„๋กœ ์—†๋Š” (์žˆ์–ด๋„ ์ „๋ถ€ ์˜์–ด๋กœ ๋˜์–ด ์žˆ๋Š”) ์ƒˆ๋กœ์šด ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์–ด๋ ค์› ์ง€๋งŒ ๊ทธ๋ž˜๋„ ์ด๋ฒˆ ๊ธฐํšŒ๋ฅผ ํ†ตํ•ด ๊ณต์‹๋ฌธ์„œ๋ฅผ ์ฝ๊ณ , ์‚ฌ์šฉ๋ฒ•์„ ์ตํžˆ๊ณ , ํƒ€์ž… ์ •์˜ ํŒŒ์ผ์„ ์ฐพ์•„๋ณด๋ฉฐ ์–ด๋– ํ•œ ๋ฐฉ์‹์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ฝ๊ณ  ์‚ฌ์šฉ๋ฒ•์„ ํŒŒ์•…ํ•ด์„œ ์‹ค์ œ๋กœ ๋‚˜์˜ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๋Š”์ง€ ์กฐ๊ธˆ์ด๋‚˜๋งˆ ์•Œ๊ฒŒ๋˜์—ˆ๊ณ , ์ž์‹ ๊ฐ์ด ์ƒ๊ฒผ๋‹ค. ๋‹ค์Œ๋ฒˆ์— ๋˜ ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด ํ”„๋กœ์ ํŠธ์— ๋„์ž…ํ•˜๋Š”๋ฐ ์ง€๊ธˆ๋ณด๋‹ค๋Š” ๋œ ํ—ค๋งฌ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

 


 

๋ ˆ์ด์•„์›ƒ์„ ์žก๊ณ  ๊ธฐ๋Šฅ๋งŒ ๋น ๋ฅด๊ฒŒ ๊ตฌํ˜„ํ•˜๋ ค ๋น ๋ฅด๊ฒŒ ๋งŒ๋“  React + JavaScript ์กฐํ•ฉ์˜ ์ฝ”๋“œ๋ฅผ ๋ณธ๋ž˜ ์ดˆ๊ธฐ์— ๋งŒ๋“ค์—ˆ๋˜ ํ”„๋กœ์ ํŠธ๋กœ ์˜ฎ๊ธฐ๋Š” ์ž‘์—…์€ ์ œ์ถœ ์ดํ‹€์„ ๋‚จ๊ธฐ๊ณ  ํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ๋‹ค. ๋ ˆ์ด์•„์›ƒ๊ณผ ์—๋””ํ„ฐ๋ฅผ ํฌํ•จํ•œ ๋Œ€๋ถ€๋ถ„์˜ ๊ธฐ๋Šฅ ๊ตฌํ˜„์ด ์ž„์‹œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์™„๋ฃŒ๋œ ์ƒํƒœ์˜€๊ธฐ ๋•Œ๋ฌธ์— ๋งˆ์Œ์ด ์กฐ๊ธˆ ํŽธํ•ด์กŒ๋‹ค. 

์ฝ”๋“œ๋ฅผ ์˜ฎ๊ธฐ๋ฉฐ TypeScript๋กœ ํฌํŒ…๋„ ํ•˜๊ณ , styled components๋ฅผ ์‚ฌ์šฉํ•ด ์Šคํƒ€์ผ๋ง๋„ ํ•˜์˜€๋‹ค. ๊ธฐ์กด์— ๊ตฌํ˜„์—๋งŒ ์ดˆ์ ์„ ๋‘์–ด ์—‰๋ง์œผ๋กœ ์ž‘์„ฑํ–ˆ๋˜ ์ฝ”๋“œ๋„ ์ •๋ฆฌํ•˜๊ณ , ํด๋” ๊ตฌ์กฐ๋„ ๋‹ค์‹œ ์งฐ๋‹ค. ๋‹ค์‹œ ์ง  ํด๋”๊ตฌ์กฐ๊ฐ€ ์ ํ•ฉํ•œ์ง€๋Š” ์‚ฌ์‹ค ์ž˜ ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ.

Git์— commit๋„ ์ž์ฃผ ํ•˜๋ฉด์„œ ๋งŒ๋“ค๋ ค๊ณ  ํ–ˆ๋Š”๋ฐ, ์ค‘๊ฐ„์— ๋‹ค๋ฅธ ํŒŒ์ผ์—์„œ ์ž„์‹œ๋กœ ๋ ˆ์ด์•„์›ƒ์„ ์งœ๋ฉฐ ์ปค๋ฐ‹ ๋กœ๊ทธ๊ฐ€ ๊ผฌ์—ฌ๋ฒ„๋ ค์„œ ์•ฝ๊ฐ„ ์˜๋ฏธ์—†๊ฒŒ ๋˜์—ˆ๋‹ค. ๊ทธ๋ž˜๋„ ๋‹ค์‹œ ํ”„๋กœ์ ํŠธ๋ฅผ ์˜ฎ๊ฒจ์˜ค๋ฉด์„œ๋Š” ์ปค๋ฐ‹ ๋กœ๊ทธ๋ฅผ ์ž˜ ๋‚จ๊ธฐ๋ ค๊ณ  ํ•ด๋ณด์•˜๋‹ค. ๋ฌผ๋ก  ์ƒ์ƒ๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ ์ค‘๊ตฌ๋‚จ๋ฐฉ์ด ๋˜์–ด๋ฒ„๋ฆฌ๊ธด ํ–ˆ์ง€๋งŒ.

 


2. ํ—ท๊ฐˆ๋ ธ๋˜ ๋ถ€๋ถ„ 1, props type ์ •์˜ ๋ฐฉ๋ฒ•

import { NoteType } from "../../type/notebookTypes";
import NoteEditor from "./Editor";

// const Note = ({ note }: NoteType) => { // โŒ ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ
const Note = ({ note }: { note: NoteType }) => { // โญ•๏ธ ์ด๋Ÿฐ ์‹์œผ๋กœ ์ž‘์„ฑ์„ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.
  return (
    <div className="note-item">
      <NoteEditor note={note} />
    </div>
  );
};

export default Note;

 

์ด๊ฑด ์‚ฌ์†Œํ•˜๊ฒŒ ํ—ท๊ฐˆ๋ ธ๋˜ ๋ถ€๋ถ„์ด๋‹ค. ํ•ญ์ƒ props์˜ type์„ ์œ„๋กœ ๋”ฐ๋กœ ๋นผ์„œ ์ •์˜ํ•˜๋‹ค ๋ณด๋‹ˆ ๋ฌธ๋ฒ•์ด ํ—ท๊ฐˆ๋ ค ์ž˜๋ชป ์ž‘์„ฑํ–ˆ์—ˆ๋‹ค.

 

3. ํ—ท๊ฐˆ๋ ธ๋˜ ๋ถ€๋ถ„ 2, JSX๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋Š” React component์˜ return null

 

JavaScript๋กœ ์ž‘์—…์‹œ์—๋Š” ์•„๋ฌด๋Ÿฐ ์—๋Ÿฌ๊ฐ€ ์—†์—ˆ๋Š”๋ฐ, TypeScript๋กœ ์ž‘์—…์„ ํ•˜๋‹ˆ return ๊ฐ’์ด ์—†์œผ๋‹ˆ ์—๋Ÿฌ๊ฐ€ ๋‚ฌ๋‹ค. customPlugin์œผ๋กœ ๋งŒ๋“  OnChangePlugin๊ณผ InitPlugin์€ JSX๋ฅผ ๋ฆฌํ„ดํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— return null์œผ๋กœ JSX๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Œ์„ ๋ช…์‹œ์ ์œผ๋กœ ๊ธฐ์ž…ํ–ˆ๋‹ค.

 

4. ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ค์› ๋˜ ์ž๋™์ €์žฅ ๊ธฐ๋Šฅ

์ž๋™์ €์žฅ์„ ๊ตฌํ˜„ํ•˜๋ผ๋Š” ๋ผ๋Š” ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ์—ˆ๋Š”๋ฐ, ์ด ๋ถ€๋ถ„์„ ์ž‘์—…ํ•˜๋Š”๋ฐ ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ค์› ๋‹ค. ๋กœ์ง์„ ์ƒ๊ฐํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ์ฝ”๋“œ๋กœ ์–ด๋–ป๊ฒŒ, ์–ด๋–ค ์ˆœ์„œ๋กœ ํ‘œํ˜„์„ ํ•ด์•ผํ•˜๋Š”์ง€ ๊ณ ๋ฏผํ•˜๋Š”๋ฐ ๋” ๋งŽ์€ ์‹œ๊ฐ„์ด ๊ฑธ๋ ธ๋˜ ๊ฒƒ ๊ฐ™๋‹ค.

save๋ฅผ ์œ„ํ•œ ํ•จ์ˆ˜๋Š” ์ด๋ฏธ zustand store์— ์ €์žฅ๋˜์–ด ์žˆ์—ˆ๊ณ , ํ”„๋กœ์ ํŠธ๋ฅผ ํ•˜๋ฉฐ ์ž„์‹œ๋กœ ๋งŒ๋“ค์–ด๋‘” ๋ฒ„ํŠผ์„ ํด๋ฆญํ•จ์œผ๋กœ์จ ์ €์žฅ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ–ˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•จ์ˆ˜์—๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์—ˆ๊ณ , ๋‹จ์ง€ ์ผ์ •ํ•œ ์‹œ๊ฐ„๋งˆ๋‹ค ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋˜์—ˆ๋‹ค. ์ƒ๊ฐํ–ˆ๋˜ ๋กœ์ง์€ ์•„๋ž˜์™€ ๊ฐ™์•˜๋‹ค.

 

1. ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋œ๋‹ค.

2. ์ƒํƒœ isModified๋Š” ์ดˆ๊ธฐ๊ฐ’ false๋ฅผ ๊ฐ€์ง„๋‹ค.

3. editor์— ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅ์„ ํ•˜๋ฉด isModified๋Š” false๊ฐ€ ๋œ๋‹ค. → ์ฆ‰, ๊ณ„์† ์ž…๋ ฅํ•˜๋Š” ์ƒํƒœ๋ผ๋ฉด isModified๋Š” ๊ณ„์†ํ•ด์„œ false ์ƒํƒœ์ด๋‹ค.

4. editor์— ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์ด ๋ฉˆ์ถ”๊ณ  3์ดˆ ํ›„ isModified๋Š” true๊ฐ€ ๋œ๋‹ค.

5. isModified๊ฐ€ true๊ฐ€ ๋˜๋ฉด auto save๋ฅผ ํ•œ๋‹ค.

6. ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง๋œ๋‹ค.

7. editorState๊ฐ€ ๋ณ€๊ฒฝ๋ ๋•Œ๋งˆ๋‹ค โ‘ก~โ‘ฅ์ด ๋ฐ˜๋ณต๋œ๋‹ค.

 

const Editor = ({ note }: { note: NoteType }) => {
  const { saveNote } = useStore();

  const [editorState, setEditorState] = useState(""); // JSON ํ˜•ํƒœ๋กœ ์ €์žฅ๋œ editorState(ํ˜„์žฌ ์—๋””ํ„ฐ์˜ ๊ธ€)
  const [isModified, setIsModified] = useState(false);

  // 1. editor์— ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅ์„ ํ•˜๋ฉด isModified๋Š” false
  const onChange = (editorState: EditorState) => {
	setIsModified(false);

	const editorStateJSON = editorState.toJSON();
	setEditorState(JSON.stringify(editorStateJSON));
	// ...
  };

  const onSave = () => {
    if (note.id && titleNode && editorState) {
      saveNote(note.id, titleNode, editorState);
    }
  };

  // 2. editor์— ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ์ด ๋ฉˆ์ถ”๊ณ  3์ดˆ ํ›„ isModified๋ฅผ true๋กœ ์„ค์ •
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      setIsModified(true);
    }, 3000);

    return () => clearTimeout(timeoutId);
  }, [editorState]);

  // 3. isModified๊ฐ€ true๊ฐ€ ๋˜๋ฉด auto save
  useEffect(() => {
    if (isModified && titleNode !== "" && editorState !== "") {
      onSave();
    }
  }, [isModified]);

  return (
    <EditorWrapper className="note-item">
      {...}
    </EditorWrapper>
  );
};

 

์ด๊ฑธ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ ๊ฑฐ์˜ 3์‹œ๊ฐ„์€ ๊ฑธ๋ฆฐ ๊ฒƒ ๊ฐ™๋‹ค.

onChange๊ฐ€ ๋ ๋•Œ๋งˆ๋‹ค ์ž๋™์ €์žฅ์„ ์œ„ํ•œ ํƒ€์ด๋จธ๊ฐ€ ์ƒ์„ฑ๋˜์–ด์•ผ ํ•˜๋Š”๋ฐ, ์ด๋ฅผ ์œ„ํ•ด useEffect์— ์–ด๋–ค๊ฑธ ์˜์กด์„ฑ ๋ฐฐ์—ด๋กœ ๋„ฃ์–ด์•ผ ํ•˜๋Š”์ง€๋„ ํ—ท๊ฐˆ๋ ธ๊ณ , ๊ทธ๋ƒฅ ์ „๋ฐ˜์ ์œผ๋กœ ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ• ์ง€ ์–ด๋ ค์› ๋˜ ๊ฒƒ ๊ฐ™๋‹ค.

๊ฒฐ๊ตญ “change ๋˜๋Š” ์ค‘์—๋Š” ๊ณ„์† isModified๋Š” false์—ฌ์•ผ ํ•˜๊ณ , change๊ฐ€ ๋ฉˆ์ถ˜ ํ›„ 3์ดˆ ํ›„์— isModified๋Š” true๊ฐ€ ๋œ๋‹ค.” ์ด๊ฑธ ์‹œ์ž‘์œผ๋กœ ๋กœ์ง์„ ์ƒ๊ฐํ•ด๋ณด๊ธฐ ์‹œ์ž‘ํ–ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค. ์ •๋ง ๋Œ๊ณ ๋Œ์•„ ์ด๊ฒƒ์ €๊ฒƒ ์‹œ๋„ํ•ด๋ณด๊ณ  ๊ตฌํ˜„๋œ ๊ธฐ๋Šฅ์ด๋‹ค.

 

โญ๏ธ

 

โฌ‡๏ธ ๊ธ€์ด ๊ธธ์–ด์ ธ์„œ ๋‚˜๋ˆ„์–ด ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ธ€ ์ž…๋‹ˆ๋‹ค. โฌ‡๏ธ

 

[Mini Project] ํ”„๋ก ํŠธ์—”๋“œ ๊ณผ์ œํ…Œ์ŠคํŠธ๋กœ ๋งŒ๋‚œ Note-App ๊ตฌํ˜„ ๊ณผ์ • ์ •๋ฆฌ (3)

[Mini Project] Note-App ๐Ÿ‘ฉ๐Ÿป‍๐Ÿ’ป Github Repository https://github.com/sw2377/note-app ์ž‘์—… ๊ธฐ๊ฐ„ : 2024. 01. 09 ~ 2024. 01. 16 ํ”„๋ก ํŠธ์—”๋“œ ์ฑ„์šฉ ๊ณผ์ œ๋กœ ์ˆ˜ํ–‰ํ•œ Note-App ๊ตฌํ˜„ ๊ณผ์ • (๊ตฌํ˜„ ์ค‘ ์—๋Ÿฌ์‚ฌํ•ญ & ๊ณ ๋ฏผํ•œ ๋ถ€๋ถ„)์„ ์ •

fay-story.com

 

๋ฐ˜์‘ํ˜•