본문 바로가기
DEV/Study

Vue v-on 핸들러 캐싱, 언제 되고 언제 안 될까?

by cha.d 2025. 8. 29.

안녕하세요. 정말 오랜만에 글을 써보는데요.

회사에서 다른 프론트엔드 개발자분들과 얘기하다가 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)

정리하면:

  1. cacheHandlers 옵션이 켜져 있을 때
  2. v-once 내부가 아닐 때
  3. 런타임 상수가 아닐 때
  4. 컴포넌트에 전달되는 멤버 표현식이 아닐 때
  5. 스코프 변수를 참조하지 않을 때

보통은 이런 조건을 만족하면 캐싱됩니다.

캐싱되지 않는 케이스

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