π£οΈ μ΄λ²€νΈ λ²λΈλ§κ³Ό μΊ‘μ³λ§μ λν΄μ μκ³ κ³μλμ?λͺ¨λ μλ°μ€ν¬λ¦½νΈ λ₯λ€μ΄λΈμμ λ΄€μλλ°;;
λ²λΈλ§μ λ²λΈ μ λ ¬μ²λΌ bubble upμ΄λΌλ λ§μμ μ μΆν μ μλ―μ΄ μ΄λ€ μ΄λ²€νΈκ° μλμμ μλ‘ μ μ΄λλ κ±°κ³ ,
μΊ‘μ³λ§μ λ°λλ‘ μμμ μλλ‘ μ μ΄λλ κ±° μλμλ?
κ°λ μ μ΄λ μ λ μκ³ μμ§λ§ μ€μ μ μ©μ νλ κ²½μ°λ‘λ μ°κ²°μ΄ λμ§ μμμλ€.
μ΄λ² κΈ°νμ μ€μ λ‘ μΊ‘μ³λ§μ νμ©ν ν μ€μ 'Transpiler, “μ¬μ©”λ§κ³ “νμ©”νκΈ°'κ° μ’μ μμμΈ κ±° κ°μμ μμλ³΄κ³ μ νλ€.
λ²μ μμμ μ λͺ¨λ₯΄λ μ μ₯μμ μ¬μ©μμ μΌκ±°μμΌν¬μ‘±μ λ‘κΉ νλ 건 λμνμλ§ κ°λ₯ν κ±° κ°κΈ° λλ¬Έμ μ μ©ν΄ λ³Ό κΈ°νκ° μμμ§λ λͺ¨λ₯΄κ² μ§λ§, μ€μ μ¬μ© μ¬λ‘λ₯Ό μμμΌ κΈ°μ΅μ λ λ¨μ κ±° κ°μμ,
μ΄λ€ μμΌλ‘ νΈλμ€νμΌλ¬μ μ΄λ²€νΈ μΊ‘μ³λ§μ νμ©ν΄μ λ‘κΉ κ³Όμ κ°μ μ νλμ§ ν μ€κ° μ΄ μ΄λ²€νΈ μΊ‘μ³λ§μ νμ©ν΄μ λ‘κΉ μ μλνν λ°©λ²μ λν΄ μ΄ν΄λ³΄κ³ μ νλ€.
λ¨Όμ μ΄μ μ μ 리νλ μ΄λ²€νΈ μ νμ λν κΈμ μ¬λ°©λ¬Έν΄μ λ²λΈλ§/μΊ‘μ³λ§μ λν΄ μ΄ν΄λ³Έ λ€, ν μ€μ κΈμ μ λ¦¬ν΄ λ³΄μ.
μ΄λ²€νΈ μ ν (event propagation)
μ΄λ²€νΈ μ νλ, dom νΈλ¦¬ μμ μ‘΄μ¬νλ dom μμ λ Έλμμ λ°μνλ μ΄λ²€νΈλ dom νΈλ¦¬λ₯Ό ν΅ν΄ μ νλλ κ²μ λ§νλ€.
λ€μ μ½λλ κ° νκ·Έμ ν΄λ¦ μ΄λ²€νΈλ‘ λ‘κ·Έλ₯Ό μ°κ³ μλλ°, μ΄λ²€νΈ μ νκ° μ΄λ»κ² λ°μνλμ§ μ΄ν΄λ³΄μ.
<body>
<p>λ²λΈλ§ μΊ‘μ³λ§ μ΄λ²€νΈ <button>λ²νΌ</button></p>
</body>
document.body.addEventListener('click',()=>{
console.log('Handler for body');
});
document.querySelector('p').addEventListener('click',()=>{
console.log('Handler for paragraph');
});
document.querySelector('button').addEventListener('click',()=>{
console.log('Handler for button');
});
λ²νΌμ ν΄λ¦νλ©΄ μ΄λ€ μΌμ΄ μκΈΈκΉ?
π μ§μ ν΄λ³΄κΈ° https://1973072.playcode.io/
λ¨Όμ μμ±λ μ΄λ²€νΈ κ°μ²΄λ μ΄λ²€νΈλ₯Ό λ°μμν¨ dom μμμΈ μ΄λ²€νΈ νκΉμ μ€μ¬μΌλ‘ dom νΈλ¦¬λ₯Ό ν΅ν΄ μ νλλ€.
μ ν λ¨κ³λ 3λ¨κ³λ‘ ꡬλΆν μ μλλ°, μΊ‘μ³λ§/ νκΉ / λ²λΈλ§μΌλ‘ ꡬμ±λλ€.
μΊ‘μ³λ§ λ¨κ³μμλ μ΄λ²€νΈκ° dom νΈλ¦¬ μ΅μλ¨μμ νμ μμλ‘ μ νλκ³ , νκΉ λ¨κ³μμλ μ΄λ²€νΈκ° μ΄λ²€νΈ νκΉμ λλ¬νμ¬ μ€ννκ³ ,
λ²λΈλ§ λ¨κ³μμλ μ΄λ²€νΈκ° νμ μμμμ μμ μμ λ°©ν₯μΌλ‘ μ νλλ€.
μ΄λ κ² μ΄λ²€νΈ μ νκ° λλ νμμ μΌλνκ³ λμμ μμΈ‘ν΄ λ³Έλ€λ©΄
'Handler for paragraph'(μΊ‘μ³λ§) -> 'Handler for button'(νκΉ)->'Handler for body'(λ²λΈλ§) μμΌλ‘ μΆλ ₯λ κ²μ΄λ€.
νμ§λ§ μ€μ λ‘ λ²νΌμ ν΄λ¦νλ©΄ λ€μκ³Ό κ°μ΄ μΆλ ₯μ΄ λλ€.
'Handler for button' -> 'Handler for paragraph' -> 'Handler for body'
μ΄λ μ΄λ²€νΈ 리μ€λμ capture optionμ νΉμ ( { capture: true } ) νμ§ μμκΈ° λλ¬Έμ μ΄λ²€νΈλ₯Ό μΊ‘μ³λ§ λ¨κ³μμ μ‘μ§ μλλ€.
λ°λΌμ button μμλ₯Ό ν΄λ¦νλ©΄ μΊ‘μ³λ§ -> νκΉ -> λ²λΈλ§ λ¨κ³μ λ°λΌμ
body μμμ p μμμ μ΄λ²€νΈλ₯Ό μΊ‘μ²λ§ νκ³ (μ€ν x), νκΉμΈ λ²νΌ μμμμ μ΄λ²€νΈκ° μ€νλκ³ ,
λ²λΈλ§ λ¨κ³μμ pμμμ bodyμμμ μ΄λ²€νΈ μμ°¨μ μΌλ‘ μ€νλλ€.
μΊ‘μ³λ§ λ¨κ³μμ μ΄λ²€νΈ μ€νμ μνλ€λ©΄ λ€μκ³Ό κ°μ΄ μ΅μ μ μΆκ°ν΄ 보면
document.querySelector('p').addEventListener('click',()=>{
console.log('Handler for paragraph');
}, { capture: true });
μ΄λ²μλ μΆλ ₯ μμκ° μ²« μμΈ‘λλ‘ λμνλ€.
π μ§μ ν΄λ³΄κΈ° https://1973090.playcode.io/
νμ§λ§ λμ λ°λΌ μ΄λ²€νΈ μ νκ° λμ§ μλλ‘ νκ³ μΆμ μλ μλ€. κ·Έλ΄ λ event.stopPropagation λ©μλλ₯Ό νμ©ν μ μλ€.
λ€λ§ μμ μμμμ λ²λΈλ§μ΄ νμν μν©μ μμ μ°¨λ¨νκΈ° λλ¬Έμ μ¬μ©μ μ£Όμλ₯Ό ν΄μΌ νλ€.
μ΄λ²€νΈ μμ (event delegation)
μ΄λ²€νΈ μμμ μ¬λ¬ κ°μ νμ dom μμμ κ°κ° μ΄λ²€νΈ νΈλ€λ¬λ₯Ό λ±λ‘νλ λμ
νλμ μμ dom μμμ μ΄λ²€νΈ νΈλ€λ¬λ₯Ό λ±λ‘νμ¬ μ¬λ¬ μμλ₯Ό νκΊΌλ²μ λ€λ£° μ μλλ‘ μ΄λ²€νΈ μ νλ₯Ό νμ©ν μ΄λ²€νΈ νΈλ€λ§ ν¨ν΄μ΄λ€.
μ΄μ data-click-log μμ±μ΄ μλ κ°μ₯ κ·Όμ ν μμλ₯Ό ν΄λ¦νλλ‘ λ³κ²½ν΄ 보μ.
<body data-click-log="body">
<p data-click-log="paragraph">
λ²λΈλ§ μΊ‘μ³λ§ μ΄λ²€νΈ <button data-click-log="button">λ²νΌ</button>
</p>
</body>
<script>
document.body.addEventListener(
'click',
event => {
const logTarget = event.target.closest('[data-click-log]');
if (logTarget) {
console.log(`Handler for ${logTarget.getAttribute('data-click-log')}`);
}
},
{ capture: true }
);
</script>
μ΄λ κ² μ΄λ²€νΈ μ ν μμ±μ νμ©ν΄ λ¨μ λ°λ³΅μ κ³Όμ μ νλ λ μ μλ€.
μ΄λ²μλ ν μ€μ κΈμμλ νΈλμ€νμΌλ¬λ₯Ό νμ©ν΄ data-click-log μμ±μ μλμΌλ‘ μ£Όμ νλ λ°©λ²μ λν΄ μ΄ν΄λ³΄μ.
ν μ€: Transpilerλ₯Ό νμ©ν λ‘κΉ
νΈλμ€νμΌλ¬λ μ½λλ₯Ό λ³νν΄ μ£Όλ λꡬλ€.
μλ₯Ό λ€λ©΄, ES6λ₯Ό ES5λ‘ λ³ννκ±°λ 리μ‘νΈμ JSX/TSλ₯Ό λΈλΌμ°μ κ° μ΄ν΄ν μ μλ μλ°μ€ν¬λ¦½νΈλ‘ λ³νν΄ μ£Όλ μν μ ν΄μ£Όκ³ ,
λΈλΌμ°μ νΈνμ± λ±μ μ μ§ν μ μλλ‘ λμμ€λ€λ μ μμ, κ°λ°μλ λΉμ¦λμ€ λ‘μ§μ μ§μ€μ ν΄μ€ μ μλ€λ μ μμ μ μ©νλ€.
babelμ΄λ swcκ° λνμ μΈ νΈλμ€νμΌλ¬μ ν΄λΉλλλ°, ν μ€λ μ΄ νΈλμ€νμΌλ¬μ νΉμ§μ μ΄λ € λ‘κΉ κ³Όμ μ κ°μ νλ€.
λ‘κΉ μ μ€κ³νλ λ°©μμ ν¬κ² λ κ°μ§λ‘ μκ°ν΄ λ³Ό μ μλ€.
// μλμΌλ‘ λ‘κΉ
ν¨μ μ€ν
<Button
onClick={() => {
log({ content: 'λ€μ' });
handleClick();
}}
>
λ€μ
</Button>
// μΆμνλ λ‘κΉ
μ»΄ν¬λνΈ νμ©
<LoggingClick>
<Button onClick={handleClick}>
λ€μ
</Button>
</LoggingClick>
λλ μμμ λ€λ£¨μλ μ΄λ²€νΈ μΊ‘μ³λ§κ³Ό data-attributeμΌλ‘ λ‘κΉ μ μ²λ¦¬ν μλ μλ€.
<Button onClick={() => {}} data-click-log>
λ€μ
</Button>
// ν΄λ¦ μ click event μΊ‘μ³λ§μ ν΅ν΄ μΈμ§νκ³ ,
// ν΄λ¦ νκΉμμ κ°μ₯ κ·Όμ ν data-click-log μμ±μ μ§λ DOM μ°Ύκ³ ,
// domμ text nodeλ₯Ό νμ
νμ¬ μ μ κ° ν΄λ¦ν μ»΄ν¬λνΈμ μ 보λ₯Ό λ€μκ³Ό κ°μ΄ λ‘κΉ
νλ€.
{
log_type: 'click',
content: 'λ€μ',
}
νμ§λ§ μμ λ°©μμ μ€νλ λλ½ μ€μμ μ·¨μ½νκ³ μ¬λ¬ νλ‘μ€κ° μ λ¬λλ μν©μμλ λμ±λ μ·¨μ½ν΄μ§ μλ°μ μλ€.
κ·Έλ¦¬κ³ λ°λ³΅/κ·μΉμ μΌλ‘ μ½λλ₯Ό λ³κ²½ν΄μΌ νλ μ§λ£¨ν μμ λ μ 무 ν¨μ¨μ μ νμν¬ κ²μ΄λ€.
<Button
type="primary"
variant="weak"
size="large"
data-cilck-log // μ€ν
onClick={() => {}}
disabled={false}
loading={false}
css={{ minWidth: 100, minHeight: 80 }}
>
λ€μ
</Button>
lint ruleμ μΆκ°νκ±°λ μλ μμ± κΈ°λ₯μ μ 곡νλ λ°©μμΌλ‘ λ¨μ μ€μλ₯Ό μ€μΌ μλ μμ§λ§,
μ΄ λ°©μμ λΉμ¦λμ€ λ‘μ§κ³Ό μ§κ²°λ μ½λλ λ‘κΉ μ½λλ₯Ό λΆλ¦¬νμ§ μλλ€λ μ μμ κ·Όλ³Έμ μΈ λ¬Έμ λ₯Ό ν΄κ²°ν μ μμ΄ μμ½λ€λ λ¬Έμ κ° μ¬μ ν μλ€.
clickable ν μμμ data-click-log μμ±μ μλμΌλ‘ μ£Όμ ν μ μλ€λ©΄? +πΌ μ½λλ₯Ό 지 νμκ° μλ€λ©΄?
μ¬κΈ°μ νΈλμ€νμΌλ¬μ μν μ΄ λΉμ λΌ μ μλ€.
νΉμ 쑰건(Clickable ν μμ)μ λ°λΌ μλ§κ²(data-click-log μμ± μΆκ°) μ½λ λ³ννκΈ°μ transplierκ° μ ν©!
<Button onClick={() => {}}>
λ€μ
</Button>
// μμ μ½λκ° μλμ²λΌ μλ λ³νλλ©΄ μ’κ² λ€!
<Button onClick={() => {}} data-click-log>
λ€μ
</Button>
ν μ€λ λ€μκ³Ό κ°μ΄ clickable (onClick, onChange, onTouchStart)μ κ°μ μ μ ν΄λ¦ λ°μ μ΄λ²€νΈ νΈλ€λ¬λ₯Ό κ·μ νκ³ ,
ν΄λΉ 쑰건μ μΆ©μ‘±νλ SWCμ Babelμ© νλ¬κ·ΈμΈμ λ§λ€μλ€.
const CLICK_EVENTS = ['onClick', 'onTouchStart', 'onChange', 'onMouseDown'];
const CLICK_LOG_ATTR = 'data-click-log';
function plugin({ types: t }: typeof babel): PluginObj {
return {
name: 'babel-plugin-tossbank-logger',
visitor: {
JSXOpeningElement(path) { // JSX νκ·Έμ μμ μμλ€μ μννλ©° μ€νν μ½λ°± μ μ
const { node } = path;
// κ° μμ μννλ©° CLICK_EVENTSμ ν΄λΉνλ clickable μ΄λ²€νΈ νΈλ€λ¬κ° μ‘΄μ¬νλ μ§ μ²΄ν¬
const hasOnClickAttribute = node.attributes.some(attr => {
return CLICK_EVENTS.includes(attr.name.name);
});
// clickable μ΄λ²€νΈκ° μ‘΄μ¬νλ©΄ CLICK_LOG_ATTR μ£Όμ
!
if (hasOnClickAttribute) {
const dataClickLogAttribute = t.jSXAttribute(t.jSXIdentifier(CLICK_LOG_ATTR), null);
node.attributes.push(dataClickLogAttribute);
}
},
},
};
}
Babelμ AST(μΆμ ꡬ문 νΈλ¦¬)λ₯Ό λ§λ€κ³ , κ° νλ¬κ·ΈμΈμκ² μ 곡νμ¬ κ° λ Έλλ€μ μννλ©° μ²λ¦¬ν μ μλλ‘ μΈν°νμ΄μ€λ₯Ό μ 곡νκ³ μλ€.
μ΄μ λ‘κΉ νλ¬κ·ΈμΈμ νμ©νλ€λ©΄ ν΄λ¦ λ‘κΉ μ μ½λμ μΆκ°ν νμ μμ΄ λΉμ¦λμ€ λ‘μ§λ§μΌλ‘ μ½λλ₯Ό μμ±ν μ μλ€.
<Button onClick={() => {}}>
λ€μ
</Button>
ν΄λΉ λ°©λ²μΌλ‘λ κ΄μ¬μ¬κ° λΆλ¦¬λ λΏλ§ μλλΌ λꡬλ μ§ λμΌνκ² λ‘κΉ μ μ²λ¦¬νλλ‘ λ³΄μ₯ν΄ μ€λ€λ μ₯μ μ΄ μλ€.
π λ§μΉλ©΄μ
μ€λμ μ΄λ²€νΈ μμ(event delegation)κ³Ό νΈλμ€νμΌλ¬λ₯Ό νμ©ν ν μ€μ λ‘κΉ μλν λ°©λ²μ λν΄ μ΄ν΄λ΄€λ€.
μ΄μ μ μ΄λ²€νΈ μμ/μ νμ λν΄ μ 리ν κΈμ΄ μμλλ°, μ€λ₯λ₯Ό λ°κ²¬!
μ± λ§ μ½κ³ νΌμμ μΌλ‘ κ·Έλ΄ κ²μ΄λ€~λΌκ³ μκ°νλλ°, μ€μ λ‘ μ½λλ₯Ό μ°μ΄λ³΄λ μ’ λ€λ₯Έ κ²°κ³Όκ° λμμλ€.
μ΄λ²€νΈ λ²λΈλ§/μΊ‘μ³λ§μ λν΄ κ°λ¬Όκ°λ¬Ό ν΄μ§ λμ―€, μ΄λ²€νΈ μ νμ λν΄ μκ³ μλ κ²μ μ κ²νκ³ , μ’μ μ€μ μ¬μ© μ¬λ‘μ λν΄ λ€λ€ λ³Ό μ μμ΄μ λμ± κΈ°μ΅μ λ¨μ κ±° κ°λ€!
ν μ€ κΈ λκΈμ μ§λ¬Έμ΄ νλ μμλλ°, λ΅μ μ°ΎκΈ° μν΄ μ΄κ²μ κ² μ°Ύμ보λ€κ° 리μ‘νΈμμλ μ΄λ²€νΈ νΈλ€λ§ λ°©μμ΄ κΈ°μ‘΄ domκ³Όλ μ’ λ€λ₯Έ ννλΌλ κ±Έ μκ² λμλλ°, λ€μμλ μ΄ λΆλΆμ λν΄ λ μ΄ν΄λ³Ό κ±° κ°λ€.
리μ‘νΈ μ΄λ²€νΈ νΈλ€λ¬λ λ²λΈλ§ μ μ€νλλ€λ μ μ 보면 onClickCaptureλ₯Ό ν΅ν΄ λ°λ‘ λͺ μνμ§ μμκΈ° λλ¬Έμ λ²λΈλ§μ νμ©ν κ² λ§λ κ±° κ°λ€.
π References
https://github.com/Pyotato/fe_study/blob/main/modern_javascript_deep_dive/40_event.md