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

  • 빌드 단계가 없음
    • ECMAScript 2015 이상, Typescript와 같은 최신 문법을 사용할 수 없음
  • CSS 빌드 & 모듈화 기능이 없음
  • template이 모두 고유한 id를 가지도록 개발자가 관리해야 함

그래서 생겨난게...

단일 파일 컴포넌트(Single File Component)

단일파일 컴포넌트: <template />, <script />, <style />로 이루어진 확장자가 .vue인 파일

<template>
    <div id="comp-id">
	    ...
    </div>  
</template>

<script>
export default {
    name : 'comp-name',
    components : { ... },
    data() {
	    return {};
    },
    ...
}
</script>

<style>
    ...
</style>

 

Vue-CLI의 vue-loader라는 구성요소가 이 파일을 파싱하고, 다른 로더들을 활용해 하나의 모듈로 조합한다.

그 중 css-loader는 CSS스타일을 전처리하고 스타일 정보를 모듈화한다.

 

기본 프로젝트 구조

Vue-CLI를 통해 프로젝트를 만들게되면, 다음과 같은 폴더 구조가 생성된다.

컴포넌트에서의 스타일

컴포넌트에서 스타일을 작성할 때에는 다음과 같은 작성 방법이 있다.

  • 전역 CSS
  • 범위 CSS(Scoped CSS)
  • CSS 모듈(CSS Module)

전역 CSS

일반적인 CSS작성을 이야기한다.

단일 파일 컴포넌트 내의 <style> 태그 안에 작성하면 된다.

<style>
.class-name {
	color: black;
}
</style>

그러나, 이것은 전역 스타일이므로 페이지 전체에서 사용된다. 

때문에 다른 컴포넌트에서도 동일한 CSS클래스명을 사용하면 충돌이 발생한다.

따라서 특정 컴포넌트만의 스타일을 지정하는 범위 CSS, CSS 모듈 방식을 사용하는 것을 추천한다.

범위 CSS(Scoped CSS)

<style scoped>
.class-name {
	color: black;
}
</style>

범위 CSS를 사용할 때에는 다음 사항을 알아두자.

  • 범위 CSS는 Attribute Selector를 사용함
    • 브라우저에서 스타일을 적용하는 속도가 느림
    • ID, 클래스, 태그명 선택자를 사용해 스타일을 적용하여, 브라우저의 실행 속도가 느려지는 문제를 보완하는 것이 좋음
  • 부모 컴포넌트에서 범위 CSS를 적용하기 위해 생성되는 attribute가 자식 컴포넌트의 루트 요소에도 등록됨
    • 부모 컴포넌트에 적용된 범위 CSS는 하위 컴포넌트에도 반영됨

CSS 모듈(CSS Module)

CSS 스타일을 마치 객체처럼 다룰 수 있게 해준다.

<template>
  <div>
    <button v-bind:class="$style.hand"> CSS Module을 적용한 버튼 </button>
    <div :class="[$style.box, $style.border]">Hello World</div>
  </div>
</template>

<script>
export default {
    created() {
        console.log(this.$style);
    }
}
</script>

<style module>
.hand { cursor:pointer; background-color:purple; color:yellow; }
.box { width:100px; height:100px; background-color:aqua; }
.border { border:solid 1px orange; }
</style>

위 코드는 아래의 HTML로 생성된다.

<div>
	<button class="_1l2s2YbgGPFeFA2amkytu7_0"> CSS Module을 적용한 버튼 </button>
	<div class="_3PFteHfWSgjUHmj2ZptXNB_0 fYMDrU15gNkPkiiXEBCss_0">Hello World</div>
</div>

hand, box, border과 같이 코드에 주어진 스타일명이 아닌, 충돌하지 않도록 생성된 다른 이름이 클래스명으로 사용된다.

 

슬롯

부모 컴포넌트에서 자식 컴포넌트로 전달할 정보가 HTML 태그를 포함하고 있다면, props를 사용해 전달하기 쉽지 않다.

슬롯을 이용하면 부모 컴포넌트에서 자식 컴포넌트로 HTML 마크업을 전달할 수 있다.

 

기본 슬롯(Default Slot)

<자식 컴포넌트>

<template>
	<div>
	...
		<div class="content">
			<!-- 슬롯 정의 -->
			<slot></slot>
		</div>
	... 
	</div>
</template>

<부모 컴포넌트>

<template>
	<div>
    ...
    	<child-component>
        	<div>
            이 안에 들어오는 모든 것들이 slot으로 자식 컴포넌트에게 전달된다.
            그리고 자식 컴포넌트에서 마크업으로 렌더링하게 된다.
            </div>
        </child-component>
    ...
    </div>
</template>

명명된 슬롯(Named Slot)

슬롯에 이름을 부여하여, 컴포넌트에 여러 개의 슬롯을 작성할 수 있다.

 

<자식 컴포넌트>

<template>
    <div>
    	<header>
            <slot name="header"></slot>
        </header>
        <section id="content">
             <slot name="content"></slot>
        </section>
        <footer>
            <slot name="footer"></slot>
        </footer>
    </div>
</template>

<부모 컴포넌트>

<template>
    <div>
    	<child-component>
            <template v-slot:header>
                <!-- header slot 안에 채울 HTML 코드 작성 -->
            </template>
             <template v-slot:content>
                <!-- header slot 안에 채울 HTML 코드 작성 -->
             </template>
            <template v-slot:footer>
                <!-- header slot 안에 채울 HTML 코드 작성 -->
            </template>
        </child-component>
    </div>
</template>

참고로, 기본 슬롯의 이름은 'default'이다.

범위 슬롯(Scoped Slot)

부모 컴포넌트에서 슬롯 내부에 들어갈 컨텐츠를 작성할 때,

자식 컴포넌트로부터 필요한 정보를 전달받아서 (부모 컴포넌트에서) 출력할 내용을 커스터마이징하기 위한 용도로 범위 슬롯이 사용된다.

 

<자식 컴포넌트>

<template>
    <div class="child">
        <slot name="type1" :cx="x" :cy="y"></slot>
        <slot name="type2" :cx="x" :cy="y"></slot>
    </div>
</template>

<부모 컴포넌트>

<template>
    <div class="parent">
        <child>
            <!-- 자식으로부터 전달받은 props를 감싼 객체를 그대로 전달받아 사용 -->
            <template v-slot:type1="p1">
                <div>{{p1.cx }} + {{p1.cy}} = 
                {{ parseInt(p1.cx) + parseInt(p1.cy) }}</div>
            </template>
            <!-- 자식으로부터 전달받을 props를 지정하여 사용 -->
            <template v-slot:type2="{cx, cy}">
                <div>{{cx }} 더하기 {{cy}} 는 
                {{ parseInt(cx) + parseInt(cy) }}입니다.</div>
            </template>
        </child>
    </div>
</template>

 

동적 컴포넌트(Dynamic Component)

화면의 동일한 위치에, 상황에 따라 다른 컴포넌트를 표현할 때 사용한다.

성능상의 이유로 상태를 유지하거나 재-렌더링을 피하길 원할 때, <keep-alive>를 이용한다.

<template>
<div>
  <div class="header">
    <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로 감싸서 캐싱하여 여러번 실행되지 않도록 함 -->
    <keep-alive>
     <!-- 동적으로 컴포넌트가 바인딩됨 -->
      <component v-bind: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 {
  components : { Home, About, Contact },
  data() {
    return { currentView : 'home' }
  },
  methods : {
    changeMenu(view) {
      this.currentView = view;
    }
  }
}
</script>

 

재귀 컴포넌트(Recursive Component)

템플릿에서 자기 자신을 호출하는 컴포넌트를 재귀 컴포넌트라고 한다.

재귀적으로 컴포넌트를 사용하려면, 반드시 name 옵션을 지정해야 한다.

<template>
  <ul>
    <li v-for="s in subs" v-bind:class="s.type">
        {{s.name}}
        <tree :subs="s.subs"></tree>
    </li>    
  </ul>
</template>

<script>
export default {
    name : 'tree',
    props : [ 'subs' ]
}
</script>

 

 

+ Recent posts