5 할 일 관리 앱 - 완료 기능 구현

source: categories/study/vue-beginner-lv5/vue-beginner-lv5_5.md

5.1 할 일 완료 기능 마크업 및 스타일링

5.1.1 vue에 scss 기능 추가하기


yarn add -D sass sass-loader@10

sass-loader 10 버전을 설치하는 이유는 vue 2.x에선 11 이상의 sass-loader가 호환되지 않아서이다.
위와 같이 설치하면 자동으로 vue에서 scss를 인식한다.
webpack.config.js에 이미 다 설정되어있기 때문.

Note

git diff

5.2 할 일 목록의 데이터 타입 정의 및 할 일 추가 기능에 적용

  • 현재 todoItems 각 요소는 String
    • 이 요소들을 Object로.. name, status, id 이렇게 3가지 값을 가지고 있는 객체 형태로..
Note

git diff


vue.runtime.esm.js?2b0e:619 [Vue warn]: Invalid prop: type check failed for prop "todoItem". Expected String, got Object 

found in

---> <TodoListItem> at src/components/TodoListItem.vue
       <App> at src/App.vue
         <Root>

위 에러 해결은 다음 시간에..

5.3 props 타입 정의 방법

Note

git diff

5.4 할 일 완료 기능 구현

Note

git diff

5.5 할 일 완료 클래스 바인딩 및 computed 타입 정의 방식 안내

FE CONF 2019

5.5.1 목차

  1. 반응성 - 왜 내 화면은 다시 그려지지 않는 걸까?
  2. DOM 조작 - 오래된 습관 버리기
  3. 라이프사이클 - 나는 인스턴스를 얼마나 알고 있나?
  4. ref 속성 - 만들다가만 ref 속성
  5. computed 속성 - 간결한 템플릿의 완성

5.5.2 발표 대상

  • Vue.js의 기본 문법을 알고 있는 웹 개발자
  • Vue.js로 막 서비스 개발을 시작한 주니어 개발자
  • Vue.js로 서비스제작중인 웹 개발자
  • Vue.js의 특징궁금한 프론트엔드 개발자

"Vue.js를 소개하는 발표가 아닙니다"

5.5.3 기대 효과

PR 보냈을 때 사수(시니어)한테 칭찬 받기

5.5.4 발표 자료 및 예제 소스는 아래 링크에서 확인

https://github.com/joshua1988/vue-five-common-mistakes

5.5.5 반응성

  1. 반응성
    • 왜 내 화면은 다시 그려지지 않는 걸까?
5.5.5.1 뷰의 반응성이란
  • 데이터의 변화에 따라 화면이 다시 그려지는 뷰의 성질


var vm = new Vue({
  data: {
    count: 0,
  }
})
vm.count += 1; // count 값이 증가하면 화면에 표시된 count도 증가


5.5.5.2 반응성은 언제 설정될까?
  • 인스턴스가 생성될 때 data의 속성들을 초기화 : 뷰 라이브러리 동작
    1. new Vue() : 인스턴스 생성
    2. Init Events & LifeCycle
    3. beforeCreate
    4. Init injections & reactivity
    5. created - 여기부터 반응성이 주입된 데이터 속성들을 다루실 수 있음
5.5.5.3 반응성에 대해 알아야 할 점
  • 생성하는 시점에 없었던 data는 반응하지 않는다.


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


5.5.5.4 반응성을 이해하지 못했을 때의 실수 1
  • 화면에서만 필요한 UI 상태 값을 다룰 때
    • 체크박스
    • 라디오 버튼
  • 예시 코드


export default {
  data() {
    return {
      users: [],
    }
  },
  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));
    },
  }
}


  • REST로 받아온 유저 목록 10개
  • 유저 목록 중 첫번째 유저를 체크하는 함수를 만들고 그 버튼을 만듦
  • 버튼을 클릭하면 유저 목록 중 첫번째 유저를 체크 (checked = true)
  • 분명 자바스크립트 상으론 checkedtrue로 되었지만 화면에선 갱신이 안됨
  • 그 반대로 checked = false도 화면 갱신이 안됨
  • vue developer 확장 프로그램으로 봐도 해당 datatrue, false 잘 바뀌지만 화면엔 갱신이 안됨

  • 이런 이슈 해결 방법
    • this.$set(this.users[0], 'checked', false)
    • root 수준의 data는 this.$set()으로 해도 안되지만, root 수준의 data 안에 이렇게 정의를 하면 반응성이 적용된다.
  • 위와 같이 수정하면 checked 값 변화에 따라 화면이 잘 갱신된다.
Note

이런 UI 상태 값은 보통 DB에서 관리 안하고 화면에서 관리해줘야하는 값들이라 이런 실수를 종종 내곤한다.

5.5.5.5 반응성을 이해하지 못했을 때의 실수 2
  • 백엔드에서 불러온 데이터에 임의 값을 추가하여 사용하는 경우


const user = {
  address: {

  },
  company: {

  },
  email: 'Sincere@april.biz',
  id: 1,
  name: 'Leanne Graham',
  phone: '1-770-736-8031 x56442',
  username: 'Bret',
  website: 'hildegard.org',
}


  • 위 코드에 region이라는 새로운 프로퍼티를 추가해서 화면과 연동
    • region이란 값은 자바스크립트 상에서 잘 바뀌나 화면에 갱신되지 않음
    • 해당 프로퍼티들에 reactivity가 주입되었는지 아닌지 확인하는 방법
      • reactivity가 주입되어 있다면 get, set이 설정되어 있음
      • reactivity가 주임 안된 regionget, set이 설정되어 있지 않다.
  • 이슈 해결 방법
    • 아까와 같이 this.$set() 함수 이용
    • 또는 팩토리 펑션 비슷하게 속성을 반환해주는 함수를 만들어놓고 호출하거나
    • 아니면 data 속성 안에 일일이 지정해놓고 사용하거나
5.5.5.6 [참고] 뷰엑스의 state도 data와 동일하게 취급


state: {
  user: { name: 'Captain' }
},
mutations: {
  // 생성하는 시점에 없었던 데이터는 반응성이 없음
  setUserAge: function(state) { state.user.age = 23; }, // x
  // 객체 속성을 임의로 추가 또는 삭제하는 경우 뷰에서 감지하지 못함
  deleteName: function(state) { delete state.user.name; } // x
}


5.5.5.7 뷰 3.0에서는 괜찮아요
  • Object.defineProperty()에서 Proxy 기반으로 변화


var obj = {};

// Vue 2.x
Object.defineProperty(obj, 'str', { ... });

// Vue 3.x
new Proxy(obj, { ... });


Vue framework reactivity의 한계가 사실 프레임워크 상의 한계가 아니라..
사실 Object에도 Object.observe() 라고 하는 API가 있는데.. 이게 deprecated 되면서 defineProperty밖에 할 수가 없었다. 반응성 주입할 때.

하지만 Vue 3.x에서는 Proxy 베이스로 가기 때문에 데이터에 어떤 속성이 동적으로 추가/삭제되는 거에 대해 다 감지할 수 있게 된다.

5.5.6 DOM 조작

  • 오래된 습관 버리기

  • (기존) 화면 조작을 위한 DOM 요소 제어 방법

    • 특정 DOM검색해서 제어하는 방법


// 네이티브 자바스크립트
document.querySelector('#app');

// 제이쿼리 라이브러리
$('#app');


  • 사용자의 입력 이벤트기반으로 한 DOM 요소 제어


// 버튼 요소 검색
var btn = document.querySelector('#btn');

// 사용자의 클릭 이벤트를 기반으로 가장 가까운 태그 요소를 찾아 제거
btn.addEventListener('click', function(event) {
  event.target.closest('.tag1').remove();
})


  • 이런식의 사고방식이 잘못되었다기 보단
  • 이런거에 너무 익숙해져서 Vue를 100% 활용을 못한다는 느낌
  • 이런거를 Vue에선 어떻게 제어할 수 있을까?
5.5.6.1 (Vue.js 방식) ref 속성을 활용한 DOM 요소 제어
  • 뷰에서 제공하는 ref 속성


<!-- HTML 태그에 ref 속성 추가 -->
<div ref="hello">Hello Ref</div>

<script>
// 인스턴스에서 접근 가능한 ref 속성
this.$refs.hello; // div 엘리먼트 정보
</script>


  • 이런 ref 속성은 뷰 뿐만 아니라 리액트, 앵귤러에서도 많이 사용함

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

    • 뷰 디렉티브에서 제공하는 속성들을 충분히 활용하면 대부분의 경우 커버 가능


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


5.5.6.2 DOM 제어 사고 전환이 필요한 실제 사례


<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>
  <!-- ... -->
</ul>

<script>
export default {
  methods: {
    removeItem(event) {
      event.target.lastChild.classList.toggle('hide');
    }
  }
}
</script>


  • 위 코드 수정


<ul>
  <li v-for="item in items" @click="removeItem">
    <span>{{ item }}</span>
    <div class="child hide">메뉴 설명</div>
  </li>
</ul>

<script>
export default {
  data() {
    return {
      items: ['메뉴 1', '메뉴 2', '메뉴 3'],
    }
  },
  methods: {
    removeItem(event) {
      event.target.lastChild.classList.toggle('hide');
    }
  }
}
</script>


  • ref 속성 적용


<ul>
  <li v-for="(item, index) in items" @click="removeItem(index)">
    <span>{{ item }}</span>
    <div class="child hide" ref="listItem">메뉴 설명</div>
  </li>
</ul>

<script>
export default {
  data() {
    return {
      items: ['메뉴 1', '메뉴 2', '메뉴 3'],
    }
  },
  methods: {
    removeItem(index) {
      this.$refs.listItem[index].classList.toggle('hide');
    }
  }
}
</script>


5.5.7 인스턴스 라이프 사이클

  • 나는 인스턴스를 얼마나 알고 있나?
5.5.7.1 인스턴스 라이프 사이클이란?
  • 뷰 인스턴스가 생성되고 소멸되기까지의 생애 주기

  1. new Vue(): 인스턴스 생성
  2. 이벤트 및 라이프사이클 초기화
  3. beforeCreate
  4. 화면에 반응성 주입
  5. created
  6. el, template 속성 확인
  7. tempalte 속성 내용을 render로 변환
  8. beforeMount
  9. vm.$el 생성 후 el 속성 값을 대입
  10. mounted
  11. 인스턴스를 화면에 부착
  12. 인스턴스의 데이터 변경
  13. beforeUpdate
  14. 화면 재 렌더링 및 데이터 갱신
  15. updated
  16. 인스턴스 내용 갱신
  17. 인스턴스 접근 가능
  18. beforeDestroy
  19. 컴포넌트, 인스턴스, 디렉트브 등 모두 해제
  20. destroyed
  21. 인스턴스 소멸
5.5.7.2 뷰의 template 속성
  • 뷰의 템플릿 속성에 대해 이해하고 있으면 좋다.
    • 왜냐하면 템플릿 속성이랑 뷰 인스턴스 라이프 사이클이랑 밀접한 관계가 있기 때문
  • 템플릿 속성이란 인스턴스, 컴포넌트의 표현부를 정의하는 속성


<-- 싱글 파일 컴포넌트 -->
<template>
  <div>{{ str }}</div>
</template>

--- 위 방식으로 정의하든 아래 방식으로 정의하든 같은 template 속성이다.
--- 정의하는 방법만 다를 뿐 같은 template 속성이다.

<script>
// 인스턴스 옵션 속성
new Vue({
  data: { str: 'Hello World' },
  template: '<div>{{ str }}</div>,
});
</script>


여튼 template 속성은 어떤 역할을 하는가?

5.5.7.3 뷰 template 속성의 정체
  • 실제 DOM 엘리먼트가 아니라 Virtual DOM(자바스크립트 객체)


<!-- 사용자가 작성한 코드 -->
<template>
  <div>{{ str }}</div>
</template>


위와 같이 자성된 코드에서 template 태그는 실제 DOM 엘리먼트가 아니다.
위는 뷰 라이브러리에서 아래처럼 변환이 된다.



// 라이브러리 내부적으로 변환한 모습
function render() {
  with(this) {
    return _c('div', [_v(_s(str))]);
  }
}


  • 구글에 vue template explorer라고 검색하시면 아래와 같이 template 태그가 어떻게 변환되는지를 볼 수 있다.
  • vue template explorer

5.5.7.4 template 속성이 실제로 유효한 시점
  • template 속성이 실제로 유효한 시점에 대해서도 제대로 이해하고 계셔야합니다.
    • 인스턴스가 화면에 부착(mounted)되고 난 후
    • mounted가 되고 난 후 template에 작성한 태그(요소)에 접근 가능

  1. beforeMount
  2. Create vm.$el and replace el with it
  3. mounted - 여기부터 작성한 태그(요소)에 접근 가능 …
5.5.7.5 인스턴스 부착 시점을 이해하지 못한 사례


<!-- 템플릿 속성 -->
<template>
  <canvas id="myChart"></canvas>
</template>

<script>
// 인스턴스 옵션
new Vue({
  created: function() {
    var ctx = document.querySelector('#myChart'); // null
    new Chart(ctx, chartOptions);
  }
})
</script>


  • 위과 같이 적용 후 안되다 보니..
  • nextTick이란 걸 검색해서 발견 후 적용하게됨
  • mounted에서 확인할 수 있는 것은 mounted에서.. nextTick은 최후의 수단


<!-- 템플릿 속성 -->
<template>
  <canvas id="myChart"></canvas>
</template>

<script>
// 인스턴스 옵션
new Vue({
  created: function() {
    this.$nextTick(function() { // 업데이트 시점 혼란 야기 및 코드 복잡도 증가
      var ctx = document.querySelector('#myChart'); 
      new Chart(ctx, chartOptions);
    })
  }
})
</script>


  • 적절한 수정 방법


<!-- 템플릿 속성 -->
<template>
  <canvas id="myChart"></canvas>
</template>

<script>
// 인스턴스 옵션
new Vue({
  mounted: function() {
    var ctx = document.querySelector('#myChart'); 
    new Chart(ctx, chartOptions);
  }
})
</script>


5.5.8 ref 속성

  • 만들다가 만 ref 속성
    • 규칙이 조금 존재함
    • 알고있으면 좋을만한 규칙들
5.5.8.1 ref 속성이란?
  • 특정 DOM 엘리먼트나 하위 컴포넌트를 가리키기 위해 사용
  • DOM 엘리먼트에 사용하는 경우 DOM 정보를 접근
  • 하위 컴포넌트(커스텀 컴포넌트)에 지정하는 경우 컴포넌트 인스턴스 정보 접근
  • v-for 디렉티브에 사용하는 경우 Array 형태로 정보 제공
Note

특정 DOM 요소를 조작하고 싶을 때 사용하는 속성

5.5.8.2 ref 속성 사용할 때 주의할 점 1
  • ref 속성은 template 코드render 함수로 변환하고 나서 생성
  • 접근할 수 있는 최초의 시점은 mounted 라이프사이클 훅


<p ref="pTag">Hello</p>

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


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


<div v-if="isUser">
  <p ref="w3c">W3C</p>
</div>

<script>
new Vue({
  data: { isUser: false, },
  mounted: function() {
    this.$refs.wrc; // undefined
  }
})
</script>


5.5.8.4 ref 속성 사용할 때 주의할 점 3
  • 하위 컴포넌트의 내용을 접근할 순 있지만 남용하면 안된다
  • 아래 방법은 지양하는 것이 좋은게
    • 컴포넌트간 데이터 통신 방식 props를 내리고 event emit으로 통신
    • 아래 처럼 접근하게되면 나중에 디버깅할 때 어려워짐


<div id="app">
  <TodoList ref="list"></TodoList>
</div>

<script>
new Vue({
  el: '#app',
  methods: {
    // 상위 컴포넌트에서 불필요하게 하위 컴포넌트를 제어하는 코드
    fetchItems: function () {
      this.$refs.list.fetchTodos();
    }
  }
})
</script>


  • 특히 위와 같은 코드는 하위 컴포넌트인 TodoList의 라이프사이클 훅을 활용해 해결 가능함


// 하위 컴포넌트인 TodoList에서 라이프 사이클 훅을 활용해 해결 가능
var TodoList = {
  created: function() {
    this.fetchTodos();
  },
  methods: {
    fetchTodos: function() { ... }
  },
}


Note

이렇게 커스텀 컴포넌트에 ref 속성을 적용하는 경우는
써드 파티 라이브러리를 사용하실 때, 그 라이브러리에서 생성된 컴포넌트에 접근하실 때 사용하시는 것이 더 좋음.

5.5.9 computed 속성

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


<p>{{ 'hello' + str + '!!' }}</p><!-- 템플릿 표현식만 이용하는 경우 -->
<p>{{ greetingStr }}</p><!-- computed 속성을 활용하는 경우 -->

<script>
new Vue({
  data: { str: 'world', },
  computed: {
    greetingStr: function () {
      return 'hello' + this.str + '!!';
    }
  }
})
</script>


5.5.9.2 computed 속성 활용처 1
  • 조건에 따라 HTML 클래스추가, 변경할 때


<li :class="{ disalbed: isLastPage }"></li>

<script>
computed: {
  isLastPage: function () {
    var lastPageCondition = this.paginationInfo.current_page >= this.paginationInfo.last_page;
    var nothingFetched = Object.keys(this.paginationInfo).length === 0;
    return lastPageCondition || nothingFetched;
  }
}
</script>


  • computed return 값을 아예 Object로..


<li :class="listItemClass"></li>

<script>
computed: {
  listItemClass: function () {
    // ...
  }
}
</script>


5.5.9.3 computed 속성 활용처 2
  • 스토어(Vuex)의 state 값을 접근할 때


<div>
  <p>{{ this.$store.state.module1.str }}</p>
  <p>{{ module1Str }}</p>
</div>

<script>
new Vue({
  computed: {
    moduleStr: function () {
      return this.$store.state.module1.str;
    }
  }
})
</script>


5.5.9.4 computed 속성 활용처 3
  • Vue i18n과 같은 다국어 라이브러리에도 활용 가능


<p>{{ 'userPage.common.filter.input.label' }}</p>
<p>{{ inputLabel }}</p>

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


  • 특정 컴포넌트에 computed가 많아질 경우, 중간에 레이어 하나(컴포넌트)를 더 줘서 props를 내려서 관리하는..
  • 그런 식으로도 코드를 줄일 수 있을 것이다.

다시 돌아와서…

이제 브라우저 콘솔창을 안봐도 해당 데이터의 타입을 알 수 있다.
위와 같이 에디터에서 바로바로 어떤 속성 및 데이터 타입은 어떤지 바로 알려준다.
위와 같은 기능들이 타입스크립트를 썼기 때문에 에디터에서 제공하는 인텔리 센스라는 기능이다.

Note

git diff