일이 지났습니다. 시의성에 유의하세요
서문
이 글은 Vue 2.0 공식 문서의 “인스턴스” 섹션부터 시작하여, 몇 가지 Vue API 사용 방법과 Vue가 특정 기능을 구현하는 원리를 탐구합니다. 또한 제 개인적인 사용 경험과 제한된 시각에서 Vue의 설계 의도를 분석하는 내용을 담고 있습니다. Vue를 접한 지 얼마 되지 않아 부족한 점이 있다면 지적해 주시면 감사하겠습니다.
참고: 기본적인 내용은 생략하고, 제가 설명이 필요하다고 생각하는 부분만 다룹니다.
Vue 인스턴스
모든 Vue.js 애플리케이션은 생성자를 통해 Vue의 루트 인스턴스를 생성하여 시작됩니다. 즉, 각 페이지의 데이터는 이 하나의 인스턴스에 의해 관리되어야 하며, 원본 데이터의 출처는 루트 인스턴스에서만 발신 및 수신되어 통합 관리되어야 합니다. 루트 인스턴스는 props를 통해 데이터를 분배하거나 events를 통해 데이터를 감지합니다. 자식 컴포넌트는 watch/computed를 통해 데이터 변화를 감지하고 즉시 업데이트하면 됩니다.
문서에는 "모든 Vue.js 컴포넌트는 사실 확장된 Vue 인스턴스"라고 설명되어 있습니다. 이는 컴포넌트에서도 인스턴스와 동일한 메서드와 훅 함수를 사용할 수 있다는 의미로 이해해야 합니다. 단, data는 예외입니다.
컴포넌트의 data는 반드시 함수여야 합니다. 컴포넌트가 재사용되기 때문에 매번 컴포넌트를 호출할 때마다 새로운 데이터를 생성해야 합니다.
데이터 프록시(proxy)는 Vue 인스턴스가 data 객체의 모든 속성을 프록시한다는 것을 의미합니다. 인스턴스 속성 $data는 프록시된 data와 구분하기 위해 data 속성 자체를 나타냅니다.
즉, vm의 data 속성이 {a: 'xheldon'}이라면, vm.a는 'xheldon'이고, vm.$data는 {a: 'xheldon'}입니다.
컴포넌트도 사실 (확장된) Vue 인스턴스입니다. 다음은 간단한 검증 예시입니다:
list.vue 컴포넌트가 있다고 가정합니다(template과 style은 생략):
1 | |
props vs data
컴포넌트를 초기화할 때, prop의 속성과 data의 속성, 그리고 computed 메서드가 모두 Vue 인스턴스에 바인딩됩니다. 그러나 props의 속성은 data의 동일한 이름 속성보다 우선순위가 높습니다. 다음은 검증 예시입니다:
1 | |
결과로 경고가 발생합니다(에러가 아니라 렌더링에는 영향을 미치지 않음):
1 | |

반면 computed에서 반환된 함수 이름과 data의 속성 이름은 중복될 수 있으며, 아무런 경고도 발생하지 않습니다. 그러나 중복된 이름으로 인해 초기화 시점에 콘솔에서 _data가 _computedWatcher보다 늦게 이 중복 속성의 getter와 setter를 설정하는 것을 확인할 수 있습니다(이것이 원인인지는 확실하지 않으며, 추후 심층 연구 후 이 글을 수정할 예정입니다). 이로 인해 덮어쓰기가 발생합니다. 관련 원리는 이 글을 참조하세요.
이들은 모두 초기화 시점에 인스턴스 속성으로 바인딩되며, 동일한 이름의 computed 속성이 덮어쓰여졌지만, Vue devtool은 여전히 정확히 표시합니다)
1 | |

Vue devtool이 정확히 표시합니다:

만약 computed의 메서드 이름과 data의 속성 이름이 중복되지 않는다면:
1 | |

또한, methods와 computed 메서드의 차이점은 후자가 캐시를 가지고 전자는 그렇지 않다는 점(즉, 후자는 의존하는 반응형 데이터가 변경되지 않는 한 재계산되지 않음) 외에도, 둘 다 값을 반환하기 위한 것이라면 methods의 메서드는 functionName() 형태로 사용되고,
computed의 메서드는 functionName으로 참조된다는 것입니다. 즉, 전자는 함수를 실행해야 하지만 후자는 그럴 필요가 없습니다.
그 이유는 computed 속성에 작성한 메서드가 속성으로 간주되어 Vue 인스턴스에 마운트되기 때문이며, 우리가 제공한 함수는 이 메서드의 getter로 사용되기 때문입니다.
반면 methods는 단순한 함수이므로, 템플릿에서 참조하거나 이벤트 메서드로 사용할 때 모두 ()를 사용해 호출해야 합니다.
따라서 실제 함수가 필요한 경우------여기서 함수 호출을 사용한다면 computed는 단순히 값을 반환하는 것이 아니라 매개변수를 전달해야 하는 함수를 반환해야 합니다------이때는 methods가 가장 적합합니다.
다음은 공식 문서와 다른 버전의 todo list 예제입니다:
템플릿:
1 | |
로직:
1 | |
이 예제에서, 이러한 템플릿을 사용하는 경우 현재 li를 클릭할 때 삭제하기 위해 매개변수를 전달해야 하므로 이 경우 반드시 methods를 사용해야 합니다.
computed를 사용할 때는 매개변수를 받지 않습니다 (이는 getter이기 때문), 심지어 함수를 반환하더라도:
1 | |
참고: 만약 동일한 이름의 함수가 computed와 methods에 모두 존재한다면, computed의 함수가 더 높은 우선순위를 가집니다 (getter와 setter 처리 순서 문제로, 자세한 내용은 소스 코드를 참조하세요). 이는 템플릿 참조와 이벤트 바인딩 모두에 적용됩니다. 다음은 검증 내용입니다:
템플릿 참조 테스트:
템플릿:
1 | |
1 | |
출력 결과는 computed의 함수인 im from computed입니다. 만약 computed가 함수를 반환하지 않는다면, {{a}}와 같은 템플릿 참조는 당연히 computed의 값을 출력할 것입니다. 이에 대한 검증은 생략합니다.
이벤트 바인딩의 경우:
인라인 핸들러 메서드를 사용할 때:
1 | |
로직:
1 | |
출력 결과는 computed입니다.
메서드 이벤트 핸들러를 사용해도 결과는 동일합니다:
1 | |
로직:
1 | |
출력 결과는 computed입니다.
이벤트 바인딩 시 computed는 항상 함수를 반환해야 합니다.
따라서 우선순위 순서는 다음과 같습니다:
props > data > computed > methods
저는 computed와 methods에서 this.xxx로 참조할 때도 동일한 우선순위가 적용될 것이라고 추측합니다. 확인해보고 싶다면 직접 테스트해보시기 바랍니다.
또한 두 경우 모두 method는 함수를 반환할 필요가 없지만, computed는 항상 함수를 반환해야 하며 매개변수를 받지 않습니다(getter이기 때문). 결론적으로, 이벤트 처리에는 methods를 사용하는 것이 가장 좋고, 데이터 바인딩/템플릿 참조에는 computed를 사용하는 것이 가장 좋습니다(캐시 기능이 있기 때문).
추가로, method로 이벤트를 바인딩할 때 ()를 붙이거나 붙이지 않아도 함수는 실행됩니다. 차이점은 다음과 같습니다:
-
()가 포함된 것을 인라인 문장이라고 부르며, 두 가지 경우로 나뉩니다: 네이티브 이벤트와 커스텀 이벤트. 두 경우 모두 매개변수를 전달할 수 있으며, 매개변수 목록이 비어 있으면 기본 매개변수arguments도 비어 있어 기본 매개변수가 존재하지 않습니다. 만약input/click과 같은 네이티브 이벤트로 트리거된 경우, 특수한$event매개변수를 네이티브event이벤트 처리로 전달할 수 있습니다. 반면 커스텀 이벤트로 트리거된 경우, 이벤트 처리 함수의 매개변수는 여전히 실제로 이벤트 처리 함수에 전달된 값에 의존하며, 커스텀 함수에는 사용할 수 있는 특별한$event객체가 없습니다.$emit으로 커스텀 이벤트를 트리거할 때 전달된 매개변수는 무시됩니다. -
()가 없는 것을 메서드 이벤트라고 부르며, 두 가지 경우로 나뉩니다: 네이티브 이벤트인 경우 네이티브event이벤트를 유일한 기본 매개변수로 전달합니다. 커스텀 이벤트인 경우$emit이벤트 시 이벤트 이름을 제외한 두 번째부터 마지막까지의 임의 개수의 매개변수가 전달됩니다.
말보다는 코드를 보여주세요
위에서 설명한 네 가지 경우는 다음과 같습니다:
()가 포함된 네이티브 이벤트
1 | |
()가 포함된 커스텀 이벤트
1 | |
()가 없는 네이티브 이벤트
1 | |
()가 없는 커스텀 이벤트
1 | |
디렉티브와 매개변수(속성)
기본 사용법:
1 | |
여기서 directive는 디렉티브라고 부르며, propName은 디렉티브의 "매개변수"라고 합니다. 실제로 매개변수의 구체적 표현은 html의 속성입니다(Vue는 내장된 매개변수/속성을 제공합니다. 예를 들어 v-bind:click="method"의 click, v-bind:href="/img/in-post/x.png"의 href 등이 있습니다. 전자는 인라인에 나타나지 않지만, 후자는 필수적이므로 인라인 속성에 나타납니다. 사용자가 정의한 매개변수/속성은 반드시 인라인 속성에 나타납니다). value는 변수입니다(비록 쌍따옴표 안에 작성되었지만) 대부분의 경우 부모 템플릿에서 가져옵니다.
propName에는 .prevent와 같은 기본 이벤트를 금지하는 등의 동작을 빠르게 조작하기 위한 수식어를 추가할 수 있습니다.
일부 디렉티브는 v-if처럼 직접 사용할 수 있지만, v-bind:href/v-on:click처럼 매개변수를 반드시 추가해야 하는 경우도 있습니다.
주의할 점은 value에 쌍따옴표가 있든 없든 결과는 동일합니다. 즉, :propName="value"와 :propName=value는 동일합니다. 특별히 명시되지 않는 한, 아래 모든 경우에 적용됩니다.
value가 불리언 값으로 변환된 후 false인 경우 propName이 제거되고, true인 경우 propName이 나타납니다. 실제로 다음과 같은 규칙을 따릅니다(커스텀 속성에만 해당):
null,undefined,false리터럴인 경우propName속성이 제거됩니다.value가 정의되지 않은 변수인 경우(예::propName="wxd"),propName이 제거되고 경고가 발생합니다:
1 | |
-
value가 배열인 경우, 배열은 객체이므로propName은 아래 두 번째 경우를 제외하고 항상 존재합니다.value는 다음과 같은 경우로 나뉩니다: -
:propName =[],:propName ="[]",:propName ="['']"인 경우value가 제거되어 속성만 있고 값이 없습니다. -
:propName =[""]인 경우 구조가 깨집니다. -
:propName =["",""]또는:propName ='["",""]'인 경우value값은","입니다. -
value가 중첩 배열인 경우, 값을 1차원화한 후value또는 재귀적 하위 요소에undefined또는null리터럴(문자열 안에 작성된 것이 아닌)이 포함되어 있으면 해당 값은 비워집니다.value또는 재귀적 하위 요소에Object가 포함되어 있으면 해당 값은[object Object]가 됩니다. -
value가 객체인 경우propName은 유지되고 값은[object Object]가 됩니다. -
value가 숫자나 문자열인 경우(예::propName = "'fff'"),propName은 유지되고value는 문자열 또는 숫자 값이 됩니다.
소스 코드를 확인해보면 실제로 이런 로직으로 동작합니다:
1 | |
이러한 규칙은 사용자 정의 속성에만 적용됩니다. 내장 속성인 경우 다르게 처리됩니다. 예를 들어 class 속성을 바인딩하는 경우:
1 | |
이는 isActive 값이 false이거나 불리언 false로 변환될 수 있는 다른 값인 경우 active 클래스 이름이 적용되지 않고, 그렇지 않으면 해당 클래스 이름이 적용됨을 의미합니다(if 문의 참/거짓 판정과 동일합니다).
필터
필터를 체인으로 연결하면 첫 번째 필터의 매개변수는 초기값이고, 이후 필터의 첫 번째 매개변수는 이전 필터의 반환값입니다. 반환값이 없으면 undefined가 됩니다.
템플릿:
1 | |
로직:
1 | |
첫 번째 filter를 제외하고 후속 filter는 초기값을 얻을 수 없습니다. 물론 매개변수를 전달하려면 방법은 다양합니다. 예를 들어 첫 번째 filter가 배열을 반환하도록 하는 등입니다.
리스트 렌더링
v-for에서 매개변수가 두 개인 경우 네이티브 js의 forEach 매개변수와 동일하게 value, key 순서입니다. of 연산자와 in 연산자를 사용하는 효과는 완전히 동일합니다. 네이티브 js에서는 다르지만요.
또한 주의할 점은 컴포넌트에 v-for를 사용할 때 부모는 자동으로 데이터를 컴포넌트에 전달할 수 없습니다. 컴포넌트는 자신의 독립적인 스코프를 가지기 때문입니다. 따라서 자식 컴포넌트에 데이터를 전달하려면 props 속성을 사용해 약간 더 번거롭게 작성해야 합니다:
1 | |
또한 v-for를 객체에 사용할 때 반복값은 객체의 값이지 키가 아닙니다. 이는 네이티브 js와 다릅니다. 네이티브 js의 for in 루프에서 값을 출력하려면 수동으로 obj['i']를 순회해야 하며, 키를 출력하려면 두 번째 매개변수 (value, key)를 작성해야 합니다:
1 | |
주의: 네이티브 js에서는 수동으로 Symbol.iterator를 구현하지 않는 한 for of 루프를 사용할 수 없지만, Vue에서는 가능합니다. 비록 효과는 for in과 완전히 동일하지만요.
리스트 렌더링에는 '제자리 재사용 원칙’이라는 작은 팁이 있습니다. 이는 무엇을 의미할까요? 위에서 언급한 tololist 예시를 다시 보면, 각 요소에 고유한 key 값을 지정하지 않은 경우:
1 | |
이 경우 'X’를 클릭해 현재 li를 삭제할 때마다 Vue는 현재 요소를 제자리에서 재사용하며, remove로 DOM 요소를 삭제하는 대신 데이터를 올바른 위치로 직접 이동시켜 reflow를 방지합니다. 아래는 chrome의 devtool로 표시된 ‘X’ 클릭 시 페이지 렌더링 상황입니다:

reflow가 발생한 부분은 맨 아래 약간만 있는 것을 확인할 수 있습니다.
반면 key를 추가한 경우:
1 | |
닫기 버튼을 클릭한 후 브라우저의 render 동작을 다시 살펴보겠습니다:

어떤 분들은 여기서 왜 Vue에서 제공하는 (value, key)의 key를 사용하지 않고 직접 value에 value.key를 구현해야 하는지 궁금해할 수 있습니다:
1 | |
그 이유는, Vue에서 제공하는 key는表面上 key처럼 보이지만 실제로 현재 데이터와 무관하기 때문입니다. 따라서 li를 삭제할 때 key는 단순히 재계산될 뿐, 삭제되거나 이동된 요소와 함께 제거되거나 상하 이동하지 않습니다. 위의 방식으로 작성하면 효과는 첫 번째 key 없는 경우와 동일하며, 여전히就地 재사용 전략을 사용합니다. 변경되는 것은 데이터뿐이고 dom 구조는 그대로 유지되므로, 직접 key를 구현해야 합니다. 대략 다음과 같은 방식입니다:
1 | |
이벤트 핸들러
이벤트 핸들러는 체이닝할 수 있지만, 일부 요소는 기본적으로 지원하지 않아 바인딩해도 의미가 없습니다. 예를 들어 div에 keyup 이벤트를 바인딩하는 경우:
1 | |
따라서 일반적으로 div에서 이벤트 버블링을 처리한 후, input에 alt+ctrl 이벤트를 바인딩합니다:
1 | |
여기서 한 가지 우려사항은, input에 @keyup.space 리스너를 사용할 때 중국어 입력법에서 스페이스는 일반적으로 단어 선택에 사용되므로, 실제 출력은 아직 선택되지 않은 단어의 병음 알파벳일까요, 아니면 스페이스를 누른 후 후보 목록의 첫 번째 단어일까요? (이는 Vue의 문제는 아니지만 여기서 언급합니다)
답은 대부분의 경우 스페이스 누른 후의 첫 번째 단어가 출력됩니다. 하지만 문장이 길어 두 번의 space를 눌러야 하는 경우, 첫 번째 누를 때는 아무것도 출력되지 않고 빈 상태이며, 두 번째 space를 눌러야 전체 단어가 출력됩니다. 여기서는 테스트를 위한 극단적인 경우를 고려했으므로, 기본적으로 스페이스 후 첫 번째 단어가 출력되며 빈 값이나 병음 알파벳이 아닙니다. 저는 Sogou mac 입력법의 단일 행 후보 모드를 사용하며, 위의 todolist로 테스트할 수 있습니다 (생략)
참고: 공식 문서에서 v-model을 설명할 때 IME에 대해 언급하는데, 바로 이 문제를 다룹니다. 입력법 사용 시 v-model도 즉시 반응하도록 하려면 input 이벤트를 바인딩하면 됩니다.
폼 컨트롤
v-model은 일반적으로 input에 사용되며, 템플릿의 input value 값은 무시됩니다. v-model은 js의 초기값만 인식하고 바인딩합니다. 따라서 v-model을 작성하면서 value 속성도 함께 작성하면, 후자는 dom 구조에는 나타나지만 js에서 값을 가져올 때는 무시되고 v-model 바인딩 값이 사용됩니다:
1 | |
1 | |
둘의 차이는 jQuery의 .data와 .attr의 차이와 유사합니다------인라인에 작성된 것은 attr('data','xxx')의 값이며, dom 구조를 확인해봐도 xxx의 값이 보입니다. 하지만 js로 실제 얻는 값은 js로 바인딩된 .data('yyy')의 값입니다------물론 attr을 사용해 dom 구조를 읽는 경우는 제외입니다. (참고: 인스턴스화 후에 attr 값을 수정하면 js로 얻는 값은 attr의 값이 됩니다. 여기서 초기값 무시는 정말 초기값만 무시하는 것입니다. 예를 들어 초기화 후 v-model이 value에 바인딩된 상태에서 dom 구조의 value 값을 수동으로 수정하면, v-model이 해당 요소의 value 값을 다시 가져올 때 수정된 attr 속성 값을 사용하며 data 상의 값은 사용하지 않습니다).
요구사항이 특이해서 v-model을 통해 input 값을 실시간으로 가져와 업데이트하지 않거나, input 값을 가져와 처리한 후 업데이트해야 하는 동시에 v-model을 사용하고 싶지 않다면 $ref를 시도해볼 수 있습니다(e.target.value도 가능합니다):
템플릿:
1 | |
로직:
1 | |
공식 문서에서도 설명하듯, v-model은 양방향 데이터 바인딩을 구현하는 syntactic sugar일 뿐입니다:
1 | |
하지만 input 이벤트를 감지하면 발생하는 문제는 입력기를 사용할 때 공백 없이 단어를 선택하기 전에도 입력 이벤트가 트리거된다는 점입니다. 따라서 이러한 요구사항이 없다면 그냥 v-model을 사용하는 것이 좋습니다.
여러 요소에 동일한 값을 바인딩하고 출력해야 하는 경우, 일반적인 요구사항은 일련의 checkbox입니다. 이때는 배열을 사용해야 합니다:
1 | |
1 | |
(제가 발견한 바로는) 이렇게 간결한 배열 사용법은 여러 checkbox 타입의 input에만 유효합니다------즉, 여러 요소가 동일한 v-model에 바인딩되지만 이들 요소의 상태는 동기화되지 않고 해당 value가 배열에 들어갑니다. 물론 methods를 사용해 어떤 입력 타입의 요소든 비슷한 효과를 구현할 수 있다고 강조하신다면------그냥 넘어가겠습니다.
단일 checkbox의 v-model은 value 값에 바인딩되며, true 또는 false입니다. :true-value와 :false-value를 통해 선택 시 값과 선택하지 않았을 때의 값을 사용자 정의할 수 있습니다.
반면 여러 라디오 버튼 radio에서 v-model은 name과 유사한 역할을 합니다------즉 그룹화에 사용되므로 radio 타입의 input에 v-model을 사용하면 name 속성을 작성할 필요가 없습니다.
select 타입의 경우 각 option에 value 속성이 지정되지 않으면 option의 값에 바인딩되며, 지정된 경우 value 속성 값이 사용됩니다. select 타입의 다중 선택 상자에서 v-model에 바인딩된 data는 반드시 배열 타입이어야 합니다. 그렇지 않으면 경고가 발생합니다(하지만 오류는 아니며, Vue가 자동으로 변환하여 정상 실행됩니다):
1 | |
주의: 위의 모든 타입에서 v-model과 value가 바인딩될 때, value 속성이 동적으로(:value="xxx") data의 다른 속성(xxx)에 바인딩된 경우, v-model에 해당하는 속성과 :value에 해당하는 속성은 동일합니다(엄격하게 동등).
컴포넌트
먼저 구분해야 할 것은 DOM 템플릿과 문자열 템플릿의 차이입니다.
HTML 템플릿은 일반적인 html 요소를 의미하며, 이러한 요소들은 Vue 인스턴스의 el 옵션을 통해 바인딩됩니다:
문자열 템플릿 부분:
1 | |
문자열 템플릿은 다음과 같습니다:
js에서template로 등록된 템플릿, 예를 들어:
1 | |
또는:
1 | |
-
<script type="text/x-tempalge"></script>로 등록된 템플릿(Handlebar와 동일) -
.vue컴포넌트 내<template>태그 안의 내용.
Vue는 브라우저가 파싱을 완료한 후에야 DOM 템플릿을 파싱하기 시작하므로, DOM 템플릿은 특정 자식 요소가 필요한 태그에서 컴포넌트를 사용할 수 없습니다. 예를 들어 select 태그의 자식 요소는 반드시 option이어야 하므로, 사용자 정의 태그 com-option은 인식되지 않습니다. 이 경우 is="component-name" 속성을 추가하여 해당 태그가 사용하는 템플릿 이름을 명시하면 됩니다.
리터럴 문법 VS 동적 문법
여기서 주의할 점은, 기본 js에서는 객체의 속성으로 숫자를 사용할 수 있지만(단, ES5에서는 문자열로 취급되며, ES6에서는 객체 속성으로 어떤 값이든 가능하지만 이후 설명과 충돌하지 않음), Vue에서는 data 속성에 숫자를 속성으로 사용할 수 없다는 것입니다. 리터럴 문법을 사용하면 숫자가 전달될 때 먼저 toString 처리되지만, 동적 문법을 사용하면 숫자로 직접 처리되며 data에 바인딩된 속성 값을 찾지 않습니다:
자식 컴포넌트 템플릿:
1 | |
자식 컴포넌트 로직:
1 | |
부모 컴포넌트-리터럴 문법:
1 | |
부모 컴포넌트-동적 문법:
1 | |
위 두 문법에서 부모 컴포넌트 로직은 모두:
1 | |
결과는, 부모 컴포넌트 리터럴 문법을 사용할 때 button을 클릭하면 자식 컴포넌트에 1이 전달되며, 문서에서도 설명한 대로 문자열 1이므로 alert에는 string이 표시됩니다. 반면 부모 컴포넌트 동적 문법에서 v-bind를 사용해 부모 컴포넌트 data의 1 속성을 바인딩했지만, 자식 컴포넌트는 1 속성에 해당하는 속성-동적 문법 값을 받지 못하고 여전히 1 값을 받으므로, button을 클릭할 때 alert에는 number가 표시됩니다.
결론: data 객체의 속성으로 숫자를 사용하지 않는 것이 좋습니다.
참고: 자식 컴포넌트에 전달된 속성이 배열이나 객체인 경우, 자식 컴포넌트에서 이 속성 값을 수정하면 부모 컴포넌트에 반영됩니다. 이는 일반적으로 바람직하지 않은데, "props down, events up"이라는 말이 있듯이(예시 생략), 가장 좋은 방법은 부모 컴포넌트에서 전달된 참조 타입의 깊은 복사본을 사용하는 것입니다. 물론 자식 컴포넌트가 부모 컴포넌트의 상태에 영향을 주어야 하는 경우라면, 행운을 빕니다.
events up 시, 자식 컴포넌트가 $emit할 때 이벤트 이름 외에 다른 매개변수를 전달하면, 이러한 매개변수는 부모 컴포넌트의 이벤트 리스너 함수에 전달됩니다:
자식 컴포넌트 템플릿:
1 | |
자식 컴포넌트 로직:
1 | |
부모 템플릿:
1 | |
부모 로직:
1 | |
비동기 업데이트 큐
Vue가 있으면 jQuery는 더 이상 필요하지 않습니다. 프레임워크의 가장 큰 장점은 동일한 작업에 대해 반복적인 코드를 작성하지 않아도 된다는 점입니다. 양방향 데이터 바인딩은 많은 DOM 조작 문제를 해결해주지만, 어떤 경우에는 jQuery가 더 유리할 때가 있습니다. 예를 들어 DOM을 조작할 때 jQuery는 콜백 함수로 함수를 받아 애니메이션이 완료되면 트리거됩니다. 하지만 우리가 사용하는 양방향 데이터 바인딩에서는 데이터를 설정한 후 DOM이 업데이트된 시점을 어떻게 알 수 있을까요? 답은 jQuery와 마찬가지로 비동기 업데이트 큐입니다.
1 | |
이렇게 작성하는 것은 개인적으로 권장하지 않습니다. 왜냐하면 가장 좋은 로직은 인스턴스의 속성/메서드 내부에 작성하는 것이지, 인스턴스 외부에 작성하는 것이 아니기 때문입니다. 다행히 Vue는 이를 구현할 수 있는 방법을 제공합니다:
1 | |
애니메이션
애니메이션에 대해 특별히 설명할 것은 없지만, JavaScript 훅 함수 중 두 가지 훅에 대해 언급해야 합니다. 바로 enterCanceled와 leaveCancelled입니다. enterCancelled는 v-if와 v-show에서 모두 사용될 수 있으며, 트리거되는 시점은 enter 이벤트가 발생한 후 애니메이션이 완료되기 전에 다른 애니메이션이 실행되어야 할 때입니다. 반면 leaveCancelled는 v-show에서만 사용되며, v-if에서는 무효화되어 절대 트리거되지 않습니다. 이 훅은 떠나는 애니메이션(즉, xxx-leave-active 애니메이션)이 완료되기 전에 다른 애니메이션이 실행될 때 트리거됩니다.
테스트 코드:
1 | |
로직:
1 | |
스타일:
1 | |
훅들의 매개변수는 enter와 leave가 el 요소 자체(기본 Element 타입 요소)와 done 콜백 함수인 반면, 다른 훅들의 매개변수는 모두 el 요소 자체입니다.
요소의 전환에는 각 요소에 key를 추가하는 것이 가장 좋습니다. 이전에 언급된 재사용 전략 때문에 전환 시 데이터가 직접 교체되어 애니메이션 효과가 나타나지 않을 수 있기 때문입니다.
공식 문서에서 전혀 설명되지 않은 한 가지는 Vue transition의 애니메이션 클래스 이름 css 작성 순서에 제한이 있다는 것입니다. v-enter와 v-leave는 반드시 v-enter-active와 v-leave-active 뒤에 작성되어야 합니다. 그렇지 않으면 무효화됩니다. 예를 들어, 버튼을 클릭했을 때 페이드 인/아웃 효과를 주고 싶다면, 버튼을 클릭한 후 버튼이 왼쪽에서 오른쪽으로 페이드 인되면서 현재 클릭된 버튼은 왼쪽에서 오른쪽으로 페이드 아웃되도록 할 수 있습니다:
로직:
1 | |
구조:
1 | |
만약 스타일이 이렇게 작성되었다면:
1 | |
애니메이션 효과가 기대와 다를 것입니다:

하지만 enter를 enter-active 뒤로 옮기면:
1 | |
완벽해집니다:

이 네 가지 css 클래스 이름의 가능한 순서를 비교해본 결과, v-enter를 v-enter-active 뒤에 배치하기만 하면 효과가 구현되며, 다른 클래스 이름은 순서에 상관없이 작동한다는 것을 발견했습니다.
transition 태그 내에는 애니메이션이 필요한 요소만 배치해야 하며 다른 요소를 넣을 수 없습니다. 만약 위 예제의 구조가 이렇게 작성되었다면:
1 | |
어떤 애니메이션 효과도 나타나지 않을 것입니다. 또한 일반 css 클래스 이름에서 일부 css 속성을 사용해 요소의 스타일을 지정한 상태에서 v-enter와 같은 애니메이션 클래스 이름에 동일한 속성을 사용하면 역시 작동하지 않습니다. 문서에서는 "이들의 우선순위가 일반 클래스 이름보다 높다"고 설명하지만, 실제로는 그렇지 않습니다(제가 잘못 이해한 것일 수도 있습니다. 수정이 필요하다면 알려주세요). 위 예제에서 button의 일반 css 속성을 설정하면:
1 | |
결과:

보시다시피, opacity만 애니메이션이 적용되고 transform은 애니메이션이 없습니다! (학생들은 animate.css를 사용할 때 Animate.css에서 동일한 속성을 사용해 요소를 미리 설정하면 애니메이션 효과가 여전히 있는지 테스트해 볼 수 있으며, issue를 제출해 주시기 바랍니다.)
transition-group과 transition은 약간 다릅니다. 외관상으로는 transition은 단순히 래핑 컨테이너일 뿐이며 페이지 구성에 참여하지 않지만, transition-group은 Vue에 의해 태그로 대체됩니다. 기본적으로는 span 태그이지만, 대체될 태그 이름을 커스터마이징할 수도 있습니다.
render 함수
render 함수를 사용하면 템플릿 작성 대신 사용할 수 있습니다. 이 함수의 매개변수인 createElement는 일반적으로 h로 작성됩니다. 컴포넌트나 태그 내의 다양한 바인딩/속성 등은 createElement에서 해당하는 JavaScript 작성법을 찾을 수 있습니다. 만약 찾을 수 없다면, 네이티브 작성법을 사용하면 됩니다. 예를 들어 .stop, .prevent 등은 직접 event.stopPropagation()과 event.preventDefault()를 사용하면 됩니다.
기타
믹스인(mixin)은 일반적인 컴포넌트 작성 과정에서(즉, 컴포넌트의 라이프사이클에서) 추가 기능을 수정하거나 추가하는 것을 의미합니다.
일부 플러그인은 위의 mixin을 기반으로 작성되었습니다. 이 외에도 플러그인은 Vue.prototype에 메서드를 추가하거나 config를 통해 전역 메서드나 속성을 추가할 수 있습니다.
라우팅은 component의 is 속성을 사용해 간단히 구현할 수 있으며, 또는 render 함수를 직접 작성해 다른 경로에 따라 다른 템플릿을 렌더링할 수도 있습니다. 물론 더 복잡한 경우에는 서드파티 라이브러리가 필요합니다.
상태 관리는 예제를 보면 각 상태 변경 과정을 기록하기 위해 wrap을 추가해야 합니다. 그리고 공식적으로 권장하는 모범 사례는 인스턴스 속성에 직접 할당할 수 있더라도 상태를 함수를 통해 수정하는 것입니다. 이렇게 하면 상태를 추적할 수 있기 때문입니다.
단위 테스트는 일반적인 단위 테스트이므로 특별히 설명할 것이 없습니다.
서버 사이드 렌더링(Server Side Render)
기본적인 아이디어를 살펴보면 매우 간단합니다. 먼저 app.js에서 Vue 인스턴스를 exports하고, index.html이라는 페이지 템플릿 파일을 새로 만듭니다(Vue.js를 포함하고 Vue 인스턴스를 마운트하는 메서드인 $mount를 추가합니다. 공식 사이트에서는 app.js도 포함하고 있는데, 이 부분은 제가 확인해 봐야 합니다). 이 템플릿에는 인스턴스의 마운트 포인트(id 속성을 가진 비어 있지 않은 요소)가 포함되어 있습니다. 그런 다음 서버 측인 server.js에서 이들을 모두 require하고, vue-server-renderer를 사용해 app.js에서 exports한 Vue 인스턴스를 렌더링한 후, 클라이언트에 반환할 때 마운트 포인트(id 속성을 가진 비어 있지 않은 요소, app.js의 템플릿이 이미 존재하기 때문)를 대체하면 됩니다.
서버 사이드 렌더링의 결과는 위에서 언급한 마운트 포인트(id 속성을 가진 비어 있지 않은 요소)에 server-rendered="true" 속성이 추가되는 것입니다(페이지 소스 코드를 우클릭해 확인하면 존재하는 것을 알 수 있으며, 이는 js가 동적으로 추가한 것이 아님을 의미합니다).
서버 측에서도 스트리밍 렌더링을 지원합니다. 먼저 html을 마운트 포인트(id 속성을 가진 비어 있지 않은 요소, 예: <div id="app"></div>)를 기준으로 split하여 a와 b 두 부분으로 나눕니다. 기존에 사용한 vue-server-renderer는 app.js를 renderToString으로 처리했지만, 스트리밍 렌더링을 지원하기 위해서는 renderToStream이라는 다른 방법을 사용해야 합니다. 그런 다음 data 이벤트를 감지하여 html의 a 부분 뒤에 추가하고, end 이벤트 이후에 html의 b 부분을 연결한 다음 최종적으로 res.send로 전송합니다.
후기
베이징에서 ping cn.vuejs.org:

회사의 FQ VPS에 ping:

dig 명령어 실행:

cloudflare 서비스를 사용하고 있는 것을 확인할 수 있습니다. 국제적인 웹사이트 운영이 쉽지 않네요, 허허.
저는 인생의 중요한 선택의 기로에 섰을 때, 누군가 최선의 방법을 알려주어 소중한 시간을 헛되이 보내지 않기를 바라곤 합니다. 그런 마음으로 저는 자주 블로그를 쓰며, 광활한 인터넷의 이 작은 구석에 제게는 단 한 번뿐인 인생 경험을 기록하여 도움이 필요한 분들에게 도움이 되기를 바랍니다.