[Vue 빠른시작] 반응형 기초 예제 추가(Options API.ver)

이전글

[Vue 빠른시작] 반응형 기초 예제 추가(Composition API.ver)



1. 반응형 상태 설정(Declaring Reactive State)

옵션 API에서는 'data' 옵션을 사용하여 컴포넌트의 반응형 상태를 선언한다. 옵션 값은 객체를 반환하는 함수여야 한다. Vue는 새 컴포넌트 인스턴스를 만들 때 함수를 호출하고, 반환된 객체를 반응형 시스템에 래핑한다. 이 객체 내 모든 속성은 해당 컴포넌트 인스턴스(메서드 및 생명주기 훅에서 'this')에서 최상위에 프락시(proxy)되어 노출된다.


예제

  <template>
    Count is: {{ count }}
  </template>

  <script>
  export default {
    data() {
      return {
        count: 1
      }
    },

    // `mounted`는 나중에 설명할 수명 주기 훅입니다.
    mounted() {
      // `this`는 컴포넌트 인스턴스를 나타냅니다.
      console.log(this.count) // => 1

      // 값을 변경할 수 있습니다.
      this.count = 2
    }
  }
  </script>

이러한 인스턴스 속성은 인스턴스를 처음 만들 때만 추가되므로, 'data'함수에 의해 반환되는 객체에 선언되었는지 확인해야 한다. 바로 사용하지 않아 빈 값이지만 나중에 값이 추가되는 속성의 경우, 'null', 'undefined' 또는 기타 임시로 어떠한 값이라도 넣어 사전에 선언해 두어야 한다.
'data'에 포함하지 않고 'this'에 직접 속성을 추가할 수도 있지만, 이후 반응형 업데이트 동작이 이루어지지 않는다.

Vue는 컴포넌트 인스턴스를 통해 기본 제공되는 API를 노출할 때 $접두사를 사용한다. 또한 내부 속성에 대해서는 _접두사를 사용한다. 따라서 'data' 함수에 의해 반환되는 객체 내 최상위 속성명은 이러한 문자 중 하나로 시작하지 않아야 한다.



1-1. 반응형(Reactive Proxy) 재정의 vs 원본

Vue 3에서는 JavaScript Proxy를 활용하여 데이터를 반응형으로 만든다. Vue2를 경험한 경우 아래와 같은 경우에 주의를 해야 한다.


예제

export default {
  data() {
    return {
      someObject: {}
    }
  },
  mounted() {
    const newObject = {}
    this.someObject = newObject

    console.log(newObject === this.someObject) // false
  }
}

'newObject' 객체를 'this.someObject'에 할당 후 접근할 경우, 이 값은 원본을 반응형으로 재정의한 프락시 객체이다. Vue2와 달리 원본 'newObject' 객체는 그대로 유지되며, 반응형으로 변하지 않는다. 항상 'this'를 통해 반응형 상태의 속성에 접근해야 한다.



2. 메서드 선언(Declaring Methods)

컴포넌트 인스턴스에 메서드를 추가하기 위해서는 methods옵션을 사용해야 한다.

export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    // 메서드는 생명 주기 훅 또는 다른 메서드에서 호출할 수 있습니다!
    this.increment()
  }
}

Vue는 'methods'에서 'this'가 컴포넌트 인스턴스를 참조하도록 항상 자동으로 바인딩한다. 따라서 메서드가 이벤트 리스너 또는 콜백으로 사용되는 경우에도 this 값은 컴포넌트 인스턴스로 유지된다.
단, 화살표 함수는 Vue가 this를 컴포넌트 인스턴스로 바인딩하는 것을 방지하므로, methods를 정의할 때 화살표 함수를 사용하는 것은 피해야 한다.

export default {
  methods: {
    increment: () => {
      // 나쁨: 여기서 `this`에 접근할 수 없습니다!
    }
  }
}

컴포넌트 인스턴스의 다른 모든 속성과 마찬가지로 methods는 컴포넌트 템플릿 내에서 접근할 수 있으며, 주로 이벤트 리스너로 사용된다.

<script>
{/* example code */}
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    this.increment()
  }
}
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>



2-1. DOM 업데이트 타이밍

반응 상태를 변경하면 DOM이 자동으로 업데이트 된다. 하지만 DOM 업데이트는 동기적으로 적용되지 않는다는 점에 유의해야 한다. 대신 Vue는 업데이트 주기의 "다음 틱"까지 버퍼링하여 얼마나 많은 상태 변경을 수행하든 각 컴포넌트가 한 번만 업데이트되도록 한다.

상태 변경 후, DOM 업데이트가 완료될 때까지 기다리려면 nextTick() 전역 API를 사용할 수 있다.

import { nextTick } from 'vue'

export default {
  methods: {
    increment() {
      this.count++
      nextTick(() => {
        // 업데이트된 DOM에 접근 가능
      })
    }
  }
}

에제

// Template.vue
<template>
  <p>{{ message }}</p>
  <button @click="updateMessage">Update Message</button>
</template>

<script>
export default {
  data() {
    return {
      message: 'Initial Message'
    }
  },
  methods: {
    updateMessage() {
        this.message = 'Updated Message';

        // Vue는 컴포넌트 인스턴스를 통해 기본 제공되는 API를 노출할 때 '$'접두사를 사용    
        this.$nextTick(() => {
            // DOM 업데이트 이후에 실행되는 콜백
            console.log('DOM이 업데이트되었습니다.');
        });
    }
  }
}
</script>

실행화면





2-2. 깊은 반응형(Deep Reactivity)

Vue는 기본적으로 반응형 상태를 내부 깊숙이 추적하므로, 중첩된 객체나 배열을 변경할 때에도 변경 사항이 감지된다.

export default {
  data() {
    return {
      obj: {
        nested: { count: 0 },
        arr: ['foo', 'bar']
      }
    }
  },
  methods: {
    mutateDeeply() {
      // 변경 사항이 감지됩니다.
      this.obj.nested.count++
      this.obj.arr.push('baz')
    }
  }
}

루트 수준에서만 반응성을 추적하는 얕은 반응형 객체를 명시적으로 생성할 수도 있지만, 이는 일반적으로 고급 사용 사례에서만 필요한 경우이다.


예제

// Template.vue
<template>
  <p>깊은 반응형 예제</p>
  <p>이름: {{ cat.name }}</p>
  <p>생년월: {{ cat.birthday }}</p>
  <p>특징 및 취미: {{ cat.Introduction }}</p>
  <button @click="updateData">Update Data</button>
</template>

<script>
export default {
  data() {
    return {
      cat: {
      name: 'Cheddar',
      birthday: '201506',
      Introduction: ['순하고 말이 많다','궁팡받기 좋아함']
      }
    }
  },
  methods: {
    updateData () {
      this.cat.name = '김체다';
      this.cat.Introduction = '뚱땡이임';
    }
  }
}
</script>

실행화면





2-3. 메서드 상태유지(Stateful Method)

어떤 경우에는 메서드 함수를 동적으로 생성해야 할 수도 있다. 예를 들어 디바운스된 이벤트 핸들러 생성

note! Debounce란?

일정 시간이 지날 때까지 이벤트 트리거를 대기하여 어플리케이션의 성늘을 개선할 수 있는 기술이다.

export default {
  methods: {
    // Lodash로 디바운싱
    click: debounce(function () {
      // ... 클릭에 응답 ...
    }, 500)
  }
}

그러나 이 접근 방식은 디바운스된 함수가 일정 시간이 지나기 전까지 유지되기 때문에 재사용되는 컴포넌트에 문제가 있다. 여러 컴포넌트 인스턴스가 동일한 디바운스 함수를 공유하는 경우 서로 간섭한다.

각 컴포넌트 인스턴스의 디바운스된 함수를 각각 독립적으로 유지하기 위해 created 생명 주기 훅에서 디바운스된 함수를 컨트롤 할 수 있는 환경을 구성할 수 있다.

export default {
  created() {
    // 이제 각 인스턴스는 자체적인 디바운스된 핸들러를 가집니다.
    this.debouncedClick = _.debounce(this.click, 500)
  },
  unmounted() {
    // 컴포넌트가 제거된 후 
    // 타이머를 취소하는 것은 좋은 방법입니다.
    this.debouncedClick.cancel()
  },
  methods: {
    click() {
      // ... 클릭에 응답 ...
    }
  }
}

Debounce

debounce의 개념이 궁금해 chat gpt를 통해 간단한 예제 코드를 받아보았다.

<!-- gpt.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Debounce Example</title>
</head>
<body>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.js"></script>
<script>
  // 간단한 예제 함수
  function handleInput(value) {
    console.log('Input:', value);
  }

  // debounce 함수를 사용하여 디바운스 효과 적용
  const debouncedHandleInput = _.debounce(handleInput, 500);

  // 사용자의 입력 시뮬레이션
  debouncedHandleInput('A'); // 이 시점에서는 아무것도 출력되지 않음
  debouncedHandleInput('AB'); // 이 시점에서는 아무것도 출력되지 않음
  debouncedHandleInput('ABC'); // 이 시점에서는 아무것도 출력되지 않음

  // 500밀리초 이후에 마지막 호출된 입력이 처리됨
  // Output: Input: ABC
</script>
</body>
</html>

주석에 나와있는 것처럼 입력하면 정해진 시간 후 마지막 입력이 출력되는 것을 확인 할 수 있었다.

실행화면





참고

Vue3 docs