본문 바로가기

책/Vue.js 퀵스타트

컴포넌트 심화

단일 파일 컴포넌트

전역 수준 컴포넌트의 문제점

  • 빌드 단계가 없으므로 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)">&#x00D7;</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