frontend/vue.js

vue.js 컴포넌트 사용법 및 기초

seul chan 2018. 1. 18. 10:20

Vue Components 사용법

기초 세팅 (main.jsApp.vue)

이번 component 예제에는 webpack을 사용하였다. 자세한 내용은 vue webpack에서 다룰 예정.

src 디렉토리에 main.js와 App.vue를 만들고, main.js에 App.vue를 등록해 주어야 한다.

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue Components</title>
  </head>
  <body>
    <div id="app">
    </div>
    <script src="/dist/build.js"></script>
  </body>
</html>

main.js

import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: '#app',
  render: h => h(App)
})

Vue components 만들기

vue component는 templatescriptstyle 크게 세가지로 구성된다.

다음은 vue component의 예시. App.vue에 다음과 같이 입력한다.

App.vue

<template>
	<div>
		<h1></h1>
	</div>
</template>

<script>
export default {
	data: function() {
		  return {
			  title: "Hello World"
		  }
	}
}
</script>

<style>
</style>

이제 브라우저에서 Hello World가 나오는 것을 볼 수 있다. App.vue라는 하나의 컴포넌트를 main.js에서 뿌려준 것.

여러 개의 컴포넌트를 사용하려면 component를 App.vue 내에서 등록하거나, main.js에서 글로벌로 등록 해 주어야 한다.

우선 새로운 component를 만들어보자. data로 content를 가지고 있는 간단한 component를 만들어 보겠다.

Content.vue

<template>
	<p><div class="wrapper wrapper-content  animated fadeInRight article">
    <div class="row">
        <div class="col-lg-10 col-lg-offset-1">
            <div class="ibox">
                <div class="ibox-content">
                    <div class="pull-right">
                    	
                        	<button class="btn btn-white btn-xs" type="button">drf</button>
                        
                    </div>
                    <div class="text-center article-title">
                    <span class="text-muted"><i class="fa fa-clock-o"></i> 9 Jan 2018</span>
                        <h1>
                            django rest framework router의 namespace
                        </h1>
                    </div>
                    	<p><code class="highlighter-rouge">Django rest framework</code>에서 <code class="highlighter-rouge">viewset</code>을 사용하는 경우 따로 url name 을 지정하지 않기 때문에 <code class="highlighter-rouge">reverse</code>등을 사용할 때 url namespace와 name을 알기가 힘들었다.</p>

<p>그래서 소스 코드를 보던 중 자동으로 url name을 생성해주는 것을 발견해서 공유한다.</p>

<p><code class="highlighter-rouge">viewset</code>을 사용하면 <code class="highlighter-rouge">list</code><code class="highlighter-rouge">detail</code> 크게 두가지의 url이 생성된다. 
간단하게 다음과 같은 url name을 입력하면 해당 viewset의 list url을 알 수 있다.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code># list의 경우
'&lt;url_namespace&gt;:&lt;base_name&gt;-list'
# detail의 경우
'&lt;url_namespace&gt;:&lt;base_name&gt;-detail'
</code></pre></div></div>

<ul>
  <li><code class="highlighter-rouge">url_namespace</code>: core url(base urls.py)에 적은 namespace</li>
  <li><code class="highlighter-rouge">base_name</code>: viewset을 <code class="highlighter-rouge">router</code>에 등록할 때 쓴 <code class="highlighter-rouge">base_name</code>
예를 들면 다음 <code class="highlighter-rouge">urls.py</code>를 쓴다면 <code class="highlighter-rouge">api:posts-list</code>를 name으로 사용하면 된다.
    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">url</span><span class="p">(</span><span class="s">r'^api/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="s">'api.urls'</span><span class="p">,</span> <span class="n">namespace</span><span class="o">=</span><span class="s">'api'</span><span class="p">)),</span>
<span class="o">...</span>
<span class="n">router</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="s">r''</span><span class="p">,</span> <span class="n">PostViewSet</span><span class="p">,</span> <span class="n">base_name</span><span class="o">=</span><span class="s">'posts'</span><span class="p">)</span>
</code></pre></div>    </div>
  </li>
</ul>

<h3 id="실제로-reverse에서-사용할-때">실제로 <code class="highlighter-rouge">reverse()</code>에서 사용할 때</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> <span class="n">reverse</span><span class="p">(</span><span class="s">'api:posts-list'</span><span class="p">)</span>
<span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">posts</span>
<span class="o">&gt;</span> <span class="n">reverse</span><span class="p">(</span><span class="s">'api:posts-list'</span><span class="p">,</span> <span class="n">kwargs</span><span class="o">=</span><span class="p">{</span><span class="s">'pk'</span><span class="p">:</span> <span class="mi">1</span><span class="p">})</span>
<span class="o">/</span><span class="n">api</span><span class="o">/</span><span class="n">posts</span><span class="o">/</span><span class="mi">1</span>
</code></pre></div></div>

                    <hr>
                    <div class="row">
                        <div class="col-md-6">
                                <h5 style="display: inline;">Tags:</h5>
                                
                                    <button class="btn btn-white btn-xs" type="button">django</button>
                                
                                    <button class="btn btn-white btn-xs" type="button">drf</button>
                                
                                    <button class="btn btn-white btn-xs" type="button">test</button>
                                
                        </div>
                        <div class="col-md-6">
                            <div class="small text-right">
                                <h5>Stats:</h5>
                                <div>
                                
                                
                                </div>
                            </div>
                        </div>
                    </div>
                    <br>
                    <div class="row">
                        <div class="col-lg-12">
                            <!-- donate -->
                            
                            <br>
                            <!-- share -->
                            <div class="a2a_kit a2a_kit_size_32 a2a_default_style">
<a class="a2a_dd" href="https://www.addtoany.com/share"></a>
<a class="a2a_button_facebook"></a>
<a class="a2a_button_twitter"></a>
<a class="a2a_button_google_plus"></a>
<a class="a2a_button_linkedin"></a>
<a class="a2a_button_email"></a>
<a class="a2a_button_wechat"></a>
<a class="a2a_button_sina_weibo"></a>
<a class="a2a_button_pocket"></a>
</div>
<script>
var a2a_config = a2a_config || {};
a2a_config.color_main = "D7E5ED";
a2a_config.color_border = "AECADB";
a2a_config.color_link_text = "333333";
a2a_config.color_link_text_hover = "333333";
</script>
<script async src="https://static.addtoany.com/menu/page.js"></script>

                            <br>
                            <!-- comment -->
                            

                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

</div>
</p>
</template>

<script>
export default {
	data: function() {
		  return {
			  content: "this is test content"
		  }
	}
}
</script>

Global

이제 위에서 만든 Content.vue 컴포넌트를 호출해야 한다. 먼저 global로 호출해서 전역에서 사용 할 수 있게 등록하는 방법이다.

main.js

import Vue from 'vue'
import App from './App.vue'
// 우선 만들어 놓은 Content.vue를 호출
import Content from './Content.vue'

// globally register component
// 이름은 맘대로 해도 상관 없다. 나는 app-content로 등록
Vue.component('app-content', Content);

new Vue({
  el: '#app',
  render: h => h(App)
})

등록한 컴포넌트를 App.vue에서 호출하여 사용 가능하다.

App.vue

<template>
	<div>
		<h1></h1>
		<app-content></app-content>
	</div>
</template>

<script>
export default {
	data: function() {
		  return {
			  title: "Hello World"
		  }
	}
}
</script>

<style>
</style>

이제 title 밑에 위에서 적은 content가 정상적으로 호출되는 것을 볼 수 있다.

App 내에서 불러서 사용

전역으로 사용하지 않고, 해당 앱 내에서 사용할 경우 해당 컴포넌트를 앱 내에서 호출 가능하다.

main.js는 기존 상태 그대로 둔 후,

main.js

import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: '#app',
  render: h => h(App)
})

App.vue에서 Content.vue를 호출한다. <script></script> 내부에서 호출한 후 이를 components에 등록 해주면 된다.

App.vue

<template>
	<div>
		<h1></h1>
		<app-content></app-content>
	</div>
</template>

<script>
import Content from './Content.vue';

export default {
	components: {
		'app-content': Content
	},
	data: function() {
		  return {
			  title: "Hello World"
		  }
	}
}
</script>

<style>
</style>

아까와 동일하게 호출되는 것을 볼 수 있다.

컴포넌트 호출 시 이름 확장

위에서 Content 컴포넌트를 app-content로 호출했는데, 이를 CamelCase로 기본으로 호출하면 동일하게 사용할 수 있다.

즉, "app-content" 대신에 appContent로 호출하면, <appContent></appContent>로도 사용할 수 있지만, <app-content></app-content>로도 사용이 가능하다.

<template>
	<div>
		<h1></h1>
		<app-content></app-content>
	</div>
</template>

<script>
import Content from './Content.vue';

export default {
	components: {
		appContent: Content
	},
	data: function() {
		  return {
			  title: "Hello World"
		  }
	}
}
</script>

<style>
</style>

뿐만 아니라 vue는 호출한 component 이름과 동일하게 호출 할 경우 축약할 수 있는 기능을 제공 해준다.

Content라는 컴포넌트를 호출했기 때문에, 아래 예시처럼 components에 Content만을 적어주면, template에서 content로 바로 사용이 가능하다.

이런 방식을 사용할 경우, <app-로 시작하는 하이픈을 사용할 수 없기 때문에 여러 앱을 동시에 사용해야 하는 경우에는 추천하지 않는다.

<template>
	<div>
		<h1></h1>
		<content></content>
	</div>
</template>

<script>
import Content from './Content.vue';

export default {
	components: {
		Content
		// this is same as
		// Content: Content
	},
	data: function() {
		  return {
			  title: "Hello World"
		  }
	}
}
</script>

Style 사용하기

기본적으로, 어떤 컴포넌트에 있는 스타일이라도 global로 적용이 된다.

이를 특정 컴포넌트에만 적용하려면 scoped를 추가해주어야 한다.

실제로 만들어진 html을 보면 data-로 시작되는 attribute가 만들어져 거기에 스타일이 추가됨.

...
<style scoped>
div {
	border: solid 1px red;
}
</style>

Component간의 데이터 전달

UserDetail을 가지고 있는 User.vue 파일에서 user의 name을 detail component로 전달하고 싶다면, props를 사용하면 된다.

리액트를 사용해 본 적이 있다면, props에 익숙 할 것이다. 상위 컴포넌트에서는 v-bind:를 사용하여 prop을 넘겨 주어야 하고, 하위 컴포넌트에서는 props 옵션을 써서 받는 props를 명시해주어야 하는 점이 react와 조금 다르다.

아래 예시에서는 UserDetail에 v-bind 옵션을 사용해서 myName이라는 props 전달 이름으로 name 값을 전달한다.

v-on:click="changeName" 메쏘드는 전달된 이름이 잘 바뀌는지 확인하기 위해 넣음.

User.vue

<template>
<div>
	<app-user-detail v-bind:myName="name"></app-user-detail>
	<button v-on:click="changeName">Change my name</button>

</div>
</template>

<script>
import UserDetail from './UserDetail.vue';

export default {
	data: function() {
		return {
			name: "Seul"
		}
	},
	methods: {
		changeName() {
			this.name = "Kim";
		}
	},
	components: {
		appUserDetail: UserDetail,
	}
}
</script>

UserDetail.vue

자식 컴포넌트인 UserDetail에서는 props로 받을 값을 명시적으로 적어 주어야 한다. (myName) 이를 적은 후에는 정상적으로 부모 컴포넌트의 값을 가져다가 쓸 수 있다.

<template>
    <div class="component">
        <h3>You may view the User Details here</h3>
        <p>Many Details</p>
		<p>User Name: </p>
    </div>
</template>

<script>
export default {
	props: ['myName']
}
</script>

싱글 페이지 어플리케이션이 아닌 경우에?는 html에서 myName(카멜케이스)를 사용할 수 없기 때문에 하이픈(-)으로 구분되는 kebab-case를 사용해야한다. 위의 예시를 보면 myName 대신 my-name을 사용하면 된다. ```javascript …


### Props validation
부모 컴포넌트로부터 정해진 데이터 타입 (`String`, `Array` 등..)이 들어와야 하거나 필수적으로 들어와야 하는 값 등이 있다면 validation을 해 줄 필요가 있다.

그럴 경우 props 객체를 object로 생성하고 그 안에 필요한 값을 넣어주면 된다.
```javascript
<template>
    <div class="component">
        <h3>You may view the User Details here</h3>
        <p>Many Details</p>
		<p>User Name: </p>
    </div>
</template>

<script>
export default {
	props: {
		myName: String
		// 만약 multiple type을 지원한다면
		// myName: [String, Array]
	},
	methods:
		reverseName() {
			this.myName.split("").reverse().join("")
		}
}
</script>

꼭 필요한 props가 있다면 required를 적용시키면 된다. 이럴 경우 myName (props)도 object로 작성해준다.

<template>
    <div class="component">
        <h3>You may view the User Details here</h3>
        <p>Many Details</p>
		<p>User Name: </p>
    </div>
</template>

<script>
export default {
	props: {
		myName: {
			type: String,
			required: true
			// 다음과 같이 default 값을 줄 수도 있다.
			// default: "John"
		}
	// object를 사용하는 경우
	// 		myName: {
	// 			type: Object,
	// 			default: function() {
	// 				return { ... }
	// 			}
	// 		}
	},
	methods:
		reverseName() {
			this.myName.split("").reverse().join("")
		}
}
</script>

props를 바꾸고 싶을 때?

vue.js의 데이터 흐름은 단방향 이기 때문에 부모 컴포넌트로부터 받은 props를 수정하는 것은 불가능 하다.

methods로 props 수정하기

하지만 모든 props는 뷰 안에서 하나의 자바스크립트 객체로 사용 가능하기 때문에, 수정/변경이 필요할 경우 methods를 사용해서 변경이 가능하다.

다음 예제는 이전 예시에서 가져온 myName을 reverse 시키는 함수를 보여준다.

<template>
    <div class="component">
        <h3>You may view the User Details here</h3>
        <p>Many Details</p>
		<p>User Name: {{ reverseName() }}</p>
    </div>
</template>

<script>
export default {
	props: ['myName'],
	methods:
		reverseName() {
			this.myName.split("").reverse().join("")
		}
}
</script>

화면에 기존 이름이 reverse 되어 표시되는 것을 볼 수 있다. 이처럼 부모로부터 받은 props 자체를 수정해서 부모 컴포넌트에 영향을 끼치는 것은 불가능하지만, 자식 컴포넌트 안에서 methods를 이용해서 해당 props를 자유롭게 사용 가능하다.

emit the event - event를 사용하여 부모 컴포넌트에 전달

위에서 언급했듯이 자식 컴포넌트 안에서 props를 수정하면 자식 컴포넌트에서만 적용되고, 부모 컴포넌트로 전달되지 않는다.

대신 부모 컴포넌트에게 props 값이 변경되었음을 알리는 방법을 사용할 수 있는데, 이 때 event를 전달한다.

vue.js 공식 문서에서 나온 “props는 아래로, events는 위로”가 이런 뜻에서 사용된 말이라고 생각한다.

이 때 event를 전달하기 위해서 $emit(eventName)을 사용한다.

아래 예시는 resetName을 클릭하면 nameWasReset이라는 이벤트를 부모 컴포넌트로 전달시키는 예시이다.

UserDetail.vue

<template>
    <div class="component">
        <h3>You may view the User Details here</h3>
        <p>Many Details</p>
		<p>User Name: {{ reverseName() }}</p>
		<button v-on:click="resetName">Reset name</button>
    </div>
</template>

<script>
export default {
	props: ['myName'],
	methods:
		reverseName() {
			this.myName.split("").reverse().join("")
		},
		resetName() {
			this.myName = "Max";
			this.$emit('nameWasReset', myName);
		}
}
</script>

User.vue

부모 컴포넌트인 User.vue에서는 v-on을 사용하여 event를 감지하도록 템플릿에 지정해야한다.

...
<app-user-detail 
	v-bind:my-name="name" 
	v-on:nameWasReset="name = $event">
</app-user-detail>
...

Component간의 data flow

component끼리의 data 흐름에서 기억해야 할 것은, vue는 Unidirectional Data flow를 가진다는 것이다.

기본적으로 자식 컴포넌트끼리 데이터 교환은 불가능하고, 부모 컴포넌트를 통해서 데이터를 전달해 주어야 한다.

이를 가능하게 해주는 방법은 크게 세가지.

  1. $emit을 사용하여 부모 컴포넌트의 data 변경

  2. callback 사용

  3. (추천하는 방법??) Event Bus

    Event Bus

    부모 컴포넌트를 통하는 게 아닌, main.js에 등록한 eventBus를 통해서 $emit$on으로 자식 컴포넌트의 데이터를 전달해주는 방법.

main.js

main.js에 EventBus를 등록해준다.

이 때 #app에 등록할 Vue instance를 선언하기 전에 eventBust를 등록해 주어야 한다.

import Vue from 'vue'
import App from './App.vue'

export const eventBus = new Vue();

new Vue({
  el: '#app',
  render: h => h(App)
})

UserEdit.vue

이후 age의 값을 변경시킬 UserEdit.vue에서 eventBus를 호출한 후, eventBus에 age가 변했다는 사실을 전달시켜준다.

이때 전달은 vue lifecycle에서 created()를 사용한다.

<template>
    <div class="component">
        <h3>You may edit the User here</h3>
        <p>Edit me!</p>
		<p>User Age: </p>
		<button v-on:click="editAge">Edit age</button>
    </div>
</template>

<script>
import { eventBus } from '../main';
export default {
	props: ['userAge'],
	methods: {
		editAge() {
			this.userAge = 30;
			eventBus.$emit('ageWasEdited', this.userAge);
		}
	}
}
</script>

UserDetail.vue

그 다음 위에서 변화된 값을 받아서 변경시킬 vue 파일 (UserDetail.vue)에서 $on을 이용하여 값을 받은 뒤 전달시켜준다.

<template>
    <div class="component">
        <h3>You may view the User Details here</h3>
        <p>Many Details</p>
		<p>User Age: </p>
    </div>
</template>

<script>
import { eventBus } from '../main.js';
export default {
	props: {
		userage: number
	},
	created() {
		eventBus.$on('ageWasEdited', (age) => {
			this.userAge = age;
		};
	}
}
</script>

eventBus 자체에 age를 변경시키는 methods 추가하기

위의 예제에서 eventBus는 그저 데이터를 emit, on 시켜주기만 할 뿐 다른 기능은 하나도 없었따.

이렇게 하지 않고, eventBus 자체에 data나 methods를 추가할 수 있다.

main.js

우선 main.js의 eventBus에 위의 예제에서 사용한 age를 $emti하는 method를 추가해준다.

import Vue from 'vue'
import App from './App.vue'

export const eventBus = new Vue({
	methods: {
		changeAge(age): {
			this.$emit('ageWasEdited', age);
		}
	}
});

new Vue({
  el: '#app',
  render: h => h(App)
})

UserEdit.vue

이후 UserEdit.vue에 있던 $emit 부분을 위에서 만든 eventBus의 method로 대체한다.

...
methods: {
	editAge() {
		this.userAge = 30;
		// 기존에 있던 부분
		// eventBus.$emit('ageWasEdited', this.userAge);
		// eventBus에서 가져오기
		eventBus.changeAge(this.userAge);
	}
}
...


'frontend > vue.js' 카테고리의 다른 글

vue.js 동적 컴포넌트 사용법 (component, keep-alive)  (0) 2018.01.20
vue.js directives(디렉티브, 지시문) 알아보기  (0) 2018.01.19
vue slot 사용법  (0) 2018.01.17
Debugging in vue.js  (0) 2018.01.13
vue 기초 (~4강)  (0) 2017.12.11