[TIL] Vue.js와 친해지기

ㅜㅜ·2023년 4월 11일
1

Today I learn

목록 보기
76/77
post-thumbnail

이번생에 Vue.js는 처음이라...

취업 준비를 하면서 느낀 건데 생각보다 Vue.js를 쓰는 회사들이 많았다.
코드너리에서 React 다음으로 Vue를 사용하는 회사들이 많았던 것을 생각하면 놀랄 일은 아닐지도...

이번에 취뽀를 하게 되고 (갸악🥹) 회사에서 Vue.js와 관련 툴들을 사용할 예정이라 첫 출근 전 조금씩 공부를 해보려고 한다.

코딩 애플에서 들었던 바로는 Vue.js는 right way가 있는 프레임워크라 리액트를 썼을 때보다 협업할 때 좋다고 했던 것 같은데, 최근에 Next.js라는 프레임워크를 사용해보니 확실히 React를 사용할 때보다 특정 방식으로 해야 내가 원하는 결과물이 나오는 경우가 많았다. 아마 그런식으로 정해진 룰을 잘 따르면 좀 더 쉽게 결과물을 만들 수 있지 않을까 기대한다.

🚨 주의 : Vue2와 Vue3 예제 코드가 섞여있음



install Vue

  • CDN 방식
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  • npm
$ npm install vue
  • vue-cli
    CLI = Command Line Interface (명령어 보조 도구)
//install vue-cli
npm install -g @vue/cli
# OR
yarn global add @vue/cli

//create project
vue create my-project
# OR
vue ui

공식문서 : Vue.js는 단일 페이지 애플리케이션를 빠르게 구축할 수 있는 공식 CLI를 제공합니다. 최신 프론트엔드 워크 플로우를 위해 사전 구성된 빌드 설정을 제공합니다. 핫 리로드, 저장시 린트 체크 및 프로덕션 준비가 된 빌드로 시작하고 실행하는데 몇 분 밖에 걸리지 않습니다. 상세한 내용은 Vue CLI 문서에서 찾아보실 수 있습니다.



Vue의 props

Vue에서도 UI를 컴포넌트로 나누게 되고 React처럼 props를 상위 컴포넌트에서 하위 컴포넌트로 내려줄 수 있다.
다만 작성 방법이 조금 다른데, 예를 들어 Root 컴포넌트의 하위 컴포넌트로 app-header라는 컴포넌트를 만들었을 때, 루트 컴포넌트가 가진 message라는 이름의 데이터를 app-header로 넘겨줄 수 있다.
넘겨줄 때 app-header 컴포넌트에는 v-bind:프롭스 속성 이름="상위 컴포넌트의 데이터 이름"과 같이 작성한다.

여기서 v-bind 속성은 디렉티브라고 합니다. 디렉티브는 Vue에서 제공하는 특수 속성임을 나타내는 v- 접두어가 붙어있으며 사용자가 짐작할 수 있듯 렌더링 된 DOM에 특수한 반응형 동작을 합니다. 공식문서 참고

//html 파일 
<body>
  <div id="app">
    <app-header v-bind:propsdata="message"></app-header>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    var appHeader = {
      template: '<h1>{{ propsdata }}</h1>',
      props: ['propsdata']
    }

    new Vue({
      el: '#app',
      components: {
        'app-header': appHeader,
      },
      data: {
        message: 'hi',
      }
    })
  </script>
</body>


event emit

event emit은 Vue.js에서 하위 컴포넌트가 상위 컴포넌트와 소통하는 방식이다.(상위 컴포넌트가 하위 컴포넌트에게 이야기하는 방식이 props이므로 그 반대가 event emit이라고 생각하면 될듯) event emit은 Vue 인스턴스의 methods 속성과 관련되며, v-on 디렉티브를 사용한다.

예제 코드
: app-header 컴포넌트와 app-content컴포넌트는 각각 상위의 root 컴포넌트로 event emit 한다.
v-on:하위 컴포넌트에서 발생한 이벤트 이름="상위 컴포넌트의 메서드 이름"

<div id="app">
    <p>{{ num }}</p>
    <app-header v-on:pass="logText"></app-header>
    <app-content v-on:increase="increaseNumber"></app-content>
  </div>
var appHeader = {
      template: '<button v-on:click="passEvent">click me</button>',
      methods: {
        passEvent: function() {
          this.$emit('pass');
        }
      }
    }
    var appContent = {
      template: '<button v-on:click="addNumber">add</button>',
      methods: {
        addNumber: function() {
          this.$emit('increase');
        }
      }
    }

    var vm = new Vue({
      el: '#app',
      components: {
        'app-header': appHeader,
        'app-content': appContent
      },
      methods: {
        logText: function() {
          console.log('hi');
        },
        increaseNumber: function() {
          this.num = this.num + 1;
        }
      },
      data: {
        num: 10
      }
    });

increaseNumber에서 this.num으로 data 속성 내에 있는 num에 접근할 수 있는 이유는 아래 이미지에서처럼 num이 실제로 Vue 인스턴스에서 가장 바깥쪽 속성에 해당하도록 저장되어 있기 때문임. (Vue가 이렇게 처리하는듯)



Vue.js의 데이터 양방향 바인딩

React는 대표적으로 단방향 데이터 바인딩을 사용하는데,

  • 컴포넌트 간에는 부모 => 자식 방향으로만 데이터가 전달되는 것을 뜻하고,
  • 컴포넌트 내에서는 js => html (물론 이벤트 함수를 사용해 Html -> event -> js -> html 과 같은 순서도 가능은 하나 결국 js가 중간에 끼어야 함)와 같이 한 방향으로만 데이터 동기화가 가능하다.

Vue는 양방향 데이터 바인딩을 제공하는데, (앵귤러도)

  • 컴포넌트 내부에서는 html과 js 사이에 ViewModel이 존재해서 html과 js 중 하나만 변경되더라도 변경 사항이 양쪽 다 반영된다.
  • 컴포넌트 간에는 부모 컴포넌트에서 자식 컴포넌트로는 Props를 통해 데이터를 전달하고, 자식 컴포넌트에서 부모 컴포넌트로는 Event Emit을 통해서 데이터를 전달한다.

Vue는 양방향 바인딩을 위해 v-model 디렉티브를 제공하는데,
대표적으로 폼 입력하는 input 태그가 함께 많이 사용된다.

공식문서 예제코드2
: 이렇게 작성하면 input에 입력된 값이 p 태그 내부에 보여지는 값에 반영된다.

<div id="app-6">
  <p>{{ message }}</p>
  <input v-model="message">
</div>
var app6 = new Vue({
  el: '#app-6',
  data: {
    message: '안녕하세요 Vue!'
  }
})


methods, v-on 디렉티브를 이용한 상호작용(이벤트)

공식 문서 예제
: 사용자가 앱과 상호 작용할 수 있게 하기 위해 v-on 디렉티브를 사용할 수 있다. Vue 인스턴스에서 메소드를 호출하는 이벤트 리스너를 추가 할 수 있음. @click="reverseMessage"와 같이 축약해서 작성할 수도 있음.

<div id="app-5">
  <p>{{ message }}</p>
  <button v-on:click="reverseMessage">메시지 뒤집기</button>
</div>
var app5 = new Vue({
  el: '#app-5',
  data: {
    message: '안녕하세요! Vue.js!'
  },
  methods: {
    reverseMessage: function () {
      this.message = this.message.split('').reverse().join('')
    }
  }
})


반복문과 조건문 in Vue.js

v-for, v-if란 디렉티브를 사용할 수 있다.

공식 문서 예제
: 요소가 보여지는지 boolean 값에 따라 제어

<div id="app-3">
  <p v-if="seen">이제 나를 볼 수 있어요</p>
  <p v-else>지금은 볼 수 없어용</p>
</div>
var app3 = new Vue({
  el: '#app-3',
  data: {
    seen: true
  }
})

v-if 외에 v-show라는 비슷한 속성이 있는데, v-show는 위처럼 seen이라는 데이터에 따라 보여지거나 보여지지 않거나를 조절할 수 있지만, 보이지 않을 때는 v-if처럼 dom에서 완전히 사라지는 것이 아니라 단순히 display:none 속성이 지정되는 것이라는 점이 차이점이다.

공식 문서 예제
: to do list 반복적으로 출력하기

<div id="app-4">
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
</div>
var app4 = new Vue({
  el: '#app-4',
  data: {
    todos: [
      { text: 'JavaScript 배우기' },
      { text: 'Vue 배우기' },
      { text: '무언가 멋진 것을 만들기' }
    ]
  }
})


같은 레벨 컴포넌트 간 통신 방법

같은 레벨 컴포넌트 간에 다이렉트로 통신할 수는 없고, 위에서 배운 event emit과 props를 이용해 둘 간의 공통된 상위 컴포넌트에 특정 값을 전달해주고, 그 값을 다시 props로 전달 받을 수 있다.

예를 들어 num이라는 값을 app-content 컴포넌트에서 app-header 컴포넌트로 전달하고 싶을 때 아래와 같이 코드를 작성해줄 수 있다.

<div id="app">
    <p>{{ num }}</p>
    <app-header v-bind:propsdata="num"></app-header>
    <app-content v-on:pass="deliverNum"></app-content>
  </div>
var appHeader = {
      template: '<div>header</div>',
      props: ['propsdata']
    }

var appContent = {
      template: '<div>content<button v-on:click="passNum">pass</button></div>',
      methods: {
        passNum: function() {
          this.$emit('pass',10);
        }
      }
    }

    new Vue({
      el: '#app',
      components: {
        'app-header': appHeader,
        'app-content': appContent
      },
      methods: {
       deliverNum: function(value) {
          this.num = value;
        }
      },
      data: {
        num: 10
      }
    });


Vue Router

Vue처럼 Vue Router도 npm installl 하는 방식이 있고, CDN을 사용하는 방식이 있다.

Install

  • CDN
//vue의 CDN이 우선 작성된 뒤 Router의 CDN이 작성되어야 함 
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3.5.3/dist/vue-router.js">
  • npm
npm install vue-router

Use

  • 라우터 인스턴스 생성
//cdn 프로젝트에서 사용 
var router = new VueRouter({
      // 페이지의 라우팅 정보      
      routes: [
        // 로그인 페이지 정보
        {
          // 페이지의 url
          path: '/login',
          // name: 'login',
          // 해당 url에서 표시될 컴포넌트
          component: LoginComponent
        },
        // 메인 페이지 정보
        {
          path: '/main',
          component: MainComponent
        }
      ]
    });

각 path에 연결된 컴포넌트들은 아래와 같이 생성해준다.

  var LoginComponent = {
      template: '<div>login</div>'
    }
    var MainComponent = {
      template: '<div>main</div>'
    }

//vue-cli 사용
//main.js에서 app에 router 세팅해주기 

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router.js";

createApp(App).use(router).mount("#app");


//router.js 파일을 생성한 뒤 작성 
//component에 입력하는 컴포넌트들은 import 해와야 함 
const routes = [
  {
    path: "/",
    component: BlogIntro,
  },
  {
    path: "/list",
    component: ItemList,
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;
  • router-link
    : 웹 페이지에서 페이지 이동을 할 때 화면에서 특정 링크를 클릭해서 페이지를 이동할 수 있게 해줌. 코드를 실행하면 화면에서는 <a> 태그로 변형된다. (to에 이동할 경로를 입력해준다) 필요시 다른 요소나 컴포넌트에 씌워줄 수도 있음.
  • router-view
    : routes에서 설정한 path로 이동 시 보여질 컴포넌트가 뿌려지는 영역으로 뿌려질 컴포넌트에 넘겨줘야 할 props를 router-view에 넘겨줄 수도 있다.
  <div id="app">//루트 컴포넌트 
    <div>
      <router-link to="/login">Login</router-link>
      <router-link to="/main">Main</router-link>
    </div>
    <router-view></router-view>
  </div>
  • 파라미터 사용하기
    : id마다 detail 페이지를 다르게 보여주고 싶을 때 아래와 같이 파라미터를 입력 받을 수 있다. 입력 받은 id는 해당 페이지에서 $route.params.id와 같이 꺼내어 사용할 수도 있게 된다.
{
    path: "/detail/:id",
    component: Detail,
  },

$route와 비슷하게 생긴 $router도 있는데, $router는 페이지를 이동하는 데 필요한 기능을 가지고 있음. $router.push('이동할 경로')를 사용하면 해당 페이지로 이동시킬 수 있음. $route가 현재 경로 관련 데이터들을 사용할 수 있는 것과는 조금 다른 쓰임!

  • Nested Routes
    : children 속성을 통해서 좀 더 깊은(?) 수준의 라우팅도 가능하다. children의 path를 작성할 때는 앞쪽에 '/'와 함께 작성하면 안 되는데, 그렇게 작성하면 root부터 시작하는 경로라고 인식함. 예제와 같이 /user/:id/profile과 같은 경로가 필요하다면 '/'를 제외하고 작성해야 함.
const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        // UserProfile will be rendered inside User's <router-view>
        // when /user/:id/profile is matched
        path: 'profile',
        component: UserProfile,
      },
      {
        // UserPosts will be rendered inside User's <router-view>
        // when /user/:id/posts is matched
        path: 'posts',
        component: UserPosts,
      },
    ],
  },
]
  • Redirect : 특정 경로로 이동했을 때, 다른 페이지로 리다이렉트도 가능하다.
const routes = [{ path: '/home', redirect: '/' }]

이 외에도 공식문서를 보면 다양한 기능들이 존재하는 것 같음.



Vue 템플릿 문법

Vue.js는 렌더링 된 DOM을 기본 Vue 인스턴스의 데이터에 선언적으로 바인딩 할 수있는 HTML 기반 템플릿 구문을 사용합니다. 모든 Vue.js 템플릿은 스펙을 호환하는 브라우저 및 HTML 파서로 구문 분석 할 수있는 유효한 HTML입니다.
내부적으로 Vue는 템플릿을 가상 DOM 렌더링 함수로 컴파일 합니다. 반응형 시스템과 결합된 Vue는 앱 상태가 변경 될 때 최소한으로 DOM을 조작하고 다시 적용할 수 있는 최소한의 컴포넌트를 지능적으로 파악할 수 있습니다.
가상 DOM 개념에 익숙하고 JavaScript의 기본 기능을 선호하는 경우 템플릿 대신 렌더링 함수를 직접 작성할 수 있으며 선택사항으로 JSX를 지원합니다.

Vue에서 말하는 템플릿 구문이라는 건 Vue 인스턴스에서 생성한 데이터와 바인당할 수 있는 HTML 기반의 구문을 말한다고 이해했다.
그리고 React의 Virtual DOM과 마찬가지로 Vue 역시 Virtual DOM을 사용해 최소한으로 DOM을 조작하는 것 같다.

Mustache 구문 (이중 중괄호)

처음 강의를 들을 때 콧수염 괄호라길래 '머요?'🤔 했던 기억이 있는데 정말 이름이 Mustache 구문이었다...

Vue에서 데이터 바인딩을 하는 가장 기본적인 형태로, 이중 중괄호를 사용해 텍스트를 삽입할 수 있다.

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

위와 같이 작성하면 해당 데이터 객체의 msg 속성 값으로 대체되게 되며, msg 속성 값이 변경될 때마다 갱신된다.

Directive : v-bind

Mustache 구문은 HTML 속성에서는 사용할 수 없기 때문에 그럴 때는 데이터 바인딩 방법으로 v-bind 디렉티브를 사용할 수 있다.

<div v-bind:id="dynamicId"></div>

위와 같이 작성하면 dynamicId 속성에 해당하는 값에 따라 위 div의 id 값이 결정될 것이다. v-bind를 생략하고 :만 붙이고 사용하는 경우도 많은 것 같다. 처음에는 v-bind를 사용하고 있다는 것을 잊지 않으려고 굳이굳이 전부다 작성했는데, 줄여서 쓰는 게 편하긴 한듯.

JavaScript 표현식

간단한 속성들만을 바인딩하는 것 외에도 Vue.js는 모든 데이터 바인딩 내에서 Js 표현식들을 작성할 수 있음. 다만 각 바인딩에 하나의 단일 표현식만 포함 가능.

//가능

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div v-bind:id="'list-' + id"></div>
//불가능

<!-- 아래는 구문입니다, 표현식이 아닙니다. -->
{{ var a = 1 }}

<!-- 조건문은 작동하지 않습니다. 삼항 연산자를 사용해야 합니다. -->
{{ if (ok) { return message } }}


watch vs. computed

computed

템플릿 내에 표현식을 넣을 수 있지만, 너무 많은 연산을 템플릿 내에서 하면 코드가 비대해지고 유지보수가 어렵다.

<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

message를 역순으로 표시한다는 것을 한 눈에 알 수 없음.
computed 속성을 사용해 리팩토링 할 수 있다.

<div id="example">
  <p>원본 메시지: "{{ message }}"</p>
  <p>역순으로 표시한 메시지: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '안녕하세요'
  },
  computed: {
    // 계산된 getter
    reversedMessage: function () {
      // `this` 는 vm 인스턴스를 가리킵니다.
      return this.message.split('').reverse().join('')
    }
  }
})

computed를 일종의 함수 결과 저장 공간이라는 말로 설명할 수도 있을 것 같은데, 렌더링이 일어날 때마다 함수를 실행하는 method와 비교했을 때 computed는 저장된 결과를 반환하므로 호출이 훨씬 적다. (캐싱)

(다음 포스팅할 Vuex를 사용할 때 mapState를 computed 내부에서 사용하고, mapMutations를 methods 내부에서 사용하는 것도 비슷한 맥락인 것 같다는 생각을 했었다.)

watch

데이터의 변화에 따라 특정 로직을 실행할 수 있는 속성

대부분의 경우 computed 속성이 더 적합하지만 사용자가 만든 감시자가 필요한 경우가 있습니다. 그래서 Vue는 watch 옵션을 통해 데이터 변경에 반응하는 보다 일반적인 방법을 제공합니다. 이는 데이터 변경에 대한 응답으로 비동기식 또는 시간이 많이 소요되는 조작을 수행하려는 경우에 가장 유용합니다.

watch vs. computed

watch 속성은 감시할 데이터를 지정하고 그 데이터가 바뀌면 이런 함수를 실행하라는 방식으로 소프트웨어 공학에서 이야기하는 ‘명령형 프로그래밍’ 방식. computed 속성은 계산해야 하는 목표 데이터를 정의하는 방식으로 소프트웨어 공학에서 이야기하는 ‘선언형 프로그래밍’ 방식. 출처

대부분의 로직이 computed로도 작성이 가능하며,computed 내에 캐싱 등 다양한 튜닝이 적용되어 있기 때문에 computed를 좀 더 사용하는 게 좋다고 한다.

 <div id="app">
    {{ num }}
  </div>
new Vue({
      el: '#app',
      data: {
        num: 10
      },
      computed: {
        doubleNum: function() {
          return this.num * 2;
        }
      },
      watch: {
        num: function(newValue, oldValue) {
          this.fetchUserByNumber(newValue);
        }
      },
      methods: {
        fetchUserByNumber: function(num) {
          axios.get(num);
        }
      }
    });

아직 두 개념이 좀 헷갈리는데, ajax 관련해서는 watch, 그리고 나머지는 computed 속성을 사용한다 정도로 생각해보려고 한다. (일단은)



slot

props와 비슷한 것 같은데 그보다 좀 더 간편하고...그렇다고 남발하면 props보다 못해지는 그런 기능이라고 할 수 있을 것 같다.

부모 컴포넌트의 오픈 클로징 태그 사이에 문자열이든, 특정 html 태그든, 데이터든! 을 입력하고, 자식 컴포넌트에 <slot></slot>을 입력해주면, 해당 슬롯 태그가 위치한 곳에 부모 컴포넌트에서 입력해준 내용이 나타나게 된다.

다만 이 슬롯은 태그 내부에 어떤 데이터를 바인딩하는 데는 사용할 수 있지만, 태그의 속성으로 부모에서 보내주는 데이터를 사용할 수는 없기 대문에 이런 경우에는 props를 사용해야 한다.

슬롯을 여러 개 사용하고 싶다면 슬롯을 여러 개 생성하고, 각각에 name 속성을 붙여줄 수 있다. 이름이 있는 슬롯에 내용을 전달하려면 <template>에 v-slot 디렉티브를 쓰고 그 속성에 앞에서 지정한 ‘name’을 넣으면 된다.

//자식 컴포넌트 내부의 슬롯에 이름 붙이기 
//이름 붙여지지 않은 슬롯은 암묵적으로 default 값이 적용됨
div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

//부모 
<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

위 코드에서 v-slot을 쓴 <template>으로 싸여있지 않은 내용물들은 디폴트 슬롯에 해당되는 것으로 간주하게 됨. 아래 코드와 같다고 생각하면 되며, 실제로 default를 명시해서 적어줄 수도 있음.

  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

실제 렌더링 되는 코드는 아래와 같다.

  <div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>


slot props?

부모가 자식이 가진 데이터를 활용해서 슬롯에 들어갈 내용을 작성해주고 시을 때 사용할 수 있음.

//자식 컴포넌트에 있는 user라는 데이터를 부모 컴포넌트에서 사용할 수 있도록 하기 
<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>


//부모 컴포넌트에서 user를 꺼내서 slot자리에 활용
//slotProps라는 이름 대신 다른 이름을 사용할 수도 있음 (slot props의 이름을 정하는것)
<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>


Options API vs Composition API

위에서 주로 사용한 개발 방법은 Vue.js에서 Options API를 사용한 방법이다. 데이터끼리, 함수끼리, 컴포넌트끼리 모아서 정리해둘 수 있음.

Composition API도 존재하는데, 관련된 기능끼리 하나로 모으고 싶으면 Composition API를 사용할 수 있음. (주로 setup()이라는 함수와 ref를 사용한다)

//공식 문서 예시 
<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return {
      count
    }
  },

  mounted() {
    console.log(this.count) // 0
  }
}
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

두 API 간에 통용되는 API와 그렇지 않은 API는 공식문서에 정리가 잘 되어 있다.

profile
다시 일어나는 중

0개의 댓글