6 Vue 관련 지식 - 틱택토 만들기(삼목) 2차원 배열 연습에 좋음

source: categories/study/vue-principle/vue-principle_6.md

6.1 2차원 배열(테이블 구조 짜기)

1. 2차원 배열

  1. 프로그램에서 데이터 형태는 대부분 2차원 배열 형태로 되어있다.
  2. 데이터 베이스도 행과 열이 있는 표로 주로 나타낸다.
    • 좋은 예시) 엑셀
  3. 2차원 배열을 잘 다루는 것이 중요하다.
  4. 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. 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를 사용하면 dataprops로 일일이 내려줄 필요도, event를 일일이 emit해줄 필요도 없다.
Note

vue는 이런 기능을 제공해줘서 자식 컴포넌트에서 쉽게 부모 컴포넌트의 data를 수정할 수 있다.

7. Vue.set

  • arr[0] = 1 이런식으로 배열 요소를 index 값으로 선택해 바꿔도 화면에 반영이 안된다 - 주의!!
  • 객체, 배열은 참조형 데이터이기 때문이다!
  • arr.push(1) 그런데 이렇게 배열의 메소드를 사용해 값을 바꾸는 건 화면에 반영이 잘된다.
  • 여튼 숫자를 사용해 바꾸는 것은 안된다는 것!
  • 그럼 해결 방법은?

    • Vue.set 활용
    • Vueimport하기 귀찮으면 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.$parentthis.$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 값을 제대로 부여했으니까 바뀐 itemtd만 다시 재렌더링 되는 것 아닌가?
  • 아래와 같이 작성하면 ExampleComponent 전체가 재렌더링 된다고해도 바뀐 td만 재렌더링 되는..거 아닌가?
  • 음.. 아래와 같은 경우가 아닌 다른 컴포넌트, 요소(element)가 같이 있는 경우엔 그 부분까지 재렌더링되는 거니깐 그런 경우를 대비해서 애초에 잘개 쪼개서 관리하자는건가?
  • 그리고 아래와 같이 indexkey값으로 할 경우 리액트에선 특별한 효과가 없다던데.. 그래서 그런 경우 데이터를 다시 재구성해야된다고까지 하던데.. 그런 경우에 데이터 재구성하기 귀찮을 때 아래와같이 차라리 컴포넌트를 쪼개서 가져가면.. 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

  • EventBusvuexdata를 중앙에서 통제한다면 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를 만드는 것이다.
  • 이런 빈 깡통 vueEventBus라고 부른다.
    • 사실 아래와 같은 빈 깡통 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를 가져와서 화면에 표시하고 mutationsdata 수정하고..
  • 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를 사용하면 상위 컴포넌트에서 하위 컴포넌트로 dataprops로 넘겨줄 필요가 없다.

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에 있다면,
    1. import는 항상 코드 제일 위에 위치
    2. main.js에 있는 TicTacToe 불러올 때 이 파일 안에 import store from './store'
    3. import store from './store' 안에 new Vuex.Store()가 먼저 실행되고 나서,
    4. 그 다음에 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) 보단 아래에 있어야한다.

Note

이런 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로 분석하기

  1. vue devtools에서 Component render 부분을 보면, td를 클릭하는 순간 9번이나 랜더링되는 것을 볼 수 있다.
    • 이는 9개의 td가 모두 랜더링 된다는 뜻이다.
    • 분명 td 한개만 클릭하고 클릭된 td 한개만 바뀌었는데, 9개가 모두 랜더링되어버린다.
    • 그 이유는 vuex를 적용한 후 TdComponent에서 computedcellData가 매번 실행되기 때문이다.
      • vuex를 적용해서 매번실행되는 것은 아니고, 해당 tableData 배열을 감시해 변경이 있을 때마다 실행해서 그런 것.
      • 성능상 큰 영향은 안끼치더라도 table, tr, td 이렇게 세세하게 나눈 의미가 퇴색된다.
      • 랜더링 최소화를 위해 컴포넌트를 나눠놨는데, 이렇게 1개 변할 때 9번 랜더링되면 그 의미가 사라진다.
      • 그래서 아래처럼 table, tr, td 나누지 말고 하나로 합쳐서 적어줘도 될 것 같다.
    • vuex를 사용하다보면 데이터를 감시하는 형태가 이런식으로 될 수 밖에 없어서 어쩔 수 없는 것 같다.
    • 그래서 아래와 같이 하나의 컴포넌트 안에서 다 해버리는 것도 지금 상황에선 괜찮을 것 같다.
    • vuex 테이블 데이터 때문에 어차피 전체가 계속 재렌더링 될거라면, 아래와 같이되도.. 상관없을 것 같다.
  2. 이렇게 TicTacToe 컴포넌트로 모두 통일하고나면 재랜더링 횟수는 1회씩만 된다.
    • TicTacToe 컴포넌트를 다시 전체 다 재랜더링하는거니깐 뭐.. 성능은 똑같아도 횟수는 1회만..
Note

즉, 잘개 쪼개는 이유는 횟수보다는 재랜더링되는 영역의 범위!를 작게 하기 위함인듯.
그런데 그마저도 vuex를 적용하면서 그렇게하질 못하니 이렇게 그냥 TicTacToe 하나로 합친 것.

Note

그리고 뷰, 리액트는 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로 쓸게 없어서 indexkey로 썼는데, keyindex로 써도 되는 경우가 있고 쓰지 말아야되는 경우가 있다.

  • [0, 1, 2, 3, 4, 5] 배열이 있다면 해당 배열의 index 값은
  • 0, 1, 2, 3, 4, 5가 될 것이다.
  • indexkey로 써도 되는 경우는 이와 같이 각 요소의 값이 지속적으로 증가하는 경우
  • 또는 [1, 2, 3, 4, 8, 9, 5] 이렇게 값이 수정되는 경우.
  • 이렇게 값이 수정되어도 각 index의 값은 안변하니깐!
  • 이런 경우는 keyindex를 써도된다.

  • 문제는 배열 요소 중 하나가 삭제되는 경우
  • 그럼 삭제된 것 이후의 요소가 다시 전부 재렌더링됨. 그 삭제된 요소 뒤의 요소들의 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>