172 vue, react 등 프론트 프레임워크에서 반복문시 할당하는 Key 프롭에 관해.. (vue3 DOC 기준이지만, 내용읽어보니 vue2도 이에 해당하는듯..? 아닌가)
source: categories/study/vue-experiance/vue-experiance_9-99_73.md
172 vue, react 등 프론트 프레임워크에서 반복문시 할당하는 Key 프롭에 관해.. (vue3 DOC 기준이지만, 내용읽어보니 vue2도 이에 해당하는듯..? 아닌가)
key 속성, in-place patch
개요
- NEW: vue가 고유한
key
를 자동으로 생성하기 때문에v-if
/v-else
/v-else-if
에서 더 이상key
가 필요하지 않습니다.- BREAKING: 수동으로
key
를 정할 경우, 각 분기는 반드시 고유한key
를 사용해야 합니다. 더이상 의도적으로 동일한key
를 사용하여 분기를 강제로 재사용할 수 없습니다.
- BREAKING: 수동으로
- BREAKING:
<template v-for>
의key
는 자식이 아닌<template>
태그에 있어야 합니다.
배경
특수한 속성인 key
는 주로 Vue의 가상 DOM 알고리즘이 노드의 ID를 식별하기 위한 힌트로 사용됩니다.
이를 통해 Vue는 어느 시점에 기존 노드를 가져오고 재사용할 수 있는지, 또 재정렬과 재생성이 필요한 때가 언제인지 파악합니다.
자세한 내용은 다음 섹션을 참고하세요.
조건부 분기에서의 key
vue 2.x에서는 v-if
/ v-else
/ v-else-if
분기에 key
를 사용하도록 권장했습니다.
<!-- Vue 2.x -->
<div v-if="condition" key="yes">Yes</div>
<div v-else key="no">No</div>
위의 예제는 Vue 3.x에서도 여전히 유효합니다.
그러나 v-if
/ v-else
/ v-else-if
분기에 key
속성을 사용하지 않는 것이 좋습니다.
이제 key
를 정의하지 않으면 조건부 분기에 고유한 key
가 자동으로 생성되기 떼문입니다.
<!-- Vue 3.x -->
<div v-if="condition">Yes</div>
<div v-else>No</div>
가장 큰 변화는 수동으로 key
를 정할 때, 각 분기가 반드시 고유한 key
를 사용해야 한다는 점입니다.
대부분의 경우, 아래와 같이 key
를 제거할 수 있습니다.
<!-- Vue 2.x -->
<div v-if="condition" key="a">Yes</div>
<div v-else key="a">No</div>
<!-- Vue 3.x (권장 솔루션: key를 제거하세요.) -->
<div v-if="condition">Yes</div>
<div v-else>No</div>
<!-- Vue 3.x (대체 솔루션: key가 항상 고유한지 확인하세요.) -->
<div v-if="condition" key="a">Yes</div>
<div v-else key="b">No</div>
template v-for 와 함께 쓰기
Vue 2.x에서는 <template>
태그가 key
를 가질 수 없었습니다.
대신 각 자식 항목에 key
를 배치할 수 있었습니다.
<!-- Vue 2.x -->
<template v-for="item in list">
<div :key="item.id">...</div>
<span :key="item.id">...</span>
</template>
vue 3.x에서는 key
가 <template>
태그에 있어야 합니다.
<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
<div>...</div>
<span>...</span>
</template>
이와 마찬가지로, <template v-for>
가 v-if
를 사용하는 자식과 함께 있을 때, key
를 <template>
태그까지 끌어 올려야합니다.
<!-- Vue 2.x -->
<template v-for="item in list">
<div v-if="item.isVisible" :key="item.id">...</div>
<span v-else :key="item.id">...</span>
</template>
<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
<div v-if="item.isVisible">...</div>
<span v-else>...</span>
</template>
Vue.js의 렌더링에 관해 - VNode와 Component 인스턴스 (2017년 글)
만약 Child 컴포넌트를 반복적으로 출력하는 Parent 컴포넌트를 만든다고 해보자.
이때 Child 컴포넌트를 출력하기 위해 <component>
엘리먼트를 이용해 보자.
이는 이후 Parent 컴포넌트가 정해진 Child 컴포넌트가 아니라 다른 임의의 컴포넌트를 출력하게 할 수도 있게 하기 위해서다.
그래서 다음과 같이 코딩을 한다.
// Parent 컴포넌트
Vue.component('parent-comp', {
name: 'parent-comp',
template: `
<div>
<h1>Parent Component</h1>
<button @click="addComp()">Add</button>
<!-- <component> 엘리먼트를 v-for로 반복시킨다. -->
<component v-for="comp in compList" :is="comp"></component>
</div>
`,
data : function() {
return {
compList : []
}
},
methods : {
addComp : function() {
this.compList.unshift( 'child-comp' ); //새로운 Child 컴포넌트를 앞에 추가하기 위해 unshift를 한다.
}
}
})
//Child 컴포넌트
Vue.component( 'child-comp', {
name : 'child-comp',
template : `
<div>
<h3>Child Component : 8</h3> <!-- count를 출력한다. -->
<button v-on:click="increase()">Increase</button>
</div>
`,
data : function() {
return {
count : 0
}
},
methods : {
increase : function() {
this.count++;
}
}
});
Child 컴포넌트는 내부적으로 count 하나를 가지고 버튼을 누를 때마다 증가하도록 한다. (이는 여러 Child 컴포넌트를 구분하기 위한 용도다.)
그리고 Parent 컴포넌트는 버튼을 누를 때마다 Child 컴포넌트를 추가하는데 이때 새롭게 추가되는 Child 컴포넌트가 처음에 오도록 compList에 대해 push 대신에 unshift를 사용했다.
우선 Child 컴포넌트를 하나 생성하고 버튼을 눌러 카운트를 좀 증가시켜둔다.
그리고나서 Child 컴포넌트를 하나 더 추가해보자.
그런데 생각한 모습과 다르다.
분명 새로 생성하는 Child 컴포넌트가 먼저 나오도록 unshift를 사용했는데, 여전히 먼저 생성된 Child 컴포넌트(카운트가 증가된 Child 컴포넌트)가 처음에 출력되는 것이다.
무엇이 문제인걸까?
이 문제를 이해하기 위해서는 VNode와 Component와의 관계에 대해 먼저 알아야 한다.
이전글에서도 언급했지만 가상 DOM 기반 Vue.js는 View에 변경이 생기면 모든 Component의 render 함수를 통해서 처음부터 다시 렌더링한다.
즉 모든 VNode를 다시 생성한다는 말이다.
그러나 이렇게 VNode를 다시 생성하더라도 Component의 인스턴스는 다시 생성하면 안된다.
Component의 인스턴스는 데이터를 포함하고 있는, 즉 상태를 가지고 있으므로 이것을 매번 다시 생성해버리면 상태가 사리지기 때문에 VNode를 다시 생성하더라도 Component의 인스턴스는 그대로 유지가 되어야한다.
이렇게 VNode와 Component는 생명주기가 다르기 때문에 Vue.js 어플리케이션은 로직과 데이터를 담고있는 Component Tree와 View가 렌더링된 VNode Tree 2개로 구성되어 있다고 할 수 있다.
그리고 이 VNode와 Component와의 연결은 VNode의 ComponentInstance 속성을 통해 이루어진다.
이 상태에서 렌더링이 다시 이루어지면 VNode가 다시 생성되고 기존의 VNode와 새로운 VNode를 비교하는 과정(patch)에 들어가게 되면, Vue.js는 기존 VNode와 새로 생성된 VNode가 같다면 기존 VNode에 있던 ComponentInstance 속성을 그대로 새로운 VNode에 대입해준다.
이렇기 때문에 렌더링이 다시 일어나 새로운 VNode를 생성하더라도 Component의 상태는 그대로 유지가 될 수 있는 것이다.
그런데 여기서 중요한 것이 Vue.js가 기존 VNode와 새로운 VNode가 같다는 것을 판단하는 기준이 뭐냐는 것이다.
그건 바로 Vue.js 소스에서 볼 수 있다.
보시다시피 가장 우선 key가 같은지를 보고 key가 같으면 tag를 비롯해서 몇가지를 검사하게된다.
그럼 앞서 만들었던 예제의 문제가 발생한 이유를 알 수 있을 것이다.
즉, 같은 컴포넌트를 추가했기 때문에 새로운 VNode가 앞에 추가되었지만, Vue.js는 새로 추가된 VNode가 기존의 VNode와 다르지 않다고 판단해 버린 것이다.
그렇기 때문에 기존의 componentInstance를 새로 생성된 VNode에 대입을 한 것이고, 그래서 여전히 처음이 기존 컴포넌트의 instance가 되는 것이다.
이것은 같은 컴포넌트를 추가하면서 key
를 설정하지 않았기 대문에 발생하는 것이다.
사실 앞선 예제를 코딩해보면 알겠지만 이미 v-for
에 key
를 설정하라고 하는 이유도 설명이 될 것이다.
그럼 앞선 예제의 문제를 해결해보자.
//Parent 컴포넌트
Vue.component( 'parent-comp', {
name : 'parent-comp',
template : `
<div>
<h1>Parent Component</h1>
<button v-on:click="addComp()">Add</button>
<component v-for="comp in compList" :is="comp.component" :key="comp.key"></component>
</div>
`,
data : function() {
return {
nextKey : 0,
compList : [ ]
}
},
methods : {
addComp : function() {
this.compList.unshift( { component : 'child-comp', key : this.nextKey++ } );
}
}
});
<component>
엘리먼트를 v-for
로 반복할 때 key
도 같이 설정하도록 수정했다.
그러면 원하는 결과를 볼 수 있다.
보다시피 새로 추가한 컴포넌트가 먼저 나온다.
render 함수를 직접 작성하는 경우에는 createElement 호출시에 데이터 파라미터에 key 설정한다.
createElement( ChildComp, { key : myKey } ... )
만약 앞선 예제의 Parent 컴포넌트를 render 함수를 직접 작성해서 만든다고 하면 다음처럼 될 것이다.
Vue.component( 'parent-comp', {
name : 'parent-comp',
render : function( createElement ) {
var children = [
createElement( 'h1', 'Parent Component' ),
createElement( 'button', { on : { click : this.addComp } }, 'Add' ),
];
for( var i = 0; i < this.compList.length; i++ )
children.push( createElement( this.compList[i].component, { key : this.compList[i].key } ) );
return createElement( 'div', children );
},
data : function() {
return {
nextKey : 0,
compList : []
}
},
methods : {
addComp : function() {
this.compList.unshift( { component : 'child-comp', key : this.nextKey++ } );
}
}
});
이상으로 VNode와 Component와의 관계에 대해 기본적인 것을 살펴보았다.
이런 내용들은 Vue.js 어플리케이션을 만들 때 주의해야할 점이기도 하지만, 특히 라이브러리 컴포넌트를 제작하는 경우에는 좀더 잘 알고 고려할 필요가 있는 것들이다.
그게 아니더라도 Vue.js 렌더링을 좀 더 깊이 이해하는 차원에서도 알아두면 나쁘지 않으리라 생각한다.
[출처] https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=jjoommnn&logNo=221092926766