본문 바로가기
FrontEnd/2024

[vue2] css position > transition 사용하기

by cariño 2023. 12. 19.
728x90
반응형

카테고리 더보기 클릭 이벤트

 

카테고리 더 보기가 접혀있을 때는 최소 아이템 4개를 보여주고, 더보기를 눌렀을 때 있는 아이템 갯수만큼 4개씩 배치된 리스트가 출력되는 형식의 모습이다. 데이터가 없는 상태라고 가정했을 때 하드코딩으로 리스트를 데이터에 넣어서 구현을 해봤다. 

 

setExpand() {
      this.expandStatus = !this.expandStatus
      const refHeight = this.$refs.prdtList.clientHeight + 24

      if (!this.expandStatus) {
        this.foldStyle.top = '106px'
      } else {
        this.foldStyle.top = `${refHeight}px`
      }
    },

 

clientHeight로 리스트의 높이값과 + 기본적으로 열려있는 값을 refHeight 변수에 담았다. 

클릭을 눌렀을 때 (카테고리 리스트가 확장이 됐을 경우): 추가된 리스트는 8개라고 가정했을 때의 높이값을 담았다. 

그리고 스타일과 클래스를 바인딩 시켜서 이벤트에서 만들어진 값들을 적용시키게 구현을 했다. 

 

 

문제점

1. 동적으로 받아오는 데이터의 개수가 달라지면? 

- 더보기의 리스트가 현재는 8개 기준으로 높이값을 고정한 상태로 스타일을 적용해줬기 때문에 8개 이상의 리스트가 들어오게 되면 그 이상의 아이템은 보여줄 수 없다.  4개이하의 리스트만 가져오게 된다면, 높이값이 맞아지지 않게 된다. 

2. 성능적인 측면

리스트 전체의 높이값을 가져오고 폴더가 넓혀지고 줄여질 때 Height값을 변경하게 했다. DOM 노드 크기를 변경하기 때문에 클릭할 때마다 해당 화면을 다시 계산하고 그리게 되어 불필요한 성능야기를 유발시키게 되버린다. 

<template>
  <div class="home-wrap" :class="componentData.currentTab">
    <!-- categories -->
    <div class="categories" :class="{ moreList: expandStatus }">
      <div ref="prdtList" class="product-list">
        <li v-for="(prdt, itemIdx) in categories" :key="`category_${itemIdx}`">
          <button type="button" class="list-wrap">
            <div v-lazy:background-image="prdt.itemImgFileUrl" class="thumbnail is-size-xsmall">
              <i v-if="!prdt.itemImgFileUrl" class="no-img"></i>
            </div>
            <p class="tit">{{ prdt.tit }}</p>
          </button>
        </li>
      </div>
      <button
        v-if="categories && categories.length > 4"
        type="button"
        class="more"
        :class="{ fold: expandStatus }"
        :style="foldStyle"
        @click="setExpand"
      >
        <p class="arrow">카테고리 더보기</p>
      </button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Categories',
  props: {
    componentData: {
      type: Object,
      default: () => {},
    },
  },
  data() {
    return {
      expandStatus: false, //더보기
      categories: [
        {
          tit: '크리스마스',
          itemImgFileUrl: 'https://~~~~~~~~~ 1,
        },
        {
          tit: '생일/축하',
          itemImgFileUrl: 'https://~~~~~~~~~ 2,       
         },
        {
          tit: '응원/감사',          
           itemImgFileUrl: 'https://~~~~~~~~~ 1,
        },
        {
          tit: 'MD상품',
          itemImgFileUrl: 'https://~~~~~~~~~ 1,
        },

        {
          tit: '크리스마스',
          itemImgFileUrl: 'https://~~~~~~~~~~5 ,
        },
        {
          tit: '생일/축하',
          itemImgFileUrl: 'https://~~~~~~~~~~5',
        },
        {
          tit: '응원/감사',
          itemImgFileUrl: 'https://~~~~~~~~~~.com ,
        },
        {
          tit: 'MD상품',
          itemImgFileUrl: 'htttps://kkkkkkkk.6,
        },
      ],
      foldStyle: {
        top: '106px',
      },
    }
  },
  watch: {},
  mounted() {},
  methods: {
    setExpand() {
      this.expandStatus = !this.expandStatus
      const refHeight = this.$refs.prdtList.clientHeight + 24

      if (!this.expandStatus) {
        this.foldStyle.top = '106px'
      } else {        
        this.foldStyle.top = `${refHeight}px`
      }
    },
  },
}
</script>

<style lang="scss" scoped>
.home-wrap {
  .categories {
    position: relative;
    height: auto;
    padding: var(--spacing-md) var(--spacing-lg) var(--spacing-sm-02);
    box-sizing: border-box;
    .product-list {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      column-gap: 12px;
      row-gap: 8px;
      li {
        height: 82px;
        list-style: none;
        .list-wrap {
          align-items: center;
          flex-direction: column;
          gap: var(--spacing-xs);
        }
      }
    }
    .more {
      position: absolute;
      left: var(--spacing-sm-02);
      width: 100%;
      height: 82px;
      background: var(--background-primary);
      padding: var(--spacing-sm-02) var(--spacing-sm-03);
      border-top: 1px solid var(--border-tertiary);
      &.fold {
        height: 42px;
        .arrow:after {
          transform: rotate(180deg);
        }
      }
      .arrow {
        position: relative;
        font-size: var(--fontsize-small);
        font-weight: var(--fontweight-medium);
        color: var(--surface-default-black);
      }
      ::v-deep .arrow:after {
        width: 16px;
        height: 16px;
        top: 10%;
        left: 63%;
        transform: rotate(0);
        background: url('../../../assets/img/icon/icon_arrow.svg') no-repeat center/contain;
      }
    }

    &.moreList {
      margin-bottom: 54px;
    }
  }
}
</style>

 

 

해결방법

vue.js에서 제공하는 Transition api를 사용하기로 했다.

어플리케이션에서 추가, 제거, 업데이트 될 때 애니메이션등 다양한 방법을 제공하는데 css 트렌지션이나 애니메이션을 위한 클래스를 자동적으로 적용하고 javascript를 사용해서 DOM을 직접 조작하는 도구이다. 

 

진입/ 진출 트랜지션에 적용되는 6개의 클래스가 있다.  이름이 지정이 되면 v대신 접두사로 이름을 붙이면 된다.

 

  1. v-enter-from: 진입 시작 상태. 엘리먼트가 삽입되기 전에 추가되고, 엘리먼트가 삽입되고 1 프레임 제거됩니다.
  2. v-enter-active: 진입 활성 상태. 모든 진입 상태에 적용됩니다. 엘리먼트가 삽입되기 전에 추가되고, 트랜지션/애니메이션이 완료되면 제거됩니다. 클래스는 진입 트랜지션에 대한 지속 시간, 딜레이 이징(easing) 곡선을 정의하는 사용할 있습니다.
  3. v-enter-to: 진입 종료 상태. 엘리먼트가 삽입된 1 프레임 추가되고(동시에 v-enter-from 제거됨), 트랜지션/애니메이션이 완료되면 제거됩니다.
  4. v-leave-from: 진출 시작 상태. 진출 트랜지션이 트리거되면 즉시 추가되고 1 프레임 제거됩니다.
  5. v-leave-active: 진출 활성 상태. 모든 진출 상태에 적용됩니다. 진출 트랜지션이 트리거되면 즉시 추가되고, 트랜지션/애니메이션이 완료되면 제거됩니다. 클래스는 진출 트랜지션에 대한 지속 시간, 딜레이 이징 곡선을 정의하는 사용할 있습니다.
  6. v-leave-to: 진출 종료 상태. 진출 트랜지션이 트리거된 1 프레임이 추가되고(동시에 v-leave-from 제거됨), 트랜지션/애니메이션이 완료되면 제거됩니다.

 

 

변경한 내 작업 본은 커스텀된 트랜지션 클래스를 사용했는데 props에 트랜지션 클래스를 정의해서 전달을 한다. 

이로써 문제점이였던 리스트의 갯수가 변경될 때 마다의 높이를 조절할 수 있게되고 transform을 사용해서 성능저하를 방지했다. 

 

자세한 방법은 공식 문서에 나와 있다. 

https://ko.vuejs.org/guide/built-ins/transition

 

Vue.js

Vue.js - The Progressive JavaScript Framework

vuejs.org

[변경 적용 후]

<Transition
        enter-active-class="animate__animated animate__tada"
        leave-active-class="animate__animated animate__bounceOutRight">
        <div class="product-list">
          <ul v-for="(prdt, itemIdx) in categories" :key="`category_${itemIdx}`">
            <li v-if="itemIdx < 4">
              <button type="button" class="list-wrap">
                <div v-lazy:background-image="prdt.itemImgFileUrl" class="thumbnail is-size-xsmall">
                  <i v-if="!prdt.itemImgFileUrl" class="no-img" :class="`no-img${itemIdx + 1}`"></i>
                </div>
                <p class="tit">{{ prdt.tit }}</p>
              </button>
            </li>
            <li v-if="expandStatus && itemIdx > 3">
              <button type="button" class="list-wrap">
                <div v-lazy:background-image="prdt.itemImgFileUrl" class="thumbnail is-size-xsmall">
                  <i v-if="!prdt.itemImgFileUrl" class="no-img" :class="`no-img${itemIdx + 1}`"></i>
                </div>
                <p class="tit">{{ prdt.tit }}</p>
              </button>
            </li>
          </ul>
        </div>
      </Transition>

      <button v-if="categories && categories.length > 4" type="button" class="more" :class="{ open: expandStatus }" @click="setExpand">
        <p class="arrow">카테고리 더보기</p>
      </button>
...

 

  methods: {
    setExpand() {
      this.expandStatus = !this.expandStatus
    },
  },

 

카테고리 더보기 펼치기 전/ 후 모습

 

 

728x90

'FrontEnd > 2024' 카테고리의 다른 글

History모드 : SPA, SSR, CSR  (0) 2024.08.05
javascript에서 this 컨트롤 하기  (0) 2024.04.01
브라우저 성능 최적화  (0) 2024.03.30
[vue2] 비밀번호 유효성 검사 연속문자 체크하기  (0) 2024.03.25
css 성능 향상  (0) 2024.03.05

댓글