Vue - 주의해야할 5가지 특징

Updated:

Intro

  • 오늘은 2019년 FEConf 때, 장기효님이 Vue.js 입문자가 실무에서 주의해야 할 5가지 특징 강의를 들었습니다.
  • 이를 바탕으로 5가지 특징을 정리해보았습니다.



1. 반응성에 대해 알아야 할 점

  • 반응성은 언제 설정될까?

    • 인스턴스가 생성될 때 data의 속성들을 초기화
  • 생성하는 시점에 없었던 data는 반응하지 않습니다.

    • <script>
      var vm = new Vue({
      	data: {
              user: {
                  name: 'Captain'
              }
         }
      })
      vm.user.age += 1; // age 값이 변하더라도 화면은 갱신되지 않는다
      </script>
          
      


반응성을 이해하지 못했을 때의 실수
화면에서만 필요한 UI 상태 값을 다룰 때 (토글, 체크박스)
  • <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>Reactivity Caveat 1</title>
        <style>
          li {
            list-style: none;
          }
        </style>
      </head>
      <body>
        <div id="app">
          <ul>
            <li v-for="user in users">
              <input
                type="checkbox"
                :name="user.name"
                :id="user.id"
                :checked="user.checked"
                @change="user.checked = !user.checked"
              />
              <span></span>
            </li>
          </ul>
          <button @click="checkFirstUser">check the first check-box</button>
          <button @click="uncheckFirstUser">uncheck the first check-box</button>
        </div>
      
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script>
          new Vue({
            el: '#app',
            data: {
              users: [],
            },
            created() {
              this.fetchUsers();
            },
            methods: {
              fetchUsers() {
                fetch('https://jsonplaceholder.typicode.com/users')
                  .then(response => response.json())
                  .then(data => {
                    this.users = data;
                  })
                  .catch(error => console.log(error));
              },
              checkFirstUser() {
                this.users[0].checked = true;
                console.log('check state : ', this.users[0].checked);
              },
              uncheckFirstUser() {
                this.users[0].checked = false;
                console.log('check state : ', this.users[0].checked);
              },
            },
          });
        </script>
      </body>
    </html>
    
    • 체크를 했음에도 false라고 뜨고, 체크를 하지 않았음에도 true라고 뜹니다.
  • 해결 방법

    • <html lang="en">
        <head>
          <meta charset="UTF-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1.0" />
          <meta http-equiv="X-UA-Compatible" content="ie=edge" />
          <title>Reactivity Caveat 1</title>
          <style>
            li {
              list-style: none;
            }
          </style>
        </head>
        <body>
          <div id="app">
            <ul>
              <li v-for="user in users">
                <input
                  type="checkbox"
                  :name="user.name"
                  :id="user.id"
                  :checked="user.checked"
                  @change="user.checked = !user.checked"
                />
                <span></span>
              </li>
            </ul>
            <button @click="checkFirstUser">check the first check-box</button>
            <button @click="uncheckFirstUser">uncheck the first check-box</button>
          </div>
          
          <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
          <script>
            new Vue({
              el: '#app',
              data: {
                users: [],
              },
              created() {
                this.fetchUsers();
              },
              methods: {
                fetchUsers() {
                  fetch('https://jsonplaceholder.typicode.com/users')
                    .then(response => response.json())
                    .then(data => {
                      this.users = data;
                      this.$set(this.users[0], 'checked', false)
                    })
                    .catch(error => console.log(error));
                },
                checkFirstUser() {
                  this.users[0].checked = true;
                  console.log('check state : ', this.users[0].checked);
                },
                uncheckFirstUser() {
                  this.users[0].checked = false;
                  console.log('check state : ', this.users[0].checked);
                },
              },
            });
          </script>
        </body>
      </html>
      


백엔드에서 불러온 데이터에 임의 값을 추가하여 사용하는 경우
  • <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>Reactivity Caveat 1</title>
      </head>
      <body>
        <div id="app">
          <div>
            <h1>User Info</h1>
            <p>Name : </p>
            <p>Email : </p>
            <p>Region : </p>
            <button @click="changeName">change Name</button>
            <button @click="changeRegion">change Region</button>
            <button @click="addReactivityToRegion">add Reactivity To Region</button>
          </div>
          <div>
            <h1>User Data</h1>
            <div>
                
            </div>
            <h1>User Region</h1>
            <div v-if="user.region">
                
            </div>
          </div>
        </div>
      
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script>
          new Vue({
            el: '#app',
            data: {
              user: {},
            },
            created() {
              this.fetchUsers();
            },
            methods: {
              fetchUsers() {
                fetch('https://jsonplaceholder.typicode.com/users/1')
                  .then(response => response.json())
                  .then(data => {
                    this.user = data;
                  })
                  .catch(error => console.log(error));
              },
              changeName() {
                this.user.name = 'Josh';
              },
              changeRegion() {
                this.user.region = 'Seoul';
                console.log('The region has been changed', this.user);
              },
              addReactivityToRegion() {
                this.$set(this.user, 'region', 'To Be Decided');
                console.log(this.user);
              },
            },
          });
        </script>
      </body>
    
    • id값이 임의로 들어가 있습니다.
    • reactivity가 주입됐는지는 get과 set을 확인합니다.
      • image-20200902211204996


뷰엑스의 state도 data와 동일하게 취급
  • state: {
    	user: {name: 'Captain'}
    },
    mutations: {
    	// 생성하는 시점에 없었던 데이터는 반응성이 없음
    	setUserAge: function(state) {
    		state.user.age = 23
    	},
    	//객체 속성을 임의로 추가 또는 삭제하는 경우 뷰에서 감지 못함
    	deleteName: function(state) {
    		delete state.user.name
    	}
    }
    
  • 그러나 뷰3.0에서는 괜찮습니다!

    Object.defineProperty()에서 Proxy기반으로 변화합니다.

    • var obj = {};
          
      //Vue 2.x
      Object.defineProperty(obj, 'str', {..})
          
      //Vue 3.x
      new Proxy(obj, {..})
      



2. DOM 조작

오래된 습관 버리기

(기존) 화면 조작을 위한 DOM 요소 제어 방법
  • 특정 DOM을 검색해서 제어하는 방법

    • // 네이티브 JS
      documment.querySelector('#app')
          
      // 제이쿼리 라이브러리
      $('#app')
      


(기존방식)사용자의 입력 이벤트를 기반으로 한 DOM 요소 제어
  • // 버튼 요소 검색
    var btn = document.querySelector('#btn');
      
    // 사용자의 클릭 이벤트를 기반으로 가장 가까운 태그 요소를 찾아 제거
    btn.addEventListener('click', function(event) {
    	event.target.closest('.tag1').remove();
    })
    


(Vue.js 방식)사용자의 입력 이벤트를 기반으로 한 DOM 요소 제어

Vue에서 제공하는 ref속성을 이용합니다.

  • <!-- HTML 태그에 ref 속성 추가 -->
    <div ref="hello"> Hello Ref </div>
      
      
    //인스턴스에서 접근 가능한 ref 속성
    this.$refs.hello; //div 엘리먼트 정보
    


(Vue.js 방식) 디렉티브를 활용한 DOM 요소 제어

Vue 디렉티브 에서 제공되는 정보를 최대한 활용

  • <ul>
    	<li v-for="(item, index) in items">
    		<span v-bind:id="index"></span>
    	</li>
    </ul>
    


Dom 제어 사고 전환이 필요한 실제 사례자
  • <ul>
    	<li v-for="(item, index) in items">
    		<span v-bind:id="index"></span>
    	</li>
    </ul>
    
  •     
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>DOM Handling</title>
        <style>
          li {
            padding: 0.6rem;
          }
          .hide {
            display: none;
          }
        </style>
      </head>
      <body>
        <div id="app">
          <ul>
            <li @click="removeItem">
              <span>메뉴 1</span>
              <div class="child hide">
                메뉴 설명
              </div>
            </li>
            <li @click="removeItem">
              <span>메뉴 2</span>
              <div class="child hide">
                메뉴 설명
              </div>
            </li>
            <li @click="removeItem">
              <span>메뉴 3</span>
              <div class="child hide">
                메뉴 설명
              </div>
            </li>
          </ul>
        </div>
      
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script>
          new Vue({
            el: '#app',
            data: {
              items: ['메뉴 1', '메뉴 2', '메뉴 3'],
            },
            methods: {
              removeItem(event) {
                event.target.lastChild.classList.toggle('hide');
              },
            },
          });
        </script>
      </body>
    </html>
    
    • removeItem을 보면 클릭한 이벤트의 위치를 기준으로 마지막 child를 쫓아가서 토글하고 있었음.

    • 이를 개선하는 방법은?

      •         
        <html lang="en">
          <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <meta http-equiv="X-UA-Compatible" content="ie=edge" />
            <title>DOM Handling</title>
            <style>
              li {
                padding: 0.6rem;
              }
              .hide {
                display: none;
              }
            </style>
          </head>
          <body>
            <div id="app">
              <ul>
              	<li v-for="(item, index) in items" @click="removeItem(index)">
              		<span></span>
              		<div class="child hide" ref="listItem">
              			메뉴 설명
              		</div>
              	</li>
              </ul>
            </div>
              
            <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
            <script>
              new Vue({
                el: '#app',
                data: {
                  items: ['메뉴 1', '메뉴 2', '메뉴 3'],
                },
                methods: {
                  removeItem(index) {
                    this.$refs.listItem[index].classList.toggle('hide')
                  },
                },
              });
            </script>
          </body>
        </html>
        
        • ref를 달아서 listItem에서 클래스리스트를 토글링 가능하다.
        • V-for은 index 지원이 가능하다.



3. 인스턴스 라이프 사이클

  • 인스턴스 라이프 사이클이란?
    • 뷰 인스턴스가 생성되고 소멸되기까지의 생애 주기
    • image-20200902212829965


Vue 템플릿 속성

인스턴스, 컴포넌트의 표현부를 정의하는 속성

  • // 인스턴스 옵션 속성
    new Vue({
    	data: { str: 'Hello World'},
    	template: '<div></div>'
    })
      
    <!--싱글파일 컴포넌트 -->
    <template>
    	<div> </div>
    </template>
    


뷰 템플릿 속성의 정체

실제 DOM 엘리먼트가 아니라 Virtual DOM (자바스크립트 객체)

  • <!--사용자가 작성한 코드 -->
    <template>
    	<div> </div>
    </template>
      
    // 라이브러리 내부적으로 변환한 모습
    function render() {
    	with(this) {
    		return _c('div', [_v(_s(str))])
    	}
    }
    



템플릿 속성이 실제로 유효한 시점

인스턴스가 화면에 부착 (mounted)되고 난 후

  1. 인스턴스 부착 시점을 이해하지 못한 사례 1

    <!DOCTYPE html>
      <body>
        <div id="app">
          <canvas id="myChart"></canvas>
        </div>
        <script>
          new Vue({
            el: '#app',
            created() {
              var ctx = document.querySelector('#myChart'); // null
              var myChart = new Chart(ctx, barChartOptions);
            },
          });
        </script>
      </body>
    </html>
    
    • mounted시점에서 querySelector를 이용하면 null이된다.
  2. 인스턴스 부착 시점을 이해하지 못한 사례 2

  • <!DOCTYPE html>
      <body>
        <div id="app">
          <canvas id="myChart"></canvas>
        </div>
        <script>
          new Vue({
            el: '#app',
            created() {
            	this.$nextTick(function() { // 업데이트 시점 혼란 야기 및 코드 복잡도 증가
            		var ctx = document.querySelector('#myChart'); // null
             		var myChart = new Chart(ctx, barChartOptions);
            	})
            },
          });
        </script>
      </body>
    </html>
    
  1. 인스턴스 부착 시점을 이해한 코드
  • <!DOCTYPE html>
      <body>
        <div id="app">
          <canvas id="myChart"></canvas>
        </div>
        <script>
          new Vue({
            el: '#app',
           	mounted() {
                var ctx = document.querySelector('#myChart'); // null
                var myChart = new Chart(ctx, barChartOptions);
            },
          });
        </script>
      </body>
    </html>
    



4. ref속성

  • 특정 DOM 엘리먼트나 하위 컴포넌트를 가리키기 위해 사용
  • DOM 엘리먼트에 사용하는 경우 DOM 정보를 접근
  • 하위 컴포넌트에 지정하는 경우 컴포넌트 인스턴스 정보 접근
  • v-for 디렉티브에 사용하는 경우 Array 형태로 정보 제공
  • 특정 DOM 요소를 조작하고 싶을 때 사용하는 속성


ref 속성을 사용할 때 주의할 점
  • ref속성은 템플릿 코드를 render 함수로 변환하고 나서 생성

  • 접근할 수 있는 최초의 시점은 mounted 라이프사이클 훅

  • <p ref="pTag">Hello</p>
      
    created: function() {
    	this.$refs.pTag; //undefined
    },
    mounted: function() {
    	this.$refs.pTag; //<p>Hello</p>
    }
      
    


ref 속성 사용할 때 주의할 점 2
  • v-if 디렉티브와 사용하는 경우 화면에 해당 영역이 그려지기 전까진 DOM 요소 접근 불가

  • <div v-if="isUser">
    	<p ref="w3c">W3C</p>
    </div>
    new Vue({
    	data: { isUser: false },
    	mounted: function() {
    		this.$refs.w3c //undefined
    	}
    })
    
  • <div v-if="isUser">
    	<p ref="w3c">W3C</p>
    </div>
    new Vue({
    	data: { isUser: true },
    	mounted: function() {
    		this.$refs.w3c //<p>W3C</p>
    	}
    })
    


ref 속성 사용할 때 주의할 점 3

하위 컴포넌트의 내용을 접근할 순 있지만 남용하면 안된다.

  • <div id="app">
    	<TodoList ref="list"></TodoList>
    </div>
      
    new Vue({
    	el: '#app',
    	methods: { //상위 컴포넌트에서 불필요하게 하위 컴포넌트를 제어하는 코드
    		fetchItems: function() { this.$refs.list.fetchTodos(); }
    	}
    })
    
  • 하위 컴포넌트의 라이프 사이클 훅을 이용

    • <div id="app">
      	<TodoList></TodoList>
      </div>
          
      var TodoList = {
      	methods: {
      		fetchTodos: function() { .. }
      	},
      	created: function() { this.fetchTodos(); }
      }
      



5. computed 속성

computed 속성이란?
  • 간결하고 직관적인 템플릿 표현식을 위해 뷰에서 제공하는 속성

  • <p>hello</p> <!-- 템플릿 표현식만 이용하는 경우 -->
    <p></p> <!--computed 속성을 활용하는 경우 -->
      
    new Vue({
    	data: { str: 'world' },
    	computed: {
    		greetingStr: function() {
    			return 'hello' + this.str + '!!'
    		}
    	}
    })
    


computed 속성 활용처 1

조건에 따라 HTML 클래스를 추가, 변경할 때

  • <li v-bind:class="{ disabled: isLastPage }"></li>
      
    computed: {
    	isLastPage: function() {
            var lastPageCondition = this.paginationInfo.current_page >= this.paginationInfo.last_page;
            var nothingFetched = Object.keys(this.paginationInfo).length === 0;
            return lastPageCondition || nothingFethced
    	}
    }
    
    • 클래스 속성이 많아지면 복잡해보인다.
    • 그렇기 때문에 computed를 통해 if로 조건식을 분기하면 깔끔해진다.
  • 깔끔한 표현은?

    • <li v-bind:class="listItemClass"></li>
          
      computed: {
      	listItemClass: function() {
      	}
      }
      


computed 속성 활용처 2

스토어(Vuex)의 state 값을 접근할 떄

  • <div>
    	<p></p>
    	<p></p>
    </div>
      
    computed: {
    	module1Str: function() {
    		return this.$store.state.module1.str
    	}
    }
    


computed 속성 활용처 3

Vue i18n과 같은 다국어 라이브러리에도 활용 가능

  • <div>
    	<p>userPage.common.filter.input.label</p>
    	<p></p>
    </div>
      
    computed: {
    	inputLabel: function() {
    		return $t('userPage.common.filter.input.label')
    	}
    }
    

Tags:

Categories:

Updated:

Leave a comment