[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