[React/JS] ๋๋๊ทธํ ๋ฌธ์์ด ๋ถ๋ฆฌ(๋ฉํ)ํ๊ธฐ / Selection API

์๋์ ๊ฐ์ ๋ฌธ์์ด์ด ์์ ๋ ๋ง์ฐ์ค๋ก ๋๋๊ทธํด์ ํ
์คํธ๋ฅผ ์ ํํ ๋๋ง๋ค <span>
ํ๊ทธ๋ก ๊ฐ์ธ๋๋ก ๊ตฌํํ๋ ๊ฒ ๋ชฉํ.
<!-- ์ด๊ธฐ ์ํ --> <p>Pharetra convallis hendrerit integer nec eleifend tellus luctus lorem dignissim</p> <!-- convallis hendrerit integer ์์ญ์ ๋๋๊ทธํ์ ๋ --> <p>Pharetra <span>convallis hendrerit integer</span> nec eleifend tellus luctus lorem dignissim</p> <!-- hendrerit ์์ญ์ ๋๋๊ทธํ์ ๋ --> <p>Pharetra <span>convallis <span>hendrerit</span> integer</span> nec eleifend tellus luctus lorem dignissim</p>
๋๋๊ทธํ ๋ฌธ์์ด ๋ฉํ
โถ ๋ง์ฐ์ค ํด๋ฆญ/ํ
์คํธ ๋๋๊ทธ/ํด๋ฆญ ํด์ โ onMouseUp
์ด๋ฒคํธ ํธ์ถ
โท ์ ํํ ์์ญ์ ๋ํ Selection ๊ฐ์ฒด ํ๋
const selection = window.getSelection();
โธ ์ฒซ๋ฒ์งธ ์ ํ ์์ญ์ ๋ํ Range ๊ฐ์ฒด ํ๋
const range = selection?.getRangeAt(0);
โน ํธ๋ค๋ฌ ์คํ ์กฐ๊ฑด ๊ฒ์ฌ
a. range
๊ฐ์ฒด๊ฐ ์๊ฑฐ๋ ํน์ deleteMode
์ฌ๋ถ
if (!range || deleteMode) return;
b. ์ ํํ ์์ญ์ .selected
ํด๋์ค ํฌํจ ์ฌ๋ถ (์ค์ฒฉ ๋๋๊ทธ ๋ฐฉ์ง)
if (range.cloneContents().querySelector('.selected')) return; // range.cloneContents ๋ฉ์๋๋ Range์ ํฌํจ๋ Node ๊ฐ์ฒด๋ฅผ ๋ณต์ฌํ ํ DocumentFragment ๋ฐํ // ๋๋๊ทธ ์์ญ : convallis hendrerit integer nec (hendrerit๋ <span>์ผ๋ก ๋ฉํ๋์ด ์์) // ๋ฐํ๊ฐ : "convallis "<span class="selected">hendrerit</span>" integer"
range.cloneContents.querySelector
๋document.querySelector
์ ๋ ํฐ์ ์ฌ์ฉ๋ฒ ๋์ผ(์ฒซ ๋ฒ์งธ ์ ํ๋ ์๋ฆฌ๋จผํธ ํน์ ์ ํ๋ ์๋ฆฌ๋จผํธ ์์ผ๋ฉดnull
๋ฐํ)range.start|endContainer
: Range ์์|์ข ๋ฃ ๋ ธ๋ ๋ฐํrange.start|endContainer.parentNode
: Range ์์|์ข ๋ฃ ๋ ธ๋์ ๋ถ๋ชจ ์์ ๋ฐํ
โบ ์ ํํ ํ
์คํธ๋ฅผ ๋ฉํ ํ ์๋ก์ด <span>
ํ๊ทธ ์์ฑ
const selectedText = range.toString(); // ๋๋๊ทธํ ์์ญ์ ๋ฌธ์์ด ๋ฐํ const span = document.createElement('span'); span.className = 'selected'; span.textContent = selectedText;
โป ๊ธฐ์กด ์ ํํ ํ
์คํธ ๋
ธ๋ ์ญ์ ํ ์์ฑํ <span>
ํ๊ทธ๋ก ๋์ฒด
range.deleteContents(); range.insertNode(span);
๋ฉํ ํ ํ๊ทธ ์ญ์
๐ก ์ด๋ฒคํธ ๋ฒ๋ธ๋ง์ผ๋ก ์ธํด ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ๋ถ๋ชจ๋ก ์ ํ๋๋ฏ๋ก, ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ๋ถ๋ชจ ์ค์์๋ง ํ ๋นํ๋ค.
โถ ๋ง์ฐ์ค ํด๋ฆญ โ ์ด๋ฒคํธ ํธ๋ค๋ฌ ์คํ
<!-- ์๋ ์์์์ hello๋ฅผ ํด๋ฆญํ๋ค๊ณ ๊ฐ์ --> <span class="selected">hello <span class="selected">world</span></span>
โท ํธ๋ค๋ฌ ์คํ ์กฐ๊ฑด ๊ฒ์ฌ
// deleteMode ์ํ์ด๊ณ , ํด๋ฆญํ ํ๊ฒ ์์๋ด selected ํด๋์ค๋ฅผ ํฌํจํ ๋๋ง ์คํ if (!deleteMode || !target.classList.contains('selected')) return;
โธ ํด๋ฆญํ ์์์ innerHTML ๋ณต์ฌ
// ์ด ๊ณผ์ ์์ ํด๋ฆญํ ์์ ๊ฐ์ฅ ๋ฐ๊นฅ์ ์์/์ข
๋ฃ ํ๊ทธ๋ ํฌํจํ์ง ์์ // ํด๋ฆญํ ์์ : <span>hello <span>world</span></span> // innerHTML : hello <span>world>/span> const spanContent = target.innerHTML;
โน ์์๋ก ์ฌ์ฉํ ์์ ์์ฑ
const textNode = document.createElement('span');
โบ ๋ณต์ฌํ ์ฝํ ์ธ ๋ฅผ ์์ ์์ฑํ ์์์ innerHTML์ ๋ณต์ฌ
textNode.innerHTML = spanContent;
โป ์๋ก์ด DocumentFragment ์์ฑ
const frag = document.createDocumentFragment();
๐ก DocumentFragment๋ ์์ ์ปจํ ์ด๋๋ก DOM ๋ ธ๋๋ฅผ ์ ์ฅํ ์ ์๋ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๋ค. ๋ค์ ๋ ธ๋์ ๋ํ CRUD ๋ฑ ์์ ์ ์ํํ ๋ ์ ์ฉ. ์ค์ DOM ํธ๋ฆฌ์ ์ํ์ง ์์ผ๋ฏ๋ก reflow/repaint๋ฅผ ์ ๋ฐํ์ง ์๊ณ (ํ๋ฉด์ ๋ค์ ๊ทธ๋ฆฌ๋ ๊ณผ์ ์๋ต) ์ฌ๋ฌ DOM ๋ ธ๋๋ฅผ ํ ๋ฒ์ ์ฝ์ ํ ์ ์๋ ์ฅ์ ์ด ์๋ค.
โผ textNode
์ ๋ชจ๋ ์์ ์์๋ฅผ frag
๋ก ์ด๋
// ๋ฐ๋ณตํ ๋๋ง๋ค textNode์ ์ฒซ๋ฒ์งธ ์์์ frag ์์์ผ๋ก ๋ถ์ while (textNode.firstChild) frag.appendChild(textNode.firstChild); // textNode ์ฒ์ ์ํ ์์ : convallis <span>hendrerit</span> integer // childNode๋ก ํํํ์ ๋ : NodeList [text, span, text] // 1ํ ์ํ ํ textNode : <span>hendrerit</span> integer // 2ํ ์ํ ํ textNode : integer // 3ํ ์ํ ํ textNode : ๋น์ด์์
hasChildNodes
๋ก ์์ ๋
ธ๋๊ฐ ์๋์ง ํ์ธํ๊ณ , ์๋ค๋ฉด append
๋ก ๋ฐ๋ก ๋ถ์ด๋ ๋ฐฉ๋ฒ๋ ์๋ค.
if (textNode.hasChildNodes()) frag.append(...textNode.childNodes)
โฝ ํด๋ฆญํ ์์๋ฅผ(๋ฉํ์ ์ญ์ ํ๊ธฐ ์ํด ํด๋ฆญํ ๋์) frag
๋ก ๋์ฒด
target.replaceWith(frag);
๋ ธ๋ ์ค์ฒฉ ๊น์ด ๊ณ์ฐ
์ค์ฒฉ ๊ธฐ์ค
์ฌ๋ฌ ํ๊ทธ๋ฅผ ์ถ๊ฐํด์(๋๋๊ทธํ ๋ฌธ์์ด์ <span>
ํ๊ทธ๋ก ๋ฉํ) ์ค์ฒฉ๋ ๊ตฌ์กฐ๊ฐ ๋์ ๋ ๊ฐ ํ๊ทธ์ ๊น์ด.

Pharetra <span class="selected"> <!-- ์ค์ฒฉ ๋ ๋ฒจ 1 --> convallis <span class="selected"> <!-- ์ค์ฒฉ ๋ ๋ฒจ 2 --> hendrerit </span> integer </span> nec eleifend tellus luctus lorem dignissim
์ค์ฒฉ ๋ ๋ฒจ ๊ณ์ฐ
์ธ์๋ก ์ ๋ฌ๋ฐ์ Node์ ๋ถ๋ชจ ์์๊ฐ selected
ํด๋์ค๋ฅผ ํฌํจํ ๋๋ง๋ค ์ค์ฒฉ ๋ ๋ฒจ + 1์ฉ ์ฆ๊ฐ
const calculateNestingLevelFromNode = (node: AnchorNode) => { let nestingLevel = 1; let parentNode = node; while (parentNode && parentNode instanceof HTMLElement) { if (parentNode.classList.contains('selected')) nestingLevel++; parentNode = parentNode.parentNode; } return nestingLevel; };
selected
ํด๋์ค๋ฅผ ๊ฐ์ง ๋ชจ๋ ํ๊ทธ์ ์ค์ฒฉ ๋ ๋ฒจ ๊ฒ์ฌ
const calculateNestingLevel = (ref: React.RefObject<HTMLParagraphElement>) => { const spans = ref.current?.querySelectorAll('.selected'); if (!spans) return; spans.forEach((spanElement) => { const span = spanElement as HTMLElement; const nestingLevel = calculateNestingLevelFromNode(span.parentNode); span.dataset.nestingLevel = nestingLevel.toString(); }); };
์ฝ๋ ์คํ ๊ฒฐ๊ณผ
Pharetra <span class="selected" data-nesting-level="1"> convallis <span class="selected" data-nesting-level="2"> hendrerit </span> integer </span> nec eleifend tellus luctus lorem dignissim
๋ ๋๋ง
DOMPurify ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด XSS ๊ณต๊ฒฉ์ ์ฌ์ฉ๋๋ ์คํฌ๋ฆฝํธ/ํ๊ทธ๋ฅผ ๊ฐ์งํ๊ณ ์ญ์ ํ ์ ์๋ค.
dangerouslySetInnerHTML
์์ฑ์ ์ด์ฉํด innerHTML ๋ฌธ์์ด์ ๊ทธ๋๋ก ๋ ๋๋งํ๋ ์์. ํ๊ทธ ์ถ๊ฐ/์ญ์ ์ ์คํ๋๋ updateStack
ํธ๋ค๋ฌ ๋ด๋ถ์์ DOMPurity.sanitize
ํจ์๋ฅผ ํธ์ถํ๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ์ถ๊ฐ์ ์ธ sanitize ์์
์ ํ์ง ์์๋ค.
const [stackIndex, setStackIndex] = useState(0); const [historyStack, setHistoryStack] = useState(DEFAULT_TEXT); return ( // ... <p dangerouslySetInnerHTML={{ __html: historyStack[stackIndex] }} /> );
ํ๊ทธ ์ถ๊ฐ ์ historyStack
์ํ๊ฐ ์
๋ฐ์ดํธ๋๊ณ , ํ์ฌ stackIndex
์ ํด๋นํ๋ historyStack
๋ ๋๋ง
// ํ๊ทธ ์ถ๊ฐ ์ historyStack[stackIndex] 'Pharetra convallis hendrerit integer ...' // ํ๊ทธ ์ถ๊ฐ ํ historyStack[stackIndex] 'Pharetra <span class="selected">convallis hendrerit integer</span> ...'
ํ์คํ ๋ฆฌ ๊ด๋ฆฌ
โถ ์ํ ์ ์ (historyStack ์ํ ๋ณํ โผ)
// ์ด๊ธฐ ์ํ [ "Pharetra convallis hendrerit integer ..." ] // ํ๊ทธ๋ฅผ ์ถ๊ฐ/์ญ์ ํ์ ๋ ์ํ [ "Pharetra convallis hendrerit integer ...", "Pharetra <span class=\"selected\" data-nesting-level=\"1\"> ..." ]
historyStack
: ํ ์คํธ ๋ฉํ ์ด๋ ฅ(๋ถ๋ชจ ์์์ธ<p>
ํ๊ทธ์ innerHTML ๋ฐํ๊ฐ)stackIndex
:historyStack
์ ํ์ฌ ์ธ๋ฑ์ค
โท ํ๊ทธ ์ถ๊ฐ / ์ญ์ ์
import DOMPurify from 'dompurify'; // ... const updateStack = () => { setHistoryStack((prevStack) => { if (!paragraphRef.current) return prevStack; // DOMPurify ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ XSS ๊ณต๊ฒฉ์ ์ฌ์ฉ๋๋ ์ฝ๋ ๊ฒ์ฌ/์ ๊ฑฐ const cleanedHTML = DOMPurify.sanitize(paragraphRef.current.innerHTML); const newStack = [...prevStack, paragraphRef.current.innerHTML]; if (newStack.length > 20) newStack.shift(); // ์ต๋ 20๊ฐ๊น์ง๋ง ์ ์ฅ setStackIndex(newStack.length - 1); // ์ญ์ ํน์ ์ถ๊ฐ์ ์๋ก์ด ์คํ์ ๊ฐ์ฅ ๋ง์ง๋ง ์ธ๋ฑ์ค๋ก ์ด๋ return newStack; }); };
- ํ๊ทธ ์ถ๊ฐ/์ญ์ ์ ํ์ฌ HTML ๋ฌธ์์ด์(๋ถ๋ชจ ์์์ innerHTML)
historyStack
์ํ์ ์ถ๊ฐ stackIndex
์ํ๋ฅผ ํ์ฌhistoryStack
์ ๊ฐ์ฅ ๋ง์ง๋ง ์ธ๋ฑ์ค๋ก ๋ณ๊ฒฝ
โธ undo / redo ๋ฒํผ ํด๋ฆญ ์
- undo :
stackIndex - 1
(stackIndex
๊ฐ0
์ด๋ฉด ๋ฒํผ ๋นํ์ฑ) - redo :
stackIndex + 1
(stackIndex
์historyStack
๋ง์ง๋ง ์ธ๋ฑ์ค ๊ฐ์ผ๋ฉด ๋ฒํผ ๋นํ์ฑ)
์ ์ฒด ์ฝ๋
React + TypeScript โผ
import React, { MouseEvent, useRef, useState } from 'react'; import classnames from 'classnames'; import DOMPurify from 'dompurify'; const initialText = 'Pharetra convallis hendrerit integer nec eleifend tellus luctus lorem dignissim'; const spanClasses = 'selected text-blue-500 before:content-["["] after:content-["]"]'; const calculateNestingLevelFromNode = (node?: Selection['anchorNode']) => { let nestingLevel = 1; let parentNode = node; while (parentNode && parentNode instanceof HTMLElement) { if (parentNode.classList.contains('selected')) nestingLevel++; parentNode = parentNode.parentNode; } return nestingLevel; }; const calculateNestingLevel = (ref: React.RefObject<HTMLParagraphElement>) => { const spans = ref.current?.querySelectorAll('.selected'); if (!spans) return; spans.forEach((spanElement) => { const span = spanElement as HTMLElement; const nestingLevel = calculateNestingLevelFromNode(span.parentNode); span.dataset.nestingLevel = nestingLevel.toString(); }); }; export default function Fico() { const [deleteMode, setDeleteMode] = useState(false); const [stackIndex, setStackIndex] = useState(0); const [historyStack, setHistoryStack] = useState([initialText]); const paragraphRef = useRef<HTMLParagraphElement>(null); const updateStack = () => { setHistoryStack((prevStack) => { if (!paragraphRef.current) return prevStack; const cleanedHTML = DOMPurify.sanitize(paragraphRef.current.innerHTML); const newStack = [...prevStack, cleanedHTML]; if (newStack.length > 20) newStack.shift(); // ์ต๋ 20๊ฐ๊น์ง๋ง ์ ์ฅ setStackIndex(newStack.length - 1); // ์ญ์ ํน์ ์ถ๊ฐ์ ์๋ก์ด ์คํ์ ๊ฐ์ฅ ๋ง์ง๋ง ์ธ๋ฑ์ค๋ก ์ด๋ return newStack; }); }; const addNewTag = () => { const selection = window.getSelection(); const range = selection?.getRangeAt(0); if (!range || deleteMode) return; if (range.cloneContents().querySelector('.selected')) return; const selectedText = range.toString(); if (selectedText.trim().length < 1) return; const span = document.createElement('span'); span.className = spanClasses; span.textContent = selectedText; const nestingLevel = calculateNestingLevelFromNode( selection?.anchorNode?.parentNode, ); span.dataset.nestingLevel = nestingLevel.toString(); range.deleteContents(); range.insertNode(span); updateStack(); }; const onUndoRedo = (type: 'undo' | 'redo') => { setStackIndex((prevIdx) => { return type === 'undo' ? prevIdx - 1 : prevIdx + 1; }); }; const deleteTag = (event: MouseEvent) => { const target = event.target as HTMLElement; if (!deleteMode || !target.classList.contains('selected')) return; const spanContent = target.innerHTML; const textNode = document.createElement('span'); textNode.innerHTML = spanContent; const frag = document.createDocumentFragment(); while (textNode.firstChild) frag.appendChild(textNode.firstChild); target.replaceWith(frag); calculateNestingLevel(paragraphRef); updateStack(); }; const onToggleDeleteMode = () => setDeleteMode((prev) => !prev); return ( <section className={classnames({ 'cursor-eraser': deleteMode })}> <div className="mb-4 flex gap-2 items-center"> <button onClick={() => onUndoRedo('undo')} disabled={stackIndex === 0}> UNDO </button> <button onClick={() => onUndoRedo('redo')} disabled={stackIndex === historyStack.length - 1} > REDO </button> <button onClick={onToggleDeleteMode}>DELETE MODE</button> <span>{deleteMode ? 'ON' : 'OFF'}</span> </div> <p onClick={deleteTag} ref={paragraphRef} className="text-2xl" onMouseUp={addNewTag} dangerouslySetInnerHTML={{ __html: historyStack[stackIndex] }} /> </section> ); }
Selection API โก๏ธ

1๊ฐ ํ
์คํธ ๋
ธ๋๋ง ์์ ๋ ์ด๋๋ฅผ ๋๋๊ทธํ๋ anchorNode
, focusNode
๋ ๋์ผํ๋ค. ๋ง์ฝ ๋๋๊ทธํ ์์ญ์ ์์๊ณผ ๋ ์ฌ์ด์ ์ฌ๋ฌ ํ
์คํธ ๋
ธ๋๊ฐ ์๋ค๋ฉด anchorNode
, focusNode
๋ ๋ค๋ฅธ ํ
์คํธ ๋
ธ๋๋ฅผ ๊ฐ์ง๊ณ , ์ธ๋ฑ์ค๋ ๊ฐ๊ฐ์ ํ
์คํธ ๋
ธ๋๋ฅผ ๊ธฐ์ค์ผ๋ก ๊ณ์ฐํ๋ค.
const selection = window.getSelection(); // ...
๋ฉ์๋
selection.toString()
: ๋๋๊ทธํ ํ ์คํธ ๋ฐํselection.getRangeAt(index)
: index(0๋ถํฐ ์์)์ ํด๋นํ๋ Range ๊ฐ์ฒด ๋ฐํselection.addRange|removeRange(range)
: ์ ํ ์์ญ์ range ์ถ๊ฐ/์ญ์ selection.removeAllRanges()
: ๋ชจ๋ ์ ํ ์์ญ ์ ๊ฑฐ.selection.empty()
๋ฉ์๋์ ๋์ผ
ํ๋กํผํฐ
selection.anchorNode
: ์ ํ์ ์์ํ ํ ์คํธ ๋ ธ๋selection.anchorOffset
:anchorNode
์์ ์ ํ์ ์์ํ ์์น์ ์ธ๋ฑ์คselection.focusNode
: ์ ํ์ ์ข ๋ฃํ ํ ์คํธ ๋ ธ๋selection.focusOffset
:focusNode
์์ ์ ํ์ ์ข ๋ฃํ ์์น์ ์ธ๋ฑ์คselection.rangeCount
: ์ ํํ ๋ฒ์์ ๊ฐ์ (๋๋๊ทธํด์ ์ฌ๋ฌ ๊ฐ์ฒด๋ฅผ ์ ํํ๋ค๋ฉด 1 ์ด์)selection.isCollapsed
: anchor/focus๊ฐ ๊ฐ์ ์ง์ ์ ์๋์ง ์ฌ๋ถ- ์ ํํ ํ
์คํธ๊ฐ ์๊ณ ์ปค์(Caret)๋ง ์กด์ฌํ๋ฉด
true
๋ฐํ - ํ
์คํธ๋ฅผ ์ ํํ๋ฉด
false
๋ฐํ
- ์ ํํ ํ
์คํธ๊ฐ ์๊ณ ์ปค์(Caret)๋ง ์กด์ฌํ๋ฉด
selection.type
:'Range'
: ํ ์คํธ ๋ฒ์๋ฅผ ์ ํํ ์ํ (anchor/focus๊ฐ ๋ค๋ฅธ ์ง์ ์ ์์)'Caret'
: ์ปค์๋ง ์กด์ฌํ๋ ์ํ(anchor/focus๊ฐ ๊ฐ์ ์ง์ ์ ์์,isCollapsed === true
)'None'
: ์ ํํ ํ ์คํธ/์ปค์๊ฐ ์๋ ์ํ(๋ก๋ฉ ์ด๊ธฐ ํน์removeAllRange
ํธ์ถ ํ)
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Algorithm] ๋ณต์กํ DOM ์์ ๋ก ๋ณด๋ DFS ํ์ ์๊ณ ๋ฆฌ์ฆ (0) | 2024.05.22 |
---|---|
[Algorithm] ๋ฐ์ดํฐ ์ถ๊ฐ, ์ญ์ , ์ ๋ ฌ๋ก ๋ณด๋ BFS / DFS ํ์ ์๊ณ ๋ฆฌ์ฆ (0) | 2024.05.21 |
[React] ๋ฆฌ์กํธ ๋์์ฑ ๋ ๋๋ง(Concurrent) ํบ์๋ณด๊ธฐ (1) | 2024.05.20 |
[Express] req.query vs req.params (0) | 2024.05.19 |
[JS] ํจ์ํ / ๊ฐ์ฒด ์งํฅ ํ๋ก๊ทธ๋๋ฐ์ ๋ฎ์ ๊ผด (0) | 2024.05.19 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[Algorithm] ๋ณต์กํ DOM ์์ ๋ก ๋ณด๋ DFS ํ์ ์๊ณ ๋ฆฌ์ฆ
[Algorithm] ๋ณต์กํ DOM ์์ ๋ก ๋ณด๋ DFS ํ์ ์๊ณ ๋ฆฌ์ฆ
2024.05.22 -
[Algorithm] ๋ฐ์ดํฐ ์ถ๊ฐ, ์ญ์ , ์ ๋ ฌ๋ก ๋ณด๋ BFS / DFS ํ์ ์๊ณ ๋ฆฌ์ฆ
[Algorithm] ๋ฐ์ดํฐ ์ถ๊ฐ, ์ญ์ , ์ ๋ ฌ๋ก ๋ณด๋ BFS / DFS ํ์ ์๊ณ ๋ฆฌ์ฆ
2024.05.21 -
[React] ๋ฆฌ์กํธ ๋์์ฑ ๋ ๋๋ง(Concurrent) ํบ์๋ณด๊ธฐ
[React] ๋ฆฌ์กํธ ๋์์ฑ ๋ ๋๋ง(Concurrent) ํบ์๋ณด๊ธฐ
2024.05.20 -
[Express] req.query vs req.params
[Express] req.query vs req.params
2024.05.19
๋๊ธ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.