Vue.js 학습 #3

하루히즘·2022년 2월 16일
0

Vue.js 학습 기록

목록 보기
3/3

SimpleBBS를 Vue.js와 Nuxt.js, Node.js 백엔드 프레임워크(미정)로 기술 스택을 변경해보기로 결정했다. 얼마나 걸릴지는 모르겠지만 일단 Vue.js 기초부터 계속 학습하고 그러면서 자바스크립트도 다시 복습하려고 한다.

학습 내용

생명주기

Spring에서 Bean 객체들의 생명주기를 관리할 수 있었던 것처럼 Vue.js에서도 그 생명주기를 관리할 수 있는 메서드를 제공한다. Vue 인스턴스를 생성할 때 다음처럼 메서드를 정의하는 방식이며 다음과 같은 종류가 있다.

new Vue({
 el: '#app',
 data: {
  // ...
 },
 created() {
  // ...
 },
 ...
}

각 생명주기 내에서는 this 키워드를 이용하여 Vue 인스턴스를 가리킬 수 있다. 중요한 것은 화살표 함수(() => { ... })를 생명주기 메서드로 사용하지 않는 것인데 그 특성상 this 키워드가 Vue 인스턴스를 가리키지 않기 때문이다.

Vue.js의 공식 가이드에서는 라이프사이클 다이어그램을 제공하고 있는데 한 번 훑어보면 그 흐름을 이해하기 좋을 것 같다. 생명주기 메서드는 이 곳에서도 간단하게 설명하고 있다.

created()

created는 인스턴스가 생성되고 생성 시 정의한 data, method 등을 활용할 수 있을 때 호출된다. 그러나 HTML 요소같은 DOM 컴포넌트가 그려지기 전에 동작하기 때문에 this.$el 처럼 DOM에 접근할 수 없으며 대신 API를 통해 외부 데이터를 가져와서 초기화하거나 그런 작업에 적합하다.

mounted()

mounted는 인스턴스가 마운트되고 DOM 컴포넌트들이 그려졌을 때 호출된다. Vue는 브라우저에서 보이는 실제 DOM과 별개로 가상의 DOM(Virtual DOM), 즉 this.$el을 메모리에 구축한다. 그리고 애플리케이션 실행 중에 발생하는 변경사항을 실제 DOM, 즉 el로 지정된 HTML 컴포넌트에 반영하기 전에 가상 DOM에 먼저 반영한다.

마운트는 이후 이 가상 DOM을 실제 DOM에 반영하는 과정을 뜻하며 this.$el 구문을 통해 DOM 트리에 접근할 수 있다. 유의할 것은 모든 컴포넌트가 그려진 것을 보장하지 않기 때문에 별도의 옵션을 사용해야 한다.

updated()

updated는 Vue 인스턴스의 데이터가 변해서 DOM을 다시 그리거나 변경할 때 호출된다. 이전의 감시자(watcher) 기능이 Vue 인스턴스 자체를 감시하고 있는 모습이라 할 수 있는데 중요한 것은 위처럼 this.$el로 DOM 트리에 접근 시 변경 DOM에 접근하게 된다.

destroyed()

destroyedvm.$destroy 메서드로 Vue 인스턴스가 파괴될 때 맨 마지막에 호출된다. 인스턴스에 등록했던 이벤트 리스너, 지시어 등은 제거된 상태다.

beforeXXX()

좀 특이한 메서드로 위의 4가지 생명주기 메서드 이름을 현재형으로 바꾸고 before을 붙이면 해당 생명주기 메서드를 실행하기 이전에 호출되는 메서드를 하나 더 만들 수 있다.

컴포넌트

Vue.js에서는 컴포넌트라는 재사용가능한 독립적인 코드를 사용하여 생산성을 높일 수 있다. 중요한 점은 컴포넌트는 하나의 Vue 인스턴스로서 지금까지는 최상위(root) 인스턴스를 하나의 Vue 인스턴스로 두었다면 이제는 컴포넌트를 사용하여 인스턴스 내부에 마치 모듈처럼 또다른 인스턴스를 두는 방식으로 활용할 수 있다.

예를 들어 트위터같은 SNS에서는 여러 트윗들이 타임라인을 따라 그려진다. 이를 HTML 코드를 하드코딩하고 반복문으로 그려낼 수도 있지만 하나의 '트윗'을 나타내는 독립적인 컴포넌트를 이용한다면 책임을 분리하고 유연하게 조합할 수 있다.

component, components

Vue.component('tweet-component', {
// options
});

컴포넌트는 위처럼 component 메서드를 이용하여 전역(global)적으로 등록할 수 있으며 Vue 인스턴스 내에서 사용해야 하기 때문에 Vue 인스턴스 생성 이전에 컴포넌트를 등록해야 한다. 전역적이라는 것은 최상위 인스턴스 뿐 아니라 다른 컴포넌트에서도 해당 컴포넌트를 재사용할 수 있다는 것을 의미한다.

만약 최상위 인스턴스에서만 컴포넌트를 재사용하도록 강제하고 싶다면, 즉 지역(local)적으로 등록하고 싶다면 Vue 인스턴스 생성 시 components에 키-값 속성으로 등록할 수 있다.

컴포넌트 등록 시에는 파라미터로 각각 컴포넌트의 이름과 속성을 전달할 수 있으며 추후 HTML 코드에 해당 컴포넌트의 이름으로 직접 커스텀 태그를 사용하여 그려낼 수 있다.

<div>
  <tweet-component>
    ...
  </tweet-component>
</div>

속성에는 template에 해당 컴포넌트의 HTML 코드를 정의하거나 props에 받을 데이터를 정의하는 등 해당 컴포넌트에 관련된 속성을 정의할 수 있다. 중요한 것은 컴포넌트의 템플릿은 하나의 최상위 요소에 담겨있어야 한다. 예를 들어 다음처럼 하나의 최상위 <div>에 담겨있는 것은 문제가 없다.

let component = {
  template: `
    <div>
      ...
    </div>
  `
}

그러나 다음처럼 두 개의 최상위 <div>에 담길 수 없다.

let component = {
  template: `
    <div>
      ...
    </div>
    <div>
      ...
    </div>
  `
}

컴포넌트를 사용하면 직접 HTML 코드를 그려넣는 것보다 의존성을 낮추고 해당 컴포넌트가 어떤 역할을 하는지 명확하게 알 수 있다. Java 같은 객체 지향 프로그래밍 언어에서 왜 클래스를 분리하고 의존성을 낮추려고 노력하는지를 생각해보면 컴포넌트의 장점도 바로 이해할 수 있다.

props

컴포넌트의 template 속성에 지정된 HTML 템플릿에 데이터를 렌더링하기 위해서는 props 옵션을 사용할 수 있다.

컴포넌트로 생성된 Vue 인스턴스는 el에서 생성된 최상위 Vue 인스턴스의 자식 인스턴스 관계로 볼 수 있다. 최상위 인스턴스에서 컴포넌트를 데이터로 렌더링하기 위해서는 컴포넌트에 데이터를 전달해줘야 하며 이를 컴포넌트 생성 시 props 라는 옵션으로 미리 지정해 두는 것이다.

Vue.component('tweet-component', {
 template: ` 
  // ...
 `,
 props: ['tweet']
});

이런 식으로 등록된 컴포넌트는 메인 HTML에서 아래처럼 사용할 수 있다.

<div id="app">
  <tweet-component v-for="tweet in tweets"
    :key="tweet.id"
    :tweet="tweet">
  </tweet-component>
</div>

참고로 컴포넌트에서는 props에 등록된 이름의 prop을 사용해야 한다. 좀 헷갈릴 수 있지만 아래처럼 컴포넌트의 prop에 Vue 인스턴스의 데이터를 bind 해서 전달하는 것을 볼 수 있다.

만약 props를 통하지 않고(예를 들어 props: ["list1"]) 직접 list를 전달하면 분명 컴포넌트 내의 지시어 표현식에서 list라는 필드를 참조하고 있지만 동작하지 않는다. 언급했듯이 부모 컴포넌트(최상위 Vue 인스턴스)에서 자식 컴포넌트로 데이터를 넘겨주는 방식으로 동작하기 때문.

단순히 어떤 파라미터를 받겠다고 명시하는 것 외에 이 prop에 대해 validation을 적용할 수도 있다. 기존에는 배열에 문자열로 프롭을 전달했다면 각 프롭을 필드로 옵션 객체를 정의해서 다음처럼 명시적으로 타입을 지정하거나 필수 여부를 전달할 수 있다.

Vue.component('tweet-component', {
 template: ` 
   // ...
 `,
 props: {
  tweet: {
   type: Object,
   required: true
  }
 }
});

이렇게 prop을 이용해 의존관계를 단방향(부모 컴포넌트가 자식 컴포넌트로 주입)으로 의존하면 컴포넌트가 상속 구조로 이루어졌을 때 그 관계를 파악하기 쉽다. 만약 자식 컴포넌트에서 부모 컴포넌트에 영향을 끼치려면 별도의 이벤트 처리로 구현해야 한다.

커스텀 이벤트

Vue.js의 커스텀 이벤트는 언급했듯이 컴포넌트 간 통신에 주로 사용된다. 예를 들어 위의 트위터 같은 SNS에서 자식 컴포넌트(트윗)에 대한 사용자 입력으로 인해 부모 컴포넌트(트윗 목록)에 변화가 필요할 경우 일련의 이벤트 처리를 통해 구현할 수 있는 것이다.

커스텀 이벤트는 Vue 인스턴스의 emit 메서드를 통해 이벤트의 이름과 이벤트 데이터를 포함하여 발생시킬 수 있다.

this.$emit('name-of-event', {
  data: {
    course: '30 days of vue'
  }
}

이렇게 발생한 이벤트는 v-on 지시어를 이용하여 해당 이벤트의 이름으로 처리기를 구현할 수 있다. 이때 처리기는 당연히 해당 컴포넌트에 등록해야 한다. 예를 들어 도서에서 제공하고 있는 트위터 예제에서 트윗 내용을 다루는 tweet-content 컴포넌트에서 'add'라는 이름의 커스텀 이벤트가 발생한다면 이 컴포넌트를 사용하는 tweet-component 컴포넌트에서는 다음처럼 @add, 즉 v-on:add로 이벤트를 처리할 수 있다.

Vue.component('tweet-component', {
 template: `
  ...
    <article class="media">
     <tweet-content :tweet="tweet" @add="doSomething">
     </tweet-content>
    </article>
  ...  
 `,
 props: ['tweet']
});

만약 이벤트 객체를 파라미터로 넘기고 싶다면 아래처럼 emit 메서드의 두 번째 파라미터에 $event를 넘길 수 있다.

<button @click="$emit('add', $event)">HELLO</button>

그러면 다음처럼 $event 변수를 참조하여 이벤트 처리기를 직접 표현식으로 구성하거나 미리 Vue 인스턴스에 정의된 함수로 처리할 수 있다.

<tweet-component v-bind:list="targetItems" v-on:add="this.console.log($event)"></tweet-component>

컴포넌트의 data

Vue 인스턴스의 생성자에서는 초기 데이터를 저장하는 공간으로 data 필드를 활용했다. Vue 컴포넌트 역시 Vue 인스턴스의 일종이기 때문에 data 필드를 사용할 수 있는데 컴포넌트에서는 초기값을 객체로 반환하는 data 함수로 정의해야 한다.

왜냐면 해당 컴포넌트를 여러번 사용할 때 Vue 인스턴스에서는 각 컴포넌트의 data를 고유하게 구분하지 않기 때문에 여러 컴포넌트 객체에서 같은 데이터 객체를 참조하는 문제가 발생하기 때문이다.

let componentObject = {
 template: '<p>{{message}}</p>',
 data() {
  return {
   message: 'Greetings!'
  }
 }
}

그렇기 때문에 각 컴포넌트에서는 각자 고유한 데이터 객체를 가질 수 있도록 data를 필드가 아니라 초기 데이터 값을 포함하는 객체를 반환하는 함수로 정의해야 한다.

template

컴포넌트의 템플릿을 정의할 때는 대개 여러 라인으로 이루어진 HTML 태그를 사용해야 하기 때문에 backtick(`)을 이용한 multi-line string으로 작성한다. 컴포넌트의 템플릿을 정의하는 방법은 위의 예제처럼 컴포넌트 선언 시 template 필드에 등록하는 방법이 권장된다.

컴포넌트를 사용하는 위치, 즉 HTML 코드 상에서 inline-template 키워드를 이용해 삽입하는 인라인 방식이나 자바스크립트(<script>)로 컴포넌트를 정의하는 방식이 있지만 둘 다 가독성 문제나 컴포넌트와 컴포넌트의 템플릿이 너무 분리된다는 문제가 있다.

그렇다고 이전처럼 문자열로 정의하는 템플릿은 IDE나 편집기의 문법 지원, 코드 하이라이트 기능을 쓸 수 없고 가독성 면에서도 딱히 좋은 방법은 아닌데 그러면 어떻게 할 수 있을까? 이를 위해서 Vue.js에서는 Single File Component라는 기능을 지원한다.

Single File Component

Single File Component는 *.vue 같은 별도의 파일에서 컴포넌트의 HTML, CSS, JS를 정의할 수 있다. HTML처럼 태그로 나뉘지만 최상위 태그를 지정할 필요 없이 <template>에 HTML 요소, <script>에 컴포넌트의 구성 요소, <style>에 CSS를 작성하면 된다.

<template>
 <div class="hello-world">
  <h2>{{ getGreeting }}</h2>
   <p>This is the Hello World component.</p>
 </div>
</template>

<script>
export default {
 name: 'HelloWorld',
 data () {
  return {
   reversedGreeting: '!dlrow olleH'
  }
 },
 computed: {
  getGreeting() {
   return this.reversedGreeting
    .split("")
    .reverse()
    .join("");
  }
 }
}
</script>

<style scoped>
.hello-world {
 width: 100%;
 text-align: center;
}
</style>

도서에서 제공하는 예제는 위와 같다. 자바스크립트 코드를 담는 <script>에서는 기존의 Vue 컴포넌트 초기화에 사용하던 객체과 동일한 타입의 객체를 export하는 것을 볼 수 있다.

Vue 인스턴스와 별도의 파일에서 컴포넌트를 모듈처럼 정의하여 전통적인 웹 개발 방식(화면별 html, css, js 파일)과 달리 컴포넌트별로 단 하나의 파일로 명확하게 관리할 수 있다는 장점이 있다.

그러나 이렇게 파일만 정의한다고 자동으로 Vue 인스턴스가 이 컴포넌트를 읽어올 순 없다. 이렇게 여러 모듈로 관리된 애플리케이션을 구성하기 위해서는 Webpack같은 빌드 툴이 필요하며(gradle 같은 건가?) 이런 것까지 같이 포함해서 개발 환경을 구성할 수 있도록 Vue.js에서는 vue-cli라는 프로그램을 지원한다.

후기

Vue.js 공부를 시작하면서 제일 궁금했던 것이 컴포넌트를 어떻게 정의하고 등록하는지였다. 빈 <div>에 직접 HTML을 작성하진 않는 것 같은데 Vue.component()로 컴포넌트를 등록한다면 수많은 컴포넌트를 어떻게 등록하는 건지 궁금했는데 Single File Component와 빌드 툴이란 걸 알 수 있었다. 이제 vue-cli를 쓰는 것 같은데 Node.js 환경(npm 등)도 좀 알아봐야겠다.

profile
YUKI.N > READY?

0개의 댓글