Vue3 기초 문법 정리

김도겸

Updated:


Intro

안녕하세요. 김도겸입니다.
Vue3 기초 문법과 사용법에 대해 공부한 내용들을 간단히 정리해보았습니다.



0. 간단한 설치 및 프로젝트 생성

다음과 같은 코드로 vue를 설치합니다.

$ npm install vue@next

다음과 같은 코드로 vue-cli를 install 후 vue create 명령어로 프로젝트를 간단하게 생성합니다.

npm install -g @vue/cli  # 또는 yarn global add @vue/cli
vue create hello-vue3


1. 인스턴스

인스턴스 개념

인스턴스란 뷰의 시작이 되는 것으로 모든 vue 앱을 만들 때 필수로 생성해야 하는 코드입니다.
인스턴스 안에는 미리 정의되어 있는 속성과 메소드들이 있기 때문에 이 기능들을 이용하여 효과적인
화면 구현이 가능합니다.

  • 가장 기초적인 vue 생성 및 data binding 예시입니다.
<div id="counter">
  Counter: {{ counter }}
</div>
const Counter = {
  data() {
    return {
      counter: 0
    }
  }
}

Vue.createApp(Counter).mount('#counter')  

html 코드로 먼저 해당 화면의 틀을 만든 후, vue 인스턴스를 생성하여 속성을 설정합니다.
data를 화면에 표시하기 위해서는 위 사진에서 보이는 것과 같이 {{ data명 }}를 사용합니다.

인스턴스 LifeCycle

인스턴스 LifeCycle(생명주기)이란 뷰의 인스턴스가 생성되어 소멸되기까지 거치는 과정을 의미합니다.

vue는 다음과 같은 과정으로 진행됩니다. lifecycle

라이프 사이클 훅

라이프 사이클 훅으로 인스턴스의 특정 시점에 원하는 로직을 구현할 수 있습니다.
따라서 반드시 라이프 사이클에 대한 이해가 필요합니다.

Vue.createApp({
  data() {
    return { count: 1 }
  },
  created() {
    // `this` points to the vm instance
    console.log('count is: ' + this.count) // => "count is: 1"
  }
})

필요한 lifecycle 훅을 입력하여 해당 주기에서 실행될 코드를 작성합니다.


2. 템플릿 문법

보간법

  • 문자열

    데이터 바인딩의 가장 기초가 되는 방식입니다.
    Mustache(이중 중괄호)기법을 사용하여 문자열을 바인딩할 수 있습니다.

      <span>메시지:{{ msg }}</span>
    

    msg 데이터가 변경될 경우 갱신됩니다. 만약 데이터가 변경되어도 갱신되지 않기를 원한다면, v-once를 사용합니다.

      <span v-once>결코 변하지 않을 것입니다: {{ msg }}</span>
    


  • 원시 HTML

    이중 중괄호는 일반 텍스트로 바인딩 되므로 만약 html을 출력하기 위해서는 v-html을 사용해야합니다.

      <p>이중 중괄호 사용: </p>
      <p>v-html 디렉티브 사용: <span v-html="rawHtml"></span></p>
    
      data() {
        return {
          rawHtml: '<span style="color: red">This should be red.</span>'
        }
      }
    


  • 속성
    속성을 부여할 때는 이중 중괄호 기법을 사용할 수 없으므로 v-bind를 사용합니다.
    기본적인 사용방법만 정리하고, 다른 여러가지 속성은 추가적으로 설명드리겠습니다.

      <div v-bind:id="dynamicId"></div>
      <button v-bind:disabled="isButtonDisabled">Button</button>
    

    v-bind:id는 id를 부여하고, v-bind:disabled는 boolean값인 isButtonDisabled의 값에 따라
    button의 활성화 여부를 결정합니다.

  • JavaScript 표현식 사용

    간단한 속성 키들을 템플릿에 바인딩하는 방법이 있으나, 데이터 바인딩 시에 javascript 표현식 기능을 사용할 수 있습니다.

        <div>{{ number + 1 }} {{ ok ? 'YES' : 'NO' }} {{ message.split('').reverse().join('')}}</div>
        <div v-bind:id="'list-' + id"></div>
    

    하지만 바인딩에서는 반드시 하나의 단일 표현식만 포함해야됩니다.
    그렇기 때문에 다음과 같이 작성하면 안됩니다.

      <!-- 아래는 문장입니다. 표현식이 아닙니다. -->
      {{ var a = 1 }}
    
      <!-- 조건문은 작동하지 않습니다. 삼항 연산자를 사용해야 합니다. -->
      {{ if (ok) { return message } }}
    


  • 동적 전달인자

    js표현식을 활용하여 대괄호로 묶어 디렉티브 전달인자로 사용할 수 있습니다.

      <a v-bind:[attributeName]="url"> ... </a>
    

    attributeName의 값이 변경되면 속성이 변경됩니다.

다음과 같이 ‘ v- ‘로 시작하는 속성들을 디렉티브라고 합니다.
v-bind:[속성명]는 :[속성명]으로 축약해서 사용 가능하고, v-on:[속성명]은 @[속성명]으로 축약하여 사용 가능합니다.


3. Data 속성 & methods

라이프 사이클 훅 파트에서의 코드를 보시면 data()라는 함수가 보입니다.
이 함수는 하나의 객체를 반환하고, 반환한 객체의 데이터들은 모두 vue 반응형 시스템에 의해 변경이 감지되어 컴포넌트 인스턴스의 $data에 저장된 후 화면에 반영됩니다.

methods 옵션을 사용하여 컴포넌트에 필요한 메소드를 추가하여 사용할 수 있습니다.

const app = Vue.createApp({
  data() {
    return { count: 4 }
  },
  methods: {
    increment() {
      // `this`는 컴포넌트 인스턴스를 참조합니다.
      this.count++
    }
  }
})

const vm = app.mount('#app')

console.log(vm.count) // => 4

vm.increment()
// vm.count에 값을 할당하면 $data.count도 갱신됩니다.
console.log(vm.$data.count) // => 5
console.log(vm.count) // => 5

메소드 내부에서 사용할 데이터는 this.{data명}을 활용하면 됩니다.

<button @click="increment">Up vote</button>
<span :title="toTitleDate(date)">
  {{ formatDate(date) }}
</span>

위처럼 템플릿에서 메소드를 직접 호출하여 사용할 수도 있습니다


4. computed & watch

computed와 watch는 모두 지정한 특정 대상이 변경될 경우 이에 반응하여 선언한 함수를 실행해주는 기능입니다.
하지만 이 둘은 같다라고는할 수 없습니다.

<div id="demo">{{ fullName }}</div>
const vm = Vue.createApp({
  data() {
    return {
      firstName: 'Foo',
      lastName: 'Bar'
    }
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName
    }
  }
}).mount('#demo')

computed는 firstName과 lastName이 변경이 될 때를 감지하여 이미 계산되어 있는 결과를 보여줍니다.
종속 대상의 값이 변경되지 않는다면 컴포넌트가 리렌더링 되도 다시 실행되지 않습니다.

const vm = Vue.createApp({
  data() {
    return {
      firstName: 'Foo',
      lastName: 'Bar',
      fullName: 'Foo Bar'
    }
  },
  watch: {
    firstName(val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName(val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
}).mount('#demo')

watch는 firstName과 lastName을 감시하며 변경될 때 fullName을 변경해줍니다.

쉽게 정리하자면 computed는 특정 값이 변경될 때만을 감지하여 반환하는 변수의 역할이 필요할 때 사용하는것이 좋다고 생각하고, watch 함수는 계속 값을 감시하여 어떤 로직이 실행되어야 할 때 트리거 역할을 해줄때 사용하는 것이 좋다고 생각합니다.


5. v-bind (바인딩)

v-bind는 간편하게 view 영역에서 간편하게 data를 연결하기 위해서 사용합니다.
v-bind를 주로 사용할 수 있는 대표적인 속성은 style, class, src(이미지 바인딩), href(링크 바인딩)가 있습니다.

사용법은 생각보다 간단합니다. v-bind:{속성명}도 가능하고, v-bind를 생략한 채 :{속성명}으로도 사용 가능합니다.

간단한 코드 예제를 몇가지 보여드리겠습니다.

  • :class

    1) 객체로 전달하기

    <div
    class="static"
    :class="{ active: isActive, 'text-danger': hasError }"
    >
    </div>
    

    위와 같이 객체에 필드를 포함하는 형식으로 작성합니다.

    data() {
      return {
        isActive: true,
        hasError: false
      }
    }
    

    객체에 포함된 데이터에 따라 다음과 같이 렌더링됩니다.

    <div class="static active"></div>
    

    isActive 또는 hasError가 변경되면 그에따라 클래스 목록이 업데이트됩니다.

    <div :class="classObject"></div>
    
    data() {
      return {
        classObject: {
          active: true,
          'text-danger': false
        }
      }
    }
    

    :class에 객체를 선언하지 않고, data()에 객체를 선언하고 사용하여도 상관없습니다.

    해당 구문은 computed를 사용해도 바인딩이 가능합니다.


    2) 배열로 전달하기

    <div :class="[activeClass, errorClass]"></div>
    

    위와 같이 배열 형식으로 클래스 목록을 작성합니다.

    data() {
    return {
      activeClass: 'active',
      errorClass: 'text-danger'
    }
    

    배열에 포함된 데이터에 따라 다음과 같이 렌더링됩니다.

    <div class="active text-danger"></div>
    

    추가적으로 조건부로 목록의 클래스를 전환하려면, 삼항 표현식을 사용하여 수행할 수 있습니다.

    <div :class="[isActive ? activeClass : '', errorClass]"></div>
    

    위와 같이 선언하면 isActive가 false일땐 errorClass만 적용되고, true일때는 activeClass를 추가로 적용됩니다.
    만약 여러 조건부 클래스가 있어 보기에 너무 복잡해 보인다면,

    <div :class="[{ active: isActive }, errorClass]"></div>
    

    위와 같이 배열 구문 내에서 객체 구문을 사용해 비교적 보기 쉽게 선언할 수 있습니다.


    3) 컴포넌트에서 클래스 추가하기

    우선적으로 컴포넌트를 간단하게 만들어보겠습니다.
    (컴포넌트의 자세한 사항은 컴포넌트 파트에서 설명드리도록 하겠습니다.)

    const app = Vue.createApp({})
    
    app.component('my-component', {
      template: `<p class="foo bar">Hi!</p>`
    })
    
    <div id="app">
      <my-component class="baz boo"></my-component>
    </div>
    

    위와 같이 컴포넌트를 선언하고, 클래스를 추가하게 되어 사용하게 되면

    <p class="foo bar baz boo">Hi</p>
    

    위처럼 template 내에 선언한 foo, bar 클래스에 컴포넌트에 선언한 baz, boo 클래스까지 추가로 렌더링됩니다.

    이는 클래스 바인딩(:class)을 사용하여도 동일하게 작동됩니다.

  • :style

    :style 사용법은 css를 사용하는 것과 매우 유사하기 때문에 매우 간단합니다.

    1) 객체로 전달하기

    객체로 전달하는 방법은 :class와 같이 두가지 방법이 있습니다.

    직접 태그 내에서 객체를 선언하여 바인딩하는 방법이 있고,

    <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
    
    data() {
      return {
        activeColor: 'red',
        fontSize: 30
      }
    }
    

    템플릿을 더 깔끔하게 만들기 위해, 스타일 객체를 따로 만들어 바인딩하는 방법이 있습니다.

    <div :style="styleObject"></div>
    
    data() {
      return {
        styleObject: {
          color: 'red',
          fontSize: '13px'
        }
      }
    }
    


    2) 배열로 전달하기

    배열 구문을 사용하면, 동일한 요소에 다음과 같이 여러 스타일 객체를 적용할 수 있습니다.

    <div :style="[baseStyles, overridingStyles]"></div>
    


    :class와 동일하게 :style도 삼항 연산자를 활용하여 렌더링할 수 있습니다.

    <div :style="[isOveride ? overridingStyles : baseStyles, textStyles ]"></div>
    

    textSytles는 기본으로 적용되고, isOveride의 값이 true일 경우 overridStyles를, false일 경우 baseStyle를 적용합니다.

    추가적으로 다중값을 선언하여 브라우저의 따라 지원하는 배열의 마지막 값만 렌더링해줄 수 있습니다.

    <div :style="{ display: [ '-webkit-box', '-ms-flexbox', 'flex' ] }"></div>
    


  • :src, :href

    :src와 :href도 사용법이 간단합니다. :src는 이미지 경로를, :href는 사이트 url을 렌더링할 수 있습니다.

    <img :src="profile" />
    <a :href="url"> test.com </a>
    
    data() {
      return {
        profile: 'test:/img/profile1.jpg',
        url: 'www.test.com'
      }
    }
    


6. v-if, v-show, v-for (렌더링)

  • v-if

    v-if는 조건에 따라 렌더링할 수 있습니다.
    사용 방법은 간단합니다.

    <h1 v-if="awesome">Vue is awesome!</h1>
    <div v-if="Math.random() > 0.5">
      Now you see me
    </div>
    
    data() {
      return {
        awesome: true;
      }
    }
    

    위와 같이 v-if 안에 들어가는 값이 boolean이어야 하고, 변수를 넣어도 가능하고, 조건식을 넣는 것도 가능합니다.
    boolean을 return하는 method 또한 가능하며, computed와 watch로도 사용가능합니다.

    v-if를 사용하게 되면 그에 반대에 한하는 조건에 따라 렌더링 해주는 v-else와 렌더링 조건을 추가하는 v-else-if도 사용 가능합니다.

    <div v-if="type === 'A'">
      A
    </div>
    <div v-else-if="type === 'B'">
      B
    </div>
    <div v-else-if="type === 'C'">
      C
    </div>
    <div v-else>
      Not A/B/C
    </div>
    


    v-if는 디렉티브이기 때문에 하나의 엘리먼트에 추가 되어야 합니다.
    따라서 div를 사용하지 않고 여러 개의 엘리먼트를 전환할 경우 template 태그를 사용하여 감쌉니다.

    <template v-if="ok">
      <h1>Title</h1>
      <p>Paragraph 1</p>
      <p>Paragraph 2</p>
    </template>
    
  • v-show

    v-shows는 v-if와 유사하게 조건에 따라 엘리먼트를 표시하는 방식이며, 사용 방법은 거의 동일합니다.

    <h1 v-show="ok">Hello!</h1>
    


    v-if와 v-show는 큰 차이가 있습니다.
    v-if는 초기 렌더링 시 조건이 거짓이면 참이 될 때까지 아무 작업도 하지 않습니다.
    v-show는 조건과 관계없이 무조건 렌더링되고, 조건에 따라 css display:block / display:none 속성을 전환합니다.

    따라서 무언가를 자주 전환해야 한다면 v-show를 사용하는 게 좋고, 런타임 시 조건이 변경되지 않는다면 v-if를 사용하는 게 좋습니다.

  • v-for

    1) 배열 렌더링

    v-for는 배열을 기반으로 리스트를 렌더링할 수 있습니다.
    사용방법은 다음과 같습니다.

    <ul id="array-with-index">
      <li v-for="(item, index) in items" :key="index">
         -  - 
      </li>
    </ul>
    
    Vue.createApp({
      data() {
        return {
          parentMessage: 'Parent',
          items: [{ message: 'Foo' }, { message: 'Bar' }]
        }
      }
    }).mount('#array-with-index')
    

    item은 items의 배열 엘리먼트의 별칭이고, index는 배열 엘리먼트의 인덱스 값입니다.
    in 대신 of도 사용 가능합니다.

    :key에 고유값(id 또는 index)을 설정하는 것을 권장하고 있습니다. (미설정 시 에러 메시지 발생함)
    v-for는 배열의 값이 추가, 삭제, 정렬, 수정될 경우에 맞춰서 렌더링됩니다.

    2) 객체 렌더링

    v-for를 사용하여 객체의 속성을 반복할 수도 있습니다.

      <ul id="v-for-object" class="demo">
        <li v-for="(value, name, index) in myObject" :key="index">
          . : 
        </li>   
      </ul>
      <ul id="v-for-number">
        <li v-for=" n in 10" :key="n">
                
        </li>
      </ul>
    
      Vue.createApp({
        data() {
          return {
            myObject: {
              title: 'How to do lists in Vue',
              author: 'Jane Doe',
              publishedAt: '2020-03-22'
            }
          }
        }
      }).mount('#v-for-object')
    

    value는 속성 값, name은 속성명(키), index는 해당 속성의 인덱스값을 의미합니다.
    v-for는 배열, 객체뿐만 아니라 정수도 사용 가능합니다.


    v-for도 template 태그를 활용하여 여러 요소의 블록을 렌더링할 수 있으며,

    <ul>
      <template v-for="item in items">
        <li></li>
        <li class="divider" role="presentation"></li>
      </template>
    </ul>
    

    component에도 적용할 수 있습니다.

    <div id="todo-list-example">
      <form v-on:submit.prevent="addNewTodo">
        <label for="new-todo">Add a todo</label>
        <input
          v-model="newTodoText"
          id="new-todo"
          placeholder="E.g. Feed the cat"
        />
        <button>Add</button>
      </form>
      <ul>
        <todo-item
          v-for="(todo, index) in todos"
          :key="todo.id"
          :title="todo.title"
          @remove="todos.splice(index, 1)"
        ></todo-item>
      </ul>
    </div>
    
    const app = Vue.createApp({
      data() {
        return {
          newTodoText: '',
          todos: [
            {
              id: 1,
              title: 'Do the dishes'
            },
            {
              id: 2,
              title: 'Take out the trash'
            },
            {
              id: 3,
              title: 'Mow the lawn'
            }
          ],
          nextTodoId: 4
        }
      },
      methods: {
        addNewTodo() {
          this.todos.push({
            id: this.nextTodoId++,
            title: this.newTodoText
          })
          this.newTodoText = ''
        }
      }
    })
    
    app.component('todo-item', {
      template: `
        <li>
              
          <button @click="$emit('remove')">Remove</button>
        </li>
      `,
      props: ['title']
    })
    
    app.mount('#todo-list-example')
    

    단 반복할 데이터를 컴포넌트로 전달하려면 props를 사용해야 합니다. props에 대한 설명은 해당 파트에서 진행하도록 하겠습니다.


7. 이벤트 핸들링(v-on)

v-on 디렉티브는 @ 기호로 축약하여 사용하며, DOM 이벤트를 듣고 트리거 될 때와 javascript를 실행할 때 사용합니다.

  <div id="basic-event">
  <button @click="counter += 1">Add 1</button>
  <p>The button above has been clicked  times.</p>
  </div>
  Vue.createApp({
    data() {
      return {
        counter: 1
      }
    }
  }).mount('#basic-event')

메소드 이벤트

복잡한 로직의 경우 이벤트 속성 값으로 할당하는 것은 지저분하고 오류를 범할 수 있습니다.
따라서 메소드를 사용하여 이벤트 처리를할 수 있습니다.

  <div id="event-with-method">
    <!-- `greet`는 메소드 이름으로 아래에 정의되어 있습니다 -->
    <button @click="greet">Greet</button>
  </div>
  Vue.createApp({
    data() {
      return {
        name: 'Vue.js'
      }
    },
    methods: {
      greet(event) {
        // 메소드 안에서 사용하는 `this` 는 Vue 인스턴스를 가리킵니다.
        alert('Hello ' + this.name + '!')
        // `event` 는 네이티브 DOM 이벤트입니다
        if (event) {
          alert(event.target.tagName)
        }
      }
    }
  }).mount('#event-with-method')

인라인 메소드 핸들러

인라인 javascript 메소드 호출도 가능합니다.

  <div id="inline-handler">
    <button @click="say('hi')">Say hi</button>
    <button @click="say('what')">Say what</button>
  </div>
  Vue.createApp({
    methods: {
      say(message) {
        alert(message)
      }
    }
  }).mount('#inline-handler')

복합 이벤트

연산자를 사용하여 이벤트 핸들러 안에서 복합 메소드를 지정할 수 있습니다:

  <button @click="one($event), two($event)">
    Submit
  </button>
  Vue.createApp({
   methods: {
    one(event) {
      // 첫번째 핸들러 로직...
    },
    two(event) {
      // 두번째 핸들러 로직...
    }
  }).mount('#inline-handler')

이벤트 종류

이벤트는 다양한 종류의 이벤트가 있습니다.
이 문서에 모든 이벤트를 담을 수 없어 대표적이고, 자주 사용하는 이벤트만 정리하였습니다.

  @click - 엘리먼트를 마우스 좌클릭  호출
  @change - 데이터가 변할 때마다 호출(주로 form data에서 사용됨)
  @input -  데이터가 입력될 때마다 호출(주로 form data에서 사용됨)
  @keyup -  키보드가 눌렀다가 때졌을  호출 (@keydown 키가 눌렸을  호출)

그 밖의 다양한 이벤트는 해당 블로그를 참고하시면 될 것 같습니다. https://leestrument.tistory.com/entry/Event-%EC%9D%98-%EB%AA%A8%EB%93%A0%EA%B2%83

이벤트 수식어

이벤트 수식어란 v-on 디렉티브에서 사용할 수 있는 다양한 수식어를 의미합니다.
수식어를 사용하여 event 객체가 가지고 있는 메소드들을 간편하게 약어 형식으로 사용한다고 보시면 됩니다.

수식어의 종류를 간단하게 표로 정리하였습니다. event modifier

키 수식어, 명령어

key command

이 밖에도 crtl, alt, shift, meta(윈도우 키[window], command키[mac]) 등도 있습니다.
마우스 버튼 수식어도 존재합니다. { .left, .right, .middle }

.exact 수식어

.exact 수식어를 사용하면 다른 시스템 수식어와 정확한 조합으로 눌러 이벤트가 실행되도록 해줍니다.

 <!-- Alt 또는 Shift와 함께 눌린 경우에도 실행됩니다. -->
 <button @click.ctrl="onClick">A</button>

 <!-- Ctrl 키만 눌려있을 때만 실행됩니다. -->
 <button @click.ctrl.exact="onCtrlClick">A</button>

 <!-- 아래 코드는 시스템 키가 눌리지 않은 상태인 경우에만 작동합니다. -->
 <button @click.exact="onClick">A</button>


8. form 입력 바인딩

v-model 디렉티브를 사용하여 폼 입력 요소들에 양방향 데이터 바인딩(데이터의 값과 엘리먼트의 값과 상호 반영됨)을 생성할 수 있습니다.

요소에 따른 사용 방법

1) input text, textarea태그는 value 속성과 input 이벤트를 사용합니다.

 <input v-model="message" placeholder="여기를 수정해보세요" />
 <p>메시지: </p>
 <span>여러 줄을 가지는 메시지:</span>
 <p style="white-space: pre-line;"></p>
 <br />
 <textarea v-model="message" placeholder="여러줄을 입력해보세요"></textarea>

 <!-- textarea에서는 텍스트 보간이 작동하지 않습니다. -->
 <!-- bad -->
 <textarea></textarea> 
 <!-- good -->
 <textarea v-model="text"></textarea>

2) 체크박스와 라디오 버튼은 checked 속성과 change 이벤트를 사용합니다.

하나의 체크박스는 단일 boolean 값을 가지고, 여러 개의 체크박스는 동일한 v-model을 할당하고, value 값을 설정하면, 선택된 체크박스들의 value가 v-model에 바인딩됩니다.
(이때 v-model은 배열 타입이어야 합니다.)

  <div id="v-model-multiple-checkboxes">
    <input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
    <label for="jack">Jack</label>
    <input type="checkbox" id="john" value="John" v-model="checkedNames" />
    <label for="john">John</label>
    <input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
    <label for="mike">Mike</label>
    <br />
    <span>Checked names: </span>
  </div>

라디오 버튼은 여러 개중에서도 하나만 선택할 수 있기에 배열이 아닌 선택된 라디오의 value값이 v-model에 바인딩됩니다.

  <div id="v-model-radiobutton">
    <input type="radio" id="one" value="One" v-model="picked" />
    <label for="one">One</label>
    <br />
    <input type="radio" id="two" value="Two" v-model="picked" />
    <label for="two">Two</label>
    <br />
    <span>Picked: </span>
  </div> 

3) select 태그는 value를 prop으로, change를 이벤트로 사용합니다.

  • 단일 셀렉트
<div id="v-model-select" class="demo">
  <select v-model="selected">
    <option disabled value="">Please select one</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
  <span>Selected: </span>
</div>
  • 다중 셀렉트
<div id="v-model-select" class="demo">
  <select v-model="selected" multiple>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
  <br />
  <span>Selected: </span>
</div>

체크박스, 라디오, 셀렉트 v-model의 경우 문자열(또는 checkbox의 boolean)의 바인딩 값을 가지는데 입력 값이 문자열이 아닌 값을 원할 때는 다음과 같이 v-bind:value를 사용할 수 있습니다.

<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
<p></p>

<input type="radio" v-model="pick" v-bind:value="{number:123}" />
<p></p>

<select v-model="selected">
  <!-- 인라인 객체 리터럴 -->
  <option :value="{ number: 123 }">123</option>
</select>
<p></p>

v-model도 수식어가 있습니다. text type input에서 사용합니다.

  .lazy - 입력된 값이 바로 바인딩되지 않고, 엔터를 누르거나 포커스가 벗어날때 이벤트 발생
  .number - 입력시 모두 문자열로 들어가게 되는데, 숫자로 변경해야  경우 사용
  .trim - 입력한 내용에서 앞뒤 공백을 제거 처리해야  경우 사용


9. 컴포넌트

컴포넌트 개념

컴포넌트는 조합하여 화면을 구성할 수 있는 특정 화면 영역을 의미합니다.
컴포넌트를 사용하게 되면 빠르게 구조화하여 일괄적인 패턴으로 개발이 가능하고, 하나의 vue 파일에 많은 요소들이 들어가야 할 때 분리하여 개발이 가능하여 관리도 편하며, 반복적인 재사용도 가능합니다.

component

컴포넌트 사용 방법

  • vue 파일 내부에서 생성하여 등록하기

    1)전역 등록

    const app = Vue.createApp({})
    app.component('component-a', {
    
      template: '<div>Componenet C</div>`
    })
    app.component('component-b', {
      template: '<div>Componenet C</div>`
    })
    app.component('component-c', {
      template: '<div>Componenet C</div>`
    })
    
    app.mount('#app')
    
    <div id="app">
      <component-a></component-a>
      <component-b></component-b>
      <component-c></component-c>
    </div>
    

    2)지역 등록

    const ComponentA = {
      template: '<div>Componenet A</div>`
    }
    const ComponentB = {
      template: '<div>Componenet B</div>`
    }
    const ComponentC = {
      template: '<div>Componenet C</div>`
    }
    
    const app = Vue.createApp({
      components: {
        'component-a': ComponentA,
        'component-b': ComponentB
      }
    })
    
    
  • component vue 파일을 따로 생성하여 등록하기 (모듈 시스템 활용) Babel이나 Webpack 같은 모듈 시스템을 사용할 경우 컴포넌트 파일을 따로 만들어 import해와서 사용하면 됩니다.

      import ComponentA from './ComponentA'
      import ComponentC from './ComponentC'
    
      export default {
        components: {
          ComponentA,
          ComponentC
        }
        // ...
      }
    

컴포넌트 통신 방법

컴포넌트를 사용할때 부모 컴포넌트에 자식 컴포넌트를 삽입하여 사용합니다. 이때 부모 컴포넌트와 자식 컴포넌트와의 데이터를 주고 받아야 하는 경우가 있는데, 이때 Props, Emit을 사용하여 데이터를 주고 받습니다.

  • Props

    Props는 컴포넌트에 등록할 수 있는 커스텀 속성입니다. 값이 prop 속성에 전달되면, 그 값은 해당 컴포넌트 인스턴스의 속성이 됩니다.

    <div id="blog-posts-demo">
        <blog-post
          v-for="post in posts"
          :key="post.id"
          :title="post.title"
        ></blog-post>
      </div>
    
    const App = {
      data() {
        return {
          posts: [
            { id: 1, title: 'My journey with Vue' },
            { id: 2, title: 'Blogging with Vue' },
            { id: 3, title: 'Why Vue is so fun' }
          ]
        }
      }
    }
    
    const app = Vue.createApp(App);
    
    app.component('blog-post', {
      props: ['title'],
      template: `<h4></h4>`
    })
    
    app.mount('#blog-posts-demo')
    

    예제 코드를 보시면 blog-post라는 컴포넌트에 posts의 title 값을 전달하기 위해 props를 활용하여 전달하였습니다. (:{props명} 활용)
    이처럼 props는 부모 컴포넌트에서 자식 컴포넌트에게 필요한 데이터를 전해주는 역할을 합니다.

  • Event Emit

    emit은 자식컴포넌트에서 발생한 이벤트를 부모 컴포넌트에서 접근하여 사용할 수 있도록 해주는 역할을 합니다.

      <div id="blog-posts-events-demo" class="demo">
        <div :style="{ fontSize: postFontSize + 'em' }">
          <blog-post
            v-for="post in posts"
            :key="post.id"
            :title="post.title"
            @enlarge-text="postFontSize += 0.1"
          ></blog-post>
        </div>
      </div>
    
      const app = Vue.createApp({
        data() {
          return {
            posts: [
              { id: 1, title: 'My journey with Vue'},
              { id: 2, title: 'Blogging with Vue'},
              { id: 3, title: 'Why Vue is so fun'}
            ],
            postFontSize: 1
          }
        }
      })
    
      app.component('blog-post', {
        props: ['title'],
        template: `
          <div class="blog-post">
            <h4></h4>
            <button @click="$emit('enlargeText')">
              Enlarge text
            </button>
          </div>
        `
      })
    
      app.mount('#blog-posts-events-demo')
    

    예제를 보시면 blog-post 컴포넌트의 버튼의 click event가 emit으로 이벤트명을 넘겨주므로,
    부모 컴포넌트에서 @를 사용하여 이벤트를 설정하면 blog-post 컴포넌트의 버튼 클릭 시 부모 컴포넌트에서 설정해놓은 이벤트가 발생합니다.

Slots

slot은 부모 컴포넌트에서 특정 엘리먼트나 컨텐츠를 자식 컴포넌트에 전달하고 싶을 때 사용합니다.

  <div id="slots-demo" class="demo">
    <alert-box>
      Something bad happened.
    </alert-box>
  </div>
  app.component('alert-box', {
  template: `
      <div class="demo-alert-box">
        <strong>Error!</strong>
        <slot></slot>
      </div>
    `
  })

템플릿 Refs

때때로 자식 요소에 javascript를 이용하여 직접 접근해야 하는 경우가 있는데, 이럴때 사용하는 것이 ref 속성입니다.

<input ref="input" />
const app = Vue.createApp({})

app.component('base-input', {
  template: `
    <input ref="input" />
  `,
  methods: {
    focusInput() {
      this.$refs.input.focus()
    }
  },
  mounted() {
    this.focusInput()
  }
})

위 예제는 컴포넌트가 마운트 되었을 때 input에 focus효과를 부여합니다.

동적 컴포넌트

컴포넌트 태그를 자신이 정의한 컴포넌트명이 아닌 그냥 component tag를 사용하고, is 속성을 사용하면 됩니다.

<div id="dynamic-component-demo" class="demo">
  <button
     v-for="tab in tabs"
     :key="tab"
     @click="currentTab = tab"
   >
    
  </button>

  <component :is="currentTabComponent"></component>
</div>
const app = Vue.createApp({
  data() {
    return {
      currentTab: 'Home',
      tabs: ['Home', 'Archive']
    }
  },
  computed: {
    currentTabComponent() {
      return 'tab-' + this.currentTab.toLowerCase()
    }
  }
})

app.component('tab-home', {
  template: `<div class="demo-tab">Home component</div>`
})

app.component('tab-archive', {
  template: `<div class="demo-tab">Archive component</div>`
})

app.mount('#dynamic-component-demo')

위 예제는 버튼 클릭 시 해당 컴포넌트를 보여주는 코드입니다. computed 속성인 currentTabComponent가 클릭한 버튼의 tab명을 가지고와서 tab-[tabName]화, component 태그의 is속성에 해당 computed 속성을 부여하면 같은 이름을 가진 컴포넌트를 component 태그에 붙여줍니다.

컴포넌트 사용 시 주의 사항

컴포넌트를 작성하거나 사용할때 권장하거나, 주의해야 할 사항을 3가지로 추렸습니다.

1) 컴포넌트 명은 kebap-case로 작성합니다.

2) 자바스크립트 내에서 prop명과 이벤트 핸들러 파라미터는 camelCase로 작성하지만 HTML 내에서 kebab-case로 해당 항목을 사용합니다.

3) ul, ol, table, select와 같은 일부 HTML 엘리먼트는 내부에 특정 엘리먼트(li, tr, option)만 표시될 수 있으므로, 컴포넌트 태그를 사용하게 되면 에러가 발생합니다.

이때는 v-is 특수 디렉티브를 사용하여 문제를 해결할 수 있습니다.

  <!-- v-is의 값은 자바스크립트 문자열 리터럴이어야 함 -->

  <!-- 잘못됨. 아무것도 렌더링 되지 않음 -->
  <tr v-is="blog-post-row"></tr>

  <!-- 올바름-->
  <tr v-is="'blog-post-row'"></tr>


컴포넌트는 설명드린 기초적인 부분 이외에 각각의 요소의 세부적인 사항이 많습니다.
컴포넌트에 대한 심화적인 부분을 보고싶으시다면 아래의 사이트를 참고하시면 좋을 것 같습니다.
Vue 공식 가이드 컴포넌트 상세 설명


10. 트랜지션 & 애니메이션

트랜지션과 애니메이션은 css를 활용하여 화면에 시각적 효과를 부여합니다.
vue에서는 이 작업에 도움이 되는 컴포넌트 및 스타일 클래스를 제공합니다.

transition 컴포넌트를 활용하여 다른 컴포넌트들의 진입/진출에 특정 애니메이션 효과를 부여합니다.
transition-group 컴포넌트를 활용하여 여러 엘리먼트들을 한번에 묶어서 바로 효과를 부여합니다.

진입 / 진출

진입/진출 트랜지션에는 6가지 클래스가 적용됩니다.

vue transition class

 .v-enter-from: enter의 시작 상태. 엘리먼트가 삽입되기 전에 적용되고 한 프레임 후에 제거됩니다.
 .v-enter-to: 진입 상태의 끝에서 실행됩니다. 엘리먼트가 삽입된 후(동시에v-enter-from 가 제거됨) 트랜지션/애니메이션이 끝나면 제거되는 하나의 프레임을 추가했습니다.
 .v-enter-active: enter의 활성 상태. 전체 진입 단계 동안 적용됩니다. 엘리먼트가 삽입되기 전에 적용됩니다. 트랜지션 / 애니메이션이 완료되면 제거됩니다.
 
 .v-leave-from: leave를 위한 시작 상태. 진출 트랜지션이 트리거 될 때 적용되고 한 프레임 후에 제거됩니다.
 .v-leave-to: 진출 상태 끝에서 실행됩니다. 진출 트랜지션이 트리거되고 (동시에 v-leave-from 가 제거됨) 트랜지션 / 애니메이션이 끝나면 제거되는 하나의 프레임을 추가했습니다.
 .v-leave-active: leave의 활성 상태. 전체 진출 상태에서 적용됩니다. 진출 트랜지션이 트리거되면 적용되고 트랜지션 / 애니메이션이 완료되면 제거됩니다.

타이밍과 효과를 잘 이용해서 더욱 보기 좋은 화면을 만들어 낼 수 있습니다. 아래 예제는 p태그를 조건부 렌더링할때 트랜지션 효과를 주는 간단한 예제입니다.

<div id="demo">
  <button @click="show = !show">
    Toggle
  </button>

  <transition name="fade">
    <p v-if="show">hello</p>
  </transition>
</div>
const Demo = {
  data() {
    return {
      show: true
    }
  }
}

Vue.createApp(Demo).mount('#demo')
/*위에서 설명한 트랜지션 클래스의 'v-' 부분을 '{트랜지션명}-' 으로 바꿔서 사용한다.'*/

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

애니메이션을 부여하는 것도 동일한 방식으로 구현하면 됩니다.

트랜지션과 애니매이션은 동시에 사용가능합니다.

.[transition-name]-enter-active,
.[transition-name]-leave-active {
  animation: bounce-in 0.5s;
}

.[transition-name]-enter-from,
.[transition-name]-leave-to {
   animation: bounce-in 0.5s reverse;
}

같은 방식으로 컴포넌트도 트랜지션 부여가 가능합니다.

<div id="demo" class="demo">
  <input v-model="view" type="radio" value="v-a" id="a"><label for="a">A</label>
  <input v-model="view" type="radio" value="v-b" id="b"><label for="b">B</label>
  <transition name="component-fade" mode="out-in">
    <component :is="view"></component>
  </transition>
</div>
.component-fade-enter-active,
.component-fade-leave-active {
  transition: opacity 0.3s ease;
}

.component-fade-enter-from,
.component-fade-leave-to {
  opacity: 0;
}

.demo {
  font-family: sans-serif;
  border: 1px solid #eee;
  border-radius: 2px;
  padding: 20px 30px;
  margin-top: 1em;
  margin-bottom: 40px;
  user-select: none;
  overflow-x: auto;
}
const Demo = {
  data() {
    return {
      view: "v-a"
    };
  },
  components: {
    "v-a": {
      template: "<div>Component A</div>"
    },
    "v-b": {
      template: "<div>Component B</div>"
    }
  }
};

Vue.createApp(Demo).mount("#demo");

위 예제는 라디오를 선택했을때 선택한 라디오의 value값의 만족하는 컴포넌트를 보여주는 예제입니다.

리스트

리스트 트랜지션도 유사한 방법으로 구현할 수 있습니다.

<div id="flip-list-demo">
  <button @click="shuffle">Shuffle</button>
  <transition-group name="flip-list" tag="ul">
    <li v-for="item in items" :key="item">
      
    </li>
  </transition-group>
</div>
body {
  margin: 30px;
}

.flip-list-move {
  transition: transform 0.8s ease;
}
const Demo = {
  data() {
    return {
      items: [1, 2, 3, 4, 5, 6, 7, 8, 9]
    };
  },
  methods: {
    shuffle() {
      this.items = _.shuffle(this.items);
    }
  }
};

Vue.createApp(Demo).mount("#flip-list-demo");

위 예제는 숫자 리스트의 순서를 섞을 때 트랜지션을 부여합니다.

위 예제들은 비교적 기초적이고 간단한 예제들을 보여드렸습니다만, 트랜지션과 애니메이션을 부여하는 방법은 엄청나게 다양합니다.
능수능란하고 적절하게 이용한다면 더욱 완성도 있고 아름다운 화면을 구성할 수 있습니다.


Tags:

Categories:

Updated:

Leave a comment