부모가 준 class와 placeholder가 엉뚱한 div에 붙은 이유
문제 발생
버튼 컴포넌트를 감싸는 래퍼를 만들었더니, 부모가 준 class가 원하는 곳이 아니라 바깥 div에 붙었습니다.
<!-- MyField.vue -->
<template>
<div class="field">
<input />
</div>
</template><MyField class="is-invalid" placeholder="이름" />is-invalid도 placeholder도 전부 div로 갔습니다. placeholder는 div에서 아무 의미가 없습니다.
원인 분석
폴스루 속성(fallthrough attributes) 입니다. Vue 공식 문서의 정의대로, 컴포넌트에 전달됐지만 받는 컴포넌트의 props나 emits에 명시적으로 선언되지 않은 속성이나 v-on 리스너를 말합니다. class, style, id가 대표적입니다.
기본 동작은 루트 요소에 자동으로 붙는 것입니다. 위 컴포넌트의 루트는 div이므로 전부 거기로 갔습니다. class와 style은 기존 값과 병합되고, v-on 리스너도 같은 규칙으로 루트에 붙습니다.
루트가 여러 개일 때는 다릅니다. 문서가 명시합니다 — 루트 노드가 하나인 컴포넌트와 달리, 루트 노드가 여러 개인 컴포넌트는 자동 속성 폴스루 동작이 없습니다. $attrs를 어디에도 바인딩하지 않으면 런타임 경고가 납니다.
해결 방안
- 자동 상속을 끄고 원하는 곳에 직접 바인딩합니다.
<script setup>
defineOptions({ inheritAttrs: false });
</script>
<template>
<div class="field">
<input class="field-input" v-bind="$attrs" />
</div>
</template>이제 placeholder는 input으로, class는 병합되어 input에 붙습니다.
class만 래퍼에 두고 싶다면 나눠 씁니다. 시각적 속성과 기능적 속성의 목적지가 다를 수 있습니다.
<script setup>
import { useAttrs, computed } from "vue";
defineOptions({ inheritAttrs: false });
const attrs = useAttrs();
const wrapperClass = computed(() => attrs.class);
const inputAttrs = computed(() => {
const { class: _class, ...rest } = attrs;
return rest;
});
</script>
<template>
<div class="field" :class="wrapperClass">
<input v-bind="inputAttrs" />
</div>
</template>-
<script setup>에서는useAttrs()로 읽습니다. 다만 문서가 짚습니다 — 이attrs객체는 항상 최신 폴스루 속성을 반영하지만 (성능상의 이유로) 반응형이 아닙니다.watch로 감시하려 하면 동작하지 않습니다. -
prop으로 선언하면 폴스루에서 빠집니다. 선언한 순간 "소비"되어
$attrs에 남지 않습니다. 컴포넌트가 직접 다뤄야 하는 값은 선언하고, 그대로 흘려보낼 것은 선언하지 않습니다. -
루트가 컴포넌트여도 계속 전달됩니다. 문서의 설명대로 어떤 컴포넌트가 다른 컴포넌트를 루트로 렌더하면 폴스루 속성이 자동으로 그 안쪽까지 전달됩니다. 래퍼를 두 겹 만들 때 유용하지만, 어디에 붙는지 추적이 어려워지므로 중간에서 명시적으로 처리하는 편이 낫습니다.
-
루트가 여러 개면 반드시 명시합니다. 경고를 무시하면 부모가 준
class가 조용히 사라집니다.
<template>
<span v-bind="$attrs">...</span>
<p>...</p>
</template>
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.