Modules

Module이란 무엇인가?


모듈이란, 애플리케이션을 구성하는 개별적 요소로서, 재사용이 가능한 코드 조각을 말한다.

모듈은 세부 사항을 캡슐화하고, 공개가 필요한 API만을 외부에 노출한다. 일반적으로, 각 모듈은 파일 단위로 분리되어 있고, 애플리케이션은 필요에 따라 모듈을 로드하여 재사용한다. 즉, 모듈은 애플리케이션에 분리되어 개별적으로 존재하다가 애플리케이션의 로드에 의해 비로소 애플리케이션의 로드에 의해 비로소 애플리케이션의 일원이 된다.

모듈은 기능별로 분리되어 작성되므로, 코드의 단위를 명확히 분리하여 애플리케이션을 구성할 수 있고, 재사용에 좋기 때문에, 개발을 효율적으로 할 수 있고, 유지보수 또한 좋다.

ES6에서 모듈을 사용할 때, export와 import 두 개의 키워드를 사용한다.


Module 사용의 이점


  • Javascript 애플리케이션을 여러 개의 파일로 분할하여 작성할 수 있다.
  • 재사용이 가능한 코드를 작성하기 때문에, 개발 효율과 유지보수에 좋다.


Export


모듈은 기본적으로 독립적인 스코프를 가지고 있어서, 모듈안에 선언된 모든 것들은 해당 모듈 내에서만 참조 할 수 있다.

어떤 모듈안에서 선언한 항목(변수/함수/클래스 등)을 외부에 공개하여 다른 모듈들이 사용할 수 있게 하려면, export를 해야한다. 이때, 선언된 변수, 함수, 그리고 클래스 모두를 export 할 수 있고, 키워드는 export를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// module.js
// 변수의 공개
export const num = 10;

// 함수의 공개
export function square(x) {
return x * x;
}

// 클래스의 공개
export class Person {
constructor(name) {
this.name = name;
}
}

각 선언문(변수/함수/클래스) 앞에 export를 붙이는 대신, export 대상을 하나의 객체로 구성하여, 한번에 export를 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// module.js
const num = 10;

function square(x) {
return x * x;
}

class Person {
constructor(name) {
this.name = name;
}
}

// 변수, 함수 클래스를 하나의 객체로 구성하여 공개
export { num, square, Person };


Import


다른 모듈에서 어떤 export된 모듈을 가져와 사용하려면, 키워드 import를 사용해 로드하여 사용할 수 있다.

1
2
3
4
5
6
7
// main.js
// 같은 폴더 내의 module.js 모듈을 로드. 확장자 js는 생략 가능.
import { num, square, Person } from './module';

console.log(num); // 10
console.log(square(10)); // 100
console.log(new Person('Lee')); // Person { name: 'Lee' }

각각의 이름을 지정하지 않고 하나의 이름으로 한꺼번에 import할 수도 있다. 이때 import되는 항목은 as 뒤에 지정한 이름의 변수에 할당된다.

1
2
3
4
5
6
7
// main.js
// lib라는 이름으로 임포트
import * as lib from './module';

console.log(module.num); // 10
console.log(module.square(10)); // 100
console.log(new module.Person('Lee')); // Person { name: 'Lee' }

이름을 변경하여 import할 수도 있다.

1
2
3
4
5
6
// main.js
import { num as n, square as sq, Person as P } from './module';

console.log(n); // 10
console.log(sq(2)); // 4
console.log(new P('Kim')); // Person { name: 'Kim' }

모듈에서 하나만을 export할 때는 default 키워드를 사용할 수 있다. 다만, default를 사용하는 경우, var, let, const는 사용할 수 없다.

1
2
3
4
5
6
// lib.js
function (x) {
return x * x;
}

export default;

위 코드를 아래와 같이 축약 표현할 수 있다.

1
2
3
4
// module.js
export default function (x) {
return x * x;
}

default 키워드와 함께 export한 모듈은 {} 없이 임의의 이름으로 import한다.

1
2
3
4
// main.js
import square from './module';

console.log(square(3)); // 9


Vue에서의 Modules 사용하는 방법


Arrow Function

화살표 함수(Arrow Function)란 무엇인가?


화살표 함수는 기존의 function 키워드 대신 화살표(=>)를 사용하여, 보다 간단히 함수를 선언하는 방식이다. 문법은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 매개변수 지정 방법
() => { ... } // 매개변수가 없을 경우
x => { ... } // 매개변수가 한 개인 경우, 소괄호를 생략할 수 있다.
(x, y) => { ... } // 매개변수가 여러 개인 경우, 소괄호를 생략할 수 없다.

// 함수 몸체 지정 방법
x => { return x * x } // single line block
x => x * x // 함수 몸체가 한줄의 구문이라면 중괄호를 생략할 수 있으며 암묵적으로 return된다. 위 표현과 동일하다.

() => { return { a: 100 }; }
() => ({ a: 100 }) // 위 표현과 동일하다. 객체는 반환할 때, 소괄호를 사용한다.

() => { // multi line block.
const x = 100;
return x * x;
};


화살표 함수의 호출


화살표 함수는 익명 함수로만 사용할 수 있기 때문에, 화살표 함수를 호출하기 위해서는 함수 표현식을 사용해야 한다.

1
2
3
4
5
6
7
// ES5
var pow = function (x) { return x * x; };
console.log(pow(5)); // 25

// ES6
const pow = x => x * x;
console.log(pow(5)); // 25

콜백함수에서도 화살표 함수를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// ES5
var arr = [2, 4, 6];
var pow = arr.map(function (x) { // x는 요소값
return x * x;
});

console.log(pow); // [ 4, 16, 36 ]

// ES6
const arr = [2, 4, 6];
const pow = arr.map(x => x * x);

console.log(pow); // [ 4, 16, 36 ]


화살표 함수와 function으로 생성한 함수의 차이점


두 방식의 가장 큰 차이점은 this 이다.

  • function 함수: 생성자 함수와 객체의 메소드를 제외한 모든 함수(내부 함수, 콜백 함수 포함) 내부의 this는 전역 객체를 가리킨다.
  • 화살표 함수: 언제나 상위 스코프의 this를 가리킨다.


function 키워드로 생성한 함수의 this


Javascirpt에서의 this는 함수를 호출할 때 함수가 어떻게 호출되었는지에 따라 this에 바인딩할 객체가 동적으로 결정된다. Javascript에서의 this를 참고한다.

생성자 함수와 객체의 메소드를 제외한 모든 함수(내부 함수, 콜백 함수 포함) 내부의 this는 전역 객체를 가리키기 때문이다. 즉, 콜백 함수 내부의 this는 전역 객체 window를 가리킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function Prefixer(prefix) {
this.prefix = prefix;
}

Prefixer.prototype.prefixArray = function (arr) {
// (1)
return arr.map(function (x) {
return this.prefix + ' ' + x; // (2)
});
};

var pre = new Prefixer('Hi');
console.log(pre.prefixArray(['Ro', 'Park']));

위치에 따라 this의 값은 달라진다.

  • (1): 여기서, this는 생성자 함수 Prefixer가 생성한 객체, 즉 생성자 함수의 인스턴스(위 예제의 경우 pre)이다.
  • (2): 여기서, this는 전역 객체 window를 가리킨다.

콜백 함수 내부의 this가 메소드를 호출한 객체(생성자 함수의 인스턴스)를 가리키게 하는 3가지 방법이 있다.

  1. that = this
  2. map(func, this)
  3. bind(this)
  1. that = this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function Prefixer(prefix) {
    this.prefix = prefix;
    }

    Prefixer.prototype.prefixArray = function (arr) {
    var that = this; // this: Prefixer 생성자 함수의 인스턴스
    return arr.map(function (x) {
    return that.prefix + ' ' + x;
    });
    };

    var pre = new Prefixer('Hi');
    console.log(pre.prefixArray(['Lee', 'Kim']));
  2. map(func, this)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Prefixer(prefix) {
    this.prefix = prefix;
    }

    Prefixer.prototype.prefixArray = function (arr) {
    return arr.map(function (x) {
    return this.prefix + ' ' + x;
    }, this); // this: Prefixer 생성자 함수의 인스턴스
    };

    var pre = new Prefixer('Hi');
    console.log(pre.prefixArray(['Lee', 'Kim']));
  3. bind(this)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Prefixer(prefix) {
    this.prefix = prefix;
    }

    Prefixer.prototype.prefixArray = function (arr) {
    return arr.map(function (x) {
    return this.prefix + ' ' + x;
    }.bind(this)); // this: Prefixer 생성자 함수의 인스턴스
    };

    var pre = new Prefixer('Hi');
    console.log(pre.prefixArray(['Ro', 'Park']));


화살표 함수의 this


화살표 함수의 this는 언제나 상위 스코프의 this를 가리킨다.

1
2
3
4
5
6
7
8
9
10
11
function Prefixer(prefix) {
this.prefix = prefix;
}

Prefixer.prototype.prefixArray = function (arr) {
// this는 상위 스코프인 prefixArray 메소드 내의 this를 가리킨다.
return arr.map(x => `${this.prefix} ${x}`);
};

const pre = new Prefixer('Hi');
console.log(pre.prefixArray(['Ro', 'Park']));

화살표 함수는 call, applay, bind 메소드를 사용하여 this를 변경할 수 없다.

1
2
3
4
5
6
window.x = 1;
const normal = function () { return this.x; };
const arrow = () => this.x;

console.log(normal.call({ x: 10 })); // 10
console.log(arrow.call({ x: 10 })); // 1


화살표 함수를 사용하면 안되는 경우


화살표 함수는 Lexical this를 지원하므로 콜백 함수로 사용하기 편리하지만, 화살표 함수를 사용하지 말아야 하는 경우도 있다.

<1> 메소드


메소드를 정의할 때는, 화살표 함수를 사용하면 안된다.

1
2
3
4
5
6
const person = {
name: 'Ro',
sayHi: () => console.log(`Hi ${this.name}`)
};

person.sayHi(); // Hi undefined

위와 같이, sayHi라는 메소드를 정의할 때 화살표 함수를 사용하게 되면, 화살표 함수 내부의 this는 상위 컨택스트인 전역 객체 window를 가리킨다.

ES6의 축약 메소드 표현을 사용하면, 다음과 같이 작성할 수 있다.

1
2
3
4
5
6
7
8
const person = {
name: 'Ro',
sayHi() { // === sayHi: function() {
console.log(`Hi ${this.name}`);
}
};

person.sayHi(); // Hi Ro


<2> prototype


prototype에 할당하기 위한 메소드를 정의할 때는, 화살표 함수를 사용하면 안된다.

1
2
3
4
5
6
7
const person = {
name: 'Ro',
};

Object.prototype.sayHi = () => console.log(`Hi ${this.name}`);

person.sayHi(); // Hi undefined

prototype에 메소드를 할당하는 경우, 일반 함수를 사용해야 한다.

1
2
3
4
5
6
7
8
9
const person = {
name: 'Ro',
};

Object.prototype.sayHi = function() {
console.log(`Hi ${this.name}`);
};

person.sayHi(); // Hi Ro


<3> 생성자 함수


생성자 함수를 사용할 때는, 화살표 함수를 사용하면 안된다. 생성자 함수는 prototype 프로퍼티를 가지며 prototype 프로퍼티가 가리키는 프로토타입 객체의 constructor를 사용한다. 그러나, 화살표 함수는 prototype 프로퍼티를 가지고 있지 않다.

1
2
3
4
5
6
const Foo = () => {};

// 화살표 함수는 prototype 프로퍼티가 없다
console.log(Foo.hasOwnProperty('prototype')); // false

const foo = new Foo(); // TypeError: Foo is not a constructor


<4> addEventListener 함수의 콜백 함수


addEventListener 함수의 콜백 함수를 사용할 때는, 화살표 함수를 사용하면 안된다.

만약, addEventListener 함수의 콜백 함수를 화살표 함수로 정의하면, 화살표 함수 내의 this는 상위 컨택스트인 전역 객체 window를 가리킨다.

1
2
3
4
5
6
var button = document.getElementById('myButton');

button.addEventListener('click', () => {
console.log(this === window); // => true
this.innerHTML = 'Clicked button';
});

그래서, addEventListener 함수의 콜백 함수를 사용할 때는, 일반 함수인 function 키워드를 사용해야 한다. 일반 함수로 정의된 addEventListener 함수의 콜백 함수 내부의 this는 이벤트 리스너에 바인딩된 요소(currentTarget)를 가리킨다.

1
2
3
4
5
6
var button = document.getElementById('myButton');

button.addEventListener('click', function() {
console.log(this === button); // => true
this.innerHTML = 'Clicked button';
});


Vue에서 화살표 함수 사용방법


Vue에서 화살표 함수는 기본적으로 ES6에서의 화살표 함수와 동일하다.

화살표 함수의 사용 여부에 대한 대표적인 경우는 2가지이다.

  • 메소드를 정의할 때
  • 콜백함수를 정의할 때


Vue에서 메소드를 정의할 때


Vue에서 메소드를 정의할 때는, 화살표 함수를 사용하면 안된다. 즉, function 키워드를 사용하는 일반 함수로 정의해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<input type="text" v-model="newTodo">
<button v-on:click="addTodo">add</button>
</div>
</template>

<script>
export default {
data: () => ({
newTodo: ''
}),
methods: {
addTodo: function () {
console.log(this.newTodo); // input에서 입력한 값을 가져옴
}
}
}
</script>

methods에서 addTodo를 정의할 때, function 키워드를 사용하면, 이 함수내의 this는 Vue 인스턴스를 가리킨다. 그래서, this.newTodo의 값을 가져올 수 있다.

만약, 화살표 함수로 addTodo를 정의하면 어떻게 될까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<input type="text" v-model="newTodo">
<button v-on:click="addTodo">add</button>
</div>
</template>

<script>
export default {
data: () => ({
newTodo: ''
}),
methods: {
addTodo: () => {
console.log(this.newTodo); // undefined
}
}
}
</script>

화살표 함수로 addTodo를 정의하면, 이 함수내의 this는 전역객체 window를 가리킨다. 그래서 값이 undefined가 된다.

대신, ES6의 축약 메소드 표현을 사용하면, 다음과 같이 작성할 수 있다.

1
2
3
4
5
methods: {
addTodo () {
console.log(this.newTodoItem) // input에서 입력한 값을 가져옴
}
}


Vue에서 콜백 함수를 정의할 때


Vue에서 콜백 함수를 정의할 때, 화살표 함수를 사용하면, 정의된 콜백 함수내의 this는 Vue의 인스턴스를 가리킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
export default {

data: {
size: 'small',
items: [ { size: 'small' }, { size: 'large' } ]
},

computed: {
filterBySize() {
return this.items.filter((item) => {
return item.size === this.size; // this는 Vue의 인스턴스를 가리킴
});
}
}

}
</script>

만약, 콜백 함수를 정의할 때, function 키워드를 사용하면, 정의된 콜백 함수내의 this는 상위 컨택스트, 즉 전역 객체 window를 가리킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
export default {

data: {
size: 'small',
items: [ { size: 'small' }, { size: 'large' } ]
},

computed: {
filterBySize() {
let size = this.size // 여기서 this는 Vue 인스턴스를 가리키기 때문에, 미리 size라는 변수에 위 data의 size의 값을 할당
return this.items.filter(function(item) {
return item.size === size;
});
}
}

}
</script>


결론: Vue에서의 화살표 함수 및 일반 함수 사용 방법


  • 메소드를 정의할 때는, 일반 함수를 사용한다. 이때, funtion 키워드르 안쓰고 싶다면, ES6의 축약 메소드 표현을 사용해서, 간단히 작성하면 된다.
  • 콜백 함수를 정의할 때는, 화살표 함수를 사용한다. 이때, 정의된 콜백 함수내의 this는 해당 컨택스트, 즉 정의된 콜백 함수를 가지고 있는 Vue 인스턴스를 가리킨다.

Computed vs. Methods vs. Watch

Computed vs. Methods


핵심은 캐싱(값의 저장)이 있냐 없냐에 차이다.

공통점: 둘 다 인스턴내에서 함수를 정의하는 부분이고, 데이터가 변동됨에 따라 안에 있는 함수를 재호출하게 된다.
차이점: 데이터가 변동되지 않는다고 가정했을 때이다.

  • Computed: 종속 대상의 값이 저장(캐싱) 되어, 이미 계산되어진 값을 가져온다.
    • 종속된 대상이 변경될 때만, 함수를 실행하고, 종속 대상이 변경되지 않으면, computed를 여러번 요청해도 계산을 다시 하지 않고, 계산되어 있는(computed) 결과를 즉시 반환한다.
    • 데이터 변동이 없는 상태에서, 이전의 계산된 값을 캐시해 두었다가 함수 호출시 재사용한다.
  • methods: 렌더링을 다시 할때마다, 항상 함수를 실행하여 함수의 로직에 결과물을 반환한다.
    • 사용할때마다, 함수의 계산을 다시 한다.


캐싱이 왜 필요할까?


computed의 속성이 계산이 많이 걸리는 함수라고 하자. 만약, 캐싱을 하지 않으면, computed의 속성의 getter 함수를 꼭 필요한 것보다 더 많이 실행하게 된다. 그래서, 캐싱을 원하지 않으면, methods를 사용한다.


결론


  • 데이터가 자주 변동되지 않으면, 비용적인 면에서는 computed가 좋다.
  • 데이터가 수시로 없데이트 되면, 계속해서 캐시를 저장하는 computed보다 methods가 더 좋다.

그래서, html에서는 computed는 변수처럼 쓰이고, methods는 함수처럼 쓰인다.
예) parse 함수

  • Computed: parse를 그대로 사용한다.
  • Methods: parse() 이런식으로 사용한다.


Computed vs. Watch


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


Computed/Watch는 언제, 왜 사용해야 할까?


Vue에서 Routing 사용하는 방법

Routing(라우팅)이란 무엇인가?


라우팅이란, 웹 페이지간의 이동 방법을 말하며, 웹 앱의 형식 중 하나인 SPA에서 주로 사용한다.

라우팅의 장점은 화면 간의 전환이 매끄러워, UX를 향상 시킬 수 있다.

일반적인 웹(MPA)에서는 특정 행동(링크 클릭, 또는 특정 이벤트)를 한 후, 페이지를 이동하게 되면, 흰색의 빈 페이지가 깜박거린 후, 이동된 화면이 나타나게 된다. 이런 부분들을 라우팅으로 처리하면 깜빡러미 없이 매끄럽게 화면이 전환될 수 있다.


Vue에서의 라우팅


Vue 라우터는 Vue에서 라우팅을 할 수 있도록 지원하는 공식 라이브러리이다.


Vue에서 라우팅 설치 및 등록


<설치>

Npm install vue-router

<등록>

1
2
3
Import VueRouter from ‘vue-router’

Vue.use(VueRouter)


기본적인 Vue Routing 사용법


Vue 라우터 라이브러리를 사용하면, 2개의 tag를 사용한다.

  • <router-link to=“url">: 페이지 이동 태그로, 화면에서는 <a>로 표시되며 클릭하게되면, to속성에 입력한 URL로 이동
  • <router-view>: 페이지 표시 태그로, 변경되는 URL에 따라 해당 컴포넌트를 출력해주는 영역

기본적인 라우팅을 만들어보자. 위에 버튼 두개가 있는데, 왼쪽은 todo 페이지로 이동, 오른쪽은 user 페이지로 이동하는 라우팅을 만들어 보자.

(1) vue-router를 설치하고, router.js를 만들어 다음과 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Todo from './components/Todo.vue'
import User from './components/User.vue'

Vue.use(VueRouter)

const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Todo },
{
path: '/user',
component: User,
}
]
})

Todo와 User 컴포넌트를 만들고, import 한뒤, router 인스턴스를 생성하고, path와 path에 맞는 컴포넌트를 맵핑한다.

‘History’ mode는 기본적으로 url에 #이 들어가 있는데, 이를 제거해 준다.

(2) main.js에 등록하여, 전역에서 사용할 수 있게 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import router from './router'

Vue.config.productionTip = false

new Vue({
store,
router,
render: h => h(App),
}).$mount('#app')

(3) App.vue에 라우터 링크를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
// App.vue
<section>
<div>
<router-link to="/"><button>Go to Todo page</button></router-link>
<br>
<router-link to="/user"><button>Go to User page</button></router-link>
</div>
</section>

<br>

<router-view></router-view>

이렇게 되면, 각 버튼을 클릭하면, 각 버튼에 해당하는 path로 이동하여, path에 맵핑된 컴포넌트를 화면에 뿌려준다.


Nested 라우팅


Nested(중첩된) 라우터는 라우터로 페이지를 이동할 때, 최소 2개 이상의 컴포넌트를 화면에 나타내는 것을 말한다.
상위 컴포넌트 1개와 하위 컴포넌트 N개로 구성할 수 있다.

예를 들어, User 페이지에서 url에 따라, User의 Profile과 User의 Post를 각각 보여주는 라우터를 만들어보자.

(1) 두 개의 컴포넌트 UserProfile과 UserPost를 만들고, 상위 컴포넌트인 User에 그 하위 컴포넌트로 UserProfile과 UserPost를 라우팅으로 등록한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Todo from './components/Todo.vue'
import User from './components/User.vue'
import UserProfile from './components/UserProfile.vue'
import UserPost from './components/UserPost.vue'

Vue.use(VueRouter)

const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Todo },
{
path: '/user',
component: User,
children: [
{
path: 'userProfile',
component: UserProfile
},
{
path: 'userPost',
component: UserPost
}
]
}
]
})

(2) 상위 컴포넌트인 User.vue에 다음과 같이 링크를 추가한다. 이때, 상위 컴포넌트에 <router-view></router-view>를 추가하여, 상위 컴포넌트내에서 그 하위 컴포넌트들이 화면에 보이게 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// User.vue
<template>
<div class="user">
<p>This is User page.</p>

<br>

<router-link to="/user/userProfile">UserProfile 컴포넌트로 이동</router-link>

<br>

<router-link to="/user/UserPost">UserPost 컴포넌트로 이동</router-link>

<br>

<router-view></router-view>
</div>
</template>


동적 라우팅


동적 라우팅이란, 일정한 패턴의 URI 경로를 하나의 컴포넌트에 연결하는 방법이다. 일정한 패턴의 URI란, 예를 들어, projects 페이지에서 각 페이지별로 상세하게 보고 싶은데, 이때 URI를 projects/1, projects/2 이런식으로 각 프로젝트를 id로 구별하는 것을 말한다. 이를 하나의 컴포넌트(Projects.vue)로 연결하여 사용한다.

하나의 메인 페이지에서 어떤 링크(버튼 등)를 클릭하면, projects 페이지가 나오고, 그 projects 페이지에는 여러개의 프로젝트들이 보여진다. 그리고, 각 프로젝트를 클릭하면, 각 프로젝트에 대한 상세 정보가 나오게 한다. 이때, 클릭 시 라우팅의 URI는 projects/1 이런식으로 지정하여 만든다.

(1) 우선, store.js에서 dummy 데이터를 만들자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.use(Vuex);
Vue.use(VueAxios, axios);

export default new Vuex.Store({
state: {
projects: [
{
id: 1,
text: 'vue',
completed: false
},
{
id: 2,
text: 'react',
completed: false
}
]
},
actions: {
},
mutations: {
}
})

(2) router.js에서 Projects에 대한 path를 등록하고, 이에 대한 children으로써, Project에 대한 path를 등록한다. 이때, path는 ‘:id’로 등록한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Projects from './components/Projects.vue'
import Project from './components/Project.vue'

Vue.use(VueRouter)

const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/projects',
component: Projects,
children: [
{
name: 'project',
path: ':id',
component: Project
}
]
}
]
})

export default router

(3) App.vue에서 Projects 페이지로 이동하는 라우터 링크를 작성한다. 그리고, 링크 클릭 시, Projects 페이지의 정보를 보여주기 위해, router-view를 아래에 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
// App.vue
<template>
<div id="app">
<p>This is main page</p>

<router-link to="/projects"><button>Go to Project page</button></router-link>

<br>

<router-view></router-view>
</div>
</template>

(4) Projects 페이지는 프로젝트가 나열되어 나오게 한다. 그리고, 각 프로젝트에 라우터 링크를 걸어, 그 링크를 클릭시, 클릭한 프로젝트에 대한 상세 정보가 나오는 Project 페이지가 나오게 한다. 이때, router-link에는 name과 params를 같이 넣어 주는데, 이는 router.js에서 등록한 그대로 작성하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Projects.vue
<template>
<div>
<p>Projects page</p>

<div v-for="project in projects" :key="project.id">
<router-link :to="{ name: 'project', params: { id: project.id } }">
[ID: {{project.id}}] {{project.text}}
</router-link>
</div>

<br>

<router-view></router-view>
</div>
</template>

<script>
export default {
computed: {
projects () {
return this.$store.state.projects
}
}
}
</script>

(5) 위에서 어떤 하나의 프로젝트를 클릭하면, 그 프로젝트 컴포넌트에서 작성한 화면을 보여준다.(Project.vue). 이때, this.$route.params.id로 id에 접근할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Project.vue
<template>
<div>

<p>Project each view: {{ this.$route.params.id }}</p>

</div>
</template>

<script>
export defualt {
}
</script>


Named 라우팅


Named 라우팅이란, 각 화면의 영역을 각각 용도, 모듈별로 구분할 수 있는데, 이때, 이름으로 구분하는 것을 말한다. 일반적인 웹 앱에서는 header, footer, body로 나누어 화면을 구성한다.

(1) App.vue에서 각 화면별로 이름을 지정하여 화면의 영역을 나눈다.

1
2
3
4
// App.vue
<router-view name="header"></router-view>
<router-view></router-view><!-- name을 지정하지 않으면, default -->
<router-view name="footer"></router-view>

(2) 각 화면에 대한 컴포넌트로 만든 뒤, router.js로 import 한다. 그리고, routes에 다음과 같이 지정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Body from './components/Body.vue'
import Header from './components/Header.vue'
import Footer from './components/Footer.vue'

Vue.use(VueRouter)

const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/',
components: {
default: Body,
header: Header,
footer: Footer
}
}
]
})

‘Components’ 속성에 Key(router-view 이름)-Value(컴포넌트)로 등록하면, 컴포넌트에서 <router-view>에 각 컴포넌트의 화면이 보여진다.

이런 컴포넌트 구조는(Header-Body(default)-Footer) 모든 페이지의 기본형식이기 때문에, path를 ‘/‘로 사용하여, 기본페이지로 사용할 수 있다.

Vue에서 Axios 사용하는 방법

Axios란 무엇인가?


axios란, Pormise 기반의 http 통신을 위한 클라이언트 라이브러리이다.

vue에는 vue-resource라는 것이 있으나, 2016년 9월 이후 업데이트가 되어있지않고, Evan You(Vue 만든사람) 또한, 공식적으로 추천하지 않고, axios 쓰는 것을 추천한다.


Axios 설치하기


npm으로 axios를 설치한다.

npm install axios vue-axios


Axios 등록하기


Vue 애플리케이션을 개발할 때, Vuex(store.js)의 actions에서 axios를 사용하므로, store.js에 axios를 등록한다.

1
2
3
4
5
6
7
8
9
10
11
// store.js
import Vue from 'vue'
import Vuex from 'vuex'

// axios & vue-axios를 import
import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.use(Vuex);
// axios & vue-axios를 등록
Vue.use(VueAxios, axios);


Axios 사용하기


1. JSON server 만들기


가상의 서버에서 데이터를 만들어 사용하기 위해, 프론트엔드 개발자가 직접 JSON server를 구축하여, axios 통신을 테스트할 수 있다.

npm install -g json-server


2. JSON server 만들기


JSON server에 가상 데이터를 만들기 위해, db.json 파일을 src 폴더 아래에 만들고, 다음과 같이 데이터를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// db.json
{
"todos": [
{
"id": "1",
"text": "vue",
"completed": false
},
{
"id": "2",
"text": "react",
"completed": false
},
{
"id": "3",
"text": "angular",
"completed": true
},
{
"id": "4",
"text": "python",
"completed": false
}
]
}


3. JSON server 띄우기(Running)


json-server --watch src/db.json --port 4000


4. Vuex(store.js)의 actions에 메소드 등록하기


actions에 등록하고, 등록할 때, mutations를 commit하여 변경된 상태를 추적가능하게 하면 된다.

서버와 통신을 할 때, CRUD라는 것이 있는데, todo 앱과 비교하면 다음과 같다.

  • Create: addTodo
  • Read: getTodo
  • Update: changeTodo(toggle)
  • Delete: deleteTodo

Vuex(store.js)의 actions에 메소드 등록할 때, 위에 4가지로 나눠서 코드를 작성해보자.


<1>. Create: addTodo


Create는 클라이언트단에서 데이터를 새로 생성하여, 서버에 추가하는 것이다. todo 앱에서는 addTodo가 된다.

Vuex(store.js)의 actions에 메소드 등록할 때, axios의 post를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// store.js
actions: {
addTodo: function(context, payload) {
// post를 사용할 때는, 요청할 uri에 보낼 데이터를 같이 요청한다.
axios.post('http://localhost:4000/todos', {
id: 4,
text: payload,
completed: false
})
// 요청이 성공하면, mutations에 commit을 하여, 원래 데이터에 요청한 데이터를 추가한다.
.then((res) => {
console.log(res);
context.commit('addTodo', res.data)
})
// 요청이 실패하면, 에러에 대한 조치를 취한다.
.catch((err) => {
console.log(err);
})
}
}

actions에서 commit한 mutations에 로직을 작성한다.

1
2
3
4
5
mutations: {
addTodo (state, todo) {
state.todoList = [...state.todoList, todo]
}
}


<2>. Read: getTodo


Read는 클라이언트단에서 데이터를 서버에 요청하여, 데이터를 받는 것이다. todo 앱에서는 getTodo가 된다.

Vuex(store.js)의 actions에 메소드 등록할 때, axios의 get을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// store.js
actions: {
getTodo: function(context) {
// get을 사용할 때는, 요청할 uri를 작성한다.
axios.get('http://localhost:4000/todos')
// 요청이 성공하면, mutations에 commit을 하여, 원래 데이터에 요청받은 데이터를 할당한다.
.then((res) => {
console.log(res);
context.commit('getTodo', res.data)
})
// 요청이 실패하면, 에러에 대한 조치를 취한다.
.catch((err) => {
console.log(err);
})
}
}

actions에서 commit한 mutations에 로직을 작성한다.

1
2
3
4
5
mutations: {
getTodo (state, data) {
state.todoList = data;
}
}


<3>. Update: changeTodo(toggle)


Update는 클라이언트단에서 기존에 있던 데이터를 서버에 요청하여, 데이터를 변경하는 것이다. todo 앱에서는 toggle이 된다.

Vuex(store.js)의 actions에 메소드 등록할 때, axios의 patch를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// store.js
actions: {
toggle: function(context, payload) {
// patch를 사용할 때는, 요청할 uri에 변경할 데이터의 uri를 붙여서 보낸다. 예를 들어, /todos/id가 올 수 있다.
axios.patch(`http://localhost:4000/todos/${payload}`)
// 요청이 성공하면, mutations에 commit을 하여, 특정 데이터가 변경된 상태로 데이터가 변경된다.
.then((res) => {
console.log(res);
context.commit('toggle', payload)
})
.catch((err) => {
console.log(err);
})
},
}

actions에서 commit한 mutations에 로직을 작성한다.

1
2
3
4
5
6
mutations: {
toggle (state, id) {
const index = state.todolist.findIndex((item)=>item.id === id);
state.todolist[index].completed = !this.todolist[index].completed;
},
}


<4>. Delete: deleteTodo


Update는 클라이언트단에서 기존에 있던 데이터를 서버에 요청하여, 데이터를 변경하는 것이다. todo 앱에서는 toggle이 된다.

Vuex(store.js)의 actions에 메소드 등록할 때, axios의 patch를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// store.js
actions: {
deleteTodo: function(context, payload) {
// delete를 사용할 때는, 요청할 uri에 제거할 데이터의 uri를 붙여서 보낸다. 예를 들어, /todos/id가 올 수 있다.
axios.delete(`http://localhost:4000/todos/${payload}`)
// 요청이 성공하면, mutations에 commit을 하여, 특정 데이터가 지워진 상태로 데이터가 변경된다.
.then((res => {
console.log(res);
context.commit('deleteTodo', payload)
}))
.catch((err) => {
console.log(err);
})
}
}

actions에서 commit한 mutations에 로직을 작성한다.

1
2
3
4
5
6
7
mutations: {
deleteTodo (state, id) {
state.todoList = state.todoList.filter((t) => {
return t.id !== id
})
}
}

Vuex 사용법

State란 무엇인가?


state는 컴포넌트 간에 공유할 data를 말한다. 각 컴포넌트에서의 data 속성과 동일하기 때문에, 각 컴포넌트의 인스턴스에서 data() 속성을 사용하는 대신, Vuex에 state로 등록하여 사용한다.


State 등록하기


store.js에 state(데이터)를 등록한다.

1
2
3
4
5
6
7
8
9
10
11
12
// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

export default new Vuex.Store({
// state안에 데이터를 등록
state: {
value: 10
}
});


State 사용하기


가져와서 사용할 각 컴포넌트에서 Vuex의 state에 접근하여 사용한다.

1
2
3
4
5
6
7
// App.vue
<template>
<div id="app">
// store.js의 state에 있는 데이터(value)에 접근하여 가져와서 사용
{{ this.$store.state.value }}
</div>
</template>

위와 같은 방식으로, 어떤 컴포넌트든 Vuex(store.js)에 접근하여 state를 가져와 사용할 수 있다.

그러나, Template의 표현식은 최대한 간소화해야 한다. 다음과 같이 computed를 사용해서 state에 접근할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div id="app">
{{ getValue }}
</div>
</template>

<script>
export default {
computed: {
getValue() {
return this.$store.state.value
}
}
}
</script>


Getters란 무엇인가?


Getters는 인스턴스의 computed(계산된 속성)과 같은 역할을 한다. 인스턴스에서 computed는 이미 어떤 값이 캐싱되어 있어, 종속된 값이 변경되지 않으면, 이미 계산되 값이 바로 나오고, 종속된 값이 변경된 경우에만 다시 재계산되어 나온다.

그래서, Getters를 사용하면 여러 컴포넌트에서 같은 로직을 비효율적으로 사용하는 것을 막는다. 즉, 각 컴포넌트에서는 수행 로직을 호출만 하면, Vuex의 Getters를 보내주어, 코드의 가독성과 성능이 좋아진다.


Getters 등록하기


store.js에 getters를 등록한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

export default new Vuex.Store({
state: {
todos: [
{ id: 1, text: 'vue', complete: true },
{ id: 2, text: 'react', complete: false },
{ id: 3, text: 'angular', complete: true }
]
},
// todos의 complete가 true인 데이터만 필터
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.complete)
}
}
});


Getters 사용하기


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// App.vue
<template>
<div id="app">
// doneTodos를 화면에 보여줌
{{ doneTodos }}
</div>
</template>

<script>

export default {
// store.js의 getters에 있는 doneTodos에 접근하여 사용
computed: {
doneTodos() {
return this.$store.getters.doneTodos
}
}
}
</script>


mapGetters 사용하기


Vuex에 내장된 helper 함수 중, mapGetters를 사용하여, 좀 더 간단하게 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// App.vue
<template>
<div id="app">
{{ doneTodos }}
</div>
</template>

<script>
// mapGetters를 import
import { mapGetters } from 'vuex'

// computed에서 mapGetters를 사용
export default {
// Vuex 의 Getters 메서드 명과 App.vue 메서드 명이 동일할 때, [] 사용
computed: mapGetters([
'doneTodos'
]),

// Vuex 의 Mutations 메서드 명과 App.vue 메서드 명을 다르게 매칭할 때, {} 사용
computed: mapGetters({
// 앞 doneTodos 해당 컴포넌트의 메서드를, 뒤 'doneTodos'는 Vuex의 getters를 의미
doneTodos: 'doneTodos'
})
}
</script>


Mutations란 무엇인가?


Mutations(변이)란, Vuex의 데이터, 즉 state 값을 변경하는 로직들을 의미한다. Vuex 저장소에서 실제로 상태를 변경하는 유일한 방법이다.

Mutations 특징

  • 모든 state의 변화를 추적해야 하기 때문에, 순차적, 즉 동기적으로 일을 처리한다.
  • 컴포넌트의 인스턴스에서는 methods에서 사용하여, Vuex의 mutations에 접근한다.
  • 이때, 인자를 받아서 Vuex로 보낼 수 있다.

Getters와의 차이점

  • 인자를 받아서 Vuex에 넘겨줄 수 있다.
  • methods에서 사용(Getters는 computed에서 사용)

Actions와의 차이점

  • mutations: 동기적 로직
  • actions: 비동기적 로직

만약, 여러 개의 컴포넌트에서 같은 state 값을 동시에 제어하게 되면, state 값이 어느 컴포넌트에서 호출해서 어떻게 변경된지를 추적하기가 어렵기 때문에, mutations에서는 동기적으로 하나씩 일을 처리하여, 상태의 변화를 정확하게 추적할 수 있다.


Mutations 등록하기


먼저 Vuex(store.js)에 mutations를 등록해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// store.js
export default new Vuex.Store({

mutations: {
getTodo (state, payload) {
state.todoList = payload;
},
// function을 같이 써줘도 된다.
addTodo: function(state, payload) {
state.todoList = [...state.todoList, payload];
},
},

})

mutations는 getters와 다르게, 인자를 받아서 Vuex에 넘겨줄 수 있다.

  • 첫번째 인자: state로, store.js에 있는 state를 의미한다.
  • 두번째 인자: payload로, 컴포넌트에서 보내주는 데이터이다.(만약 데이터가 없다면, 생략 가능)


Mutations 사용하기


컴포넌트의 인스턴스에서는 methods에서 사용하여, commit으로 Vuex(store.js)의 mutations에 접근한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// App.vue
<template>
<section>
<div class="row justify-content-center mt-4">
<input v-model="inputField" v-on:keyup.enter="addTodo" class="mr-1" placeholder="Todo Item" />
<button @click="addTodo" class="btn btn-primary">Add Todo</button>
</div>
</section>
</template>


<script>

export default {
// store.js에서의 mutations 중 'getTodo'에 접근
mounted() {
this.$store.commit('getTodo')
},
// store.js에서의 mutations 중 'addTodo'에 접근
methods: {
addTodo: function() {
if(this.inputField) {
// 'addTodo'는 mutations에 있는 'addTodo', this.inputField는 Vuex(store.js)에 넘겨줄 데이터
this.$store.commit('addTodo', this.inputField);
this.inputField = '';
}
},
}
}
</script>

참고로, main.js에 전역 인스턴스에 store를 등록했기 때문에, this.$store를 사용할 수 있다.


mapMutations 사용하기


Vuex에 내장된 helper 함수 중, mapMutations를 사용하여, 좀 더 코드 가독성을 높일 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// App.vue
// mapMutations를 import 해야함
import { mapMutations } from 'vuex'

methods: {
// Vuex 의 Mutations 메서드 명과 App.vue 메서드 명이 동일할 때, [] 사용
...mapMutations([
'addTodo'
]),

// Vuex 의 Mutations 메서드 명과 App.vue 메서드 명을 다르게 매칭할 때, {} 사용
...mapMutations({
addTodo: 'addTodo' // 앞 addTodo는 해당 컴포넌트의 메서드를, 뒤 'addTodo'는 Vuex의 mutations를 의미
})
}


Actions란 무엇인가?


actions란, Vue에서 비동기적으로 처리되야 하는 것들을 관리하기 위한것이다. 즉, 서버와의 http 통신이나 setTimeout() 등과 같이, 결과를 받아올 타이밍이 예측되지 않은 로직을 actions에 선언하고 처리한다.

대표적으로 비동기인 다음의 2가지를 actions에서 주로 사용한다.

  • http통신
  • setTimeout()


<1> http 통신: axios 사용


Actions 등록하기


먼저 Vuex(store.js)에 actions를 등록해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// store.js
export default new Vuex.Store({

actions: {
addTodo: function(context, payload) {
axios.post('http://localhost:4000/todos', {
id: 4,
text: payload,
completed: false
})
.then((res) => {
// 상태(state)가 변경된 것을 추적하기 위해, mutations의 메소드를 호출(commit)
context.commit('addTodo', res.data)
})
.catch((err) => {
console.log(err);
})
},
}

})


Mutations 등록하기


actions에 등록을 했어도, 상태가 변경된 것을 추적하기 위해서는 mutations의 메소드를 호출(commit) 해야한다. 그리고, mutations에 actions에 등록한 메소드를 작성한다.

1
2
3
4
5
mutations: {
addTodo (state, todo) {
state.todoList = [...state.todoList, todo]
},
}


Actions 사용하기


actions를 호출할 때는 dispatch를 사용한다.

1
2
3
4
5
6
7
// App.vue
methods: {
addTodo() {
// 'addTodo'는 actions에 있는 'addTodo', this.inputField는 Vuex(store.js)에 넘겨줄 데이터
this.$store.dispatch('addTodo', this.inputField);
}
},


mapActions 사용하기


Vuex에 내장된 helper 함수 중, mapActions를 사용하여, 좀 더 간단하게 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// App.vue
import {mapActions} from 'vuex';

export default {
methods: {
// Vuex 의 Actions 메서드 명과 App.vue 메서드 명이 동일할 때, [] 사용
...mapActions([
'addTodo',
'getTodo'
])

// Vuex 의 Actions 메서드 명과 App.vue 메서드 명을 다르게 매칭할 때, {} 사용
...mapActions({
addTodo: 'addTodo' // 앞 addTodo는 해당 컴포넌트의 메서드를, 뒤 'addTodo'는 Vuex의 actions를 의미
})
},
}


<2> setTimeout() 사용


Actions 등록하기


먼저 Vuex(store.js)에 actions를 등록해야 한다.

1
2
3
4
5
6
7
8
9
10
// store.js
export default new Vuex.Store({

delayTime: function (context, payload) {
return setTimeout(function () {
commit('addTodo', payload); // mutations에 있는 'addTodo' 호출
}, 5000);
}

})


Mutations 등록하기


actions에 등록을 했어도, 상태가 변경된 것을 추적하기 위해서는 mutations의 메소드를 호출(commit) 해야한다. 그리고, mutations에 actions에 등록한 메소드를 작성한다.

1
2
3
4
5
mutations: {
addTodo (state, todo) {
state.todoList = [...state.todoList, todo]
},
}


Actions 사용하기


actions를 호출할 때는 dispatch를 사용한다.

1
2
3
4
// App.vue
addTime(time) {
this.$store.dispatch('delayTime', time);
}

Vuex란 무엇인가?

Vuex란 무엇인가?


Vuex란, 상태관리 패턴에 대한 라이브러리로써, 애플리케이션의 모든 컴포넌트에 대한 중앙집중식 저장소 역할을 하며, 예측 가능한 방식으로 상태를 변경할 수 있다.


Vuex는 왜 사용하는가?


Vuex는 상태관리(State Management)를 위해 사용한다. 여기서 말하는 상태(State)란, 데이터를 말하며, 컴포넌트간에 데이터 통신 및 전달을 효율적으로 관리를 쉽게 하기 위해 Vuex를 사용한다.

Vuex는 중대형 규모의 앱 컴포넌트들을 관리할 때 좋은데, 일반적으로 앱의 규모가 커지면 다음과 같은 문제가 생긴다.

  • 단순 부모-자식간의 데이터 통신이 아닌, 그 중간에 많은 컴포넌트들이 있을 경우, 데이터 통신 방식이 복잡해짐
  • EventBus를 사용하여 상하위 관계가 아닌 컴포넌트들간의 통신시에 관리가 안됨

이런 문제를 해결하기 위해, Vuex를 사용하여 모든 데이터(State)를 한 곳에서 중앙 집중식으로 관리한다. 즉, Vuex는 공통의 상태를 공유하는 여러 컴포넌트가 있을 경우, 이를 Vuex 한 곳(전역)에서 집중적으로 관리하는 역할을 한다. 그러나, 만약 공통으로 공유하는 것이 아닌, 개별의 로컬 컴포넌트에서 사용하는 경우에는, 꼭 Vuex에 저장할 필요가 없다.


Vuex Architecture


위에서, Vuex는 상태관리 패턴에 대한 라이브러리라고 했다. 즉, 상태관리 패턴이란, 상태를 관리하기 위한 하나의 패턴방식을 말한다. Vuex에서의 패턴방식은 단반향 데이터 흐름을 따른다.

Vuex One-Way Data Flow
출처: Vuex One-Way Data Flow

상태관리는 3가지로 구성되어 있다.

  • state: 컴포넌트간에 공유하는 data
  • view: data가 보여지는 template
  • actions: 사용자의 입력에 따라 반응하는 methods
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
new Vue({

// state
data () {
return {
msg: 'hello world'
}
},

// view
template: `
<div>{{ msg }}</div>
`,

// actions
methods: {
changeMsg () {
this.msg = 'world hello'
}
}
})


Vuex Architecture


전체적인 Vuex Architecture는 다음과 같다.

Vuex Architecture
출처: Vuex Architecture


Vuex 설치 및 등록


  1. npm을 이용해서 설치한다.

    1
    npm install vuex
  2. Vuex를 등록할 js 파일을 새로 생성하는데, 보통 관례로 store.js로 만든다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // store.js
    import Vue from 'vue'
    import Vuex from 'vuex'

    Vue.use(Vuex);

    export default new Vuex.Store({
    //
    });
  3. Vuex를 전역에서 사용하기 위해, main.js에 등록한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // main.js
    import Vue from 'vue'
    import App from './App.vue'
    // store를 import
    import store from './store'

    Vue.config.productionTip = false

    new Vue({
    // store를 루트(전역) 인스턴스에 등록
    store,
    render: h => h(App),
    }).$mount('#app')

루트 인스턴스에 store 옵션을 제공함으로써 저장소는 루트의 모든 하위 컴포넌트에 주입되고, 하위 컴포넌트에서 this.$store로 사용할 수 있다. (루트 인스턴스: main.js에 있는 최상단의 인스턴스)

Vue Architecture

Component 단위로 개발하는 Vue


Vue는 컴포넌트 단위로 개발할 수 있다. 컴포넌트(Component)란, html, css, javascript를 1개의 파일 단위로 나눠서 개발하는 것을 말한다. Vue에서는 이를 싱글 파일 컴포넌트라고 하는데, UI 단위로 나눠서 개발하여, 코드의 재사용 측면에서 좋다.

즉, Vue.js는 컴포넌트를 조합해 전체 애플리케이션을 만든다. 이때, 컴포넌트들은 부모-자식 관계를 갖는 트리구조로 작성할 수 있다.

Vue 프로젝트를 생성하게 되면, 다양한 파일이 만들어진다.


index.html


index.html: 어플리케이션 전체의 뼈대가 되는 html 파일이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>what</title>
</head>
<body>
<noscript>
<strong>We're sorry but what doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>


main.js


main.js: Vue 인스턴스를 생성해주는 파일이다.

1
2
3
4
5
6
7
8
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
render: h => h(App),
}).$mount('#app')

이 자바스크립트 파일은 new 연산자를 사용해서 Vue 인스턴스를 생성해준다. Vue는 기본적으로 인스턴스를 생성하여 개발을 하는데, 컴포넌트 단위로 개발을 할 경우, main.js에서 new 연산자로 Vue 인스턴스를 생성하게 해주고, 각 컴포넌트에서 각 UI에 맞게 인스턴스를 활용해서 컴포넌트를 개발한다.

render에서 h() 안에 있는 ‘App’은 첫 화면에 뿌려질 Vue 컴포넌트를 의미한다.

1
render: h => h(App)

$mount() 안에 있는 ‘#app’은 id를 ‘app’으로 가지고 있는 html 태그를 의미한다. 이 태그로 마운트한다는 것인데, 마운트한다는 것은 위 App 컴포넌트를 이 html 태그(id를 ‘app’으로 가지고 있는 태그)에 연결시킨다는 의미이다.

1
$mount('#app')

즉, 위 index.html에 있는 <div id="app"></div> 안으로 App 컴포넌트를 연결(넣는다)한다는 뜻이다.


component.vue


component.vue: 재사용성을 고려해 UI 단위로 개발하는 vue 파일이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
name: 'app',
components: {
HelloWorld
}
}
</script>

<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

component는 위와 같이 3가지로 구성되어 있다.

  • template
  • script
  • style

Vue의 MVVM 패턴에 따라 나누면 다음과 같은 3가지로 구성되어 있다.

  • ViewModel
  • View
  • Model


1. ViewModel

Vue.js에서 모든 Vue 인스턴스는 ViewModel인데, 이 ViewModel은 Model과 View 사이에서 양방향 데이터 바인딩을 해줌으로써, 동기화 역할을 한다. ViewModel은 Vue 생성자 혹은 하위 클래스들에 의해 인스턴스화된다.

1
var vm = new Vue({ /* options */ })


2. View

Vue.js는 DOM 기반의 템플릿을 사용하는데, View의 실제 돔(DOM)은 Vue instance에 의해 관리된다. 각 Vue 인스턴스는 해당 DOM 요소에 연관된다. Vue 인스턴스가 생성되면 필요한 데이터 바인딩을 설정하는 동안 루트 요소의 모든 자식 노드를 반복적으로 탐색한다. 뷰가 컴파일이 되면 데이터 변경에 반응할 수 있게 된다.

1
2
3
4
5
6
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<p>{{ msg }}</p>
</div>
</template>

즉, 해당 HTML 컴포넌트 범위(해당 DOM 요소가 담긴 템플릿)를 Vue 인스턴스의 vm.$el에 설정한다. 뷰는 Vue 인스턴스가 생성 될 때, 컴파일 되며 뷰모델의 다양한 동작 기능들이 바인딩 된다. 그렇게 됨으로써 뷰의 변경이 감지되면 뷰모델이 이를 감지하여 반응할 수 있게된다. 또한, 뷰의 변경은 집단적으로, 비동기로 실행되기 때문에 높은 성능을 발휘한다.


3. Model

Vue.js에서 모델은 단순히 자바스크립트 객체 혹은 데이터 객체로 표현할 수 있다. 데이터 객체의 프로퍼티를 조작하면 이를 관찰하는 뷰 인스턴스가 변경을 알린다. Vue.js는 ES5 getter/setters로 데이터 객체의 프로퍼티를 변환한다. 그렇기에 뷰를 변경하기 위해 Vue에 명시적으로 신호를 보낼 필요가 없다. 그리고, 각 뷰 인스턴스는 데이터 객체에 있는 모든 프로퍼티들을 프록시한다.

1
2
3
4
5
6
7
8
9
10
11
12
export default {
name: 'app',
components: {
HelloWorld
},
// Model
data: function() {
return {
msg: 'hello'
}
}
}

package.json, node_modules, 그리고 package-lock.json간의 관계

npm이란 무엇인가?


npm(node package manager)은 노드 패키지 매니저의 약자로써, 모듈(패키지) 관리(설치, 업데이트, 삭제)를 하기위한 매니저이다.

노드의 정의는 크게 2가지로 나눌 수 있다.

  • 서버: 네트워크를 통해 클라이언트에 정보나 서비스를 제공하는 컴퓨터 또는 프로그램을 말한다.
  • 자바스크립트 런타임: 자바스크립트 프로그램을 컴퓨터에서 실행할 수 있게 하는 환경을 말한다.

npm에서의 노드는 자바스크립트 런타임을 의미한다. 즉, 대부분의 자바스크립트 프로그램은 패키지라는 이름으로 npm에 등록되어 특정 기능을 하는 패키지가 필요하면, npm에서 찾아 설치하면 된다. npm에 업로드된 노드 모듈(자바스크립트 기반으로 만들어진 프로그램 파일)을 패키지라고 부른다. 그래서, npm을 자바스크립트 패키지 매니저라고도 부른다.

모듈이 다른 모듈을 사용할 수 있는 것처럼, 어떤 패키지가 다른 패키지를 사용할 수 있다. 이런 관계를 의존 관계(dependencies)라고 부른다.

이런 패키지 매니저가 npm 말고도, 페이스북에서 만든 yarn도 있다.


npm은 왜 사용하는가?


npm에는 약 60만 개(2018년 6월 기준)의 패키지가 등록되어 있는데, 이는 대부분 오픈소스로 등록되어 있어 웹개발을 할 때 많은 도움이 된다.

또한, 새로운 모듈(패키지)를 설치하거나 관리를 위해, npm은 npm cli를 제공해서 cli 명령어로 설치, 업데이트 및 삭제를 쉽게 할 수 있다.


package.json이란 무엇인가?


package.json이란, 생성한 프로젝트의 메타정보와 이 프로젝트가 의존하고 있는(설치한) 모듈들에 대한 정보들을 json 형태로 모아놓은 파일이다.

package.json 파일내에 있는 각 속성들에 대해서는 다음 링크를 참고하면 좋을 것 같다.
package.json에 대한 자세한 설명


package.json은 왜 사용하는가?


만약 이 package.json 파일을 사용하지 않을 경우 다음과 같은 문제가 발생할 수 있다.

  • 프로젝트에서 사용하는 외부 모듈들이 많아지게 되면, 관리하기가 어려워진다.
  • 각 패키지들은 고유한 버전이 있기 때문에, 따로 기록해 두어야 한다. 왜냐하면, 패키지의 버전도 빈번하게 업데이트가 되기 때문이다.
  • 새로운 프로젝트를 진행할 때, 필요한 모듈들이 많다면 매번 npm 명령으로 설치해야 하는 번거로움이 있다.

이런 경우, 필요한 패키지들의 목록을 파일로 정리해놓고, 목록 파일을 이용하여 단 한번의 명령어로 필요한 패키지들을 모두 설치할 수 있다. 이러한 패키지 정의 파일을 package.json 파일이라고 한다. 즉, package.json은 프로젝트에 대한 메타정보, 그리고 설치한 패키지의 의존성 및 버전을 관리하는 파일이다.

vue cli로 Vue 프로젝트를 생성하면, 자동으로 package.json 파일이 만들어 지지만, npm init을 사용하면, package.json 파일을 직접 만들 수도 있다. 그리고, 팀 내에서 동일한 개발환경을 구축하려고 할 때, 이미 작성된 package.json 파일이 있다면, 팀 내에 배포하여 동일한 개발환경을 빠르게 구축할 수 있다.


node_modules란 무엇인가?


Vue 프로젝트를 생성하면, package.json 파일 뿐만 아니라, node_modules 디렉토리가 같이 생성된다. package.json에는 프로젝트가 의존하고 있는 모듈들에 대한 정보가 나와있고, node_modules 디렉토리에는 package.json에 있는 모듈 뿐만 아니라, package.json에 있는 모듈이 의존하고 있는 모듈 전부를 포함하고 있다. 그래서 node_modules 디렉토리안에는 정말 많은 모듈들이 들어가 있다. npm으로 새로운 모듈(패키지)를 설치하게 되면, package.json과 node_modules에 추가된다.

참고로, git에 커밋할 때, node_modules을 제외해도 된다. 왜냐하면, node_modules가 없어도, package.json에 설치한 패키지들이 모두 있기 때문에, npm install로 node_modules를 언제든지 설치가 가능하기 때문이다.


package-lock.json이란 무엇인가?


package-lock.json은 이 package-lock.json이 생성되는 시점의 의존성 트리(node_modules)에 대한 정보를 가지고 있는 파일을 말한다. 의존성 트리는 package.json에 등록된 모듈과 그 모듈들이 의존하고 있는 모듈 전부를 포함하고 있기 때문에, 결국 package-lock.json도 이 모든 모듈들을 가지고 있다. npm을 사용해서 node_modules나 package.json을 수정하게 되면, package-lock.json 또한 자동으로 업데이트가 된다.


package-lock.json은 왜 사용하는가?


package.json 파일에는 의존성 모듈(dependencies)의 version range가 사용된다. version range란, 특정 버전이 아닌, 버전의 범위를 의미한다. 예를 들어, npm install express로 express를 설치하면, package.json에는 ‘^4.10.3’(Caret Ranges)과 같이 버전 범위가 추가된다. 이 버전의 express가 추가된 package.json을 가지고 npm install을 실행하면, 현재는 4.10.3 버전이 설치되지만, express의 버전이 업데이트된 상태로 publish가 된 후에, 동일한 package.json 파일로 npm install을 실행했을 경우, 원래 버전이 아닌, 새로 업데이트된 버전으로 express가 변경된다. 이럴 경우, 기존에 가지고 있던 node_modules(의존성 트리)에 있던 모듈의 버전과 충돌이 일어나, 오류를 발생시킬 수 있다. 이 문제를 해결하기 위해, package-lock.json을 사용하는 것이다.

package-lock.json은 node_modules(의존성 트리)에 대한 정보를 가지고 있는데, package-lock.json이 업데이트가 되는 시점에 node_modules(의존성 트리)을 재생성할 수 있다. 그래서, package-lock.json 파일이 있다면, npm install로 package.json과 package-lock.json에 있는 모듈이 새로 업데이트되는 동시에, node_modules(의존성 트리)가 새로 생성되어, 각 파일이 가지고 있는 모듈의 버전을 동일하게 맞출 수가 있게 된다.

즉, package.json에 있는 모듈의 버전은 npm install을 수행하는 시점에 따라 달라진다. 이 말은, npm install을 수행하는 시점에 publish 되어있는 모듈의 버전으로 업데이트가 된다는 뜻이다. 이렇게 되면, package.json과 package-lock.json에 있는 모듈이 같은 버전으로 업데이트가 되고, 이때 package-lock.json 때문에 node_modules(의존성 트리)가 재생성되어, 3개의 파일에 있는 모듈이 모두 같은 버전으로 맞춰지게 되어 오류가 안나게 된다.

이런 이유로, git에 커밋할 때, package.json 파일 뿐만 아니라, packge-lock.json 파일 또한 같이 커밋을 해야 한다.


패키지 version


노드 패키지들의 버전은 세 자리로 되어있는데, 이는 SemVer 방식의 버전 넘버링을 따르기 때문이다. SemVer는 Semantic Versioning(유의적 버전)의 약어인데, 이는 버전을 구성하는 세 자리가 모두 의미가 있다는 뜻이다.

서비스를 개발하다 보면, 정말 많은 패키지들을 사용하게 되는데, 이런 많은 패키지들이 서로 얽히다 보면 문제가 생길 수 있다. 예를 들어, 어떤 패키지의 버전을 업그레이드 했는데, 그것을 사용하는 다른 패키지에서 에러가 발생한다면 문제가 된다. 따라서 버전 번호를 어떻게 정하고, 올려야 하는지를 명시하는 규칙을 만들었는데, 이것이 바로 SemVer 이다.

버전은 세 자리로 구성되어 있다.

  • major 버전: 하위 호환이 안될 정도로 패키지의 내용이 수정되었을 때 올린다.
    • 주 버전이 0이면, 초기 개발 중이라는 뜻이다.
    • 1부터는 정식 버전이다.
    • 만약, 1.7.1에서 2.0.0으로 올렸다면, 1.7.1 버전 패키지를 사용하고 있던 사람들이 2.0.0으로 업데이트 했을 때, 에러가 발생할 확률이 크다.
  • minor 버전: 하위 호환이 되는 업데이트 시에 올린다.
    • 만약, 1.7.1에서 1.8.0으로 올렸다면, 1.7.1 사용자가 1.8.0으로 업데이트 했을 때, 아무 문제가 없어야 한다.
  • patch 버전: 새로운 기능이 추가되었다기 보다는, 기존 기능에 문제가 있어 수정한 것을 내놓았을 때, patch 버전을 올린다.
    • 1.7.0에서 1.7.1으로 올렸다면, 업데이트 후 문제가 없어야 한다.

새 버전을 배포한 후에는, 그 버전의 내용을 절대 수정하면 안된다. 만약, 수정 사항이 생기면, major버전, minor 버전, patch버전 중 하나를 의미에 맞게 올려서 새로운 버전으로 배포해야 한다.

버전의 숫자마다 의미가 부여되어 있기 때문에, 다른 패키지를 사용할 때도 버전만 보고 에러 발생 여부를 판단할 수 있다.

만약, 의존하는 패키지의 major 버전이 업데이트 되었다면, 기존 코드와 호환이 되지 않을 확률이 크기 때문에, 미리 주의를 기울여야 한다. 만약, minor나 patch 버전으로 업데이트 되었다면, 상대적으로 안심하고 버전을 올리 수 있다.

package.json에는 버전 말고도, 다른 기호(‘^’, ‘~’ 등)들이 있다. 이런 기호들은 버전에는 포함되지 않지만, 설치 또는 업데이트 시 어떤 버전의 범위를 설치해야 하는지 알 수 있다.

  • ^(캐럿): minor 버전까지만 설치 또는 업데이트한다.
    • 예를 들어, npm install express@^1.7.1 이라면, 1.7.1 <= 버전 < 2.0.0까지 설치가 되고, 2.0.0은 설치되지 않는다.
  • ~(틸트): patch 버전까지만 설치 또는 업데이트한다.
    • npm install express@~1.7.1 이라면, 1.7.1 <= 버전 < 1.8.0까지 설치가 된다.

~ 보다 ^가 많이 사용되는 이유는, minor 버전까지는 하위 호환이 되기 때문이다.

npm semver calculator에 방문하면, 패키지 별로 버전 표기법을 사용하여, 업데이터 버전 범위를 확인 가능하다.

참고로, npm install 명령어의 패키명 뒤에 @버전을 추가하면 패키지 버전을 지정하여 설치할 수 있다.


nodemon이란?


nodemon은 프로젝트 폴더의 파일들을 모니터링하고 있다가, 파일이 수정될 경우 자동으로 서버를 재시작을 해준다. nodemon을 위해서 소스에 다른 설정을 추가할 필요도 없기 때문에 상당히 편리하게 사용할 수 있습니다.


–save와 –dev


npm install 모듈명 --save 을 입력하면, 설치하는 모듈을 package.json에 등록할 수 있다.(npm@5 부터는 –save는 기본옵션이다.)
그래서, npm@5 부터는 –save 옵션을 사용하지 않더라도, 모든 install 명령은 package.json의 dependencies에 설치되어 관리된다.
그리고, npm install 모듈명 --save --dev을 입력하면, 설치하는 모듈을 package.json에 등록할 수 있을 뿐만 아니라, –dev 때문에, package.json 파일에서 devDepencies에 등록된다. devDepencies에서 관리하는 모듈들은 개발용 모듈들이고, depencies에서 관리하는 모듈들은 배포용 모듈들이다.

package.json에 명시된 모든 의존 패키지를 한번에 설치하기 위해서는, npm install 명령어를 사용하면 된다.


자주 사용하는 npm 명령어


목적 npm 명령어
package.json 생성 npm init
패키지 로컬 설치 npm install package-name
패키지 전역 설치 npm install -g package-name
패키지 개발 설치 npm install –save –dev package-name
package.json의 모든 패키지 설치 npm install
package.json의 모든 패키지 설치 npm install
로컬/개발 패키지 제거 npm uninstall package-name
전역 패키지 제거 npm uninstall -g package-name
패키지 업데이트 npm update package-name
버전 확인 npm -v
npm 명령어 설명 참조 npm help

Vue 프로젝트 Directory 구조

Vue 프로젝트 Directory 구조 생성


Vue 프로젝트를 vue-cli 명령어로 입력해서 프로젝트 구조를 생성한다.

1
vue create <project name>

vue-cli 2.x 버전에서는 프로젝트 생성시, 선택가능한 템플릿(simple, webpack 등)이 있었으나, vue-cli 3 버전부터는 기본적인 프로젝트를 먼저 생성하고, 필요한 플러그인을 그때마다 추가하여 자신이 원하는 구조로 프로젝트를 변경할 수 있게 되었다.


플러그인 설치


기본적으로, 프로젝트를 생성할 때 pre-installed 플러그인을 추가하여 설치할 수 있다. 이후, 필요한 플러그인이 있을 때마다 다음과 같은 명령어로 이미 생성된 프로젝트에 플러그인을 추가할 수 있다.

1
vue add @vue/eslint

@vue/eslint 명령어는 @vue/cli-plugin-eslint 명령어의 약어다.

플러그인 중에서 vue-roter와 vuex는 특별한 케이스로 다음과 같이 설치한다.

1
2
vue add router
vue add vuex


Vue 프로젝트 파일과 구조


Vue 프로젝트 생성시, 생성되는 파일과 구조는 다음과 같다.

Directory/File Description
dist Production용으로 빌드를 하면, 최종 결과물이 나오는 Directory
public 공용으로 접근 가능한 Directory
public/index.html 어플리케이션의 뼈대가 되는 html 파일
public/favicon.io 웹브라우저 주소창에 표시되는 웹사이트를 대표하는 아이콘
src 애플리케이션의 소스코드가 있는 Directory
src/main.js Vue 인스턴스를 생성하는 javascript 파일
src/App.vue Vue 애플리케이션의 가장 최상위 컴포넌트
src/assets 이미지 등 static assets이 모여 있는 Directory
src/components Vue 컴포넌트들이 모여 있는 Directory
src/router.js Vue Router로 설정한 라우팅 정보
src/views 화면을 전체 구성하는 View와 관련된 파일(라우팅과 관련된 컴포넌트들이 모여 있는 Directory)
test 테스트 관련된 코드가 있는 Directory
babel.config.js babel을 위한 설정 파일
node_modules yarn 또는 npm으로 설치한 의존성 모듈들이 있는 Directory
package.json Vue 프로젝트의 메타 정보와 해당 프로젝트에서 사용하는 패키지(모듈)(의존성관련 정보)들의 정보가 저장되는 파일
package-lock.json node_modules(의존성 트리) Directory에 대한 모든 정보를 가지고 있는 파일