안녕하세요. 정말 오랜만에 글을 써보는데요.
회사에서 다른 프론트엔드 개발자분들과 얘기하다가 Vue v-on이 어떻게 동작하는지 궁금증이 생겨 내부 코드를 직접 뜯어봤습니다. 그 내용을 정리하면서 이벤트 핸들러 캐싱이 어떻게 동작하는지 살펴봤습니다. 😆
핸들러 캐싱이 필요한 이유
문제 상황: 만약 캐싱이 없다면?
<!-- ParentComponent.vue -->
<template>
<div>
<p>카운트: {{ count }}</p>
<button @click="count++">카운트 증가</button>
<ChildComponent @custom-event="() => console.log('clicked!')" />
</div>
</template>
부모가 리렌더링될 때마다 인라인 핸들러가 새로 만들어지면 함수 참조가 달라져서 자식 입장에서는 props가 변경된 것으로 인식합니다. 상황에 따라 불필요한 리렌더링이 생길 수 있습니다.
캐싱의 효과
// 캐싱 없음: 매번 새로운 함수
render1: handler = () => {} // 참조: 0x1234
render2: handler = () => {} // 참조: 0x5678
// 캐싱 적용: 동일한 참조 유지
render1: handler = cache(() => {}) // 0x1234
render2: handler = cache(() => {}) // 0x1234
이런 방식으로 Vue는 핸들러를 캐싱해 불필요한 리렌더링을 줄여줍니다. 다만 모든 케이스에서 꼭 필요한 건 아니고, 성능 최적화 차원에서 안전하게 적용되는 정도라고 이해하면 됩니다.
캐싱되는 조건
초기 조건 (vOn.ts#L82)
let shouldCache: boolean = context.cacheHandlers && !exp && !context.inVOnce
상세 조건 (vOn.ts#L100-L115)
shouldCache =
context.cacheHandlers &&
!context.inVOnce &&
!(exp.type === NodeTypes.SIMPLE_EXPRESSION && exp.constType > 0) &&
!(isMemberExp && node.tagType === ElementTypes.COMPONENT) &&
!hasScopeRef(exp, context.identifiers)
정리하면:
cacheHandlers옵션이 켜져 있을 때v-once내부가 아닐 때- 런타임 상수가 아닐 때
- 컴포넌트에 전달되는 멤버 표현식이 아닐 때
- 스코프 변수를 참조하지 않을 때
보통은 이런 조건을 만족하면 캐싱됩니다.
캐싱되지 않는 케이스
1. 표현식 없음 (vOn.ts#L83)
<button @click>Click</button>
2. v-once 내부 (vOn.ts#L103)
<div v-once>
<button @click="handleClick">Click</button>
</div>
3. 런타임 상수 (vOn.ts#L106)
<script setup>
const CONSTANT_HANDLER = () => console.log('constant')
</script>
<template>
<button @click="CONSTANT_HANDLER">Click</button>
</template>
4. 컴포넌트 멤버 표현식 (vOn.ts#L112)
<transition @enter="obj.handleEnter"> ... </transition>
5. 스코프 변수 참조 (vOn.ts#L115)
<li v-for="item in list" :key="item.id">
<button @click="handleClick(item)">{{ item.name }}</button>
</li>
이런 경우들은 의도적으로 캐싱하지 않도록 설계돼 있습니다. 캐싱이 안 된다고 해서 문제되는 건 아니고, 올바른 동작을 보장하기 위한 선택입니다. 🙂
멤버 표현식 최적화 (vOn.ts#L120-L126)
캐싱 가능한 멤버 표현식의 경우 특별한 최적화 처리를 합니다.
if (shouldCache && isMemberExp) {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
exp.content = `${exp.content} && ${exp.content}(...args)`
} else {
exp.children = [...exp.children, ` && `, ...exp.children, `(...args)`]
}
}
// 원본
@click="obj.method"
// 변환 후 (최신 값 접근 보장)
@click="obj.method && obj.method(...args)"
인라인 구문 래핑 (vOn.ts#L138-L153)
인라인 구문이나 캐싱 가능한 멤버 표현식은 함수로 래핑합니다.
if (isInlineStatement || (shouldCache && isMemberExp)) {
// wrap inline statement in a function expression
exp = createCompoundExpression([
`${
isInlineStatement
? !__BROWSER__ && context.isTS
? `($event: any)`
: `$event`
: `${
!__BROWSER__ && context.isTS ? `\n//@ts-ignore\n` : ``
}(...args)`
} => ${hasMultipleStatements ? `{` : `(`}`,
exp,
hasMultipleStatements ? `}` : `)`,
])
}
// 인라인 구문
@click="count++"
// → ($event) => (count++)
// 멤버 표현식 (캐싱 시)
@click="obj.method"
// → (...args) => (obj.method && obj.method(...args))
실제 캐싱 적용 (vOn.ts#L170-L175)
if (shouldCache) {
ret.props[0].value = context.cache(ret.props[0].value)
}
성능 영향
<template>
<div v-for="item in items" :key="item.id">
<!-- 캐싱 없으면: 부모 업데이트 시 전체 리렌더링 발생 가능 -->
<!-- 캐싱 있으면: 변경된 아이템만 리렌더링 -->
<ItemComponent @click="handleClick" />
</div>
</template>
마무리
핸들러 캐싱은 Vue가 불필요한 리렌더링을 줄이기 위해 넣은 최적화 장치입니다. 하지만 모든 상황에 적용되는 건 아니고, 조건에 따라 캐싱되기도 하고 새로 만들어지기도 합니다.
캐싱이 되지 않아도 대부분 문제로 이어지지는 않습니다. 성능 이슈가 눈에 띄면 그때 규칙을 확인해도 충분합니다. 😉
'DEV > Study' 카테고리의 다른 글
| Ubuntu에서 도커 설치하기 (0) | 2020.11.29 |
|---|