# Vue Tutorial

# 개발환경 설정

# 1. node.js 설치하기

https://nodejs.org/ko/

# 2. 텍스트에디터 설치하기

추천 에디터

  1. WebStorm - 유료
  2. vscode - 무료

# 3. vue cli로 프로젝트 생성

npx @vue/cli create 프로젝트이름

프로젝트 생성시 여러가지 옵션들을 선택하게되는데, 여기에 대해 잘 모른다면, 두번째 선택지에서 Use NPM을 선택하시고 나머지는 기본을 선택하시면 됩니다. 3.0.4 기준으로는 선택지가 한개밖에없네요. 그냥 선택하고 넘어가시면 됩니다.

# 4. 에디터에서 생성한 프로젝트열기

에디터에서 생성한 프로젝트의 디렉토리를 추가하거나 open 하면됩니다.

# 5. 로컬에서 웹 띄우기

npm run serve

# 6. 브라우저에서 생성한 프로젝트 확인하기

http://127.0.0.1:8080/
# or
http://localhost:8080/

빈페이지가 아닌, 아래와 같은 화면이 나오면 성공


저번에 vue cli를 활용해서 vue 프로젝트를 간단하게 생성했습니다.

아래 그림은 생성한 프로젝트의 src/App.vue를 열어본 모습입니다.

코드를 보시면, 상단에 template내에 html태그'처럼' 생긴것들이 눈에 보입니다.

그리고 HelloWorld 태그를 보시죠.

html태그에서는 볼수 없던 태그이죠?

이름만 봐도 누군가 커스텀으로 만든것처럼 보입니다.

위 그림을 보시죠

저런식으로 export default로 내보낸 vue 객체내에,

components 객체에 변수를 바인딩하게되면,

템플릿에서 사용가능한 컴포넌트가 됩니다.

HelloWorld는 컴포넌트라는 것이죠.

import HelloWorld from "./components/HelloWorld.vue";

스크립트의 첫줄을 보시면 위의 코드를 보실 수 있는데요,

HelloWorld 컴포넌트는 components 디렉토리에 존재하는걸 알 수 있습니다.

그렇다면 컴포넌트라는게 무엇일까요.

이해하기쉽게 **'html 엘리먼트의 집합체'**라고도 표현할 수 있겠습니다.

정확하게는 vue 객체라고 표현하는게 올바릅니다.

html 엘리먼트가 없는 컴포넌트도 있거든요.

vue 컴포넌트에는 다양한 옵션(기능)들이 존재합니다.

최종적으로 vue의 여러가지 컴포넌트들을 조합해서 하나의 웹사이트가 구성됩니다.

그리고 싱글파일컴포넌트(SFC)라는 용어가 있는데요,

HelloWorld.vue 처럼, 확장자를 vue로 가지면서,

export default하는 객체가 있다면, 그것을 싱글파일컴포넌트라고 부릅니다.

(파일하나에 여러개의 vue 컴포넌트를 생성할 수 있습니다.)


이제 컴포넌트에 대한 감이 어느정도 잡혔습니다.

프로젝트에 컴포넌트를 추가해보며, 조금 더 이해하도록 합시다.

먼저 src/components 폴더내에 ByeWorld.vue를 생성합니다.

그리고 내용을 작성하도록 합니다.

먼저 템플릿부터 작성하겠습니다만, App.vue에서 보이는 구조처럼

템플릿, 스크립트, 스타일 순서로 코드를 작성하면 됩니다.

<template>
  <div class="text">
    Good Bye World!
  </div>
</template>

딱히 기능이 없는, div태그 하나뿐인 컴포넌트입니다.

이제 바로 아래에 작성할 자바스크립트는

<script>
 <!--여기에 스크립트 작성-->
</script>

이런식으로 스크립트 태그안에 작성해주세요.

vue/cli로 프로젝트를 생성하고 기본 소스코드를 보면

세미콜론이 생략되어있는것을 볼수있는데, 저는 세미콜론을 사용하는것을 선호합니다.

취향이므로 적절히 생략해주세요.

같은 맥락(?)으로 객체 마지막 프로퍼티뒤에 콤마붙이는걸 좋아합니다. (배열제외)

export default {
  name: "ByeWorld", //이름은 컴포넌트파일의 이름과 같게해주세요
};

마지막으로 스타일은 이렇게 작성해주세요

마찬가지로 style태그로 꼭 묶어주세요

<style>
 <!--여기에 스타일 작성-->
</style>
.text {
  font-size: 15px;
}

모두 작성하셨다면 아래 그림과 같은 소스코드형태가 됩니다.

정말 간단한 싱글파일컴포넌트를 작성해보았습니다.

만들기만했으므로, 웹에는 보이지않습니다.

이제 작성한 컴포넌트가 웹에 보여질수있도록 import 해봅시다.

src/App.vue 를 열어주세요.

먼저 스크립트 부분에

import ByeWorld from "./components/ByeWorld.vue";

이 코드를 추가해주시구요.

import한 ByeWorld컴포넌트를 components에도 추가해줍니다.

그리고 상단의 템플릿에서도 ByeWorld를 추가해줍니다.

설명대로 진행하셨다면, 소스코드는 아래그림처럼 됩니다.

소스를 저장하시고 웹을 확인해보시면, 페이지 하단에 Good Bye World! 문구가 추가된걸 볼 수 있습니다.


저번에 컴포넌트 추가하는방법을 익혔습니다!

그렇다면 이제 컴포넌트에 대해 좀 더 자세히 알아보도록 합시다.

# 배경지식

컴포넌트 기본에서 언급했던 것과 같이, vue.js에서 컴포넌트는 vue객체일뿐입니다.

싱글파일컴포넌트로 컴포넌트를 생성할때,

export default {
  ...
};

이런식으로 객체를 생성하고, export 합니다.

그냥 객체일뿐인데, 메소드나 라이프싸이클훅에서 this를 콘솔에 찍어보면,

vue객체가 됩니다.

export 하고나서 컴포넌트를 바인딩하기전에,

vue 라이브러리 내부에서 뭔가 처리해주는게 틀림없습니다.

# 단방향 데이터 플로우

최신 ui 라이브러리(react, vue)가 채택하고있는 데이터플로우는 단방향입니다.

기본적으로는 부모컴포넌트에서 자식으로 데이터를 내려주고, 자식은 받아서 렌더링만 하는게

권장되는방법입니다.

그런데 개발하다보면, 꼭 그렇게만 할수없을때도 있습니다.

자식에서 부모컴포넌트로 데이터를 돌려줘야할때도있고.. props로 받은 데이터를 수정하고 싶을때도 있습니다.

# 이벤트버스

이렇게 난처할때, 사용할 수 있는게 vue.js에서는 이벤트입니다.

vue.js는 친절하게도 이벤트 인터페이스를 아주 편하게 구현해놨습니다. 사용만 하면됩니다.

@input, @change 이런걸 보신적있나요? 이런게 모두 이벤트입니다.

@가 v-on의 축약형이란걸 알게되는순간, 아~ 하고 이해하게됩니다.

아래서 설명하고있지만 이벤트는 emit으로 쏘고, on으로 받거든요.

this.$emit("change");

를 통해서 상위컴포넌트로 이벤트를 전달할 수도 있죠. (상위컴포넌트에서 @change, 혹은 $on으로 받습니다)

여기에서 this는, vue객체겠죠. 여기서 이벤트버스를 사용할 수 있는 힌트를 얻었습니다.

$emit으로 이벤트를 전달하고 $on으로 받는다고했죠?

그리고 그 두개의 메소드는 vue객체의 메소드죠.

그러면 이제 main.js처럼 entry point에 이벤트버스를 생성해보겠습니다.

// ...some code
import Vue from "vue";
Vue.prototype.$eventBus = new Vue();
// ...some code

쨘! 이렇게하면 모든 vue객체에서 $eventBus로 접근하여 이벤트버스를 사용할수 있습니다!

this.$eventBus.$emit;
this.$emit;

이렇게 두개의 이벤트를 사용할 수 있게 됐습니다.

프로토타입에 등록한 $eventBus는 부모자식 상관없이 이벤트를 주고 받을 수 있습니다.

그런데 this.$emit은 자기 자신의 부모컴포넌트 '인스턴스'로만 이벤트를 쏘게됩니다.


쨘.. 이런식으로 컴포넌트 state가 있는데, 각 state가 바뀔때마다, 이벤트버스로 데이터변경을 알리고싶을때,

메소드를 동적으로 만들때! vuex의 mapMutations처럼!!! 이럴때 유용합니다.

하나하나 event emit하고, 받는곳에서는 on으로 하나하나 다 받기 귀찮잖아요.

자바스크립트니까 동적으로 구현해봅시다.

먼저 state!

const SURVEY_INFO_STATE = {
  surveyName: "",
  startDate: "",
  startTime: "",
  endDate: "",
  endTime: "",
  surveyOrgan: "",
  surveyDescription: "",
};

뷰 컴포넌트 data에 맵핑하고싶다면 ...SURVEY_INFO_STATE 이런식으로 스프레드 오퍼레이터를 쓰면됩니다.

이해가 잘 안 되실까봐 친절하게 아래 예제코드도 있어여!ㅎㅎ

data: vm => ({
  ...SURVEY_INFO_STATE,
}),

이제 동적으로 메소드를 만들어봅시다.

const surveyInfoMethods = {};
Object.keys(SURVEY_INFO_STATE).forEach((stateName) => {
  surveyInfoMethods[
    `handleChange${capitalizeFirstLetter(stateName)}`
  ] = function(v) {
    this[stateName] = v;
    this.$eventBus.$emit(`change-${stateName}`, v);
  };
});

위에서 생성한 메소드도 뷰 컴포넌트에 아래와 같이 맵핑하면 됩니다

methods: {
  ...surveyInfoMethods,
},

capitalizeFirstLetter가 뭔지 모르실까봐...아래 또 구현체를 드립니다.

export const capitalizeFirstLetter = (string = "") => {
  return string.charAt(0).toUpperCase() + string.slice(1);
};

이벤트는 아래와 같이 on으로 받을수있습니다.

Object.keys(SURVEY_INFO_STATE).forEach((stateName) => {
  this.$eventBus.$on(`change-${stateName}`, (v) => {
    this[stateName] = v;
  });
});

간단하쥬?


# .vue

컴포넌트를 생성하는 대표적인 방법중에 하나가

vue 라는 확장자로 컴포넌트를 만드는 방법입니다. (SFC)

vue 파일에는 보통 순서대로 템플릿, 스크립트, 스타일이 들어가는데,

이것을 vue-loader라는 친구가 처리해줍니다.

# vue-loader

vue-loader는 npm의 의존성을 관리하는 package.json의 dev dependency에서 찾아볼 수 있습니다.

dev dependency이므로 컴파일단계에서 *.vue를 vue-loader가 해석해줍니다.

그래서 브라우저에서 해석할 수 없는 vue확장자가 자연스럽게 실행되는것처럼 느껴지죠.

# virtual dom (가상돔)

또 하나의 중요한 개념이 등장했습니다.

바로 뷰객체 혹은 SFC에서 template에 해당하는 부분인, virtual dom입니다.

템플릿내부를 보면, 마치 태그와 생김새가 같습니다.

하지만 내부적으로는 createElement와 같은 vue라이브러리의 메소드가

템플릿을 파싱하여 자바스크립트 객체로 변형시킵니다.

react에서는 render function 내에 작성한 jsx문법이 있다면, createElement와 같거나 비슷한 메소드로

js object로 변경하는 작업이 존재합니다. render function은 아래에서 더 자세히 소개합니다.

dom을 직접 변경하는 것은 비용이 많이 들어가는 작업입니다.

모던 웹에서는 실시간으로 dom이 자꾸자꾸 변경되고,

받아오는 데이터도 계속해서 변합니다.

그런 무거운 작업을 최소화시키고자 하는것이 virtual dom입니다.

데이터변경이 있으면, dom을 바로 변경하지않고 내부적으로 바뀐 부분만

계산하여, 그 부분만 변경하게 됩니다.

예를 들어, 외부 데이터로 [1,2,3] 이라는 데이터를 받았고

템플릿에서

<div>1</div>
<div>2</div>
<div>3</div>

이런식으로 렌더링한다고 가정합니다.

그런데 [1,2,3,4]로 데이터가 갱신됐습니다. 화면을 다시 렌더링해야합니다.

<div>1</div>
<div>2</div>
<div>3</div>

위에서 아래처럼 변경될것입니다.

<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>

직접 변경과, virtual dom은 여기서 차이가 발생합니다.

직접 dom을 조작하는 로직이라면 (차이점을 계산하는 라이브러리 혹은 알고리즘이 없다고 했을때)

해당하는 기존 템플릿을 제거하고, 새로운 템플릿을 렌더링하여 특정 위치에 삽입할것입니다.

그런데 요즘 프론트라이브러리, 프레임워크는 기존 dom에 4만 추가하는 방법을 사용합니다.

여기에 이제 더 복잡한 템플릿이나 로직이 추가된다면, virtual dom이 훨씬 유리할것입니다.

react에도 virtual dom개념이 존재하지만, vue와는 조금 다른 알고리즘을 사용하여서

같은 virtual dom이지만 속도차이가 존재합니다.

angular.js (v1)은 dom을 직접 조작하여서 속도가 상대적으로 느립니다.

하지만 위에서 예제로 설명한 간단한 list렌더링의 경우에는 key라는 속성을 이용하여,

list에서 어느부분이 변경됐는지 감지하고, 계산하여 바뀐부분만 변경합니다.

# render function

위에서 virtual dom을 js object로 변경할때, createElement라는 숨어있는 메소드를 이용한다고 말씀드렸습니다.

화면에 컴포넌트를 렌더링할때는 render라는 메소드를 사용합니다.

life cycle을 확인해보시면, 프레임워크마다 차이는 있겠지만

컴포넌트가 마운트되기전에 render function을 실행합니다.

여기서 잠깐 life cycle method에 대해 설명합니다.
life cycle method 는 말 그대로
컴포넌트의 생명주기입니다.
특정 타이밍이 되면, 자동으로 실행하는 메소드입니다. (프레임워크에서 호출합니다.)
우리가 직접 호출하지않죠.
그리고 심지어 컴포넌트가 사라질때도 컨트롤할수있습니다. (unmount || destroy)

그리고 컴포넌트가 업데이트되어도 실행합니다.

react에서는 render function을 구현하지만, vue에서는 그렇지않습니다.

템플릿부분에 작성하면, vue-loader가 자동으로 생성해주죠.

# v-for

v-for는 아마 v-if와 함께 템플릿에서 가장 많이 쓰게될 디렉티브이므로,

한번 알아둘때, 잘 알아두는것이 좋습니다!

우선, v-for는 말 그대로 반복을 의미합니다.

예를들어, 스크립트에 아래와 같은 배열이 있다고 가정합시다.

data: (vm) => ({
  cardList: [
    { text: "쥬얼리1", value: 1 },
    { text: "쥬얼리2", value: 2 },
    { text: "쥬얼리3", value: 3 },
    { text: "쥬얼리4", value: 4 },
  ],
});

이것을 템플릿에 바인딩하기위해, v-for를 사용할수 있습니다.

<div v-for="(item, index) in cardList">{{item.text}}</div>

대충 예상이 되시나요? 아래와 같이 렌더링되어야할겁니다!

<div>쥬얼리1</div>
<div>쥬얼리2</div>
<div>쥬얼리3</div>
<div>쥬얼리4</div>

하지만 자바스크립트 콘솔에서는 key 어쩌고 저쩌고하는 에러, 혹은 경고를 뿜어낼것입니다.

처음 리스트를 반복해서 렌더링할때는 문제없지만,

배열이 변경될때, key값은 어떤 배열이 변경됐는지 계산하기위한 속성으로 사용하기때문에,

update를 위해 지정해주는것이 좋습니다. 그러므로 키는 각 배열에서 고유한값으로 지정해주는것이 좋습니다.

중복된 키가 존재하면, 배열이 변경되고 dom변경시 예상하지못한 결과가 나올수있겠죠.

물론 콘솔에 에러를 보기싫은것도 당연하고요!

그런데 중요한점은, v-for를 사용할때 key값으로 index를 사용하면 안됩니다.

고유한값이긴하지만, 그냥 순서번호일뿐이에요. 배열의 중간값.. 길이가 3인 배열에서 index 1에 해당하는 배열을 빼더라도,

index 1에 해당하는 배열은 기존 배열의 index 2에 해당하는 배열이 되겠죠.

이 말은 위에 쥬얼리1,2,3,4 배열을 보시면, 쥬얼리2가 index 1이죠?

쥬얼리2를 배열에서 빼버리면, 쥬얼리3의 index가 1이 됩니다.

당연히 배열이 변경되고 업데이트될때 예상치못한 버그가 발생할수있죠.

단순히 배열 끝만 변경하는 push, pop같은 메소드만 사용하거나,

배열의 변경이 없을거라면 index를 key값으로 해도 상관없습니다.

그러므로 key값을 index가 아닌 고유의 값으로 잘 설정해줍시다.

<div v-for="(item, index) in cardList" :key="item.value">{{item.text}}</div>

# vuex

facebook의 flux 패턴과 매우 유사하다.

단방향 데이터흐름을 갖고있으며, 용어도 거의 같다.

흐름을 정리하자면,

컴포넌트 => 비동기로직 => 동기로직 => 상태 이다.

다시 정리하면,

Component(View) => Actions => Mutations => State

vue에서 컴포넌트의 상태를 data라고 한다.

vuex에서 어플리케이션의 상태를 state라고 한다.

비동기 로직은 actions에서.

component에서 비동기로직(actions)을 dispatch 한다.

비동기로직에서 동기로직으로 commit 한다.

이 과정들을 통해서 app의 state가 바뀌면 re-render한다.

간단한 counter를 만들어놓은 예제

https://github.com/jewelism/vuex_vue-router_example/tree/master/src/store

# vue-rx

# v-stream 디렉티브

스트림의 시작점으로 사용할 수 있다.

v-stream:click="myStream$"

# domStream

템플릿에서 사용한 스트림을

domStreams: [‘myStream$’]

이런식으로 props처럼 써주면 인스턴스내에서 this.myStream$으로 사용할 수 있다.

vue-rx의 내부적으로 domStreams의 배열의 값들로 subject 인스턴스를 생성해주고 있다.

# subscriptions()

subscriptions 메소드는 computed라고 생각하면되는데,

observable을 subscribe하고, 데이터를 받아온다는점.

this.$watchAsObservable은 vue-rx의 내부적으로 vue의 watch를 사용하여

값이 변하는지 확인하고, observer의 next메소드를 통해 값이 변화한것을 알려준다.

$watchAsObservable를 사용하면, 데이터의 변화를 감지하여 observable로 변환시켜준다.

# v-model-with-props

vuetify 라이브러리를 사용하여 개발하는도중, 문제가 생겼다.

v-text-field라는 vuetify에서 제공하는 컴포넌트를 사용하고싶었다.

그리고 그 컴포넌트를 감싸고있는 똑같은 모양의 검색바를 만들고 사용하는데,

<div class="searchBarWrapper">
  <div></div>
  <div class="searchBar">
    <v-text-field
            v-model="propModel"
            :label="placeholder || '검색'"
            box/>
  </div>
</div>

이런모양의 템플릿을 매번 게시판마다 넣어줘야해서, 컴포넌트화를 시켰다.

v-model으로 받는 데이터(상태)가 바뀌어야하는데,

그 데이터를 props로 받기때문에, 변경할수없는문제가 생겼다.

그래서 v-model을 삭제하고 @change="handleChangeText" 를 추가하여, 부모컴포넌트에서 메소드를 만들고

props로 내려서 문자열 변경을 감지하는 방법을 사용했다.

그런데, @change가 트리거되는 시점이, 입력하다가 엔터를 치거나, 포커스아웃되면 그때 트리거되는 방식이다.

입력값이 변동되면 바로바로 검색되게하는 기획인데, 기능이 어긋난다.

그래서 바로바로 입력에 반응하는 양방향바인딩 v-model 디렉티브를 props와 함께 사용하는 방법을 아래에서 소개한다.

우선 검색바 컴포넌트이다. 템플릿은 위에 작성되어있다.

export default {
  name: "SearchBar",
  props: {
    placeholder: String,
    handleChangeText: Function,
  },
  data: () => ({
    text: "",
  }),
  computed: {
    propModel: {
      get() {
        return this.text;
      },
      set(value) {
        this.text = value;
        this.handleChangeText(value);
      },
    },
  },
};

v-model을 computed로 바인딩했다.

그러면 검색바에 뭔가 입력하면 셋팅되어있는 text라는 상태에 저장되고,

가져올때 가져온다. 이건 그냥 템플릿에 보여주기위한 페이크라고 보면된다.

실제 부모한테 바뀐 입력값을 전달하는것이 이제 computed의 set함수이다.

첫번째라인은 페이크로 보여주기위한 값을 설정하는것이고,

두번째라인, props로 받은 handleChangeText함수를 호출하는데, 파라미터로 입력값을 전달한다.

그러면 이제 부모컴포넌트에서 검색바 객체를 생성할때,

props로 handleChangeText메소드만 간단하게 구현해주면된다.

부모컴포넌트의 템플릿

<search-bar :handle-change-text="handleChangeSearchText"/>

메소드

handleChangeSearchText(text) {
  this.searchInputText = text;
},

이런식이다. 이렇게 구현하면, v-model에 props를 바인딩하는것과 유사한 기능을 구현할수있다.