개발 기술/사소하지만 놓치기 쉬운 개발 지식

[vue] v-show 선언 위치에 따라 렌더링 버그가 발생한다고?

by GicoMomg 2024. 11. 7.

1. 들어가며…

  • PrimeVue(vue UI 라이브러리)에는 textarea의 높이를 자동으로 조정하는 autoResize옵션이 있다.
  • 이 옵션을 사용하면, 텍스트 길이에 따라 textarea의 높이를 조절 할 수 있다. 그런데 만약 v-show를 사용해 textarea를 렌더링할 경우, autoResize가 작동하지 않는 현상이 발생한다.



  • 아래는 primevue 코드를 참고해, autoResize만을 구현한 코드이다.
TextArea 코드 자세히 보기
  <template>
    <textarea
      ref="textarea"
      class="textarea"
      :value="props.value"
      @input="onInput"
    ></textarea>
  </template>

  <script setup lang="ts">
  import { onMounted, onUpdated, ref } from "vue";

  const props = defineProps({
    value: {
      type: String,
      required: true,
    },
  });
  const emit = defineEmits(["updated"]);

  const autoResize = ref(true);
  const textarea = ref<HTMLTextAreaElement | null>(null);

  const resize = () => {
    if (!textarea.value) return;

    textarea.value.style.height = "auto";
    textarea.value.style.height = textarea.value.scrollHeight + "px";

    const isOverflow =
      parseFloat(textarea.value.style.height) >=
      parseFloat(textarea.value.style.maxHeight);
    if (isOverflow) {
      textarea.value.style.overflowY = "scroll";
      textarea.value.style.height = textarea.value.style.maxHeight;
    } else {
      textarea.value.style.overflow = "hidden";
    }
  };

  const onInput = (event) => {
    emit("updated", event.target.value);
    onResize();
  };

  onMounted(() => {
    onResize();
  });

  onUpdated(() => {
    onResize();
  });

  const onResize = () => {
    if (autoResize.value) {
      resize();
    }
  };
  </script>

  <style>
  .textarea {
    resize: none;
  }
  </style>

  • 우선 코드를 간단히 설명하자면, resize()는 textArea의 높이를 조정하는 함수이다.
  • 이 함수는 onMounted, onUpdated훅에서 실행된다.
const resize = () => {
    if (!textarea.value) return;

    // (A)
    textarea.value.style.height = 'auto'; 

    // (B)
    textarea.value.style.height = textarea.value.scrollHeight + 'px';

    // (C)
    const isOverflow = parseFloat(textarea.value.style.height) >= parseFloat(textarea.value.style.maxHeight);  
    if (isOverflow) {
        textarea.value.style.height = textarea.value.style.maxHeight;
    }

    // (D)
    textarea.value.style.overflowY = isOverflow ? 'scroll' : 'hidden';
};
코드 번호 설명
(A) 먼저 textarea.value.style.height를 'auto'로 설정하여 높이를 초기화한다.
(B) scrollHeight 값을 기반으로 현재 텍스트가 차지하는 높이만큼 textarea.value.style.height를 설정한다.
(C) maxHeight 값과 비교하여 높이가 초과하는지 확인한다. 만약 maxHeight를 초과할 경우 overflowY 스타일을 'scroll'로 설정하여 스크롤이 나타나도록 하고, 높이를 maxHeight에 맞춘다.
(D) 높이가 maxHeight에 도달하지 않으면 overflowY를 'hidden'으로 설정하여 스크롤을 제거한다.

  • onInputtextarea에 입력할 때마다 호출되며, 입력 값을 업데이트하고 자동으로 크기를 조정한다.
const onInput = (event: Event) => {
     // (E)
     emit("updated", event.target.value);

    // (F)
    if (autoResize.value) {
        resize();
      }
};
코드 번호 설명
(E) event.target.value을 emit을 사용해 이벤트를 전송한다.
F) autoResize가 활성화되어 있으면 resize 함수를 호출하여 텍스트 입력에 따라 textarea의 높이를 동적으로 조정한다.

이처럼 textArea의 autoResize는 컴포넌트가 렌더링되거나 업데이트될 때,
콘텐츠 길이에 따라 scrollHeight를 textArea의 높이로 지정한다.


  • 하지만 <TextArea>v-show를 사용하는 방식에 따라 렌더링 문제가 발생한다
  • 우선 <TextArea>에 직접 v-show를 추가하면 autoResize가 정상 작동한다.

<TextArea v-show="toggle" :value="value" @updated="onInput" />

const toggle = ref(false);
const value = ref("");

const onClick = () => {
  toggle.value = !toggle.value;
  if (toggle.value) {
    value.value = "hellllloooooooooooooooowwwwww";
  }
};

const onInput = (v) => {
  value.value = v;
}

  • 그러나 <div> 요소로 <TextArea>를 감싸고, <div>v-show를 적용하면 autoResize 기능이 작동하지 않는다

<div v-show="toggle">
    <TextArea :value="value" @updated="onInput" />
</div>

const toggle = ref(false);
const value = ref("");

const onClick = () => {
  toggle.value = !toggle.value;
  if (toggle.value) {
    value.value = "hellllloooooooooooooooowwwwww";
  }
};

const onInput = (v) => {
  value.value = v;
}

  • 그렇다면 왜 부모 요소에 v-show를 적용하면 문제가 생기는 걸까?
  • 이번 시간에는 v-show의 특성을 이해하고 문제가 발생하는 원인을 분석해 보았다.




2. v-show 선언 위치에 따라 문제가 발생하는 이유

1) v-show 이해하기

(1) v-show의 작동 방식

  • v-show는 요소를 보이게 하거나 숨기기 위해 CSS의 display 속성을 변경한다. 그래서 v-show를 사용하면 요소가 화면에 나타나거나 사라지게 할 수 있다.
  • v-show는 요소를 단지 눈에 보이지 않게 숨기기 때문에, DOM에는 여전히 남아있다.
  • 반면 v-if는 조건에 따라 요소를 웹 페이지에서 완전히 없애거나 다시 추가한다. 따라서 렌더링 속도와 요소의 상태 관리 방식이 v-show와 다르다.
  • 다음 코드는 v-show실제 구현 코드이다.

동작
beforeMount 요소가 화면에 추가되기 전 단계이다. 만약 v-show 값이 false라면, 화면이 렌더링되기 전에 요소를 미리 숨긴다.
mounted 요소가 실제로 화면에 나타날 때이다. 만약 애니메이션이나 전환 효과가 있다면 이 시점에 실행된다. 그래서 에서 v-show 디렉티브를 사용해도 애니메이션이 작동한다.
updated v-show에 바인딩된 값이 변경되면 실행된다. 예를 들어 v-show에 바인딩된 변수값이 false에서 true로 변경되면, updated 훅이 호출되어 요소가 화면에 보이게 된다.
beforeUnmount 요소가 DOM에서 제거되기 전 단계로, display를 업데이트한다.
  • 보시다시피 v-show는 초기 렌더링 시 display 값을 저장된다.

  • 그리고 디렉티브에 바인딩된 값이 truedisplay값을 초기에 저장했던 값으로 변경하고, 값이 false라면 displaynone으로 변경한다.

  • 여기서 우리가 주의 깊게 봐야할 곳은 “updated”훅 실행 코드이다.

  • v-showupdated 훅은 디렉티브가 지정된 컴포넌트에서만 실행된다.

  • 따라서 <textarea>의 상위 요소에 v-show를 사용하면 상위 요소는 업데이트 되지만, 하위 요소인 <textarea>에서는 onUpdated 훅이 실행되지 않는다.

  • 이 때문에 resize() 함수가 호출되지 않아 텍스트가 변경되어도 <textarea>의 높이가 조정되지 않는 문제가 발생한다.



2) 원인과 해결방법

(1) 문제: 상위 요소에 v-show를 사용하면 textArea 높이가 조정되지 않음

<div v-show="toggle">
    <TextArea :value="value" />
</div>
  • 부모 <div>v-show를 사용하면, 그 안에 있는 <TextArea>의 높이가 자동으로 조절되지 않는다.
  • 즉, 텍스트를 입력해도 텍스트 상자의 크기가 늘어나지 않는다.

(2) 원인: updated 훅이 호출되지 않기 때문

  • TextArea 컴포넌트는 onUpdated 라이프사이클 훅에서 resize() 함수를 호출해 높이를 조정한다.
onUpdated(() => {
    if (autoResize.value) {
        resize();
    }
});
  • 하지만 부모 요소에 v-show를 사용하면 TextArea의 onUpdated가 호출되지 않는다.
  • 이는 v-show가 지정된 컴포넌트에서만 updated 훅이 실행되기 때문이다.
  • TextArea 컴포넌트는 css상 숨겨지거나 보이게 되므로 onUpdated 훅이 호출되지 않고, resize() 함수도 실행되지 않다.
  • 결과적으로 내용이 변경되어도 높이가 제대로 조정되지 않는다.

(3) 해결방법: ResizeObserver 사용하기

  • ResizeObserver는 요소의 크기가 변할 때 이를 감지할 수 있다.
  • 만약 부모요소에 v-show가 적용된 경우 자식 요소(textarea)가 css상 사라졌다 보여지는데, ResizeObserver가 그 변화를 감지하고 TextArea의 높이를 조정한다.
  • 다음은 ResizeObserver를 사용해서 수정한 코드이다.
import { onMounted, onUnmounted } from 'vue';

let resizeObserver;

// (A)
onMounted(() => {
    if (textarea.value && autoResize.value) {
        resize();
        resizeObserver = new ResizeObserver(resize);
        resizeObserver.observe(textarea.value); // (B)
    }
});

// (C)
onUnmounted(() => {
    if (resizeObserver) {
        resizeObserver.disconnect();
    }
});
코드 번호 설명
(A) - 컴포넌트가 렌더링 때 ResizeObserver를 생성하여 textarea 요소를 관찰한다. - resize() 함수가 바로 호출되어 초기 높이를 설정한다. - 이후 ResizeObserver를 설정하여 textarea 요소의 크기 변화를 감시한다.
(B) - ResizeObserver가 textarea의 크기 변경을 감시한다. - textarea의 크기가 변경될 때마다 resize() 함수를 실행하여 높이를 조정한다.
(C) - 컴포넌트가 페이지에서 제거되기 전에 ResizeObserver가 더 이상 textarea를 감시하지 않도록 수정한다.
  • ResizeObserver를 사용하면 textarea의 크기가 변경될 때마다 높이를 자동으로 조정할 수 있다.

아래 gif는 문제가 해결된 모습이다.

수정된 TextArea 코드 자세히 보기
  <template>
  <textarea
    ref="textarea"
    class="textarea"
    :value="props.value"
    @input="onInput"
  ></textarea>
</template>

<script setup lang="ts">
import { onMounted, onUpdated, onUnmounted, ref } from "vue";

const props = defineProps({
  value: {
    type: String,
    required: true,
  },
});
const emit = defineEmits(["updated"]);

const autoResize = ref(true);
const textarea = ref<HTMLTextAreaElement | null>(null);

let resizeObserver;

const resize = () => {
  if (!textarea.value) return;

  textarea.value.style.height = "auto";
  textarea.value.style.height = textarea.value.scrollHeight + "px";

  const isOverflow =
    parseFloat(textarea.value.style.height) >=
    parseFloat(textarea.value.style.maxHeight);
  if (isOverflow) {
    textarea.value.style.overflowY = "scroll";
    textarea.value.style.height = textarea.value.style.maxHeight;
  } else {
    textarea.value.style.overflow = "hidden";
  }
};

const onInput = (event) => {
  onResize();
  emit("updated", event.target.value);
};

onMounted(() => {
  if (textarea.value && autoResize.value) {
    resize();
    resizeObserver = new ResizeObserver(resize);
    resizeObserver.observe(textarea.value);
  }
});

onUnmounted(() => {
  if (resizeObserver) {
    resizeObserver.disconnect();
  }
});

const onResize = () => {
  if (autoResize.value) {
    resize();
  }
};
</script>

<style>
.textarea {
  resize: none;
}
</style>



3) v-show 사용시 발생할 수 있는 다른 문제들

(1) 이벤트 리스너 제거 문제

  • v-show를 사용할 때, 컴포넌트가 DOM에서 제거되지 않고 계속 유지되기에, 요소에 등록된 이벤트 리스너도 계속 유지된다.
  • 이렇게 되면 사용자가 보지 않는 상태에서도 이벤트 리스너가 메모리에 남아 있어 불필요한 자원을 소비하게 된다.
  • 만약 v-show 상태에서 자식 컴포넌트의 이벤트 리스너가 계속 작동하게 되면, 예상치 못한 동작이나 성능 저하를 야기할 수 있다.
  • 이런 경우에는 필요하지 않은 이벤트 리스너를 적절히 관리하거나, v-if를 사용하는 것이 더 효율적일 수 있다.

(2) 애니메이션 및 트랜지션 문제

  • v-show를 사용할 때 CSS 애니메이션이나 트랜지션을 추가하면 예상대로 작동하지 않을 수 있다.
  • 특히 요소가 display: none으로 숨겨진 상태에서는 애니메이션이 동작하지 않으며, 요소를 다시 보여줄 때 애니메이션 효과가 생략될 수 있다.
  • 트랜지션 효과가 필요한 경우, v-if를 사용하여 컴포넌트를 새로 렌더링하거나, Vue에서 제공하는 <transition> 컴포넌트를 함께 사용하면 좀 더 매끄러운 전환 효과를 구현할 수 있다.

(3) 스타일 및 레이아웃 재계산 문제

  • v-show를 사용하여 요소를 숨긴 후 다시 보여주게 되면, 요소의 스타일과 레이아웃을 다시 계산해야 하는 문제가 발생할 수 있다.
  • 특히 복잡한 레이아웃의 경우, 이러한 재계산이 성능에 영향을 줄 수 있다.
  • 또한, 레이아웃을 변경하는 스크립트가 특정 시점에 요소의 크기를 기반으로 계산을 진행하고 있었다면,
  • display: none 상태에서는 요소의 크기가 0으로 간주되므로 예상치 못한 결과가 나올 수 있다.




3. 마치며…

v-show 디렉티브는 요소를 숨기거나 보여주는 데 유용하지만, 그 동작 원리를 이해하는 게 중요하다. v-show를 사용하면 updated 훅이 호출된다고 생각할 수 있지만, 실제로는 v-show가 직접 적용된 컴포넌트에서만 updated 훅이 발생하며, 그 하위 요소들은 CSS를 통해 단순히 보이거나 숨겨질 뿐 훅이 동작하지 않는다.

따라서 이러한 점을 인지하고 상황에 맞게 v-if, ResizeObserver을 적절히 활용하면 보다 나은 컴포넌트를 구현할 수 있을 것이다 🙂


반응형

댓글