728x90

Vue의 watch를 활용해 수치가 변경될 때 부드러운 페이딩(또는 숫자 애니메이션) 효과를 구현하려면, CSS 전환(transition)이나 JavaScript로 보간(interpolation)을 조합해야 합니다. 단순히 watch만으로는 애니메이션이 적용되지 않으므로, Vue의 반응형 데이터와 CSS 애니메이션, 또는 외부 라이브러리(예: gsap나 anime.js)를 사용하는 방법이 있습니다.
여기서는 두 가지 방법을 제안합니다:
-
CSS transition과 클래스 토글을 활용한 방법 (간단하고 가벼움)
-
JavaScript로 숫자 보간 애니메이션을 구현한 방법 (더 정교한 제어 가능)
실무에서 간단히 적용할 수 있는 CSS 기반 방법을 먼저 보여드리고, 더 정교한 JavaScript 기반 방법도 추가로 설명하겠습니다.
728x90
방법 1: CSS transition과 클래스 토글로 페이딩 효과
watch로 값 변화를 감지하고, CSS 클래스를 토글해 페이딩 효과를 적용합니다. 숫자가 바뀔 때마다 DOM 요소를 잠깐 투명하게 만들었다가 다시 나타나게 하는 방식입니다.
코드 예제
<template>
<div class="sales-grid">
<h2>지역별 월별 판매량 (단위: 개)</h2>
<table>
<thead>
<tr>
<th>지역</th>
<th v-for="month in months" :key="month">{{ month }}</th>
<th>합계</th>
</tr>
</thead>
<tbody>
<tr v-for="(region, index) in salesData" :key="region.name">
<td>{{ region.name }}</td>
<td v-for="(sales, monthIdx) in region.sales" :key="monthIdx">
<input
type="number"
v-model.number="salesData[index].sales[monthIdx]"
min="0"
@input="logChange(region.name, months[monthIdx], $event.target.value)"
/>
</td>
<td :class="{ 'fade': fadeStates[index] }">{{ rowTotals[index] }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import { ref, computed, watch } from 'vue';
export default {
setup() {
const months = ['1월', '2월', '3월'];
const salesData = ref([
{ name: '서울', sales: [120, 150, 180] },
{ name: '부산', sales: [90, 110, 130] },
{ name: '대구', sales: [70, 85, 95] }
]);
const fadeStates = ref([false, false, false]); // 각 행의 페이딩 상태
// 합계 계산
const rowTotals = computed(() => {
return salesData.value.map(region =>
region.sales.reduce((sum, sales) => sum + sales, 0)
);
});
// salesData 변경 감지 및 페이딩 효과 적용
watch(
() => salesData.value.map(r => r.sales.join()), // sales 배열의 변화 감지
(newValues, oldValues) => {
newValues.forEach((_, index) => {
if (newValues[index] !== oldValues[index]) {
fadeStates.value[index] = true; // 페이딩 시작
setTimeout(() => {
fadeStates.value[index] = false; // 페이딩 종료
}, 500); // 애니메이션 지속 시간과 맞춤
}
});
},
{ deep: true }
);
const logChange = (region, month, newValue) => {
if (newValue < 0) {
alert('판매량은 음수가 될 수 없습니다!');
return;
}
console.log(`${region}의 ${month} 판매량이 ${newValue}로 변경됨`);
};
return {
months,
salesData,
rowTotals,
fadeStates,
logChange
};
}
};
</script>
<style>
.sales-grid {
padding: 20px;
max-width: 800px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
th {
background-color: #f4f4f4;
}
input[type="number"] {
width: 60px;
padding: 4px;
text-align: center;
}
/* 페이딩 애니메이션 */
.fade {
animation: fadeEffect 0.5s ease-in-out;
}
@keyframes fadeEffect {
0% { opacity: 0; }
100% { opacity: 1; }
}
</style>
동작 방식
-
fadeStates 배열로 각 행의 페이딩 상태를 관리.
-
watch가 salesData의 변화를 감지하면 해당 행의 fadeStates를 true로 설정.
-
CSS의 @keyframes와 animation 속성으로 숫자가 투명해졌다가 부드럽게 나타나는 효과 적용.
-
0.5초 후 fadeStates를 false로 되돌려 애니메이션이 반복되지 않게 함.
한계
-
숫자가 직접적으로 보간되지 않고, 단순히 페이드 인/아웃만 적용됨.
-
합계 값 자체가 부드럽게 증가/감소하는 느낌은 없음.
728x90
방법 2: JavaScript로 숫자 보간 애니메이션
숫자가 시작값에서 끝값까지 부드럽게 변하도록 하려면, JavaScript로 값을 보간해야 합니다. 이를 위해 requestAnimationFrame을 사용하거나, 외부 라이브러리 없이 간단히 구현할 수 있습니다.
코드 예제
<template>
<div class="sales-grid">
<h2>지역별 월별 판매량 (단위: 개)</h2>
<table>
<thead>
<tr>
<th>지역</th>
<th v-for="month in months" :key="month">{{ month }}</th>
<th>합계</th>
</tr>
</thead>
<tbody>
<tr v-for="(region, index) in salesData" :key="region.name">
<td>{{ region.name }}</td>
<td v-for="(sales, monthIdx) in region.sales" :key="monthIdx">
<input
type="number"
v-model.number="salesData[index].sales[monthIdx]"
min="0"
@input="logChange(region.name, months[monthIdx], $event.target.value)"
/>
</td>
<td>{{ animatedTotals[index] }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import { ref, computed, watch } from 'vue';
export default {
setup() {
const months = ['1월', '2월', '3월'];
const salesData = ref([
{ name: '서울', sales: [120, 150, 180] },
{ name: '부산', sales: [90, 110, 130] },
{ name: '대구', sales: [70, 85, 95] }
]);
const animatedTotals = ref([0, 0, 0]); // 애니메이션용 합계 값
// 실제 합계 계산
const rowTotals = computed(() => {
return salesData.value.map(region =>
region.sales.reduce((sum, sales) => sum + sales, 0)
);
});
// 숫자 애니메이션 함수
const animateValue = (start, end, duration, callback) => {
let startTime = null;
const step = (timestamp) => {
if (!startTime) startTime = timestamp;
const progress = Math.min((timestamp - startTime) / duration, 1);
const value = Math.floor(start + (end - start) * progress);
callback(value);
if (progress < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
};
// salesData 변경 시 애니메이션 적용
watch(
() => rowTotals.value,
(newTotals, oldTotals) => {
newTotals.forEach((total, index) => {
if (total !== oldTotals[index]) {
animateValue(oldTotals[index] || 0, total, 500, (value) => {
animatedTotals.value[index] = value;
});
}
});
},
{ immediate: true } // 초기 값 설정
);
const logChange = (region, month, newValue) => {
if (newValue < 0) {
alert('판매량은 음수가 될 수 없습니다!');
return;
}
console.log(`${region}의 ${month} 판매량이 ${newValue}로 변경됨`);
};
return {
months,
salesData,
animatedTotals,
logChange
};
}
};
</script>
<style>
.sales-grid {
padding: 20px;
max-width: 800px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
}
th {
background-color: #f4f4f4;
}
input[type="number"] {
width: 60px;
padding: 4px;
text-align: center;
}
</style>
동작 방식
-
animatedTotals는 화면에 표시될 애니메이션 값.
-
rowTotals는 실제 합계를 계산.
-
watch가 rowTotals 변화를 감지하면, animateValue 함수로 이전 값(oldTotals)에서 새 값(newTotals)까지 0.5초 동안 보간.
-
requestAnimationFrame으로 부드러운 애니메이션 구현.
장점
-
숫자가 실제로 시작값에서 끝값까지 스무스하게 변함.
-
애니메이션 속도와 타이밍을 정밀하게 조정 가능.
단점
-
코드가 약간 복잡해지고, 성능에 약간의 영향을 줄 수 있음.
추천
-
간단한 페이딩: 방법 1(CSS)을 사용하면 구현이 빠르고 가볍습니다.
-
숫자 보간 애니메이션: 방법 2(JavaScript)를 사용하면 더 프로페셔널한 UI를 제공할 수 있습니다. 실무에서 사용자 경험을 중시한다면 이 방법을 추천합니다.
추가로 외부 라이브러리(예: gsap)를 사용하면 더 쉽게 구현할 수도 있습니다. 어떤 방향으로 더 발전시키고 싶으신지 알려주시면 추가로 도와드리겠습니다!
728x90
'Vue.js 를 배워보자' 카테고리의 다른 글
Vue.js에서 watch로 버튼과 그리드의 disabled 상태 동적 제어하기 (0) | 2025.03.16 |
---|---|
GSAP와 Vue.js로 만드는 화려한 애니메이션 예제 (0) | 2025.03.15 |
Vue.js로 장바구니 수량 관리 기능 만들기 외 2개 예제 (0) | 2025.03.14 |
Vue.js 그리드 데이터 흐름 제어: 임시 이동, 커서 위치 갱신, 롤백, 반응형 데이터 처리 A to Z (0) | 2025.03.13 |
Vue.js와 Axios로 데이터셋을 그리드로 로딩하고 클릭 시 Input Text에 자동 바인딩하기 (0) | 2025.03.04 |