단일 파일 컴포넌트
전역 수준 컴포넌트의 문제점
- 빌드 단계가 없으므로 ECMAScript2015, TypeScript와 같은 최신 자바스크립트 문법을 사용할수 없음. 따라서 HTML 내부에 직접 ECMAScript2015 이전 버전의 자바스크립트 코드를 작성해야 함
- CSS를 지원하지 않음. 컴포넌트들은 고유한 스타일 정보를 포함하는 경우가 많은데, 전역컴포넌트에서는 CSS스타일을 빌드하고 모듈화 할 수 있는 기능을 제공하지 않음
- 컴포넌트의 템플릿이 작성될 때 HTML 파일안에 여러 개의 <templete/> 태그가 작성되어 식별하기 어려움. 또한 템플릿마다 고유한 id를 부여하고 컴포넌트들도 고유한 이름을 지정해야 함
vue cli를 이용하여 프로젝트 생성
- vue-loader : <template>, <script>, <style>이 작성된 .vue파일을 파싱하고 다른 로더들을 활용해 하나의 모듈로 조합
- css-loader : CSS 스타일 전처리, 모듈화
- vue create [프로젝트명] 으로 프로젝트 생성 가능
C:\JetBrains\vscode_workspace>vue create totolistapp
Vue CLI v4.5.9
? Please pick a preset: (Use arrow keys)
> Default ([Vue 2] babel, eslint)
Default (Vue 3 Preview) ([Vue 3] babel, eslint)
Manually select features
Vue CLI v4.5.9
✨ Creating project in C:\JetBrains\vscode_workspace\totolistapp.
🗃 Initializing git repository...
⚙️ Installing CLI plugins. This might take a while...
yarn install v1.22.10
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.1.3: The platform "win32" is incompatible with this module.
info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.13: The platform "win32" is incompatible with this module.
info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
success Saved lockfile.
Done in 35.00s.
🚀 Invoking generators...
📦 Installing additional dependencies...
yarn install v1.22.10
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.1.3: The platform "win32" is incompatible with this module.
info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.13: The platform "win32" is incompatible with this module.
info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 13.52s.
⚓ Running completion hooks...
📄 Generating README.md...
🎉 Successfully created project totolistapp.
👉 Get started with the following commands:
$ cd totolistapp
$ yarn serve
App.vue 의 구조
- <templete> 에는 id를 부여하지 않음
- <script> 영역에서는 Vue 컴포넌트의 template를 지정하지 않음
- Vue.component()로 이름과 template 속성을 지정하지 않음
- name 속성을 지정할 수 있음
- 반드시 객체를 export 해야함
- 컴포넌트에서 사용할 스타일은 <style> 내부에 작성
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
App.vue 를 화면에 담기 위한 main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
main.js, 단일컴포넌트들을 빌드, 참조하여 보여줄 페이지 public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>
We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly
without JavaScript enabled.
Please enable it to continue.
</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
Vue-CLI를 이용한 todolist
src/EventBus.js
import Vue from 'vue';
var eventBus = new Vue();
export default eventBus;
src/components/InputTodo.vue
<style>
* {
box-sizing: border-box;
}
.input {
border: none; width: 75%; height: 35px; padding: 10px;
float: left; font-size: 16px;
}
.addbutton {
padding: 10px; width: 25%; height: 35px; background: #d9d9d9;
color: #555; float: left; text-align: center;
font-size: 13px; cursor: pointer; transition: .3s;
}
.addbutton:hover {
background-color: #bbb;
}
</style>
<template>
<div>
<input type="text" class="input" id="task" placeholder="입력 후 엔터!"
v-model.trim="todo" @keyup.enter="addTodo">
<span class="addbutton" @click="addTodo">추 가</span>
</div>
</template>
<script type="text/javascript">
import eventBus from '../EventBus';
export default {
name : 'input-todo',
data : function(){
return { todo: ""}
},
methods : {
addTodo : function() {
eventBus.$emit('add-todo',this.todo);
this.todo = "";
}
}
}
</script>
src/components/List.vue
<style>
* {box-sizing: border-box;}
ul { margin: 0; padding: 0;}
ul li {
cursor: pointer; position: relative; padding: 8px 8px 8px 40px;
background: #eee; font-size: 14px; transition: .2s;
-webkit-user-select : none;-moz-user-select : none;
-ms-user-select : none;user-select : none;
}
ul li:hover { background: #ddd;}
ul li.checked::before {
content : '';
position: absolute;
border-color: #fff;
border-style: solid;
border-width : 0px 1px 1px 0px;
top: 10px;
left: 16px;
transform: rotate(45deg);
height: 8px;
width: 8px;
}
.close {position: absolute; right:0; top:0; padding: 8px 12px;}
.close:hover { background-color: #f44336; color: #fff;}
</style>
<template>
<ul id="todolist">
<li v-for="a in todolist" :key="a.id"
:class="checked(a.done)" @click="doneToggle(a.id)">
<span>{{ a.todo }}</span>
<span v-if="a.done">(완료)</span>
<span class="close" @click.stop="deleteTodo(a.id)">×</span>
</li>
</ul>
</template>
<script type="text/javascript">
import eventBus from '../EventBus';
export default {
created : function() {
eventBus.$on('add-todo',this.addTodo);
},
data : function(){
return {
todolist : [
{id : 1, todo: "영화보기", done: false},
{id : 2, todo: "주말 산책", done: true},
{id : 3, todo: "ES6 학습", done: false},
{id : 4, todo: "잠실 야구장", done: false},
]
};
},
methods: {
checked : function(done) {
if(done) return {checked: true};
else return {checked: false};
},
addTodo : function(todo) {
if(todo === "") return;
this.todolist.push(
{id: new Date().getTime(), todo: todo, done: false}
);
},
doneToggle : function(id) {
var index = this.todolist.findIndex((item) => item.id === id);
this.todolist[index].done = !this.todolist[index].done;
},
deleteTodo : function(id) {
var index = this.todolist.findIndex((item) => item.id === id);
this.todolist.splice(index,1);
}
}
}
</script>
src/components/TodoList.vue
<style>
* {box-sizing: border-box;}
.header { background-color: purple; padding: 30px; color:yellow; text-align: center;}
.header::after { content: ''; display: table; clear:both;}
</style>
<template>
<div id="todolistapp">
<div class="header" id="header">
<h2>Todo List App</h2>
<input-todo></input-todo>
</div>
<list></list>
</div>
</template>
<script type="text/javascript">
import InputTodo from './InputTodo.vue';
import List from './List.vue';
export default {
name : 'todo-list',
components : {InputTodo, List}
}
</script>
src/main.js
import Vue from 'vue'
import TodoList from './components/TodoList.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(TodoList),
}).$mount('#app')
yarn serve 실행
C:\JetBrains\vscode_workspace\totolistapp>yarn serve
yarn run v1.22.10
$ vue-cli-service serve
INFO Starting development server...
98% after emitting CopyPlugin
DONE Compiled successfully in 393ms
App running at:
- Local: http://localhost:8080/
- Network: http://192.168.0.13:8080/
Note that the development build is not optimized.
To create a production build, run yarn build.
컴포넌트에서의 스타일
범위 CSS(Scoped CSS)
- style 태그에 scoped 속성을 붙이면 전역 CSS 와 범위 CSS를 구분해서 작성할 수 있음
- 범위 CSS는 Attribute Selector 를 사용하기 때문에 브라우저에서 스타일을 적용하는 속도가 느림. 따라서 반드시 ID선택자, 클래스 선택자, 태그명 선택자로 요소를 선택해 스타일을 적용해야 함
<template>
<div class="main">{{msg}}</div>
</template>
<script>
export default {
name :'Child1',
data : function() {
return { msg : 'Child1'}
}
}
</script>
<style scoped>
.main { border: solid 1px black; background-color: yellow;}
</style>
<!--
name : 'Child2'
<style scoped>
.main { border: solid 1px black; background-color: aqua;}
</style>
-->
<!--
결과
name : 'Child1'
<style type="text/css">
.main { border: solid 1px black; background-color: yellow;}
</style>
name : 'Child2'
<style type="text/css">
.main[data-v-3a3c19c6] { border: solid 1px black; background-color: aqua;}
</style>
-->
- 부모 컴포넌트에 적용된 범위 CSS는 하위 컴포넌트에도 반영됨
- 결과에서 data-v-3a2e0245 속성이 상속됨
<!-- src/components/Child11.vue 자식 컴포넌트-->
<template>
<div cass="test">
<h3>Child - Child</h3>
</div>
</template>
<style scoped>
.test { font-style:italic; }
</style>
<!-- src/components/Child1.vue 부모 컴포넌트 -->
<template>
<div class="main test">
{{ msg }}
<child11 />
</div>
</template>
<script type="text/javascript">
import Child11 from './Child11.vue';
export default {
name : 'Child1',
components : { Child1 },
data() {
return {
msg : 'Child1'
}
}
}
</script>
<style scoped>
.main { border: solid 1px black; background-color: yellow;}
.test { padding: 10px; text-decoration: underline; border: solid 1px black;}
</style>
<!--
결과
<div data-v-3a2e0245="" class="main test">
Child1
<div data-v-0b9bd83c="" data-v-3a2e0245="" cass="test">
<h3 data-v-0b9bd83c="">Child - Child</h3>
</div>
</div>
-->
CSS 모듈
- CSS모듈은 CSS 스타일을 마치 객체처럼 다룰 수 있게 함
- <style module> 과 같이 사용하면 됨
- Vue 인스턴스 내에서 $style이라는 계산형 속성에서 이용가능
<template>
<div>
<button :class="$style.hand">CSS Module을 적용한 버튼</button>
</div>
</template>
<script>
export default {
created() {
console.log(this.$style);
}
}
</script>
<style module>
.hand { cursor: pointer; background-color: purple; color: yellow;}
</style>
<!--
.hand 가 충돌되지 않도록 다른이름으로 변경하여 사용
<div>
<button class="Module1_hand_1l2s2">CSS Module을 적용한 버튼</button>
</div>
-->
- 적용해야 할 클래스가 여러개라면 배열문법 이용가능
<div :class="[$style.box, $style.border]"> Hello World</div>
슬롯
슬롯을 이용해 부모 컴포넌트에서 자식 컴포넌트로 HTML 마크업을 전달
슬롯의 기본 사용법
- vue create slottest
src/components/SpeechBox.vue
<!-- -->
<template>
<div class="container">
<div class="header">{{headerText}}</div>
<div class="content">
<slot></slot>
</div>
<div class="footer">{{footerText}}</div>
</div>
</template>
<script>
export default {
props : ['headerText','footerText']
}
</script>
<style scoped>
.container { width: 300px; margin:10px; padding: 2px; border: solid 1px gray; float:left;}
.header {padding: 4px 20px; background-color: orange; color: aqua; text-align: center;}
.footer {padding: 2px 20px; background-color: aqua; text-align: left;}
.content { padding: 10px; height: auto; min-height: 40px; text-align: left;}
</style>
src/App.vue
<template>
<div id="app">
<speech-box :headerText="A.header" :footerText="A.footer">
<div>
<p>{{ A.message }}</p>
</div>
</speech-box>
<speech-box :headerText="B.header" :footerText="B.footer">
<div>
<p class="senders-content">{{ B.message }}</p>
</div>
</speech-box>
</div>
</template>
<script>
import SpeechBox from './components/SpeechBox.vue'
export default {
name: 'App',
components: {
SpeechBox
},
data() {
return {
A: {
header : '오바마 대통령 고별 선언문',
footer : '2017.01.10 - 시카고',
message : '국민 여러분, ......(지면상 생략)'
},
B: {
header : '버니샌더스 경선 패배 연설문',
footer : '2016.07.25-필라델피아 웰스파고',
message : '감사하빈다. 여러분 정말 감사합니다. ......(지면상 생략)'
}
}
}
}
</script>
<style scoped>
.senders { background-color: antiquewhite;}
.senders-content { font-family: 굴림; text-decoration: underline;}
</style>
명명된 슬롯
이름이 부여된 명명된 슬롯을 사용하면 컴포넌트에 여러 개의 슬롯을 작성할 수 있다.
src/components/NamedSlot.vue
<template>
<div id="pagewrap">
<header>
<slot name="header"></slot>
</header>
<aside id="sidebar">
<slot name="sidebar"></slot>
</aside>
<section id="content">
<slot name="content"></slot>
</section>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<style scoped>
/* 전체 구조 */
#pagewrap { padding: 5px; width: 960px; margin: 20px auto; }
header { height: 100px; padding: 0 15px;}
#content { width: 696px; float: left; padding: 5px 15px; min-height: 300px;}
#sidebar { width: 200px; padding: 5px 15px; float:left;}
footer { clear: both; padding: 0 15px;}
@media screen and (max-width: 980px) {
#pagewrap {width: 94%;}
#content { clear: both; padding: 1% 4%; width: auto; float: none;}
#sidebar {clear: both; padding: 1% 4%; width: auto; float: none;}
header, footer {padding: 1% 4%;}
}
/* 공통 스타일 */
#content { background : #f8f8f8;}
#sidebar {background: #f0efef;}
header, #content, #middle, #sidebar {margin-bottom: 5px;}
#pagewrap, header, #content, #middle, #sidebar, footer { border: solid 1px #ccc;}
</style>
src/AppNamed.vue
<template>
<div id="app">
<layout>
<h1 slot="header">헤더영역</h1>
<div slot="sidebar">
<ul class="menu">
<li v-for="sidebar in sidebars" :key="sidebar.menu">
<a :href="sidebar.link">{{sidebar.menu}}</a>
</li>
</ul>
</div>
<div slot="content">
<h2>컨텐트 영역</h2>
<p>김수한무 거북이와 두루미 ...(생략)</p>
<p>김수한무 거북이와 두루미 ...(생략)</p>
<p>김수한무 거북이와 두루미 ...(생략)</p>
<p>김수한무 거북이와 두루미 ...(생략)</p>
</div>
<p slot="footer">Footer text</p>
</layout>
</div>
</template>
<script>
import Layout from './components/NamedSlot.vue'
export default {
data() {
return {
sidebars : [
{menu: "Home", link : "#"},
{menu: "About", link : "#"},
{menu: "Contact", link : "#"},
{menu: "Vue.js", link : "#"}
]
};
},
components : { Layout }
}
</script>
<style scope>
ul.menu { position: relative; padding: 5px; list-style: none; font-style: italic;}
ul.menu a { text-decoration: none;}
</style>
범위슬롯
지금까지의 슬롯은 부모 -> 자식으로 정보를 전달하는데, 간혹 자식 -> 부모로 속성을 전달하여 부모 컴포넌트측에서 출력할 내용을 커스터마이징할 필요가 있음. 이런경우에 범위슬롯을 사용.
scr/components/ScopedSlot.vue
<template>
<div class="child">
<input type="text" v-model="x"/>
<br>
<input type="text" v-model="y">
<br>
<slot name="type1" :cx="x" :cy="y"></slot>
<slot name="type2" :cx="x" :cy="y"></slot>
</div>
</template>
<script>
export default {
data() {
return {x: 4, y: 5};
}
}
</script>
<style scoped>
.child { padding: 5px; border: solid 1px gray;}
</style>
src/AppScoped.vue
<template>
<div class="parent">
<child>
<template slot="type1" slot-scope="p1">
<div>{{ p1.cx }} + {{ p1.cy }} = {{ parseInt(p1.cx) + parseInt(p1.cy) }}</div>
</template>
<template slot="type2" slot-scope="p2">
<div>{{ p2.cx }} 더하기 {{ p2.cy }} 는 {{ parseInt(p2.cx) + parseInt(p2.cy) }} 입니다.</div>
</template>
</child>
</div>
</template>
<script>
import Child from './components/ScopedSlot.vue'
export default {
components : {Child}
}
</script>
<style scoped>
.parent {padding: 5px; border:dashed 2px black;}
</style>
동적 컴포넌트
화면의 동일한 위치에서 여러 컴포넌트를 표현할 때 사용함
Home.vue, About.vue, Contact.vue
<template>
<div>
<h1>About</h1>
<h3>{{ ( new Date().toTimeString()) }}</h3>
</div>
</template>
src/App.vue
<template>
<div>
<div class="header">
<h1 class="headerText">(주) OpenSG</h1>
<nav>
<ul>
<li><a href="#" @click="changeMenu('home')">Home</a></li>
<li><a href="#" @click="changeMenu('about')">About</a></li>
<li><a href="#" @click="changeMenu('contact')">Contact</a></li>
</ul>
</nav>
</div>
<div class="container">
<keep-alive include="about,home">
<component :is="currentView"></component>
</keep-alive>
</div>
</div>
</template>
<script>
import Home from './components/Home.vue'
import About from './components/About.vue'
import Contact from './components/Contact.vue'
export default {
name: 'App',
components: {
Home, About, Contact
},
data() {
return { currentView : 'home'}
},
methods : {
changeMenu : function(view) {
this.currentView = view;
}
}
}
</script>
<style scoped>
.header { background-color: aqua; padding: 10px 0 0 0;}
.headerText { padding: 0 20px;}
ul {
list-style-type: none; margin: 0; padding:0;
overflow: hidden; background-color: purple;
}
li {float: left;}
li a {
display: block; color: yellow; text-align: center;
padding: 14px 16px; text-decoration: none;
}
li a:hover {
background-color: aqua; color: black;
}
</style>
keep-alive 지정
<keep-alive include="about,home">
<component :is="currentView"></component>
</keep-alive>
재귀 컴포넌트
- 템플릿에서 자기 자신을 호출하는 컴포넌트
- 주의할 점은 반드시 name 옵션을 지정해야 함
src/components/Tree.vue
<template>
<ul>
<li v-for="s in subs" v-bind:class="s.type" :key="s.name">
{{ s.name }}
<tree :subs="s.subs"></tree>
</li>
</ul>
</template>
<script>
export default {
name : 'tree',
props : ['subs']
}
</script>
src/components/About.vue
<template>
<div>
<h1>About</h1>
<h3>{{ ( new Date().toTimeString()) }}</h3>
<h4>조직도</h4>
<tree :subs="orgcharts"></tree>
</div>
</template>
<script>
import Tree from './Tree.vue';
export default {
name : 'about',
components : {Tree},
data() {
return {
orgcharts : [
{name : "(주) OpenSG", type : 'company', subs : [
{name : 'SI 사업부',type : 'division', subs : [
{name : 'SI 1팀', type: 'team'},
{name : 'SI 2팀', type: 'team'}
]},
{name : "BI 사업부", type : 'division', subs : [
{name : 'BI 1팀', type: 'team'},
{name : 'BI 2팀', type: 'team'},
{name : 'BI 3팀', type: 'team'}
]},
{name : "솔루션 사업부", type : 'division', subs : [
{name : 'ESM팀', type: 'team'},
{name : 'MTS팀', type: 'team'},
{name : 'ASF팀', type: 'team'}
]},
{name : '총무팀', type: 'team'},
{name : '인사팀', type: 'team'}
]},
]
}
}
}
</script>
<style>
li.company { color: blue;}
li.division { color: steelblue;}
li.team { color: tomato;}
</style>
'책 > Vue.js 퀵스타트' 카테고리의 다른 글
Vuex를 이용한 상태관리 (0) | 2020.11.27 |
---|---|
axios를 이용한 서버통신 (0) | 2020.11.25 |
Vue-CLI 도구 (0) | 2020.11.22 |
ECMAScript 2015 (0) | 2020.11.22 |
컴포넌트 기초 (0) | 2020.11.21 |