14 Jul 2019
|
Javascript
Vue
본 강의는 Inflearn의 김정환 개발자 님의 강의(실습 UI 개발로 배워보는 순수 javascript 와 VueJS 개발)를 듣고 배운 내용을 정리한 포스팅 입니다.
단일 파일 컴포넌트(Single File Component) 구현
지금까지 컴포넌트를 통해 작성한 코드를 살펴보면 index.html + component.js로 구성되어 있는 것을 알 수 있음. Vue에서는 컴포넌트를 .vue라는 확장자를 이용하여 하나의 단일컴포넌트로 구현할 수 있도록 기능을 지원하고 있음.
예를 들어, TabComponent.vue라는 파일만 보면 index.html과 TabComponent.js 파일 둘다를 볼 필요 없게됨. 가독성이 훨씬 더 증가하며, 유지보수에도 편리하다는 장점이 생김.
1) SFC 설치
npm install vue-cli global #Vue Cli 전역으로 설치
vue list #template 확인하기
vue init webpack-simple #webpack-simple 설치
npm install #개발 서버 가동을 위해 npm install
npm run dev #개발 서버 가동
2) SFC 기본 구성 & 시작하기
- src 폴더의 main.js가 바로 진입점. 여기서부터 시작함
import Vue from 'vue'
import App from './App.vue'
new Vue({
el: '#app',
render: h => h(App)
})
- 메인 어플리케이션은 App.vue에서 만듦. template & script & style 이라는 3부분으로 구성됨.
<template>
</template>
<script>
export default {
name: 'app',
data () {
return {
}
}
}
</script>
<style>
</style>
- base로 사용하였던 style.css를 root 디렉토리에 붙여넣은 후, index.html과 연결시킴.
- viewport 설정을 head 태그 내 진행
- src 폴더 내 models 폴더 복사 & 저장
<!DOCTYPE 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>base_sfc</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app"></div>
<script src="/dist/build.js"></script>
</body>
</html>
- header 출력하기; App.vue에 코드 삽입
<template>
<div>
<header>
<h2 class="container">검색</h2>
</header>
</div>
</template>
- src 폴더 내 components 폴더 생성
- FormComponent.vue 생성 후 기본 구조에 따라 코드 작성 후, Component 에서 사용했던 template & script 코드를 불러옴
- 어느 template에 사용할 것이냐에 대해 명시하는 부분이었던 template 속성은 삭제 (바로 위에 있는 template에 사용하므로 쓸 필요가 없음)
<template>
<form v-on:submit.prevent="onSubmit">
<input type="text" v-model="inputValue" v-on:keyup="onKeyup"
placeholder="검색어를 입력하세요" autofocus>
<button v-show="inputValue.length" v-on:click="onReset"
type="reset" class="btn-reset"></button>
</form>
</template>
<script>
export default {
// template: '#search-form',
props: ['value'],
data() {
return {
inputValue: this.value
}
},
watch: {
value(newVal, oldVal) {
this.inputValue = newVal
}
},
methods: {
onSubmit() {
this.$emit('@submit', this.inputValue.trim())
},
onKeyup() {
if (!this.inputValue.length) this.onReset()
},
onReset() {
this.inputValue = ''
this.$emit('@reset')
},
}
}
</script>
4) App.vue
- 컴포넌트 import & components 속성에 추가
- FormComponent를 출력하는 부분을 header 태그 바로 아래에 작성(이전 프로젝트에서 작성한 코드 그대로 사용)
- components 프로젝트 내 app.js 에서 사용했던 data, created와 methods를 그대로 사용
- Model import
<template>
<div>
<header>
<h2 class="container">검색</h2>
</header>
<div class="container">
<search-form v-bind:value="query" v-on:@submit="onSubmit" v-on:@reset="onReset"></search-form>
</div>
</div>
</template>
<script>
import SearchModel from './models/SearchModel.js'
import KeywordModel from './models/KeywordModel.js'
import HistoryModel from './models/HistoryModel.js'
import FormComponent from './components/FormComponent.vue'
export default {
name: 'app',
data () {
return {
query: '',
submitted: false,
tabs: ['추천 검색어', '최근 검색어'],
selectedTab: '',
keywords: [],
history: [],
searchResult: []
}
},
created() {
this.selectedTab = this.tabs[0]
this.fetchKeyword()
this.fetchHistory()
},
components: {
'search-form': FormComponent
},
methods: {
onSubmit(query) {
this.query = query
this.search()
},
onReset(e) {
this.resetForm()
},
onClickTab(tab) {
this.selectedTab = tab
},
onClickKeyword(keyword) {
this.query = keyword;
this.search()
},
onClickRemoveHistory(keyword) {
HistoryModel.remove(keyword)
this.fetchHistory()
},
fetchKeyword() {
KeywordModel.list().then(data => {
this.keywords = data
})
},
fetchHistory() {
HistoryModel.list().then(data => {
this.history = data
})
},
search() {
SearchModel.list().then(data => {
this.submitted = true
this.searchResult = data
})
HistoryModel.add(this.query)
this.fetchHistory()
},
resetForm() {
this.query = ''
this.submitted = false
this.searchResult = []
}
}
}
</script>
- 같은 방식으로 나머지 component도 단일 파일 컴포넌트로 변경해서 구현 할 수 있음.
5) ResultComponent.vue
<template>
<div v-if="data.length">
<ul>
<li v-for="item in data">
<img v-bind:src="item.image">
</li>
</ul>
</div>
<div v-else>
검색어로 찾을수 없습니다
</div>
</template>
<script>
export default {
props: ['data', 'query'],
}
</script>
6) ListComponent.vue
<template>
<div v-if="data.length">
<ul class="list">
<li v-for="(item, index) in data" v-on:click="onClickList(item.keyword)">
<span v-if="keywordType" class="number"></span>
<span v-if="historyType" class="date"></span>
<button v-if="historyType" class="btn-remove" v-on:click.stop="onRemoveList(item.keyword)"></button>
</li>
</ul>
</div>
<div v-else>
<span v-if="keywordType">추천 검색어가 없습니다</span>
<span v-if="historyType">최근 검색어가 없습니다</span>
</div>
</template>
<script>
export default {
props: ['data', 'type'],
computed: {
keywordType() {
return this.type === 'keywords'
},
historyType() {
return this.type === 'history'
},
},
methods: {
onClickList(keyword) {
this.$emit('@click', keyword)
},
onRemoveList(keyword) {
this.$emit('@remove', keyword)
}
}
}
</script>
7) TabComponent.vue
<template>
<ul class="tabs">
<li v-for="tab in tabs" v-bind:class="{active: tab === selectedTab}" v-on:click="onClickTab(tab)">
</li>
</ul>
</template>
<script>
export default {
props: ['tabs', 'selectedTab'],
methods: {
onClickTab(tab) {
this.$emit('@change', tab)
}
}
}
</script>
8) App.vue
<template>
<div>
<header>
<h2 class="container">검색</h2>
</header>
<div class="container">
<search-form v-bind:value="query" v-on:@submit="onSubmit" v-on:@reset="onReset"></search-form>
</div>
<div class="content">
<div v-if="submitted">
<search-result v-bind:data="searchResult" v-bind:query="query"></search-result>
</div>
<div v-else>
<tabs v-bind:tabs="tabs" v-bind:selected-tab="selectedTab" v-on:@change="onClickTab"></tabs>
<div v-if="selectedTab === tabs[0]">
<list v-bind:data="keywords" type="keywords" v-on:@click="onClickKeyword"></list>
</div>
<div v-else>
<list v-bind:data="history" type="history" v-on:@click="onClickKeyword" v-on:@remove="onClickRemoveHistory">
</list>
</div>
</div>
</div>
</div>
</template>
<script>
import SearchModel from './models/SearchModel.js'
import KeywordModel from './models/KeywordModel.js'
import HistoryModel from './models/HistoryModel.js'
import FormComponent from './components/FormComponent.vue'
import ResultComponent from './components/ResultComponent.vue'
import ListComponent from './components/ListComponent.vue'
import TabComponent from './components/TabComponent.vue'
export default {
name: 'app',
data () {
return {
query: '',
submitted: false,
tabs: ['추천 검색어', '최근 검색어'],
selectedTab: '',
keywords: [],
history: [],
searchResult: []
}
},
components: {
'search-form': FormComponent,
'search-result':ResultComponent,
'list':ListComponent,
'tabs':TabComponent,
},
created() {
this.selectedTab = this.tabs[0]
this.fetchKeyword()
this.fetchHistory()
},
methods: {
onSubmit(query) {
this.query = query
this.search()
},
onReset(e) {
this.resetForm()
},
onClickTab(tab) {
this.selectedTab = tab
},
onClickKeyword(keyword) {
this.query = keyword;
this.search()
},
onClickRemoveHistory(keyword) {
HistoryModel.remove(keyword)
this.fetchHistory()
},
fetchKeyword() {
KeywordModel.list().then(data => {
this.keywords = data
})
},
fetchHistory() {
HistoryModel.list().then(data => {
this.history = data
})
},
search() {
SearchModel.list().then(data => {
this.submitted = true
this.searchResult = data
})
HistoryModel.add(this.query)
this.fetchHistory()
},
resetForm() {
this.query = ''
this.submitted = false
this.searchResult = []
}
}
}
</script>
14 Jul 2019
|
Javascript
Vue
본 강의는 Inflearn의 김정환 개발자 님의 강의(실습 UI 개발로 배워보는 순수 javascript 와 VueJS 개발)를 듣고 배운 내용을 정리한 포스팅 입니다.
Result Component 구현
export default {
template:'#search-result'
}
- index.html내 검색 결과를 나타내는 태그를 잘라내 파일 하단으로 이동 후 template 태그로 묶어줌
<div class="content">
<div v-if="submitted">
<!-- 코드 이동 -->
</div>
<template id="search-result">
<div v-if="searchResult.length">
<ul>
<li v-for="item in searchResult">
<img v-bind:src="item.image">
</li>
</ul>
</div>
<div v-else>
검색어로 찾을수 없습니다
</div>
</template>
- app.js 내 컴포넌트 import 후 components에 추가
import ResultComponent from './components/ResultComponent.js'
components: {
'search-result' : ResultComponent
},
- index.html에 컴포넌트를 기존의 위치에 입력
- Vue 인스턴스가 갖고 있는 검색 결과 값을 v-bind를 이용하여 ResultComponent에 넘겨 줄 것.
- ResultComponent에서 props로 data를 받음
- 결과값을 뿌려주는 부분에 대한 변수명을 변경(
search.length
-> data.length
)
<div class="content">
<div v-if="submitted">
<search-result v-bind:data="searchResult"></search-result>
</div>
export default {
template:'#search-result',
props: ['data'],
}
<template id="search-result">
<div v-if="data.length">
<ul>
<li v-for="item in data">
<img v-bind:src="item.image">
</li>
</ul>
</div>
<div v-else>
검색어로 찾을수 없습니다
</div>
</template>
- 검색어(query) 또한 ResultComponent로 넘겨줌
<div class="content">
<div v-if="submitted">
<search-result v-bind:data="searchResult" v-bind:query="query"></search-result>
</div>
export default {
template:'#search-result',
props: ['data', 'query'],
}
List Component 구현
추천 검색어와 최근검색어는 형식이 유사함. 이들을 출력할 수 있는 ListComponent를 만들어 공유해서 쓰도록 하자.
1) 기본 코드 작성
- index.html를 보면 분기문에 따라 추천 검색어 또는 최근 검색어 코드가 작성되어있음. 둘중 하나를 써서 공유할 에정이므로, 하나만 잘라내어 파일 하단에 template 태그 안으로 이동
- v-if & v-for의 변수는, 추천/최근 검색어에 공통으로 쓰므로, 변수명을 수정
keywords.length
-> data.length
(item, index) in keywords
-> (item, index) in data
onClickKeyword
-> onClickList
- 최근 검색어 목록에만 존재하는 날짜와 삭제 버튼에 대한 내용도 추가로 갖고온 후, 변수명 또한 수정
onClickRemoveHistory
-> onRemoveList
// ListComponent
export default {
template: '#List'
}
<!-- index.html -->
<div v-if="selectedTab === tabs[0]">
<!-- 코드 이동 -->
</div>
<template id="List">
<div v-if="data.length">
<ul class="list">
<li v-for="(item, index) in data" v-on:click="onClickList(item.keyword)">
<span class="number"></span>
<span class="date"></span>
<button class="btn-remove"
v-on:click.stop="onRemoveList(item.keyword)"></button>
</li>
</ul>
</div>
<div v-else>
추천 검색어가 없습니다
</div>
</template>
- app.js 내 컴포넌트 import 및 등록
import ListComponent from './components/ListComponent.js'
components: {
'search-form': FormComponent,
'search-result' : ResultComponent,
'list' : ListComponent,
},
- index.html 내 추천 검색어 & 최근 검색어를 출력하는 부분을 수정
- list 디렉티브 사용 & v-bind를 이용하여 Vue 인스턴스가 갖고 있는 값을 ListComponent로 넘김
<div v-if="selectedTab === tabs[0]">
<list v-bind:data="keywords"></list>
</div>
<div v-else>
<list v-bind:data="history"></list>
</div>
- Vue 인스턴스로부터 받아온 값을 props에 저장 & methods 내 기본 함수 틀 생성
export default {
template: '#List',
props: ['data'],
methods: {
onClickList(keyword) {
},
onRemoveList(keyword) {
}
}
}
2) 결과 화면 다듬기
- 추천 검색어 리스트에는 불필요한 기능(날짜, x버튼)이 나타나고 있음. 리스트가 추천 검색어인지 최근 검색어인지 식별할 수 있는 방법이 필요함. =>
type
데이터 사용 & props
에 추가
<!-- index.html -->
<div v-if="selectedTab === tabs[0]">
<list v-bind:data="keywords" type="keywords"></list>
</div>
<div v-else>
<list v-bind:data="history" type="history"></list>
</div>
// ListComponent.js
export default {
template: '#List',
props: ['data', 'type'],
}
- type의 값에 따라 인덱스 & 날짜 & x버튼을 출력하고 감추게 분기문 작성
<!-- index.html -->
<li v-for="(item, index) in data" v-on:click="onClickList(item.keyword)">
<span v-if="type === 'keywords'" class="number"></span>
<span v-if="type === 'history'" class="date"></span>
<button v-if="type === 'history'"class="btn-remove"
v-on:click.stop="onClickRemoveList(item.keyword)"></button>
</li>
3) 추천 검색어 클릭 시, 검색 기능 동작 구현
- ListComponent.js 에서 클릭 이벤트가 발생했을 때, 이벤트를 재정의 하여 app.js로 넘겨주면 됨.
$emit
사용
- 최근 검색어의 경우, 검색어를 삭제하는 이벤트에 대한 코드를 추가로 입력
methods: {
onClickList(keyword) {
this.$emit('@click', keyword)
},
onRemoveList(keyword) {
this.$emit('@remove', keyword)
}
}
<div v-if="selectedTab === tabs[0]">
<list v-bind:data="keywords" type="keywords" v-on:@click="onClickKeyword"></list>
</div>
<div v-else>
<list v-bind:data="history" type="history" v-on:@click="onClickKeyword"
v-on:@remove="onClickRemoveHistory"></list>
</div>
4) Computed
- type값이 keyword 인지, 혹은 history인지에 따라서 출력하는 부분이 달라짐.
- template 태그 안에 조건문 같은 코드가 있을 경우, 코드의 가독성이 떨어짐. 이때 Vue에서 제공하는 computed를 사용.
<span v-if="type === 'keywords'" class="number"></span>
<span v-if="type === 'history'" class="date"></span>
- ListComponent.js 내 computed 속성 추가
- type이 keywords 인지 history 인지를 반환하는 함수 작성
computed: {
keywordType() {
return this.type === 'keywords'
},
historyType() {
return this.type === 'history'
},
},
<div v-if="data.length">
<ul class="list">
<li v-for="(item, index) in data"
v-on:click="onClickList(item.keyword)">
<span v-if="keywordType" class="number"></span>
<span v-if="historyType" class="date"></span>
<button v-if="historyType"class="btn-remove"
v-on:click.stop="onRemoveList(item.keyword)"></button>
</li>
</ul>
</div>
<div v-else>
<span v-if="keywordType">추천 검색어가 없습니다</span>
<span v-if="historyType">최근 검색어가 없습니다</span>
</div>
5) watch - 추천 검색어를 검색폼에 띄우기
추천 검색어를 클릭하여 결과가 뜬 후, devtools를 통해 디버깅을 해보면, 아래와 같은 결과를 얻을 수 있다. 검색폼에 클릭한 추천 검색어가 뜨지 않는 이유는 searchForm 내 inputValue
값이 저장되어 있지 않았기 때문임.
- <Root>
- <SearchForm>
- props - value: “이탈리아”
- data - inputValue: “”
- FormComponent 내 watch 속성 추가
- watch:어떤 View Model을 감시하고 있다가, 그 값이 변경되면, 행동을 수행하는 함수
- watch 함수에 props 내 value를 호출 함.
- 이전 값과 새로운 값을 인자로 받아와, inputValue 에 새로운 값을 저장시킴.
export default {
template: '#search-form',
props: ['value'],
data() {
return {
inputValue: this.value
}
},
watch: {
value(newVal, oldVal) {
this.inputValue = newVal
}
},