Vue.js - VueCLI (node.js), 컴포넌트(1) - props, emits, provide, inject

hanjae_99·2023년 12월 27일

Udemy

목록 보기
5/9
post-thumbnail

Node.js 설치 및 VueCLI 설치

이전 게시글에서는 스크립트 링크를 html 파일 내에 작성하여 Vue 앱을 이용하였다.
이렇게 해서 브라우저에서 해당 index.html 파일을 그대로 여는 방식보다는 node.js 를 이용하여 http localhost 를 이용한 개발 방식을 구축하는것이 좀 더 큰 프로젝트를 진행할때도 용이하다.

node.js 설치

먼저 node.js 는 다른 블로그를 참고하여 설치를 진행하였다 (M1 mac 기준)

homebrew 를 이용하여 nvm (node version manager) 을 먼저 다운로드 후 nvm 을 통해 node 버전별 다운로드 및 관리를 진행

$ brew install nvm

이후 nvm 환경변수 등록
현재 zshell 을 사용하고 있으므로 .zshenv 파일을 작성

$ vi ~/.zshenv

vi 편집기에 진입 후 아래 코드를 입력

export NVM_DIR="$HOME/.nvm"
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"  # This loads nvm
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"  # This loads nvm bash_completion

환경변수 적용

$ source ~/.zshenv

이후 nvm 명령어를 사용하여 lts 버전의 node 를 다운로드해주자

$ nvm install --lts

node 와 npm 모두 lts 버전으로 정상적으로 설치완료!

Vue CLI 설치

공식사이트를 참고하여 설치
Vue CLI 를 이용하면 리액트 초기 앱을 구축하는것과 같이
vue 커맨드를 이용하여 초기 vue 앱을 간단히 설정가능하다.
또한 .vue 확장자의 컴포넌트 파일 관리도 용이해진다.

npm install -g @vue/cli
# OR
yarn global add @vue/cli

npm 이나 yarn 패키지 매니저를 사용하여 VueCLI 설치

이후엔 vue create <app-name> 명령어로 vue app 을 생성 가능

이후 여러 선택 안내문이 나오는데, 사용할 vue 버전에 따라 선택 후
특별히 같이 설치할 패키지가 없다면 기본 상태로 설치 진행

설치가 완료되면 React 와 유사하게 package.json, node 모듈 폴더, public 및 src 폴더가 생성된다.

초기 create 명령어로 생성된 vue app 은 node 모듈 폴더도 자동 생성하나, GitHub 와 같은 곳에서 공유받은 파일은 크기가 상대적으로 큰 모듈 폴더가 제외되어 있으므로 npm install 명령어로 패키지파일 설치를 진행해주자.

이후 npm run serve 명령을 통해 개발용 localhost 서버를 띄운 후
(아마도 기본은 8080 포트)
개발을 진행하면 된다!

여러 앱? 여러 컴포넌트!

하나의 HTML 페이지 내에서 여러 독립적인 Vue 앱을 생성 후 제어할 수 있다.
반면에 SPA(single page application) 를 구축하는 경우 일반적으로
하나의 '루트 앱' 으로 작업하고 여러 컴포넌트를 사용하여 사용자 인터페이스를 개발한다.

Vue 앱은 서로 독립적이므로 통신이 불가능(앱 간에 데이터 공유 및 업데이트 불가)
이에 반해 컴포넌트는 컴포넌트 간에 데이터를 교환할 수 있는 특정 통신 매커니즘이 존재.
따라서 여러 컴포넌트를 연결시켜 하나의 루트 앱으로 작업해도 하나의 연결된 UI 를 구축 가능함.

  • FriendContact.vue
<template>
  <li>
    <h2>{{ name }}</h2>
    <button @click="toggleDetails">
      {{ detailsAreVisible ? 'Hide' : 'Show' }} Details
    </button>
    <ul v-if="detailsAreVisible">
      <li>
        <strong>Phone:</strong>
        {{ phoneNumber }}
      </li>
      <li>
        <strong>Email:</strong>
        {{ emailAddress }}
      </li>
    </ul>
  </li>
</template>

<script>
export default {
  data() {
    return {
      detailsAreVisible: false,
      friend: {
        id: 'manuel',
        name: 'Manuel Lorenz',
        phone: '0123 45678 90',
        email: 'manuel@localhost.com',
      },
    };
  },
  methods: {
    toggleDetails() {
      this.detailsAreVisible = !this.detailsAreVisible;
    },
  },
};
</script>

<style>
...
</style>

컴포넌트 사용 간단 예시

React 에서의 컴포넌트와 유사하게 사용자 정의 커스텀 태그를 만들고, 해당 태그 내에서
관리할 데이터 프로퍼티나, 로직을 설정해주면 된다.

template 영역은 로직이 출력될 html 코드 부분을 작성하고 한 컴포넌트 내에서는 해당 script 영역과 로직, 데이터를 공유한다.

script 영역은 vue app 구성 객체와 같이 해당 컴포넌트내에서 사용될 data, methods, computed 프로퍼티등을 작성한다.

style 영역은 css 에 사용된다.

  • main.js
import { createApp } from 'vue';

import App from './App.vue';
import FriendContact from './components/FriendContact.vue';

const app = createApp(App);

app.component('friend-contact', FriendContact);

app.mount('#app');

이렇게 작성된 컴포넌트는 최상위 부모 컴포넌트인 App.vue 에서 관리되도록 하며, main.js 파일에서 index.html 파일과 마운트 및 vue 메인 앱 - 컴포넌트 간의 연결을 설정한다.

app.component(_) 코드로 작성하며, 첫번째 인자는 컴포넌트 이름을 지정
이때 '-' 대시 기호를 사용하여 두개의 문자로 지어주어야 한다.(기본 HTML 태그와의 중복 방지)
두번째 인자로 프로퍼티와 로직을 선언한 객체(컴포넌트)를 전달해준다. (.vue 확장자인 컴포넌트 파일 전달)

이렇게 컴포넌트를 활용하면 각 컴포넌트가 캡슐화되어 독립적으로 작용하게 된다.

컴포넌트 통신 - props, emits, provide, inject

UI 를 컴포넌트별로 분리하여 구현하였더라도 각 컴포넌트끼리 통신이 필요한 경우가 있다.
해당 경우에 사용될 props 와 이벤트처리 emits 에 대해 알아보자.

  • App.vue
<template>
  <section>
    <header>
      <h1>My Friends</h1>
    </header>
    <new-friend @add-contact="addContact"></new-friend>
    <ul>
      <friend-contact
        v-for="friend in friends"
        :key="friend.id"
        :id="friend.id"
        :name="friend.name"
        :phone-number="friend.phone"
        :email-address="friend.email"
        :is-favorite="friend.isFavorite"
        @toggle-favorite="toggleFavoriteStatus"
        @delete-contact="deleteContact"
      ></friend-contact>
    </ul>
  </section>
</template>

먼저 App.vue 파일을 살펴보면 최상위 부모 컴포넌트로써 자식 커스텀 태그인
friend-contact, new-friend 컴포넌트를 지니고 있다.

이때 부모 -> 자식으로 단방향 데이터 전달이 가능한데 이때 사용되는것이 props 이다.

현재 위 코드에선 friend-contact 컴포넌트에 동적으로 key, id, name, ..., is-favorite 등 여러 props 를 전달하고 있다.

  • FriendContact.vue
<template>
  <li>
    <h2>{{ name }} {{ isFavorite ? '(favorite)' : '' }}</h2>
    <button @click="toggleFavorite">Toggle Favorite</button>
    <button @click="toggleDetails">
      {{ detailsAreVisible ? 'Hide' : 'Show' }} Details
    </button>
    <ul v-if="detailsAreVisible">
      <li>
        <strong>Phone:</strong>
        {{ phoneNumber }}
      </li>
      <li>
        <strong>Email:</strong>
        {{ emailAddress }}
      </li>
    </ul>
    <button @click="$emit('delete-contact', id)">Delete</button>
  </li>
</template>

<script>
export default {
  // props: ['name', 'phoneNumber', 'emailAddress', 'isFavorite'],
  props: {
    id: {
      type: String,
      required: true,
    },
    name: {
      type: String,
      required: true,
    },
    phoneNumber: {
      type: String,
      required: true,
    },
    emailAddress: {
      type: String,
      required: true,
    },
    isFavorite: {
      type: Boolean,
      required: false,
      default: false,
      // validator: function (value) {
      //   return value === '1' || value === '0';
      // },
    },
  },
  emits: ['toggle-favorite', 'delete-contact'],
  data() {
    return {
      detailsAreVisible: false,
      friend: {
        id: 'manuel',
        name: 'Manuel Lorenz',
        phone: '0123 45678 90',
        email: 'manuel@localhost.com',
      },
    };
  },
  methods: {
    toggleDetails() {
      this.detailsAreVisible = !this.detailsAreVisible;
    },
    toggleFavorite() {
      this.$emit('toggle-favorite', this.id);
    },
  },
};
</script>

friend-contact 컴포넌트는 script 영역에서 props 프로퍼티로 해당 props 를 받을 수 있는데, 이때 받을 타입과 필수 여부를 검증할 수 있다.

다만 문제점이 있는데, 데이터의 흐름방향은 단방향이기 때문에 자식에서 변경사항이 있을 경우, 부모 컴포넌트에게 전달할 수가 없다는 것!

이럴 때 사용하는 방법으로 이벤트 발생기, 즉 emits 이 있다.

위 코드에서는 즐겨찾기 친구와, 특정 친구의 연락처를 삭제하는 기능을 emits 으로 구현하였다.

emits 은 vue 내장객체로 $ 달러기호를 사용하여 호출하고, 상위 컴포넌트에 사용자가 지정한 문자열 이름으로 된 이벤트를 전달한다. 이때 매개변수도 같이 넘길 수 있다.

data 프로퍼티와 같이 emits 프로퍼티를 작성하면 개발 시, 해당 컴포넌트가 어떠한 이벤트를 발생시키고 있는지 확인할 수 있어 편리하다.

  • App.vue
<template>
  <section>
    <header>
      <h1>My Friends</h1>
    </header>
    <new-friend @add-contact="addContact"></new-friend>
    <ul>
      <friend-contact
        v-for="friend in friends"
        :key="friend.id"
        :id="friend.id"
        :name="friend.name"
        :phone-number="friend.phone"
        :email-address="friend.email"
        :is-favorite="friend.isFavorite"
        @toggle-favorite="toggleFavoriteStatus"
        @delete-contact="deleteContact"
      ></friend-contact>
    </ul>
  </section>
</template>

<script>
export default {
  data() {
    return {
      friends: [
        {
          id: 'manuel',
          name: 'Manuel Lorenz',
          phone: '0123 45678 90',
          email: 'manuel@localhost.com',
          isFavorite: true,
        },
        {
          id: 'julie',
          name: 'Julie Jones',
          phone: '0987 654421 21',
          email: 'julie@localhost.com',
          isFavorite: false,
        },
      ],
    };
  },
  methods: {
    toggleFavoriteStatus(friendId) {
      const toggleFriend = this.friends.find(
        (friend) => friend.id === friendId
      );
      toggleFriend.isFavorite = !toggleFriend.isFavorite;
    },
    deleteContact(friendId) {
      const newFriendList = this.friends.filter(
        (friend) => friend.id !== friendId
      );
      this.friends = newFriendList;
    },
  },
};
</script>

App.vue 파일에서 v-on (@ 기호) 를 이용하여 지정한 이름의 이벤트를 감지할 수 있고, 첨부된 매개변수를 이용하여 특정 요소를 삭제하는 로직도 구현이 가능하다.

이렇게 기본적으로 컴포넌트간 통신은 props, emits 을 이용하여 이루어진다.
하지만 컴포넌트내에 또 컴포넌트가 있고, 징검다리형태로 데이터가 전달되는데 해당 징검다리가 여러개, 즉 데이터를 전달하는 컴포넌트와 사용할 컴포넌트의 거리가 멀때 provide, inject 라는 내장 기능을 활용할 수 있다.

  • App.vue
<template>
  <div>
    <active-element
      :topic-title="activeTopic && activeTopic.title"
      :text="activeTopic && activeTopic.fullText"
    ></active-element>
    <knowledge-base></knowledge-base>
  </div>
</template>

<script>
export default {
  data() {
    return {
      topics: [
        {
          id: 'basics',
          title: 'The Basics',
          description: 'Core Vue basics you have to know',
          fullText:
            'Vue is a great framework and it has a couple of key concepts: Data binding, events, components and reactivity - that should tell you something!',
        },
        {
          id: 'components',
          title: 'Components',
          description:
            'Components are a core concept for building Vue UIs and apps',
          fullText:
            'With components, you can split logic (and markup) into separate building blocks and then combine those building blocks (and re-use them) to build powerful user interfaces.',
        },
      ],
      activeTopic: null,
    };
  },
  provide() {
    return {
      topics: this.topics,
    };
  },
};
</script>
  • KnowledgeBase.vue
<template>
  <section>
    <h2>Select a Topic</h2>
    <knowledge-grid></knowledge-grid>
  </section>
</template>

<script>
export default {};
</script>
  • knowledgeGrid.vue
<template>
  <ul>
    <knowledge-element
      v-for="topic in topics"
      :key="topic.id"
      :id="topic.id"
      :topic-name="topic.title"
      :description="topic.description"
    ></knowledge-element>
  </ul>
</template>

<script>
export default {
  inject: ['topics'],
};
</script>

다른 예시 파일의 컴포넌트를 보자.

여기서 knowledgeBase.vue 컴포넌트는 사실상 중간다리에 불과하고
실제로는 knowledgeGrid.vue 에서 topics 가 사용되므로
이때 provide, inject 를 사용하여 먼 거리의 통신을 간단하게 수행할 수 있다.

그럼 props, emits 대신 모두 다 provide, inject 를 사용하는건 어떨까?

물론 provide, inject 를 사용하여 중간다리를 건너뛰고 실제 사용되는곳에서만 사용되는건 굉장히 합리적이고 간결한 코드 작성이 될 수 있다.
하지만 props, emits 는 기본적인 컴포넌트간 통신 매커니즘이고, provide, inject 를 남용하게 되면 간단한 프로젝트면 몰라도 규모가 클 경우 어디에서 어디로 참고가 되고있는지 컴포넌트를 싹 다 확인해야 하기에
징검다리의 개수가 정말 많거나(컴포넌트간 거리가 멀거나) 할때만 사용하는 것이 좋을 것 같다.

참고

VueCLI - installation
[Node.js] Mac 맥(m1) - homebrew, node, npm, nvm 설치 관련

profile
단 하나밖에 없는 톱니바퀴

0개의 댓글