본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로웹 개발 › 프론트엔드 › Vue

데이터 변화 감지

목차

데이터 변화 감지

상품 단가가 1,000원이고 수량이 3개라면 합계는 3,000원이다. 이때 상태에 price, quantity, total을 모두 저장하면 단가나 수량을 변경할 때마다 합계도 빠짐없이 고쳐야 한다. 사용자가 입력한 원본만 상태에 저장하고, 합계는 원본으로부터 계산하면 이 동기화 문제를 줄일 수 있다.

이 글은 기존 코드와 같은 Vue 2 Options API를 기준으로 설명한다. Vue 2는 지원이 종료된 버전이므로 기존 프로젝트를 읽는 학습 예제로 사용한다. Vue 2 지원 종료 안내

입력값과 파생값을 분리하기

아래 HTML은 Vue 2 개발용 빌드를 불러와 실행할 수 있다. 할인율은 실제 요금 정책이 아닌 계산 예제의 입력값이다.

<div id="app">
  <label>단가 <input v-model.number="price" type="number" min="0"></label>
  <label>수량 <input v-model.number="quantity" type="number" min="0" step="1"></label>
  <p v-if="!valid">단가와 정수 수량을 확인하세요.</p>
  <template v-else>
    <p>합계: {{ subtotal }}원</p>
    <p>10% 할인 후: {{ discounted }}원</p>
  </template>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
const app = new Vue({
  el: '#app',
  data: { price: 1000, quantity: 3 },
  computed: {
    valid() {
      return Number.isFinite(this.price) && this.price >= 0
        && Number.isInteger(this.quantity) && this.quantity >= 0;
    },
    subtotal() {
      return this.valid ? this.price * this.quantity : null;
    },
    discounted() {
      return this.subtotal === null ? null : Math.round(this.subtotal * 0.9);
    }
  }
});
</script>

처음 화면에는 합계 3,000원과 할인 후 2,700원이 나타난다. 수량을 4로 바꾸면 두 값이 각각 4,000과 3,600으로 바뀐다. discounted가 subtotal을 읽고, subtotal이 단가·수량을 읽는 관계를 코드에 직접 표현했다.

숫자 입력을 비웠을 때는 .number를 붙여도 빈 문자열이 들어갈 수 있다. Number.isFinite와 정수 조건을 따로 검사한 이유다. HTML의 min 속성은 입력을 돕지만 서버 검증을 대신하지 않는다. 통화 계산에서는 최소 화폐 단위의 정수, 소수 정밀도, 반올림 시점을 별도의 도메인 규칙으로 정해야 한다.

flowchart LR
    P[price] --> V[valid]
    Q[quantity] --> V
    P --> S[subtotal]
    Q --> S
    V --> S
    S --> D[discounted]
    D --> H[화면 렌더링]

이 그림의 화살표는 값을 복사하는 순서가 아니라 어떤 값이 무엇에 의존하는지를 보여준다. Vue는 computed getter가 실행될 때 읽은 반응형 속성을 추적한다. 관련 입력이 바뀌면 캐시를 무효화하고, 다음 접근에서 다시 계산한다.

computed와 메서드의 실행 차이

computed는 읽기 문법이 app.subtotal이다. methods에 같은 계산을 넣으면 app.calculateSubtotal()처럼 호출한다. 템플릿의 메서드는 렌더링 과정에서 호출되지만 computed는 의존 값이 같으면 이전 계산 결과를 재사용한다.

예를 들어 검색 결과 10,000개를 필터링하는 계산을 화면 세 곳에서 읽는다면 캐시가 도움이 된다. 그렇다고 모든 작은 식을 computed로 만들어야 하는 것은 아니다. 여러 위치에서 쓰이거나 이름을 붙였을 때 의미가 선명해지는 파생값을 먼저 옮긴다. Computed와 캐시

다음 computed는 현재 시각 표시용으로 적합하지 않다.

computed: {
  now() {
    return Date.now();
  }
}

Date.now()에는 Vue가 감시하는 반응형 의존성이 없다. 시간이 지났다는 이유만으로 다시 실행되지 않는다. 시계를 만들려면 타이머가 갱신하는 상태를 두고 컴포넌트가 사라질 때 타이머를 해제해야 한다.

watch는 파생값보다 부수 효과에 사용하기

watch는 검색어 변경에 따른 요청, 외부 위젯 갱신처럼 상태 변화 뒤에 다른 작업이 필요한 경우에 맞는다. 단가가 바뀔 때 total을 덮어쓰는 watcher와 수량이 바뀔 때 total을 덮어쓰는 watcher를 각각 만들면 계산 규칙을 두 군데 관리하게 된다.

검색 요청에는 이전 요청이 늦게 끝나는 문제가 있다. 사용자가 java 다음 javascript를 입력했는데 첫 번째 응답이 나중에 도착하면 최신 목록을 덮어쓸 수 있다. watcher를 붙이는 것만으로 해결되지 않으므로 요청 취소 또는 요청 번호를 비교하는 로직이 필요하다. 또 입력마다 요청하면 서버를 과도하게 호출하므로 디바운스와 로딩·오류 상태를 함께 설계한다.

Computed getter 안에서 요청을 보내거나 다른 상태를 수정하지 않는다. getter를 읽는 횟수가 외부 작업 횟수를 결정하게 되면 렌더링과 동작을 추론하기 어려워진다.

Vue 2에서 변화가 감지되지 않는 경우

Vue 2는 초기 데이터 속성을 getter/setter로 변환한다. 초기 객체에 없던 속성을 나중에 직접 추가하거나 배열의 특정 인덱스를 직접 대입하는 경우에는 주의가 필요하다. Vue 2 반응성 제약

// data에 profile과 items가 먼저 선언되어 있다고 가정한다.
app.profile = { ...app.profile, nickname: '개발자' };
app.items = app.items.map((item, index) =>
  index === 1 ? { ...item, selected: true } : item
);

이처럼 새 객체·배열을 만들어 이미 선언된 속성에 할당하면 변경 경계가 명확하다. Vue 2에서는 Vue.set이나 splice도 사용할 수 있지만 이 글의 예시는 상태 교체 방식을 사용한다. Vue 3의 Proxy 기반 반응성에는 같은 제약을 그대로 적용하면 안 된다.

마지막으로 데이터 할당과 DOM 반영은 같은 순간이 아니다. Vue는 변경을 모아 비동기적으로 화면을 갱신한다. 값 변경 직후 DOM 높이나 텍스트를 읽어야 한다면 await app.$nextTick() 뒤에 읽는다. 단순히 파생값을 계산하려고 DOM을 다시 읽는 설계는 피한다. 원본 상태를 기준으로 계산하면 렌더링 시점을 기다릴 이유가 없다.

같은 카테고리의 글