목차
Vue.js 정리
다음 화면에는 상품명, 수량, 합계가 있다. 수량을 변경하는 코드는 합계 DOM을 찾아 직접 고치지 않는다. Vue에 원본 상태와 계산 규칙을 선언하면 Vue가 변경된 값을 화면에 반영한다. Vue를 시작할 때는 이 상태와 렌더링의 관계를 먼저 잡는 편이 각 디렉티브를 따로 외우는 것보다 도움이 된다.
이 글은 2020년에 사용한 Vue 2의 new Vue와 Options API를 설명한다. 지원 종료된 버전이므로 기존 프로젝트 이해용으로 읽는다. Vue 3에서는 앱 생성 방식과 일부 API가 달라졌다. Vue 2 지원 종료, Vue 3 소개
하나의 상태에서 화면 만들기
<!DOCTYPE html>
<html lang="ko">
<head><meta charset="utf-8"><title>Vue 상품 합계</title></head>
<body>
<div id="app">
<h1>{{ product.name }}</h1>
<p>단가: {{ formatPrice(product.price) }}</p>
<button type="button" @click="quantity = Math.max(0, quantity - 1)">감소</button>
<span>{{ quantity }}개</span>
<button type="button" @click="quantity += 1">증가</button>
<p>합계: {{ formatPrice(total) }}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
product: { id: 1000, name: '햄버거', price: 5000 },
quantity: 1
},
computed: {
total() {
return this.product.price * this.quantity;
}
},
methods: {
formatPrice(value) {
return new Intl.NumberFormat('ko-KR', {
style: 'currency', currency: 'KRW', maximumFractionDigits: 0
}).format(value);
}
}
});
</script>
</body>
</html>
처음 수량은 1, 합계는 5,000원이다. 증가 버튼을 한 번 누르면 원본 상태 quantity가 2로 바뀌고 total은 10,000이 된다. 화면에 찍을 문자열은 formatPrice가 만든다. 숫자 계산과 출력 형식을 분리했으므로 쉼표나 통화 기호가 계산에 끼어들지 않는다.
data는 원본 상태, computed는 상태로부터 얻는 파생값, methods는 이벤트나 명시적 호출에 쓸 동작으로 읽는다. 모든 값을 data에 저장하면 합계까지 직접 맞춰야 하고, 모든 로직을 템플릿 안에 쓰면 화면 구조가 계산식에 묻힌다.
템플릿과 반응성은 어떻게 연결되는가
Vue는 템플릿을 렌더링 함수로 컴파일하고, 반응형 데이터를 읽어 가상 DOM을 만든 뒤 실제 DOM에 반영한다. 데이터가 바뀌면 관련 렌더링 작업이 예약된다. 브라우저 DOM을 일정 간격으로 전부 훑으며 변경을 찾는 것으로 이해하면 안 된다.
flowchart LR
I[이벤트] --> S[상태 변경]
S --> Q[갱신 작업 예약]
Q --> R[렌더링 결과 계산]
R --> D[DOM 갱신]
한 번의 작업 안에서 값을 여러 번 바꿔도 매번 DOM을 즉시 다시 쓰는 것은 아니다. DOM 반영이 끝난 후 크기를 읽거나 포커스를 옮겨야 한다면 $nextTick으로 해당 시점을 기다린다. 반응성과 비동기 업데이트 큐
루트 Vue 인스턴스는 앱의 시작점이지만 앱이 커지면 화면을 컴포넌트로 분리한다. 컴포넌트마다 필요한 상태를 소유하고, 부모는 props로 데이터를 전달하며 자식은 이벤트로 변경 의도를 알린다. 모든 상태를 하나의 거대한 루트 객체에 넣으면 어느 화면이 값을 바꾸는지 추적하기 어려워진다.
생명주기는 자원을 얻고 돌려주는 경계
| Vue 2 훅 | 관찰할 시점 | 주로 할 일 |
|---|---|---|
beforeCreate | 상태 초기화 전 | 초기화 이전 동작 파악 |
created | 반응형 데이터·메서드 준비 후 | DOM에 의존하지 않는 초기 작업 |
mounted | 컴포넌트 DOM 마운트 후 | DOM이 필요한 외부 위젯 연결 |
updated | 데이터 변경으로 DOM 갱신 후 | 갱신 뒤 DOM 확인이 필요한 작업 |
beforeDestroy | 인스턴스 해제 직전 | 직접 만든 타이머·리스너 정리 |
destroyed | 인스턴스 해제 후 | 해제 완료 시점 확인 |
이름은 beforeDestroy다. beforeDestory라고 쓰면 Vue가 생명주기 훅으로 호출하지 않는다. 또한 updated에서 매번 렌더링에 쓰는 상태를 바꾸면 추가 갱신을 유발하는 루프가 생길 수 있다. Vue 인스턴스와 생명주기
// 컴포넌트 옵션의 일부
mounted() {
this.onResize = () => this.measurePanel();
window.addEventListener('resize', this.onResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.onResize);
}
measurePanel은 해당 컴포넌트의 메서드로 구현한다. 제거할 때 같은 함수 참조를 넘겨야 하므로 등록한 함수를 보관한다. Vue가 관리하지 않는 전역 이벤트나 외부 라이브러리의 자원은 직접 해제해야 한다.
텍스트 출력과 HTML 삽입의 차이
{{ product.name }}과 v-text는 문자열을 텍스트로 출력한다. v-html은 문자열을 HTML로 해석하므로 입력의 의미가 달라진다. 사용자 댓글이나 외부 응답을 그대로 v-html에 넣는 방식은 XSS 위험을 만든다. 일반 텍스트면 텍스트 바인딩을 쓰고, 서식 있는 문서가 꼭 필요하면 허용할 태그·속성과 정제 과정을 별도로 설계한다.
Vue 2의 출력 필터를 쓰던 코드도 볼 수 있지만 필터가 계산 오류나 보안 문제를 자동으로 해결하지는 않는다. 가격 문자열을 직접 나누고 쉼표를 끼워 넣는 구현은 0이나 소수, 통화 단위를 빠뜨리기 쉽다. 위 예시는 표준 Intl.NumberFormat에 표시 형식을 맡기고 숫자 계산은 별도로 유지한다.
처음 코드를 읽을 때는 data에서 원본을 찾고, computed에서 파생 관계를 찾고, 템플릿의 이벤트가 어떤 메서드로 이어지는지 따라간다. 이 세 가지가 설명되지 않는다면 디렉티브를 더 추가하기 전에 상태의 소유 위치부터 정리한다.