[Observer API ํํค์น๊ธฐ] 4. MutationObserver
โฐ 2024-06-24 (์) 02:18:32
MutationObserver
MutationObserver
๋ DOM์ ๋ณํ๋ฅผ ๊ฐ์งํ๋ ์ต์ ๋ฒ๋ค. DOM๊ณผ ๊ด๋ จ๋ ์ด๋ค ์์๊ฐ ๋ณํ๋ ์ง ๊ฐ์งํ ์ ์๋ค. ์ด๋ฅผํ
๋ฉด, width
ํน์ padding
๊ฐ์ style
ํ๊ทธ๋ ๋ฌผ๋ก , class
๋ id
, ์์ ๋
ธ๋์ ์ถ๊ฐ ๋ฑ์ด ์๋ค.
DOM์ ๋ณํ๋ฅผ ๊ฐ์งํ๋ค๋ ํน์ง์ผ๋ก ์ธํด, DOM์ ์ฌ์ด์ฆ ๋ณํ๋ฅผ ๊ฐ์งํ๋ ResizeObserver
๋ฅผ ๋์ฒดํ ์ ์๋ค.
TYPESCRIPT
1 2 3
const mo = new MutationObserver(callback); mo.observe(tag, options);
options
์์ ๋ฑ๋ก ์ options
๋ฅผ ์ ๋ฌํ์ฌ ์ธ๋ถ ์ต์
์ ์กฐ์ ํ ์ ์๋ค. MutationObserverInit
ํ์
์ ๊ฐ์ง๋ค.
์์ฑ ํํฐ (attributeFilter)
attributeFilter
์ ์ํ๋ ์์ฑ๋ช
์ ๋ฐฐ์ด๋ก ์ง์ ํ๋ฉด, ํด๋น ์ด๋ฆ์ ๊ฐ์ง ์์ฑ์ ๋ณํ๋ง์ ํํฐ๋งํ์ฌ ๊ฐ์งํ๋ค.
TYPESCRIPT
1 2 3 4
{ // ... attributeFilter: [ 'id', 'class' ] }
์์ ๊ฐ์ด ์ง์ ํ ๊ฒฝ์ฐ, id
๋ฐ class
๊ฐ ๋ณํ ๋๋ง ์ต์ ๋ฒ๊ฐ ๊ฐ์งํ๋ค. ๋ง์ฝ ์๋ฌด ๊ฐ๋ ํ ๋นํ์ง ์์ undefined
์ผ ๊ฒฝ์ฐ, ๋ชจ๋ ๋ณํ๋ฅผ ๊ฐ์งํ๋ค.
์ด์ ์์ฑ๊ฐ (attributeOldValue)
MutationObserver
๋ DOM์ด ๋ณํ๋์ ๋ ๋์ํ๋ค. ์ด ๋, ์ํฉ์ ๋ฐ๋ผ ๋ณํ ์ด์ ์ ๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก ๋ก์ง์ ๊ตฌ์ฑํ๊ธฐ๋ ํ๋๋ฐ, ์ด ๋ attributeOldValue
์ค์ ์ด ์ ์ฉํ ๊ฒ์ด๋ค.
TYPESCRIPT
1 2 3 4
{ // ... attributeOldValue: true }
์์ ๊ฐ์ด ์ง์ ํ ๊ฒฝ์ฐ, ์์ฑ ๋ณํ๋ฅผ ๊ฐ์งํ ์, ๋ณํ ์ด์ ์ ๊ฐ๋ ๊ฐ์ด ์ ๊ณตํด์ค๋ค. ์ด์ ์์ฑ์ด ์กด์ฌํ ์ ์๋ ์ฒซ ๋ณํ๊ฑฐ๋, ์์ฑ์ด ์๋ ๋ณํ(์์ ๋
ธ๋์ ์ถ๊ฐ ๋ฑ)์ผ ๊ฒฝ์ฐ null
์ด ๋ฐํ๋๋ค.
์์ฑ ๋ณํ ๊ฐ์ง ์ฌ๋ถ (attributes)
DOM ์์ฑ ๋ณํ์ ๊ฐ์ง ์ฌ๋ถ๋ฅผ ์ง์ ํ๋ค.
TYPESCRIPT
1 2 3 4
{ // ... attributes: true }
id
, class
, style
๊ฐ์ DOM์ ์์ฑ ๋ณํ๋ฅผ ๊ฐ์งํ๊ณ ์ถ์ ๊ฒฝ์ฐ, ํด๋น ์ค์ ์ true
๋ก ์ง์ ํด์ค๋ค.
attributeFilter
ํน์ attributeOldValue
์ ๊ฐ์ด ์ง์ ๋ ๊ฒฝ์ฐ, ํด๋น ์ต์
์ ์๋ตํ ์ ์๋ค.
ํ
์คํธ ๋
ธ๋ ๋ณํ ๊ฐ์ง ์ฌ๋ถ (characterData)
DOM์ ํ
์คํธ ๋
ธ๋ ๋ณํ(node.value
)์ ๊ฐ์ง ์ฌ๋ถ๋ฅผ ์ง์ ํ๋ค.
TYPESCRIPT
1 2 3 4
{ // ... characterData: true }
DOM์ textContent
๊ฐ์ด ๋ฐ๋ ๊ฒฝ์ฐ, ์ด๋ฅผ ๊ฐ์งํ๋ค.
TYPESCRIPT
1 2 3 4 5 6 7
function handleChange: ChangeEventHandler<HTMLInputElement> = (e) => { const tag = document.getElementById('target'); if (tag?.firstChild?.textContent) { tag.firstChild.textContent = e.currentTarget.value; } }
์์ ๊ฐ์ด tag.firstChild.textContent
๊ฐ์ด ๋ณํ ๊ฒฝ์ฐ, characterData
์ด๋ฒคํธ๊ฐ ๊ฐ์ง๋๋ค.
์ด์ ํ
์คํธ ๋
ธ๋ ๊ฐ (characterDataOldValue)
true
๋ก ์ง์ ํ ๊ฒฝ์ฐ, attributeOldValue
์ ๋น์ทํ๊ฒ ํ
์คํธ ๋
ธ๋ ๊ฐ ๋ณํ ์, ๋ณํ ์ด์ ์ ๊ฐ์ ๊ฐ์ด ๋ฐํํด์ค๋ค.
TYPESCRIPT
1 2 3 4
{ // ... characterDataOldValue: true }
์ด์ ์์ฑ์ด ์กด์ฌํ ์ ์๋ ์ฒซ ๋ณํ๊ฑฐ๋, ์์ฑ์ด ์๋ ๋ณํ(์์ ๋
ธ๋์ ์ถ๊ฐ ๋ฑ)์ผ ๊ฒฝ์ฐ null
์ด ๋ฐํ๋๋ค.
์์ ๋
ธ๋ ๋ณํ ๊ฐ์ง ์ฌ๋ถ (childList)
๋์ DOM์ ์์ ๋ ธ๋๊ฐ ์ถ๊ฐ๋๊ฑฐ๋ ์ญ์ ๋ ๊ฒฝ์ฐ๋ฅผ ๊ฐ์งํ๋ค.
TYPESCRIPT
1 2 3 4
{ // ... childList: true }
์์ ๊ฐ์ ์ค์ ์ ํตํด, ํน์ DOM์ ๊ตฌ์กฐ ๋ณํ๋ฅผ ๋ฅ๋์ ์ผ๋ก ๊ฐ์งํ ์ ์๋ค.
ํ์ ๋
ธ๋ ๋ณํ ๊ฐ์ง ์ฌ๋ถ (subtree)
subtree
์ต์
์ผ๋ก ๊ฐ์ง ๋ฒ์๋ฅผ ํ์ ๋
ธ๋๊น์ง ํ์ฅํ ์ ์๋ค.
TYPESCRIPT
1 2 3 4
{ // ... subtree: true }
์ ์ต์
์ ํ์ฑํํ๋ฉด, ํ์ ๋
ธ๋์ attributes
, characterData
, childList
๋ณํ๊น์ง ๊ฐ์งํด์ค๋ค. ๊ฐ์ง ๊ธฐ์ค์ ์ต์
์ ํ ๋นํ ๋ถ๋ชจ์ ๊ธฐ์ค๊ณผ ๋์ผํ๋ค.
์๋ฅผ๋ค์ด, attributes
๊ฐ์ง๋ง ํ์ฑํํ๊ณ subtree
๋ฅผ ์ ์ฉํ๋ฉด, ์์ ๋ฐ ํ์ ๋
ธ๋ ์ ์ฒด์ ์์ฑ ๋ณํ ์ฌ๋ถ๋ฅผ ๊ฐ์งํ ์ ์๋ค.
callback
์ฝ๋ฐฑ ๋ฉ์๋๋ฅผ ํตํด DOM ๋ณํ ์ ๋์ํ ๋ก์ง์ ์ง์ ํ ์ ์๋ค. MutationRecord
ํ์
์ ๊ฐ์ง๋ค. ์ด๋ฅผ ์ฝ๋๋ก ํ๊ธฐํ๋ฉด ์๋์ ๊ฐ๋ค.
TYPESCRIPT
1
const mo = new MutationObserver((record) => {});
์ถ๊ฐ๋ ๋
ธ๋ (addedNodes)
addedNodes
๋ ์ถ๊ฐ๋ ๋
ธ๋๊ฐ ์์ ๊ฒฝ์ฐ, ์ด๋ฅผ ๋ฐฐ์ด ํํ๋ก ๋ฐํํ๋ค. ๋
ธ๋ ์ถ๊ฐ, ์ญ์ ์ ๊ด๋ จ๋ childList
์ต์
์ด ํ์ฑํ๋ ๊ฒฝ์ฐ ๋ฐํ๋๋ค.
HTML
1 2 3 4 5 6 7 8 9
<!-- ์ด๊ธฐ๊ฐ --> <div id="target"> </div> <!-- addedNodes ๋ฐํ๋จ --> <div id="target"> <p>just added!</p> </div>
์ ์์์ ๊ฒฝ์ฐ, #target
ํ์์ ์ถ๊ฐ๋ ๋
ธ๋ p
์ ์ ๋ณด๊ฐ ๋ด๊ธด๋ค.
์ญ์ ๋ ๋
ธ๋ (removedNodes)
removedNodes
๋ ์ญ์ ๋ ๋
ธ๋๊ฐ ์์ ๊ฒฝ์ฐ, ์ด๋ฅผ ๋ฐฐ์ด ํํ๋ก ๋ฐํํ๋ค. ์กฐ๊ฑด์ addedNodes
์ ๊ฐ๋ค.
HTML
1 2 3 4 5 6 7 8 9
<!-- ์ด๊ธฐ๊ฐ --> <div id="target"> <p>just added!</p> </div> <!-- removedNodes ๋ฐํ๋จ --> <div id="target"> </div>
๋ณ๊ฒฝ๋ ์์ฑ๋ช
(attributeName)
attributeName
๋ ์์ฑ์ด ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ, ๋ณ๊ฒฝ๋ ์์ฑ์ ์ด๋ฆ์ ๋ฐํํ๋ค. ์์ฑ์ ๋ณํ์ ๊ด๋ จ๋ attributes
์ต์
์ด ํ์ฑํ ๋ ๊ฒฝ์ฐ ๋ฐํ๋๋ค.
HTML
1 2 3 4 5 6 7 8 9
<!-- ์ด๊ธฐ๊ฐ --> <div id="target"> </div> <!-- attributeName ๋ฐํ๋จ --> <div id="target" class="hi"> </div>
์ ์์์ ๊ฒฝ์ฐ, #target
์์ฑ์ผ๋ก ์ถ๊ฐ๋ ์์ฑ๋ช
class
๊ฐ ๋ฐํ๋๋ค.
๋ณ๊ฒฝ๋ ์์ฑ ๋ค์์คํ์ด์ค (attributeNamespace)
attributeNamespace
์์ฑ ๋ค์์คํ์ด์ค๊ฐ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ, ๋ณ๊ฒฝ๋ ์์ฑ ๋ค์์คํ์ด์ค๋ฅผ ๋ฐํํ๋ค. ๋ง์ฐฌ๊ฐ์ง๋ก, attributes
์ต์
์ด ํ์ฑํ ๋ ๊ฒฝ์ฐ ๋ฐํ๋๋ค.
๋ณ๊ฒฝ๋ ์์ฑ์ด ๋ค์์คํ์ด์ค๋ฅผ ๊ฐ์ง๊ณ ์์ ๊ฒฝ์ฐ, ๋ค์์คํ์ด์ค๋ฅผ ๊ฐ์ด ๋ฐํํ๋ค๊ณ ์ดํดํ๋ฉด ๋๋ค.
HTML
1 2 3 4 5 6 7 8 9
<!-- ์ด๊ธฐ๊ฐ --> <div id="target"> </div> <!-- attributeName ๋ฐํ๋จ --> <div id="target" class="hi"> </div>
๋ง์ฝ class
์์ฑ ์ setAttributeNS
๋ฅผ ํตํด ns
๋ผ๋ ๋ค์์คํ์ด์ค๋ก ๋ฌถ์ด ์์ฑํ๋ค๋ฉด, ์ฝ๋๋ ์๋์ ๊ฐ์ด ๊ตฌ์ฑ๋๋ค.
TYPESCRIPT
1
tag.setAttributeNS("ns", "class", "hi");
์์ ๊ฐ์ ์์์์, attributeName
์ class
๋ฅผ, attributeNamespace
์ ns
๋ฅผ ๋ฐํํ๊ฒ ๋๋ค.
๋ณ๊ฒฝ๋ ํ๊ทธ์ ๋ค์ ํ์ ํ๊ทธ (nextSibling)
์ถ๊ฐ ํน์ ์ ๊ฑฐ๋ ๋
ธ๋์ ๋ค์ ํ์ DOM์ ๋ฐํํ๋ค. ๋
ธ๋ ์ถ๊ฐ, ์ญ์ ์ ๊ด๋ จ๋ childList
์ต์
์ด ํ์ฑํ๋ ๊ฒฝ์ฐ ๋ฐํ๋๋ค.
๋ณ๊ฒฝ๋ ํ๊ทธ์ ์ด์ ํ์ ํ๊ทธ (previousSibling)
์ถ๊ฐ ํน์ ์ ๊ฑฐ๋ ๋ ธ๋์ ์ด์ ํ์ DOM์ ๋ฐํํ๋ค.
๋ณ๊ฒฝ ์ด์ ์ ๊ฐ (oldValue)
๋ณ๊ฒฝ ์ด๋ฒคํธ๊ฐ ๊ฐ์ง๋ ๊ฒฝ์ฐ, ๋ณ๊ฒฝ ์ด์ ์ ๊ฐ์ ๋ฐํํ๋ค. attributeOldValue
๋ฐ characterDataOldValue
์ต์
์ด true
์ผ ๊ฒฝ์ฐ์๋ง ๊ฐ์ ๋ฐํํ๋ค.
์ด๋ฒคํธ ๋์ (target)
์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ํ๊ทธ๋ฅผ ๋ฐํํ๋ค.
์ด๋ฒคํธ ํ์
(type)
๋ฐ์ํ ์ด๋ฒคํธ์ ํ์ ์ ๋ฐํํ๋ค. ์๋์ ๊ฐ ์ค, ๋ฐ์ํ ์ด๋ฒคํธ์ ์ด๋ฆ์ ๋ฐํํ๋ค.
attributes
characterData
childList
observer
MutationObserver
๊ฐ์ฒด๋ฅผ ๋ฐํํด์ค๋ค. ์ด๋ฅผ ํตํด ์ฝ๋ฐฑ ๋ฉ์๋ ๋ด์์๋ MutationObserver
๋ฅผ ์ฐ์์ ์ผ๋ก ๋ค๋ฃฐ ์ ์๋ค.
React์์ ์ปค์คํ
ํ
์ผ๋ก ๊ฐํธํ๊ฒ ์ฌ์ฉํ๊ธฐ
MutationObserver
๋ฅผ ์ปค์คํ
ํ
์ ํตํด ๊ฐํธํ๊ฒ ์ฌ์ฉํด๋ณด์.
TYPESCRIPT
1 2 3 4
export function useMutationObserver(): void { // }
์์ ๊ฐ์ด useMutationObserver
๋ฉ์๋๋ฅผ ์ ์ํ๋ค.
useMutationObserver
๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์ ์๋์ ์ธ ์์๊ฐ ํ์ํ๋ค.
- ๋์ DOM
- ์ฝ๋ฐฑ ๋ฉ์๋
- ์ต์
์ ์ธ ์์๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ์ ์ํ์. 1๋ฒ์ ๊ฒฝ์ฐ, ์ผ๋ฐ์ ์ผ๋ก HTMLElement
๊ฐ ํ์ํ์ง๋ง, ํด๋น ํ
์์๋ HTMLElement
ํ์
๋ฟ๋ง ์๋๋ผ, string
์ ๋ฐ์ #id
, .class
์ ๊ฐ์ ์ ํ์๋ก ํ๊ทธ๋ฅผ ํ์ฉํ ์ ์๋๋ก ๋ง๋ค ๊ฒ์ด๋ค.
TYPESCRIPT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
import { useEffect } from "react"; export type UseMutationObserverCallback = (entry: MutationRecord) => void; /** * MutationObserver ์ ์ฉ ํ ๋ฉ์๋ * * @param {Element | string | null} ref: Element * @param {UseMutationObserverCallback} callback: ์ฝ๋ฐฑ ๋ฉ์๋ * @param {MutationObserverInit} options: ์ต์ */ export function useMutationObserver(ref: Element | string | null, callback: UseMutationObserverCallback, options: MutationObserverInit): void { useEffect(() => { const ro = new MutationObserver((entries) => { entries.forEach(callback); }); }, [ ref, callback, options ]); }
MutationObserver
๋ฅผ ์ด๊ธฐํํ์ฌ mo
๋ณ์๋ก ํ ๋นํ๋ค.
์ดํ ref
๋ก ์ง์ ํ DOM์ ํ ๋นํ๋ค.
ref
๋ ์ธ ๊ฐ์ง ์ํ์ ๋ฐ๋ผ ์๋์ ๊ฐ์ ๋ถ๊ธฐ๋ฅผ ๊ฑฐ์น๋ค.
null
์ผ ๊ฒฝ์ฐ -> ํจ์คํ๋ค.string
์ผ ๊ฒฝ์ฐ ->document.querySelector
๋ฉ์๋๋ก ํ๊ทธ๋ฅผ ์ ํํ ๋ค, ํด๋น ํ๊ทธ๋ฅผ ๋ฑ๋กํ๋ค.HTMLElement
์ผ ๊ฒฝ์ฐ -> ์ด๋ฏธElement
์ด๋ฏ๋ก, ์ฆ์ ๋ฑ๋กํ๋ค.
typeof ref === "string"
๊ตฌ๋ฌธ์ ํตํด ref
๊ฐ ๋ฌธ์์ด์ธ์ง๋ฅผ ํ๋ณํ์ฌ ๊ตฌํํ๋ค.
mo.observe()
๋ฉ์๋๋ฅผ ํตํด ํ๊ทธ๋ฅผ ๋ฑ๋กํ ์ ์๋ค.
TYPESCRIPT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
import { useEffect } from "react"; export type UseMutationObserverCallback = (entry: MutationRecord) => void; /** * MutationObserver ์ ์ฉ ํ ๋ฉ์๋ * * @param {Element | string | null} ref: Element * @param {UseMutationObserverCallback} callback: ์ฝ๋ฐฑ ๋ฉ์๋ * @param {MutationObserverInit} options: ์ต์ */ export function useMutationObserver(ref: Element | string | null, callback: UseMutationObserverCallback, options: MutationObserverInit): void { useEffect(() => { const mo = new MutationObserver((entries) => { entries.forEach(callback); }); // DOM์ด ์ ํจํ ๊ฒฝ์ฐ if (ref) { // ref๊ฐ ๋ฌธ์์ด์ผ ๊ฒฝ์ฐ if (typeof ref === 'string') { const tag = document.querySelector(ref); // ํ๊ทธ๊ฐ ์ ํจํ ๊ฒฝ์ฐ if (tag) { mo.observe(tag, options); } } // DOM์ผ ๊ฒฝ์ฐ else { mo.observe(ref, options); } } }, [ ref, callback, options ]); }
๋ง์ง๋ง์ผ๋ก, ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง ๋ ๋๋ง๋ค MutationObserver
์ ๋ฑ๋ก์ด ์ค์ฒฉ๋์ง ์๋๋ก, ์ด๊ธฐํ ์ฝ๋๋ฅผ ๋ฃ์ด์ค๋ค.
mo.disconnect()
๋ฉ์๋๋ฅผ ํตํด MutationObserver
๋ฅผ ์ ๊ฑฐํ ์ ์๋ค.
์ ์ฒด ์ฝ๋
TYPESCRIPT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
import { useEffect } from "react"; export type UseMutationObserverCallback = (entry: MutationRecord) => void; /** * MutationObserver ์ ์ฉ ํ ๋ฉ์๋ * * @param {Element | string | null} ref: Element * @param {UseMutationObserverCallback} callback: ์ฝ๋ฐฑ ๋ฉ์๋ * @param {MutationObserverInit} options: ์ต์ */ export function useMutationObserver(ref: Element | string | null, callback: UseMutationObserverCallback, options: MutationObserverInit): void { useEffect(() => { const mo = new MutationObserver((entries) => { entries.forEach(callback); }); // DOM์ด ์ ํจํ ๊ฒฝ์ฐ if (ref) { // ref๊ฐ ๋ฌธ์์ด์ผ ๊ฒฝ์ฐ if (typeof ref === 'string') { const tag = document.querySelector(ref); // ํ๊ทธ๊ฐ ์ ํจํ ๊ฒฝ์ฐ if (tag) { mo.observe(tag, options); } } // DOM์ผ ๊ฒฝ์ฐ else { mo.observe(ref, options); } } return () => { mo.disconnect(); }; }, [ ref, callback, options ]); }
์ ์ฒด ์ฝ๋๋ ์์ ๊ฐ๋ค.
CodeSandbox๋ฅผ ํตํ ์์
CodeSandbox๋ก ๊ฐ๋จํ ์์๋ฅผ ๊ตฌํํ๋ค.
๋ง์น๋ฉฐ
์ด์ ๊ฐ์ด MutationObserver
๋ฅผ ํ์ฉํ๋ฉด DOM์ ๋ณํ๋ฅผ ๋ฅ๋์ ์ผ๋ก ๊ฐ์งํ ์ ์๋ค.
๊ฐํน, React ์ปดํฌ๋ํธ์์ ์์๋ฅผ ๋ณ๊ฒฝํ ๊ฒฝ์ฐ, ์ด๋ฅผ ํ ์ปดํฌ๋ํธ์์ ์ฐ๊ด์ง์ด ์ถ๊ฐ์ ์ธ ์ฝ๋๋ฅผ ์คํํ ํ์๊ฐ ์๊ธฐ๊ธฐ๋ ํ๋ค. ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ปดํฌ๋ํธ์ ๋ถํ์ํ ํ ์ปดํฌ๋ํธ ๋ก์ง์ ๋ฃ๊ธฐ๋ ํ๋ค.
MutationObserver
๋ฅผ ํ์ฉํ๋ฉด ์ด๋ฅผ ๋ถ๋ฆฌํ์ฌ ๊ฐ๋จํ๊ฒ ๊ตฌํํ ์ ์๋ค.
๋จ์ ์ต์ ๋ฒ๋ ์๋์ ์ผ๋ก ์ฌ์ฉ์ฑ์ด ๋ฎ์ ๊ฒ๋ค์ด๋ค. ๊ทธ ์ค, ์ฑ๋ฅ๊ณผ ๊ด๋ จ๋ PerformanceObserver
์ ๋ํด ๋ค๋ฃฐ ์์ ์ด๋ค.
๐ท๏ธ ํ๊ทธ
์ฝ์ด์ฃผ์ ์ ๊ณ ๋ง์์!
๋์์ด ๋์ จ๋ค๋ฉด, ๊ณต๊ฐ์ด๋ ๋๊ธ์ ๋ฌ์์ฃผ์๋ ๊ฑด ์ด๋ค๊ฐ์?
๋ธ๋ก๊ทธ ์ด์์ ํฐ ํ์ด ๋ฉ๋๋ค.