6 Vue 관련 지식 - 틱택토 만들기(삼목) 2차원 배열 연습에 좋음
source: categories/study/vue-principle/vue-principle_6.md
6.1 2차원 배열(테이블 구조 짜기)
1. 2차원 배열
- 프로그램에서 데이터 형태는 대부분 2차원 배열 형태로 되어있다.
- 데이터 베이스도 행과 열이 있는 표로 주로 나타낸다.
- 좋은 예시) 엑셀
- 2차원 배열을 잘 다루는 것이 중요하다.
- vue에서도 당연히 2차원 배열을 화면에 잘 표시하는 것이 중요하다.
2. 왜 아래처럼 컴포넌트를 세부적으로 나누는지 알아두는 것이 중요
tr
따로,td
따로 컴포넌트를 나누는 이유! 중요하다.- 한 컴포넌트 안에
table
,tr
,td
다 넣으면 안되나?
3. 아래와 같이하면 TicTacToe, table-component, tr-component, td-component 이렇게 4단계가 되어버린다. 이렇게 4단계로 구조를 나눈 이유는?
위와 같이 0, 0 요소를 재렌더링할 때, TicTacToe
, table
, tr
, td
이것들이 모두 한 파일에 있다면, 전체가 다시 재렌더링 되어버린다.
아주 작은 일부분만 바뀌더라도 전체가 다시 그려져야된다는 말이다.
위는 3 x 3 테이블이니까 그렇게 많지않아서 9개가 모두 재렌더링된다해도 문제가 없긴하겠지만,
만약 위 테이블이 1000 x 1000 이라면?
한칸 클릭해서 한칸만 바뀌었는데, 100만개가 다시 랜더링 되어야하는 상황이 발생.
성능상으로 많은 문제가 된다.
우리가 원하는 것은 정말 바뀌어야하는 부분만 재렌더링 되어야 한다는 것이다.
그렇기 때문에 그부분만 랜더링되도록 하기 위해 아래처럼 컴포넌트를 잘개 나누는 것이다.
- 여기서
key
속성 관련해서 의문점이 생김
4. 그런데 이런 구조로짜면 level이 너무 깊어진다. 이거 괜찮은거야?
지금 아래와 같은 간단한 예시도 level이 4단계로 깊어지고 있다. 이거 괜찮은건가?
데이터를 하위 컴포넌트로 벌써 이미 4번이나 넘겨주고 있다.
이는 아주 귀찮은 구조이다.
이 level이 10개, 100개.. 이렇게된다면 넘겨줄 수 있을까?
거의 불가능하다.
그래서 vue가 제공하는 것이 있다.
vuex 이전에 좀 더 간단하게 할 수 있도록 제공하는 방법이 있다.
5. TicTacToe 컴포넌트의 turn 데이터를 4단계 하위 컴포넌트인 TdComponent에서 어떻게 바꿀 수 있을까?
- 1차원적인 방법으로
turn
을 하위 컴포넌트로props
로 내려주는 것이다.- 그치만 이런 방법은 보기만해도 답답하다.
- 100단계면 100번을 내려줘야하니까..
- 이를 대비할 수 있도록 vue에서 대비해놓은 것이 있다.
- 바로
this.$root.$data
,this.$parent.$data
this.$root
는 해당 컴포넌트의 가장 상위 컴포넌트를 가리키고this.$parent
는 해당 컴포넌트의 한단계 위 컴포넌트를 가리킨다.
6. this.$root, this.$parent
- 이렇게 한번에 접근하는 방법은 리액트에는 없다.
- 뷰는 이런식의 방법이 다행히 가능은 하다.
this.$root
,this.$parent
를 사용하면data
를props
로 일일이 내려줄 필요도,event
를 일일이emit
해줄 필요도 없다.
vue
는 이런 기능을 제공해줘서 자식 컴포넌트에서 쉽게 부모 컴포넌트의 data
를 수정할 수 있다.
7. Vue.set
arr[0] = 1
이런식으로 배열 요소를index
값으로 선택해 바꿔도 화면에 반영이 안된다 - 주의!!- 객체, 배열은 참조형 데이터이기 때문이다!
arr.push(1)
그런데 이렇게 배열의 메소드를 사용해 값을 바꾸는 건 화면에 반영이 잘된다.- 여튼 숫자를 사용해 바꾸는 것은 안된다는 것!
-
그럼 해결 방법은?
Vue.set
활용Vue
를import
하기 귀찮으면this.$set
을 활용해도된다.- 객체의 key로 값을 바꾸는 경우도 화면에 반영되지 않는다. 그것도 이걸 활용해야된다.
8. vuex의 필요성 (리액트에서 리덕스를 사용하는 이유)
this.$root
는 괜찮다.root
는 딱 한개뿐이잖아?- 최상위 데이터 수정은 괜찮은데 만약 중간 컴포넌트의 데이터를 수정하다보면,
this.$parent.$parent.$data
이런식의 코드가 작성될 수 있다.
즉, 이렇게 할아버지 컴포넌트의 데이터를 수정하는 경우도 있고, this.$parent.$parent.$parent.$data
이렇게 증조 할아버지의 컴포넌트 데이터를 수정하는 경우가 있다.- 여기서 문제가
this.$parent
이렇게 해놓으면 솔직히parent
컴포넌트가 누군지 한번에 알기가 어렵다.- 현재 예시 컴포넌트는 간단하니까
td
컴포넌트의 부모 컴포넌트가tr
이란걸 쉽게 알 수 있는데, - 컴포넌트가 수천개라면 어떤 컴포넌트에
this.$parent
가 있다면, 그this.$parent
가 누구인지 알아내기 정말 힘들다. this.$parent
쓸 때, 이런 가독성 문제가 발생한다. 뭔가 코드가 애매하다싶으면 가독성이 안좋아지는 것이다.
- 현재 예시 컴포넌트는 간단하니까
- 그래서 차라리 모든 데이터들을 중앙 통제실 역할을하는
vuex
에서 한번에 관리를하고 - 그럼 모든 데이터가 중앙 통제실
vuex
에 있기 때문에 거기서 데이터를 꺼내오면 된다. - 그래서 보통 프로젝트 규모가 커지면
this.$parent
나this.$root
대신에vuex
를 사용하게된다. (중앙 데이터 통제실) - 프로젝트 규모가 크면
this.$parent
가 누군지 모르게된다. -
vuex
를 쓰면 비동기도 깔끔하게 다룰 수 있다. - 리액트에서
vuex
에 대응되는게 리덕스이다.
그런데 리액트는this.$root.$data
,this.$parent.$data
이런걸 둘 다 지원 안하기 때문에 리덕스를 사용하는 것이 필수인데, -
뷰는
this.$root
,this.$parent
같은 것들을 지원해서 그나마vuex
없이도 잘 돌아가게끔 할 수 있다. - 여튼 좀 더 보기좋고, 다루기 편하게하려면 데이터는 웬만하면 한 곳에 몰아놓거나 관리하기 쉽게 해주는 것이 좋다.
9. vue devtools
- data가 바뀔 때 화면이 바뀐다. (반응성)
- 재랜더링은 1회만 되는게 제일 바람직하다.
아니 무조건 1회만 되게해야된다.
바뀐 곳은 1곳인데 9번이 재렌더링된다던가하면 문제인 것이다.
그래서 컴포넌트를 쪼개서 만드는 것이 중요하다. - 이를 항상
devtools
에서 확인해야된다. -
그리고 60 프레임 이상으로 유지되는지도 확인해줘야한다.
- 리액트에 비해 뷰는 최적화하는 것이 엄청 까다롭지는 않다.
웬만하면 자동으로 해줘서 까다롭지는 않은데 이렇게 확인은 반드시 해줘야된다.
10. 의문점, 컴포넌트를 반복할 때, key 값을 부여하잖아? key값 부여하는 이유 자체가 값이 바뀐 컴포넌트'만' 재렌더링하기 위해서 아닌가? 그럼 굳이 컴포넌트 안나눠도 key값만 제대로 부여하면 되는거아냐?
- 아래처럼 작성해도
key
값을 제대로 부여했으니까 바뀐item
의td
만 다시 재렌더링 되는 것 아닌가? - 아래와 같이 작성하면
ExampleComponent
전체가 재렌더링 된다고해도 바뀐td
만 재렌더링 되는..거 아닌가? - 음.. 아래와 같은 경우가 아닌 다른 컴포넌트, 요소(element)가 같이 있는 경우엔 그 부분까지 재렌더링되는 거니깐 그런 경우를 대비해서 애초에 잘개 쪼개서 관리하자는건가?
- 그리고 아래와 같이
index
를key
값으로 할 경우 리액트에선 특별한 효과가 없다던데.. 그래서 그런 경우 데이터를 다시 재구성해야된다고까지 하던데.. 그런 경우에 데이터 재구성하기 귀찮을 때 아래와같이 차라리 컴포넌트를 쪼개서 가져가면..key
값으로 성능향상에 별로 도움 안되는index
를 줘도 괜찮으니깐.. 바뀌는 부분만 재렌더링 되니깐.. 음.. 그런 의미일까?
<template>
<table>
<tr>
<td
v-for="(item, index) in example"
:key="index"
>{{ item }}</td>
</tr>
</table>
</template>
<script>
export default {
name: 'ExampleComponent',
data() {
return {
example: ['a', 'b', 'c'],
}
}
}
</script>
<style>
</style>
11. EventBus
EventBus
는vuex
가data
를 중앙에서 통제한다면event
를 중앙에서 통제한다고 보면된다.TrComponent
,TdComponent
이렇게 하위 컴포넌트에 이벤트들이 흩어져있는 경우가 있다.- 이렇게 또 이벤트가 산개해있으면 어떤 컴포넌트에서 어떤 이벤트가 실행되어 어떤 데이터를 바꿀지 모른다.
- 그래서 이런 것들을 차라리 중앙에서 통제하자. 즉,
vuex
와 목적은 같은 것이다.
12. 이런 EventBus를 사용하면 무엇이 좋냐
TdComponent
에서TableComponent
에 접근하려면this.$root.$data
이런걸 사용했어야 했다.- 그런데
EventBus
를 통해 이벤트를 루트 컴포넌트로 올리면this.$root.$data
를 사용할 필요가 없어진다. - 덕분에 코드 파악이 쉬워진다. (최상위 컴포넌트에서 모든 것을 처리할 수 있게된다.)
13. EventBus의 단점?
root
컴포넌트의 코드 길이가 길어진다는 단점?- 중앙 통제를 하기 때문에
root
컴포넌트에 데이터 처리가 다 들어있고해서 찾기 쉽고 그래서 좋지만 - 반대로 동시에
root
컴포넌트의 코드가 너무 길어질 수도 있다는 것이 단점이다.
14. EventBus는 많이 쓰이나요?
- 생각외로 많이 쓰인다.
data
가 여러군데 흩어져있으면 파악하기 힘들기 때문에vuex
를 쓰는 것처럼event
도 여러군데 흩어져있으면 어떤 이벤트가 어떤걸 부르는지 헷갈려진다.- 그래서
event
도 중앙 통제를 하기 위해EventBus
를 사용하는 거기 때문에EventBus
도 실무에서 많이 쓰인다.
EventBus
를 사용하기 위해 아래와 같이 깡통vue
를 하나 만든다.vue
에서 기본적으로 제공하는 메소드를 사용하기 위해 빈 깡통vue
를 만드는 것이다.- 이런 빈 깡통
vue
를EventBus
라고 부른다.- 사실 아래와 같은 빈 깡통
vue
는 자유롭게 활용이 가능한데, 여기서 저희는EventBus
용도로만 사용할 거기 때문에EventBus.js
라고 이름을 지은 것이다.
- 사실 아래와 같은 빈 깡통
// EventBus.js
import Vue from 'vue';
export default new Vue();
<template>
<div>
<div>{{ turn }}님의 턴입니다.</div>
<table-component
:table-data="tableData"
></table-component>
<div v-if="winner">{{ winner }}님의 승리!</div>
</div>
</template>
<script>
import TableComponent from './TableComponent';
import EventBus from './EventBus';
export default {
name: 'TicTacToe',
components: {
TableComponent,
},
data() {
return {
tableData: [
['', '', ''],
['', '', ''],
['', '', ''],
],
turn: 'O',
winner: '',
}
},
created() {
EventBus.$on('clickTd', this.onClickTd);
},
beforeDestroy() {
EventBus.$off('clickTd', this.onClickTd);
},
methods: {
onClickTd(rowIndex, cellIndex) {
// const rootData = this.$root.$data;
// console.log(this.$root.$data);
// console.log(this.$parent.$data);
// this.$root.$data.tableData[this.rowIndex][this.cellIndex] = this.$root.$data.turn; // 이렇게 말고 (이렇게하면 화면에 반영이 안된다)
// Vue.set(this.$root.$data.tableData[this.rowIndex], this.cellIndex, this.$root.$data.turn); // 이렇게 바꿔줘야한다. Vue.set 이게 딥카피 기능을 해주는 역할인듯
// 위와 같이 Vue를 import해서 사용하기 귀찮다면
// this.$set을 활용해도된다.
this.$set(this.tableData[rowIndex], cellIndex, this.turn);
let win = false;
if (
this.tableData[rowIndex][0] === this.turn &&
this.tableData[rowIndex][1] === this.turn &&
this.tableData[rowIndex][2] === this.turn
) {
win = true;
}
if (
this.tableData[0][cellIndex] === this.turn &&
this.tableData[1][cellIndex] === this.turn &&
this.tableData[2][cellIndex] === this.turn
) {
win = true;
}
if (
this.tableData[0][0] === this.turn &&
this.tableData[1][1] === this.turn &&
this.tableData[2][2] === this.turn
) {
win = true;
}
if (
this.tableData[0][2] === this.turn &&
this.tableData[1][1] === this.turn &&
this.tableData[2][0] === this.turn
) {
win = true;
}
if (win) { // 승자가 있는 경우
this.winner = this.turn;
// 다음 게임을 위해 turn 값 및 tableData 초기화
this.turn = 'O';
this.tableData = [['', '', ''], ['', '', ''], ['', '', '']];
} else { // 승자가 아직 없는 경우
let all = true; // all이 true이면 무승부라는 뜻
this.tableData.forEach(row => { // 무승부 검사
row.forEach(cell => {
if (!cell) {
all = false;
}
})
})
if (all) { // 무승부인 경우
this.winner = '';
this.turn = 'O';
this.tableData = [['', '', ''], ['', '', ''], ['', '', '']];
} else { // 게임이 아직 안 끝난 경우
this.turn = this.turn === 'O' ? 'X' : 'O';
}
}
}
},
}
</script>
<style>
</style>
<template>
<table>
<tr-component
v-for="(rowData, index) in tableData"
:key="index"
:row-data="rowData"
:row-index="index"
></tr-component>
</table>
</template>
<script>
import TrComponent from './TrComponent';
export default {
name: 'TableComponent',
components: {
TrComponent,
},
props: {
tableData: {
type: Array,
}
}
}
</script>
<style>
table {
border-collapse: collapse;
}
td {
border: 1px solid #000;
width: 40px;
height: 40px;
text-align: center;
}
</style>
<template>
<tr>
<td-component
v-for="(cellData, index) in rowData"
:key="index"
:row-index="rowIndex"
:cell-data="cellData"
:cell-index="index"
></td-component>
</tr>
</template>
<script>
import TdComponent from './TdComponent';
export default {
name: 'TrComponent',
components: {
TdComponent,
},
props: {
rowData: {
type: Array,
},
rowIndex: {
type: Number,
}
}
}
</script>
<template>
<td @click="onClickTd">{{ cellData }}</td>
</template>
<script>
// import Vue from 'vue';
import EventBus from './EventBus';
export default {
name: 'TdComponent',
props: {
cellData: {
type: String,
},
rowIndex: {
type: Number,
},
cellIndex: {
type: Number,
}
},
methods: {
onClickTd() {
if (this.cellData) return; // 남이 누른칸을 누른 경우
EventBus.$emit('clickTd', this.rowIndex, this.cellIndex);
}
}
}
</script>
14. vuex 구조 세팅하기
vuex
는 스토어를 여러개 만들어도된다.- 리액트의
redux
는 스토어를 딱 하나만 만들어야된다. vue
가 아무래도react
의 단점들을 개선해나가며 만든 프레임워크이기 때문에react
,angular
의 단점들을vue
가 많이 개션을 함
npm i vuex
# OR
yarn add vuex
14.1 state 값을 변경할 때, mutations를 사용하는 이유
mutations
를 사용하면vue devtools
에서 내가 어떻게 데이터를 바꿨는지 추적하기가 쉬워진다.- 이전까지
this.data
를 사용해 데이터를 마음대로 바꿨었다. - 이렇게하면 나중에 프로젝트 규모가 커지면 해당 데이터가 왜 바뀌었는지 추적하기가 어려워진다.
- 하지만
mutations
를 사용하면 데이터가 바뀔 때마다 어떤 동작이 있었는지를 보여주기 때문에 데이터를 어떻게 바꿨는지 추적할 수가 있게되고, 역순으로 돌아갈 수도 있게된다.
14.2 store의 state값을 가져올 때 computed 사용
- 아래와 같은 방식으로하면
data
는 모두vuex
에 있고, 컴포넌트들은vuex
에서data
를 가져와서 화면에 표시하고mutations
로data
수정하고.. data
들이store
에 모여져있기 때문에 관리하기 편해진다.
14.3 Vue와 Vuex 연결하기 (연결 안하면 에러 발생)
Vue.use(Vuex)
//store.js
에서 연결import store from './store'
,export default
안에store
// 최상위(root
) 컴포넌트에도 연결해줘야함- 이렇게 연결해줘야 그제서야
this.$store
를 사용할 수 있다.
14.4 vue devtools 두번째 탭 (vuex)
vue devtools
두번째 탭에서vuex
state
를 볼 수 있다.- 이전까진 컴포넌트에서 각 데이터를 확인했지만, 이제는 데이터를
vuex
로 중앙 통제실에서 관리하고 있기 때문에,
두번째 탭에서vuex
에 있는data
를 보면된다. -
그리고
mutations
이나actions
,getters
가 실행될 때마다 실행된 내역들이 보이게된다. vuex
를 사용하면 상위 컴포넌트에서 하위 컴포넌트로data
를props
로 넘겨줄 필요가 없다.
14.5 각 컴포넌트마다 store에 있는 state, getters, mutations, actions를 일일이 가져오기 귀찮음 - mapState, mapGetters, mapMutations, mapActions
14.6 Vue.use()
Vue.use(Vuex);
:this.$store
생김Vue.use(axios);
:this.$axios
생김
14.7 Vue.use(vuex)를 main.js에서 한번에 연결해줄 수 있나요?
- 그렇게하면 불러오는 순서가 꼬여버린다.
- 만약
Vue.use(Vuex)
가store.js
말고main.js
에 있다면,import
는 항상 코드 제일 위에 위치main.js
에 있는TicTacToe
불러올 때 이 파일 안에import store from './store'
import store from './store'
안에new Vuex.Store()
가 먼저 실행되고 나서,- 그 다음에
main.js
에 있는Vue.use(Vuex)
실행
위와 같이 순서가 어긋난다.
Vue.use(Vuex)
는 항상 new Vuex.Store()
보다 위에 있어야한다.
import
가 항상 위에 위치해야하고 TicTacToe
컴포넌트에서 import
해오는 것들과 순서가 꼬여서 new Vuex.Store()
가 먼저 실행된 후에
Vue.use(Vuex)
가 실행되어버린다.
사실 Vue.use(Vuex)
코드를 main.js
에서 처리하면 깔끔한데,
위와 같은 제약 때문에 어쩔 수 없이 store.js
에서 연결을 해준 것이다.
무조건 new Vuex.Store()
가 Vue.use(Vuex)
보단 아래에 있어야한다.
이런 import
순서 같은 경우는 안맞으면 알아서 에러메시지가 발생하기 때문에 너무 엄격하게 외울 필요가 없다.
// store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex); // this.$store 생김
// Vue.use(axios); // this.$axios 생김
// 미들웨어 개념과 비슷
export const SET_WINNER = 'SET_WINNER';
export const CLICK_CELL = 'CLICK_CELL';
export const CHANGE_TURN = 'CHANGE_TURN';
export const RESET_GAME = 'RESET_GAME';
export const NO_WINNER = 'NO_WINNER';
export default new Vuex.Store({
state: { // vue의 data와 비슷
tableData: [
['', '', ''],
['', '', ''],
['', '', ''],
],
turn: 'O',
winner: '',
},
getters: { // vue의 computed와 비슷 // 이런걸 활용하는 이유는 caching되기 때문!
turnMessage(state) {
return state.turn + '님이 승리하셨습니다.';
}
},
mutations: { // state를 수정할 때 사용 - 동기적으로
// 함수명을 동적 속성으로 부여
// 뷰에서는 함수명을 이런식으로 보통 부여한다. 이유가 있다.
[SET_WINNER](state, winner) { // mutation 함수는 대문자로 짓는게 암묵적인 룰
state.winner = winner;
},
[CLICK_CELL](state, { row, cell }) {
// vuex는 this.$set이 없기 때문에 아래와 같이 Vue.set으로 해줘야된다.
Vue.set(state.tableData[row], cell, state.turn);
},
[CHANGE_TURN](state) {
state.turn = state.turn === 'O' ? 'X' : 'O';
},
[RESET_GAME](state) {
state.turn = 'O';
state.tableData = [
['', '', ''],
['', '', ''],
['', '', '']
]
},
[NO_WINNER](state) {
state.winner = '';
},
},
actions: { // 비동기를 사용할 때, 또는 여러 mutation을 연달아 실행할 때
}
})
<template>
<div>
<div>{{ turn }}님의 턴입니다.</div>
<table-component></table-component>
<div v-if="winner">{{ winner }}님의 승리!</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import store from './store';
import TableComponent from './TableComponent';
export default {
store,
name: 'TicTacToe',
components: {
TableComponent,
},
// data() {
// return {
// data: 1
// }
// },
computed: {
...mapState(['winner', 'turn']),
// ...mapState({
// // winner: state => state.winner, // 화살표 함수는 this 사용 불가
// winner(state) {
// return state.winner;
// // return state.winner + this.data; // 일반 함수는 this 사용 가능
// },
// turnState: 'turn',
// })
// winner() {
// return this.$store.state.winner;
// },
// turn() {
// return this.$store.state.turn;
// }
}
}
</script>
<style>
</style>
<template>
<table>
<tr-component
v-for="(rowData, index) in tableData"
:key="index"
:row-index="index"
></tr-component>
</table>
</template>
<script>
import TrComponent from './TrComponent';
export default {
name: 'TableComponent',
components: {
TrComponent,
},
computed: {
tableData() {
return this.$store.state.tableData;
},
// ...mapGetters도 존재
turnMessage() {
return this.$store.getters.turnMessage;
}
}
}
</script>
<style>
table {
border-collapse: collapse;
}
td {
border: 1px solid #000;
width: 40px;
height: 40px;
text-align: center;
}
</style>
<template>
<tr>
<td-component
v-for="(cellData, index) in rowData"
:key="index"
:row-index="rowIndex"
:cell-index="index"
></td-component>
</tr>
</template>
<script>
import TdComponent from './TdComponent';
export default {
name: 'TrComponent',
components: {
TdComponent,
},
props: {
rowData: {
type: Array,
},
},
computed: {
rowData() {
return this.$store.tableData[this.rowIndex];
}
}
}
</script>
<template>
<td @click="onClickTd">{{ cellData }}</td>
</template>
<script>
// import Vue from 'vue';
import { mapState } from 'vuex';
import { CLICK_CELL, SET_WINNER, RESET_GAME, CHANGE_TURN, NO_WINNER } from './store'; // 이렇게 사용하면 함수명 오타낼 위험이 없다.
export default {
name: 'TdComponent',
props: {
cellData: {
type: String,
},
rowIndex: {
type: Number,
},
cellIndex: {
type: Number,
}
},
computed: {
...mapState({
tableData: state => state.tableData,
turn: state => state.turn,
cellData(state) {
return state.tableData[this.rowIndex][this.cellIndex];
},
}),
// cellData() {
// return this.$store.state.tableData[this.rowIndex][this.cellIndex];
// },
// tableData() {
// return this.$store.state.tableData;
// },
// turn() {
// return this.$store.state.turn;
// }
},
methods: {
onClickTd() {
if (this.cellData) return; // 남이 누른칸을 누른 경우
// const rootData = this.$root.$data;
// console.log(this.$root.$data);
// console.log(this.$parent.$data);
// this.$root.$data.tableData[this.rowIndex][this.cellIndex] = this.$root.$data.turn; // 이렇게 말고 (이렇게하면 화면에 반영이 안된다)
// Vue.set(this.$root.$data.tableData[this.rowIndex], this.cellIndex, this.$root.$data.turn); // 이렇게 바꿔줘야한다. Vue.set 이게 딥카피 기능을 해주는 역할인듯
// 위와 같이 Vue를 import해서 사용하기 귀찮다면
// this.$set을 활용해도된다.
// this.$set(this.tableData[rowIndex], cellIndex, this.turn);
this.$store.commit(CLICK_CELL, { row: this.rowIndex, cell: this.cellIndex }); // mutations를 부를 땐 $store를 활용한다.
let win = false;
if (
this.tableData[this.rowIndex][0] === this.turn &&
this.tableData[this.rowIndex][1] === this.turn &&
this.tableData[this.rowIndex][2] === this.turn
) {
win = true;
}
if (
this.tableData[0][this.cellIndex] === this.turn &&
this.tableData[1][this.cellIndex] === this.turn &&
this.tableData[2][this.cellIndex] === this.turn
) {
win = true;
}
if (
this.tableData[0][0] === this.turn &&
this.tableData[1][1] === this.turn &&
this.tableData[2][2] === this.turn
) {
win = true;
}
if (
this.tableData[0][2] === this.turn &&
this.tableData[1][1] === this.turn &&
this.tableData[2][0] === this.turn
) {
win = true;
}
if (win) { // 승자가 있는 경우
// this.winner = this.turn;
this.$store.commit(SET_WINNER, this.turn);
// 다음 게임을 위해 turn 값 및 tableData 초기화
// this.turn = 'O';
// this.tableData = [['', '', ''], ['', '', ''], ['', '', '']];
this.$store.commit(RESET_GAME);
} else { // 승자가 아직 없는 경우
let all = true; // all이 true이면 무승부라는 뜻
this.tableData.forEach(row => { // 무승부 검사
row.forEach(cell => {
if (!cell) {
all = false;
}
})
})
if (all) { // 무승부인 경우
// this.winner = '';
this.$store.commit(NO_WINNER);
// this.turn = 'O';
// this.tableData = [['', '', ''], ['', '', ''], ['', '', '']];
this.$store.commit(RESET_GAME);
} else { // 게임이 아직 안 끝난 경우
// this.turn = this.turn === 'O' ? 'X' : 'O';
this.$store.commit(CHANGE_TURN);
}
}
}
}
}
</script>
14.8 Vuex devtools로 분석하기
- vue devtools에서 Component render 부분을 보면, td를 클릭하는 순간 9번이나 랜더링되는 것을 볼 수 있다.
- 이는 9개의 td가 모두 랜더링 된다는 뜻이다.
- 분명 td 한개만 클릭하고 클릭된 td 한개만 바뀌었는데, 9개가 모두 랜더링되어버린다.
- 그 이유는
vuex
를 적용한 후TdComponent
에서computed
의cellData
가 매번 실행되기 때문이다.vuex
를 적용해서 매번실행되는 것은 아니고, 해당tableData
배열을 감시해 변경이 있을 때마다 실행해서 그런 것.- 성능상 큰 영향은 안끼치더라도
table
,tr
,td
이렇게 세세하게 나눈 의미가 퇴색된다. - 랜더링 최소화를 위해 컴포넌트를 나눠놨는데, 이렇게 1개 변할 때 9번 랜더링되면 그 의미가 사라진다.
- 그래서 아래처럼
table
,tr
,td
나누지 말고 하나로 합쳐서 적어줘도 될 것 같다.
vuex
를 사용하다보면 데이터를 감시하는 형태가 이런식으로 될 수 밖에 없어서 어쩔 수 없는 것 같다.- 그래서 아래와 같이 하나의 컴포넌트 안에서 다 해버리는 것도 지금 상황에선 괜찮을 것 같다.
vuex
테이블 데이터 때문에 어차피 전체가 계속 재렌더링 될거라면, 아래와 같이되도.. 상관없을 것 같다.
- 이렇게
TicTacToe
컴포넌트로 모두 통일하고나면 재랜더링 횟수는 1회씩만 된다.TicTacToe
컴포넌트를 다시 전체 다 재랜더링하는거니깐 뭐.. 성능은 똑같아도 횟수는 1회만..
즉, 잘개 쪼개는 이유는 횟수보다는 재랜더링되는 영역의 범위!를 작게 하기 위함인듯.
그런데 그마저도 vuex
를 적용하면서 그렇게하질 못하니 이렇게 그냥 TicTacToe
하나로 합친 것.
그리고 뷰, 리액트는 Virtual DOM(반복문에선 key
값을 꼭 붙여줘! 이것때문에 붙이는거!)이란 것을 사용해서 실재로 devtools에서 updated가 9회 떴어도 실재로 9번 재렌더링되거나 그런건 아닐 거임.
실제로 바뀐 데이터만 재랜더링 하기 때문에.
devtools에서는 단순히 계산상 보여주는 수치일 뿐이고, 화면에 실제로 다시그려지는건 별개의 문제이기 때문.
그래서 거기까지는 크게 신경 안써도 된다.
<template>
<div>
<div>{{ turn }}님의 턴입니다.</div>
<table>
<tr v-for="(rowData, rowIndex) in tableData" :key="rowIndex">
<td @click="onClickTd(rowIndex, cellIndex)" v-for="(cellData, cellIndex) in rowData" :key="cellIndex">{{ cellData }}</td>
</tr>
</table>
<div v-if="winner">{{ winner }}님의 승리!</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import store, { CLICK_CELL, SET_WINNER, RESET_GAME, CHANGE_TURN, NO_WINNER } from './store';
export default {
store,
name: 'TicTacToe',
computed: {
...mapState(['winner', 'turn']),
// ...mapState({
// // winner: state => state.winner, // 화살표 함수는 this 사용 불가
// winner(state) {
// return state.winner;
// // return state.winner + this.data; // 일반 함수는 this 사용 가능
// },
// turnState: 'turn',
// })
// winner() {
// return this.$store.state.winner;
// },
// turn() {
// return this.$store.state.turn;
// }
},
methods: {
onClickTd(rowIndex, cellIndex) {
if (this.cellData) return; // 남이 누른칸을 누른 경우
// const rootData = this.$root.$data;
// console.log(this.$root.$data);
// console.log(this.$parent.$data);
// this.$root.$data.tableData[this.rowIndex][this.cellIndex] = this.$root.$data.turn; // 이렇게 말고 (이렇게하면 화면에 반영이 안된다)
// Vue.set(this.$root.$data.tableData[this.rowIndex], this.cellIndex, this.$root.$data.turn); // 이렇게 바꿔줘야한다. Vue.set 이게 딥카피 기능을 해주는 역할인듯
// 위와 같이 Vue를 import해서 사용하기 귀찮다면
// this.$set을 활용해도된다.
// this.$set(this.tableData[rowIndex], cellIndex, this.turn);
this.$store.commit(CLICK_CELL, { row: rowIndex, cell: cellIndex }); // mutations를 부를 땐 $store를 활용한다.
let win = false;
if (
this.tableData[rowIndex][0] === this.turn &&
this.tableData[rowIndex][1] === this.turn &&
this.tableData[rowIndex][2] === this.turn
) {
win = true;
}
if (
this.tableData[0][cellIndex] === this.turn &&
this.tableData[1][cellIndex] === this.turn &&
this.tableData[2][cellIndex] === this.turn
) {
win = true;
}
if (
this.tableData[0][0] === this.turn &&
this.tableData[1][1] === this.turn &&
this.tableData[2][2] === this.turn
) {
win = true;
}
if (
this.tableData[0][2] === this.turn &&
this.tableData[1][1] === this.turn &&
this.tableData[2][0] === this.turn
) {
win = true;
}
if (win) { // 승자가 있는 경우
// this.winner = this.turn;
this.$store.commit(SET_WINNER, this.turn);
// 다음 게임을 위해 turn 값 및 tableData 초기화
// this.turn = 'O';
// this.tableData = [['', '', ''], ['', '', ''], ['', '', '']];
this.$store.commit(RESET_GAME);
} else { // 승자가 아직 없는 경우
let all = true; // all이 true이면 무승부라는 뜻
this.tableData.forEach(row => { // 무승부 검사
row.forEach(cell => {
if (!cell) {
all = false;
}
})
})
if (all) { // 무승부인 경우
// this.winner = '';
this.$store.commit(NO_WINNER);
// this.turn = 'O';
// this.tableData = [['', '', ''], ['', '', ''], ['', '', '']];
this.$store.commit(RESET_GAME);
} else { // 게임이 아직 안 끝난 경우
// this.turn = this.turn === 'O' ? 'X' : 'O';
this.$store.commit(CHANGE_TURN);
}
}
}
}
}
</script>
<style>
</style>
15 slot
- 리액트에는
children
이란 개념이 있다.
15.1 slot은 왜 쓸까?
- 화면에 나타낼 때 위치는
slot
인데, 즉 하위 컴포넌트인데, 적는 위치는 상위 컴포넌트에 적을 수 있다. - 즉, 상위 컴포넌트의 데이터를 가져다 쓸 수 있게 해준다. 원래 하위 컴포넌트에 있었다면 가져다 쓰지 못했을 텐데.
- 이것도 데이터를 중앙에서 관리한다 개념과 일맥상통한다.
- IOC (Inversion of Control), 제어의 역전, 디팬던시 인젝션
- 자식 컴포넌트에서 관리해야될거를 부모 컨트롤에서 제어하도록! 자식 컴포넌트에서 제어를 해야되는데 제어권이 부모로 넘어감!
- 라이브러리를 사용할 때 이런
slot
을 활용해IOC
를 마련해둔 라이브러리들이 있는데 이런 라이브러리들이 커스터마이징하기에 더 쉽다.
16. 반복문에서 key를 index로 쓰면 안좋다. 불필요한 재렌더링
- 이번 예제에서는 마땅히
key
로 쓸게 없어서index
를key
로 썼는데,key
를index
로 써도 되는 경우가 있고 쓰지 말아야되는 경우가 있다.
[0, 1, 2, 3, 4, 5]
배열이 있다면 해당 배열의index
값은0, 1, 2, 3, 4, 5
가 될 것이다.index
를key
로 써도 되는 경우는 이와 같이 각 요소의 값이 지속적으로 증가하는 경우- 또는
[1, 2, 3, 4, 8, 9, 5]
이렇게 값이 수정되는 경우. - 이렇게 값이 수정되어도 각
index
의 값은 안변하니깐! - 이런 경우는
key
로index
를 써도된다.
- 문제는 배열 요소 중 하나가 삭제되는 경우
- 그럼 삭제된 것 이후의 요소가 다시 전부 재렌더링됨. 그 삭제된 요소 뒤의 요소들의
index
가 모두 하나씩 당겨졌기 때문. - 그래서 배열의
key
값을index
로 하는게 항상 나쁜 것은 아니지만, 배열의 요소가 삭제되는 경우, 그 삭제된 요소 뒤의 요소들이 앞으로 다 당겨지므로, 그것들이 모두 불필요하게 전부 재렌더링된다. - 계속 추가되거나 수정되는 것은 상관없다. 단지 삭제되는 경우가 문제!
17. 그럼 배열의 각 요소에 고유값을 어떻게 부여하는게 좋을까?
index
값에 뒤에 아무 랜덤 문자열을 붙여줘도 된다.index + 'abc'
이렇게.- 그런데 사실
key
정하는게 매우 애매하다. - 고유한 값이 없는 경우도 있기 때문이다.
Math.random()
? 이런것도 괜찮을듯?index + Math.random()
<template>
<div>
<div>{{ turn }}님의 턴입니다.</div>
<table-component>
<tr v-for="(rowData, rowIndex) in tableData" :key="rowIndex">
<td @click="onClickTd(rowIndex, cellIndex)" v-for="(cellData, cellIndex) in rowData" :key="cellIndex">{{ cellData }}</td>
</tr>
</table-component>
<div v-if="winner">{{ winner }}님의 승리!</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import store, { CLICK_CELL, SET_WINNER, RESET_GAME, CHANGE_TURN, NO_WINNER } from './store';
import TableComponent from './TableComponent';
export default {
store,
name: 'TicTacToe',
components: {
TableComponent,
},
computed: {
...mapState(['winner', 'turn']),
// ...mapState({
// // winner: state => state.winner, // 화살표 함수는 this 사용 불가
// winner(state) {
// return state.winner;
// // return state.winner + this.data; // 일반 함수는 this 사용 가능
// },
// turnState: 'turn',
// })
// winner() {
// return this.$store.state.winner;
// },
// turn() {
// return this.$store.state.turn;
// }
},
methods: {
onClickTd(rowIndex, cellIndex) {
if (this.cellData) return; // 남이 누른칸을 누른 경우
// const rootData = this.$root.$data;
// console.log(this.$root.$data);
// console.log(this.$parent.$data);
// this.$root.$data.tableData[this.rowIndex][this.cellIndex] = this.$root.$data.turn; // 이렇게 말고 (이렇게하면 화면에 반영이 안된다)
// Vue.set(this.$root.$data.tableData[this.rowIndex], this.cellIndex, this.$root.$data.turn); // 이렇게 바꿔줘야한다. Vue.set 이게 딥카피 기능을 해주는 역할인듯
// 위와 같이 Vue를 import해서 사용하기 귀찮다면
// this.$set을 활용해도된다.
// this.$set(this.tableData[rowIndex], cellIndex, this.turn);
this.$store.commit(CLICK_CELL, { row: rowIndex, cell: cellIndex }); // mutations를 부를 땐 $store를 활용한다.
let win = false;
if (
this.tableData[rowIndex][0] === this.turn &&
this.tableData[rowIndex][1] === this.turn &&
this.tableData[rowIndex][2] === this.turn
) {
win = true;
}
if (
this.tableData[0][cellIndex] === this.turn &&
this.tableData[1][cellIndex] === this.turn &&
this.tableData[2][cellIndex] === this.turn
) {
win = true;
}
if (
this.tableData[0][0] === this.turn &&
this.tableData[1][1] === this.turn &&
this.tableData[2][2] === this.turn
) {
win = true;
}
if (
this.tableData[0][2] === this.turn &&
this.tableData[1][1] === this.turn &&
this.tableData[2][0] === this.turn
) {
win = true;
}
if (win) { // 승자가 있는 경우
// this.winner = this.turn;
this.$store.commit(SET_WINNER, this.turn);
// 다음 게임을 위해 turn 값 및 tableData 초기화
// this.turn = 'O';
// this.tableData = [['', '', ''], ['', '', ''], ['', '', '']];
this.$store.commit(RESET_GAME);
} else { // 승자가 아직 없는 경우
let all = true; // all이 true이면 무승부라는 뜻
this.tableData.forEach(row => { // 무승부 검사
row.forEach(cell => {
if (!cell) {
all = false;
}
})
})
if (all) { // 무승부인 경우
// this.winner = '';
this.$store.commit(NO_WINNER);
// this.turn = 'O';
// this.tableData = [['', '', ''], ['', '', ''], ['', '', '']];
this.$store.commit(RESET_GAME);
} else { // 게임이 아직 안 끝난 경우
// this.turn = this.turn === 'O' ? 'X' : 'O';
this.$store.commit(CHANGE_TURN);
}
}
}
}
}
</script>
<style>
</style>
<template>
<table>
<slot />
<!-- slot 여러개 사용할 때는 공식문서 참고! -->
<slot>
<!-- 기본값 -->
<tr>
<td></td>
</tr>
</slot>
</table>
</template>
<script>
export default {
name: 'TableComponent',
}
</script>
<style>
table {
border-collapse: collapse;
}
td {
border: 1px solid #000;
width: 40px;
height: 40px;
text-align: center;
}
</style>