프로토타입(Prototype) 뽀개버리기!! 

 

함수도 객체다

함수의 기본 기능인 코드 실행뿐만 아니라, 함수 자체가 일반 객체처럼 프로퍼티들을 가질 수 있다.

function add(x, y) { 
return x+y; 
}

add.status = 'OK';
console.log(add.status)
  1. add( ) 함수를 생성할 때 함수 코드는 함수 객체의 [[Code]] 내부 프로퍼티에 자동으로 저장된다(이것은 ECMAScript 명세서를 참조한 것이다* ).
  2. add() 함수에 마치 일반 객체처럼 status 프로퍼티를 생성하고 저장한 것을 확인할 수 있다.
  3. status 프로퍼티도 일반 객체에서의 접근 방식처럼 add.status를 이용해 접근 가능하다. 이처럼 자바스크립트에서 함수는 특정 기능의 코드를 수행하는 역할뿐만 아니라, 일반 객체처럼 자신의 프로퍼티를 가질 수 있는 특별한 객체라고 볼 수 있다.

함수는 객체이기 때문에, 일반 객체처럼 취급될 수 있다.

따라서 다음과 같은 동작이 가능하다.

  • 리터럴에 의해 생성
  • 변수나 배열의 요소,객체의 프로퍼티 등에 할당 가능
  • 함수의 인자로 전달 가능
  • 함수의 리턴값으로 리턴 가능
  • 동적으로 프로퍼티를 생성 및 할당 가능

이와 같은 특징 때문에 함수를 일급 객체라고 부른다.

더보기

일급 객체(First Class)

일급 객체라는 말은 컴퓨터 프로그래밍 언어 분야에서 쓰이는 용어로서, 위에 나열한 기능이 모두 가능한 객체를 일급 객체라고 부른다. 자바스크립트 함수가 가지는 이러한 일급 객체의 특성으로 함수형 프로그래밍이 가능하다.

정리하면,

자바스크립트 함수의 기능은 C나 자바와 같은 다른 언어 함수의 기능과 거의 비슷하다.

 

하지만 기본적인 기능 외에도, 자바스크립트에서 함수를 제대로 이해하려면, 함수가 일급 객체라는 것을 아는 것이다.

즉, 함수가 일반 객체처럼 값으로 취급된다는 것을 이해해야 한다. 이 때문에 함수를 변수나 객체, 배열 등에 값으로도 저장할 수 있으며, 다른 함수의 인자로 전달한다거나 함수의 리턴값으로도 사용 가능하다는 것을 알아야 한다.

하지만 일반 객체와는 조금 다르다

무엇이 다를까?

함수 객체만의 표준 프로퍼티가 정의되어 있다.

function add(x, y) { 
    return x+y; 
}

console.dir(add);

 

arguments, caller, length 등과 같은 다양한 프로퍼티가 기본적으로 생성된다. 이러한 프로퍼티들이 함수를 생성할 때 포함되는 표준 프로퍼티다.

참고로 ECMA5 스크립트 명세서에는 모든 함수가 length와 prototype 프로퍼티를 가져야 한다고 기술하고 있다.

  • arguments: 호출 시 전달되는 인자 값
  • caller: 자신을 호출한 함수를 나타낸다
  • name: 함수의 이름
  • length: 함수가 정상적으로 실행될 때 기대되는 인자의 개수
  • __proto__: 부모 역할을 하는 프로토타입 객체를 가리킨다. = [[Prototype]] = __proto__ → Function.prototype
  • prototype: 함수가 생성될 때 만들어지며, 단지 constructor 프로퍼티 하나만 있는 객체를 가리킨다. 그리고 prototype 프로퍼티가 가리키는 프로토타입 객체의 유일한 constructor 프로퍼티는 자신과 연결된 함수를 가리킨다. 즉, 자바스크립트에서는 함수를 생성할 때, 함수 자신과 연결된 프로토타입 객체를 동시에 생성하며, 이 둘은 각각 prototype 과 constructor 라는 프로퍼티로 서로를 참조하게 된다.

함수 객체는 항상 부모 역할을 하는 객체를 가리킨다: 암묵적 프로토타입 링크

모든 자바스크립트 객체는 자신의 프로토타입을 가리키는 [[Prototype]] 이라는 내부 프로퍼티를 가진다.크롬 브라우저는 이 프로퍼티가 __proto__ 프로퍼티로 구현되어 있다.

이것이 가리키는 부모 역할을 하는 프로토타입 객체는 Function.prototype 객체이다.

 

그리고 모든 함수는 Function Prototype 객체의 프로퍼티나 메소드를 마치 자신의 것처럼 상속받아 그대로 사용할 수 있다. - constructor, toString(), apply(), call(), bind()

 

그런데 ECMAScript 명세서에는 Function.prototype은 함수라고 정의하고 있다.
그리고 함수 역시 객체이므로, 자신의 부모 역할을 하는 프로토타입 객체를 가리킨다.
그렇다면 이러한 규칙에 의해 Function.prototype이 함수니까, 이것도 Function.prototype 객체, 즉, 자기 자신을 부모가 갖는 것인가?

 

ECMAScript 명세서에는 예외적으로 Function.prototype 함수 객체의 부모는 자바스크립트의 모든 객체의 조상격인 Object.prototype 객체라고 설명하고 있다.
때문에 Function Prototype 객체의 proto 프로퍼티는 Object.prototype 객체를 가리킨다.

 

함수는 Prototype 객체랑 같이 다닌다: prototype 프로퍼티

함수 객체의 prototype 프로퍼티는 함수가 생성될 때 만들어진다.

이 프로퍼티는 단지 constructor 프로퍼티 하나만 있는 객체를 가리킨다.

 

그리고 prototype 프로퍼티가 가리키는 프로토타입 객체의 유일한 constructor 프로퍼티는 자신과 연결된 함수를 가리킨다.

즉, 자바스크립트에서는 함수를 생성할 때, 함수 자신과 연결된 프로토타입 객체를 동시에 생성하며, 이 둘은 각각 prototype 과 constructor 라는 프로퍼티로 서로를 참조하게 된다.

 

예제로 살펴보자.

function myFunction() { 
    return true;
}

console.dir(myFunction.prototype);
console.dir(myFunction.prototype.constructor);

  1. myFunction() 이라는 함수를 생성했다. 함수가 생성됨과 동시에 myFunction() 함수의 prototype 프로퍼티에는 이 함수와 연결된 프로토타입 객체가 생성된다.
  2. myFunction.prototype은 myFunction() 함수의 프로토타입 객체를 의미한다. constructor, __proto__ 라는 두 개의 프로퍼티를 가진다.
    이 객체는 myFunction() 함수의 프로토타입 객체이므로 constructor 프로퍼티가 있다.
    이 객체 역시 자바스크립트 객체이므로, 예외 없이 자신의 부모 역할을 하는 __proto__ 가 있다.
  3. myFunction.prototype.constructor 의 결과 값을 보면, myFunction() 함수를 가리키고 있다.

함수 객체와 프로토타입 객체는 prototype 과 constructor 라는 프로퍼티를 통해 서로를 참조하는 관계이다.

 

프로토타입의 두 가지 의미

자바스크립트는 다른 언어와는 다르게 프로토타입 기반의 객체지향 프로그래밍을 지원한다.

객체지향 프로그래밍에서는 클래스를 정의하고 이를 통해 객체를 생성하지만, 자바스크립트에는 이런 개념이 없다.대신에 객체 리터럴이나 생성자 함수로 객체를 생성한다.

 

이렇게 생성된 객체의 부모 객체가 바로 '프로토타입' 객체이다.

모든 객체에는 자신의 부모인 프로토타입 객체를 가리키는 참조 링크 형태의 숨겨진 프로퍼티([[Prototype]] 프로퍼티)가 있는데, 이러한 링크를 암묵적 프로토타입 링크(Implicit prototype link) 라고 부른다.

 

주의할 점은, 암묵적 프로토타입 링크와 prototype 프로퍼티를 구분해야 한다.

모든 객체는
자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 프로토타입 객체를
자신의 부모 객체로 설정하는 암묵적 프로토타입 링크로 연결한다.

예제를 살펴보자.

function Person(name) {
    this.name = name;
};

var foo = new Person('foo');

console.dir(Person);
console.dir(foo);

 

Person() 생성자 함수로 생성된 foo 객체는 Person() 함수의 프로토타입 객체를 암묵적 프로토타입 링크로 연결한다.

따라서
Person 함수 객체의 prototype 프로퍼티 = foo 객체의 __proto__ 프로퍼티
가 성립한다.

prototype 프로퍼티는 함수 입장에서 자신과 링크된 프로토타입 객체를 가리키는 것이고,
__proto__(암묵적 프로토타입 링크) 프로퍼티는 객체의 입장에서 자신의 부모 객체인 프로토타입 객체를 가리키는 것이다.

 

프로토타입 체이닝

객체는 자기 자신의 프로퍼티 뿐만 아니라, 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티 또한 자신의 것처럼 접근하는 것이 가능하다.

이것을 가능하게 하는 것이 바로 프로토타입 체이닝이다.

자바스크립트에서는 특정 객체의 프로퍼티나 메서드에 접근하려고 할 때,
해당 객체에 접근하려는 프로퍼티 또는 메서드가 없으면
암묵적 프로토타입 링크를 따라 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티를 차례대로 검색한다.

이것을 프로토타입 체이닝이라고 한다.

객체 리터럴 프로토타입 체이닝

리터럴로 생성된 객체는 내부적으로 Object() 함수 객체를 통해 생성된다.

Object() 함수는 암묵적 프로토타입 링크로 Object.prototype 객체를 가리킨다.

 

이에 따라 어떻게 프로토타입 체이닝이 이뤄지는지 예제를 살펴보자.

var myObject = {
    name: 'foo',
    sayName: function() {
        console.log('My Name is ' + this.name);
    }
}

myObject.sayName();
console.log(myObject.hasOwnProperty('name'));
console.log(myObject.hasOwnProperty('nickName'));
myObject.sayNickName();

왜 myObject 객체가 hasOwnProperty() 메서드를 호출할 때는 에러가 발생하지 않았을까?

 

객체 리터럴로 생성한 객체는 Object()라는 내장 생성자 함수로 생성된 것이다.

Object() 생성자힘수도힘수객체이므로prototype이라는프로퍼티 속성이 있다.

 

따라서 Object() 함수의 prototype 프로퍼티가 가리키는 Object.prototype 객체를 자신의 프로토타입 객체로 연결한다.

 

  1. myObject.sayName(); : 해당 객체 내에 메서드가 있어서 바로 수행된다.

  2. myObject.hasOwnProperty('name') : myObject 내에는 hasOwnProperty() 라는 메서드가 없다.
    따라서 암묵적 프로토타입 링크를 따라, 그것의 부모 역할을 하는 Object.prototpye 프로토타입 객체에서 hasOwnProperty() 메서드가 있는지 검색한다.
    hasOwnProperty() 메서드는 자바스크립트 표준 API 로 Object.prototype 객체에 포함되어 있다.
    따라서 코드가 정상적으로 수행된다.

생성자 함수로 객체를 생성하는 경우의 프로토타입 체이닝

생성자 함수로 객체를 생성하는 경우는 객체 리터럴 방식과 약간 다른 프로토타입 체이닝이 이뤄진다.

예제를 살펴보자.

function Person(name, age, hobby) {
    this.name = name;
    this.age = age;
    this.hobby = hobby;
};

var foo = new Person('foo', 30, 'tennis');

console.log(foo.hasOwnProperty('name')); // true
console.dir(Person.prototype);

foo.hasOwnProperty('name') 를 실행하면 먼저 foo 객체에서 hasOwnProperty() 메서드를 찾는다.

 

foo 객체에는 없으므로, 암묵적 프로토타입 링크를 통해 부모 객체인 Person.prototype 에서 hasOwnProperty() 메서드를 찾는다.

 

Person.prototype 에도 없으므로, Person.prototype 의 프로토타입 객체에서 찾는다. Person.prototype 객체도 역시 자바스크립트 객체이므로 Object.prototype 을 프로토타입 객체로 가진다. 따라서 Object.prototype 에서 hasOwnProperty() 메서드를 찾는다.

 

Object.prototype 객체의 hasOwnProperty() 메서드를 실행한다.

 

위의 예제들을 통해 알 수 있듯이 Object.prototype 객체프로토타입 체이닝의 종점이다.

 

이와 같은 방식으로 자바스크립트의 숫자, 문자열, 배열 등에서 사용되는 표준 메서드들의 경우, Number.prototype, String.prototype, Array.prototype 등에 정의되어 있다.

 

프로토타입 체이닝 활용하기

빌트인 프로토타입에 메서드 추가하기

자바스크립트는 Object.prototype, String.prototype 등과 같이 표준 빌트인 프로토타입 객체에도 사용자가 직접 정의한 메서드들을 추가하는 것을 허용한다.

Object.prototype.testMethod = function() {
    console.log('test method');
}

프로토타입 객체에 동적으로 프로퍼티 추가/삭제하기

프로토타입 객체 역시 자바스크립트 객체이므로 일반 객체처럼 동적으로 프로퍼티를 추가/삭제하는 것이 가능하다.

그리고 이렇게 변경된 프로퍼티는 실시간으로 프로토타입 체이닝에 반영된다.

function Person(name) {
    this.name = name;
};

var foo = new Person('foo');

foo.sayHello(); // Uncaught TypeError: foo.sayHello is not a function

Person.prototype.sayHello = function() {
    console.log('Hello');
};

foo.sayHello(); // Hello

프로토타입 객체의 this 바인딩

프로토타입 객체는 메서드를 가질 수 있다.

만약 프로토타입 메서드 내부에서 this 를 사용한다면 이는 어디에 바인딩 될까?

 

객체의 메서드 호출 시 this 바인딩 규칙이 그대로 적용된다.

즉, 메서드 호출 패턴대로, this는 그 메서드를 호출한 객체에 바인딩된다.

function Person(name) {
    this.name = name;
};

var foo = new Person('foo');

Person.prototype.getName = function() {
    return this.name;
};

Person.prototype.name = 'person';

console.log(foo.getName()); // foo

console.log(Person.prototype.getName()); // person

foo.getName() 실행 시 getName() 메서드는 foo 객체에서 찾을 수 없으므로, 프로토타입 체이닝이 발생한다.
결과적으로 Person.prototype의 getName() 이 실행된다.
그런데 getName() 메서드를 호출한 객체는 foo 객체이므로, this 는 foo 객체에 바인딩이 된다.

 

Person.prototype.getName() 실행 시 Person.prototype 객체의 getName() 이 실행된다.
getName()을 호출한 객체는 Person.prototype 이므로 this는 Person.prototype 객체에 바인딩된다.

 

디폴트 프로토타입 객체를 다른 객체로 변경하기

디폴트 프로토타입 객체는 함수가 생성될 때 같이 생성되는 프로토타입 객체를 말한다.
이 객체를 다른 객체로 변경하는 것이 가능하다.

이러한 특징을 이용해서 객체지향의 상속을 구현하는 것이다.

 

여기서 주의할 점이 있다.

생성자 함수의 프로토타입 객체가 변경되면, 변경된 시점 이후에 생성된 객체들은 변경된 프로토타입 객체로 암묵적 프로토타입 링크가 이뤄진다.

 

반면, 생성자 함수의 프로토타입이 변경되기 이전에 생성된 객체들은 기존 프로토타입 객체로의 링크를 유지한다.

function Person(name) {
    this.name = name;
};

console.log(Person.prototype.constructor); // Person 함수 객체

var foo = new Person('foo');

console.log(foo.country); // undefined

Person.prototype = {
    country: 'korea'
};

console.log(Person.prototype.constructor); // Object 함수 객체

var bar = new Person('bar');

console.log(foo.country); // undefined
console.log(bar.country); // korea

console.log(foo.constructor); // Person 함수 객체
console.log(bar.constructor); // Object 함수 객체

Person.prototype 을 리터럴 객체로 변경한 후, Person.prototype.constructor 값이 Object() 생성자 함수로 출력된 것을 주목하자.

 

변경한 프로토타입 객체는 리터럴로 생성하여 단지 country 프로퍼티만 있다. 즉, 디폴트 프로토타입 객체처럼 constructor 프로퍼티가 없다.

 

이 경우에도 암묵적 프로토타입 링크는 존재하기 때문에 프로토타입 체이닝은 발생한다.

 

모든 자바스크립트의 리터럴 객체는 프로토타입 객체로 Object.prototype 을 가진다. 따라서 Object.prototype 의 constructor 가 참조하고 있는 Object() 함수를 연결하여, Object() 생성자 함수가 출력되는 것이다.

 

 

전역 수준 컴포넌트의 문제점

  • 빌드 단계가 없음
    • ECMAScript 2015 이상, Typescript와 같은 최신 문법을 사용할 수 없음
  • CSS 빌드 & 모듈화 기능이 없음
  • template이 모두 고유한 id를 가지도록 개발자가 관리해야 함

그래서 생겨난게...

단일 파일 컴포넌트(Single File Component)

단일파일 컴포넌트: <template />, <script />, <style />로 이루어진 확장자가 .vue인 파일

<template>
    <div id="comp-id">
	    ...
    </div>  
</template>

<script>
export default {
    name : 'comp-name',
    components : { ... },
    data() {
	    return {};
    },
    ...
}
</script>

<style>
    ...
</style>

 

Vue-CLI의 vue-loader라는 구성요소가 이 파일을 파싱하고, 다른 로더들을 활용해 하나의 모듈로 조합한다.

그 중 css-loader는 CSS스타일을 전처리하고 스타일 정보를 모듈화한다.

 

기본 프로젝트 구조

Vue-CLI를 통해 프로젝트를 만들게되면, 다음과 같은 폴더 구조가 생성된다.

컴포넌트에서의 스타일

컴포넌트에서 스타일을 작성할 때에는 다음과 같은 작성 방법이 있다.

  • 전역 CSS
  • 범위 CSS(Scoped CSS)
  • CSS 모듈(CSS Module)

전역 CSS

일반적인 CSS작성을 이야기한다.

단일 파일 컴포넌트 내의 <style> 태그 안에 작성하면 된다.

<style>
.class-name {
	color: black;
}
</style>

그러나, 이것은 전역 스타일이므로 페이지 전체에서 사용된다. 

때문에 다른 컴포넌트에서도 동일한 CSS클래스명을 사용하면 충돌이 발생한다.

따라서 특정 컴포넌트만의 스타일을 지정하는 범위 CSS, CSS 모듈 방식을 사용하는 것을 추천한다.

범위 CSS(Scoped CSS)

<style scoped>
.class-name {
	color: black;
}
</style>

범위 CSS를 사용할 때에는 다음 사항을 알아두자.

  • 범위 CSS는 Attribute Selector를 사용함
    • 브라우저에서 스타일을 적용하는 속도가 느림
    • ID, 클래스, 태그명 선택자를 사용해 스타일을 적용하여, 브라우저의 실행 속도가 느려지는 문제를 보완하는 것이 좋음
  • 부모 컴포넌트에서 범위 CSS를 적용하기 위해 생성되는 attribute가 자식 컴포넌트의 루트 요소에도 등록됨
    • 부모 컴포넌트에 적용된 범위 CSS는 하위 컴포넌트에도 반영됨

CSS 모듈(CSS Module)

CSS 스타일을 마치 객체처럼 다룰 수 있게 해준다.

<template>
  <div>
    <button v-bind:class="$style.hand"> CSS Module을 적용한 버튼 </button>
    <div :class="[$style.box, $style.border]">Hello World</div>
  </div>
</template>

<script>
export default {
    created() {
        console.log(this.$style);
    }
}
</script>

<style module>
.hand { cursor:pointer; background-color:purple; color:yellow; }
.box { width:100px; height:100px; background-color:aqua; }
.border { border:solid 1px orange; }
</style>

위 코드는 아래의 HTML로 생성된다.

<div>
	<button class="_1l2s2YbgGPFeFA2amkytu7_0"> CSS Module을 적용한 버튼 </button>
	<div class="_3PFteHfWSgjUHmj2ZptXNB_0 fYMDrU15gNkPkiiXEBCss_0">Hello World</div>
</div>

hand, box, border과 같이 코드에 주어진 스타일명이 아닌, 충돌하지 않도록 생성된 다른 이름이 클래스명으로 사용된다.

 

슬롯

부모 컴포넌트에서 자식 컴포넌트로 전달할 정보가 HTML 태그를 포함하고 있다면, props를 사용해 전달하기 쉽지 않다.

슬롯을 이용하면 부모 컴포넌트에서 자식 컴포넌트로 HTML 마크업을 전달할 수 있다.

 

기본 슬롯(Default Slot)

<자식 컴포넌트>

<template>
	<div>
	...
		<div class="content">
			<!-- 슬롯 정의 -->
			<slot></slot>
		</div>
	... 
	</div>
</template>

<부모 컴포넌트>

<template>
	<div>
    ...
    	<child-component>
        	<div>
            이 안에 들어오는 모든 것들이 slot으로 자식 컴포넌트에게 전달된다.
            그리고 자식 컴포넌트에서 마크업으로 렌더링하게 된다.
            </div>
        </child-component>
    ...
    </div>
</template>

명명된 슬롯(Named Slot)

슬롯에 이름을 부여하여, 컴포넌트에 여러 개의 슬롯을 작성할 수 있다.

 

<자식 컴포넌트>

<template>
    <div>
    	<header>
            <slot name="header"></slot>
        </header>
        <section id="content">
             <slot name="content"></slot>
        </section>
        <footer>
            <slot name="footer"></slot>
        </footer>
    </div>
</template>

<부모 컴포넌트>

<template>
    <div>
    	<child-component>
            <template v-slot:header>
                <!-- header slot 안에 채울 HTML 코드 작성 -->
            </template>
             <template v-slot:content>
                <!-- header slot 안에 채울 HTML 코드 작성 -->
             </template>
            <template v-slot:footer>
                <!-- header slot 안에 채울 HTML 코드 작성 -->
            </template>
        </child-component>
    </div>
</template>

참고로, 기본 슬롯의 이름은 'default'이다.

범위 슬롯(Scoped Slot)

부모 컴포넌트에서 슬롯 내부에 들어갈 컨텐츠를 작성할 때,

자식 컴포넌트로부터 필요한 정보를 전달받아서 (부모 컴포넌트에서) 출력할 내용을 커스터마이징하기 위한 용도로 범위 슬롯이 사용된다.

 

<자식 컴포넌트>

<template>
    <div class="child">
        <slot name="type1" :cx="x" :cy="y"></slot>
        <slot name="type2" :cx="x" :cy="y"></slot>
    </div>
</template>

<부모 컴포넌트>

<template>
    <div class="parent">
        <child>
            <!-- 자식으로부터 전달받은 props를 감싼 객체를 그대로 전달받아 사용 -->
            <template v-slot:type1="p1">
                <div>{{p1.cx }} + {{p1.cy}} = 
                {{ parseInt(p1.cx) + parseInt(p1.cy) }}</div>
            </template>
            <!-- 자식으로부터 전달받을 props를 지정하여 사용 -->
            <template v-slot:type2="{cx, cy}">
                <div>{{cx }} 더하기 {{cy}} 는 
                {{ parseInt(cx) + parseInt(cy) }}입니다.</div>
            </template>
        </child>
    </div>
</template>

 

동적 컴포넌트(Dynamic Component)

화면의 동일한 위치에, 상황에 따라 다른 컴포넌트를 표현할 때 사용한다.

성능상의 이유로 상태를 유지하거나 재-렌더링을 피하길 원할 때, <keep-alive>를 이용한다.

<template>
<div>
  <div class="header">
    <nav>
      <ul>
        <li>
          <a href="#" @click="changeMenu('home')">Home</a>
        </li>
        <li>
          <a href="#" @click="changeMenu('about')">About</a>
        </li>
        <li>
          <a href="#" @click="changeMenu('contact')">Contact</a>
        </li>
      </ul>
    </nav>
  </div>

  <div class="container">
    <!-- keep-alive로 감싸서 캐싱하여 여러번 실행되지 않도록 함 -->
    <keep-alive>
     <!-- 동적으로 컴포넌트가 바인딩됨 -->
      <component v-bind:is="currentView"></component>
    </keep-alive>
  </div>
  
</div>
</template>

<script>
import Home from './components/Home.vue';
import About from './components/About.vue';
import Contact from './components/Contact.vue';

export default {
  components : { Home, About, Contact },
  data() {
    return { currentView : 'home' }
  },
  methods : {
    changeMenu(view) {
      this.currentView = view;
    }
  }
}
</script>

 

재귀 컴포넌트(Recursive Component)

템플릿에서 자기 자신을 호출하는 컴포넌트를 재귀 컴포넌트라고 한다.

재귀적으로 컴포넌트를 사용하려면, 반드시 name 옵션을 지정해야 한다.

<template>
  <ul>
    <li v-for="s in subs" v-bind:class="s.type">
        {{s.name}}
        <tree :subs="s.subs"></tree>
    </li>    
  </ul>
</template>

<script>
export default {
    name : 'tree',
    props : [ 'subs' ]
}
</script>

 

 



뷰 최신 버전인 2.5.17 버전에서 트랜지션을 사용해보아따.


참고로 그 사용 방법은 Vue.js 공식 페이지에 기가막히게 잘 나와있다.




뷰 트랜지션을 써보다가 마주친 문제를 설명하기에 앞서, 그놈을 어떤식으로 사용하는지 알아보즈아~.




뷰(Vue) 트랜지션 사용하기


뷰 트랜지션을 이용하여, 로고 3개를 차례로 뿅뿅뿅 나타나게하는 효과를 적용해보려고 한다.


뷰 템플릿(template) 작성

 
<transition-group name="fade" v-on:after-enter="fadeNext" tag="div" class="text-center">
     <img v-show="fade[0]" key="0" class="logo" src="../assets/logo.png">
     <img v-show="fade[1]" key="1" class="logo" src="../assets/logo.png">
     <img v-show="fade[2]" key="2" class="logo" src="../assets/logo.png">
</transition-group>
  


먼저 template 부분인데, 아주아주 간단하다.


  1. 그냥 "transition-group" 이라는 태그를 사용해주면 되고, 

  2. 효과 이름을 정해서 name="내가 정한 트랜지션 이름" 의 형태로 속성을 넣어주고,

  3. 자식 엘리먼트들에다가 'key'를 부여해주자.

  4. 그리고, 이 효과가 나타나게 해주는 switch 역할을 하는 v-show="내가 정한 switch 데이터 변수명" 을 추가해준다(요놈 스위치 변수는 아래 script 코드 부분에서 추가할거심).

  5. 마지막으로, 우리는 세 개의 로고가 뿅뿅뿅 차례차례 뜨도록 하는게 목적이니까, 첫번째 로고가 다 뜨면 다음 로고가 뜨도록 하는 메소드를 호출해주어야 한다. 
    요거슨 뷰에서 트랜지션 훅(hook)을 제공해주니, 그거슬 이용하면 된다. 
    사용하는 방법은 다양한데, 나는 'v-on' 디렉티브를 이용하는 방법을 선택했다.
    v-on:after-enter="내가 정한 메소드명" 형태로 집어넣어 주면 된다(훅 메소드도 아래 script 코드 부분에서 가할 것이니 인내하자).



<참고>


트랜지션 훅(hook) 메소드는 총 8가지가 있다.


v-on:before-enter
v-on:enter
v-on:after-enter
v-on:enter-cancelled

v-on:before-leave
v-on:leave
v-on:after-leave
v-on:leave-cancelled


각각이 어떤 것인지는 뷰 가이드에 두말하면 입아프게 너무나도 자세히 나와 있으니, 


조오기 위에 링크 눌러서 참고하시길.





아직까지는 아무런 효과를 적용해주지 않았으므로 걍 아래와 같은 정적인 상태가 보일 거심.









뷰 스타일(style) 코드 작성

얘가 뿅뿅 나타나도록 CSS 코드로 Fade-in 효과를 만들어보자.


  
.fade-enter {
  opacity: 0;
}
.fade-enter-active {
  transition: opacity .5s;
};
 


이 코드는, "fade" 라는 이름의 뷰 트랜지션을 정의해준다.


트랜지션 진입 시 처음 상태"fade-enter"로 정해주고,

트랜지션 효과"fade-enter-active"로 정해준다.


위 코드를 통해 "fade"라는 뷰 트랜지션은

=> 처음에는  'opacity: 0' 에서 시작해서 0.5초동안 opacity가 1로 변하는 트랜지션

이라고 이해하면 된다.




뷰 스크립트(script) 코드 작성

이제 중요한 것 2가지가 남았다.


1. 처음에 트랜지션 효과가 일어나도록 해주는 스위치를 만들어주는 것과,


2. 첫번째 엘리먼트에 트랜지션 효과가 끝났으면, 그 다음 엘리먼트에 효과가 일어나도록 하는 훅 메소드를 만들어주는 것.


 
let fadeIdx = 0

export default {
  ...
  data() {
    return {
      fade: [false, false, false]
    }
  },
  methods: {
    fadeNext: function() {
       this.fade.splice(fadeIdx++, 1, true)
    }
  },
  mounted() {
    setTimeout(this.fadeNext, 1000)
  }
}
...
 


fade 배열은 각각의 로고 이미지들의 트랜지션을 시작시켜주는 스위치들의 배열이다.


default 값이 모두 false 이므로, 어디선가 누군가가 값을 true로 바꿔주지 않으면, 트랜지션 효과는 일어나지 않는다.



fadeNext 함수다음 순서의 트랜지션 스위치를 켜는 역할을 한다.


즉, fade 배열에서, 다음 인덱스의 스위치를 켜는 역할을 한다.


인덱스 값의 default가 0이므로, 맨 처음 트랜지션도 해당 함수로 켜면 된다.


(위 코드에서는 mount가 되면, 1초 뒤에 맨 처음 로고의 트랜지션이 시작되도록 해놔씀)






결과물, 그리고...







읭...?


뭔가 이상하지 않음?


원하던 아름다운 그림이 아닐 거시다.


'by WANZARGEN' 이 위에있다가, 로고 트랜지션이 시작되어야 제자리로 내려간다...


by WANZARGEN 요거슬 괜히, 그냥, 한 번 짱인 척 할라고 넣은 것이 결코 아녀. 



그 뿐만이 아님.


얘네들이 제자리에서 뿅뿅뿅 하고 뜨는게 아니라, 하나가 생길때마다 왼쪽으로 밀려난다는...





문제점


이러한 현상이 나타나는 이유는 아주 심플하다.


뷰의 트랜지션은 기본적으로 v-if, v-show 를 사용하여 트랜지션을 시작시키도록 되어있다.



뭔말이냐,


아까 위에서 '트랜지션 스위치' 역할을 하는 변수가 있다고 했잖음?


그 친구가 true면  v-if = true  혹은  v-show = true  가 되고, 해당 엘리먼트가 화면에 보여지기 시작한다는 말이 된다.



이렇게 엘리먼트가 화면에 출력되는 때가 트랜지션이 시작되는 때라고 이해하면 된다.




요까지만 말해도 이해하는 당신, 아래 '참고2'는 제끼십쇼.





<참고2>


    • v-if

      이 친구는 그 대응 값이 false이면 DOM 트리에 아예 렌더링 자체가 안된다.


    • v-show

      이 친구는 그 대응 값이 false이면 렌더링은 되지만, 출력이 안된다.

      그 출력이 안되는 방식이 중요한데, 해당 엘리먼트의 스타일을 "display: none" 처리 해버린다.

      "display: none" 처리를 하게 되면, 해당 엘리먼트가 차지하는 영역 또한 사라지게 된다.

      해당 영역을 살리고 싶으면,  "visibility" 속성을 이용하면 된다.

      "visibility" 속성을 'hidden' 으로 설정하면 엘리먼트가 차지하는 영역은 살리되, 안보이게 할 수 있다.

      또 다른 방법으로는, "opacity" 속성 값을 0으로 두어 투명하게 하는 방법이 있다.

 



자신의 영역이 없다가, 트랜지션 스위치가 켜지는 때에 영역이 생겨버리니...


그에 따른 부작용으로 '안예쁜', '의도한 바 없는' 애니메이션이 되버린 것이다.




비교해서 보자.


원하지 않은 결과물

 이것이 내가 원한 그 것

 

 



 

로고의 영역이 없다가 생기니까 'by WANZARGEN' 문구가 위에 있다가 내려오는 것이고,


로고 이미지를 감싸고 있는 엘리먼트가 '가운데 정렬'인 상황에서,


엘리먼트들이 하나씩 늘어나니, 그에 따라 왼쪽으로 밀리는 것이다.



ㅠ_ㅠ


왜 이런걸 고려하지 않았을까.. 




솔루션


시간이 좀 걸려도, 트랜지션을 하루 이틀 쓸게 아니라서 

나는 뷰의 '커스텀 디렉티브'를 만들어 해결했다.


참고로 vuetifyjs 등 디자인 프레임웍을 이용해 해결하면 더 쉽고 빠르게 원하는 트랜지션을 만들 수 있을 것이지만..

나는 커스텀 디렉티브를 좀 더 심도있게 다뤄볼 겸 + 아주 라이트하고 특정한 상황에도 동작하는게 필요해서 직접 만들어 해결했다.


















  1. vue맨 2018.12.24 11:38

    정말 내용이 간지러운데를 제대로 긁어주시네요 효자손이세요? 다음번에는 믹스인에 대해서좀 알아보고 적어주세요 제발요 부탁입니다.

  2. 허허 2019.06.20 23:12

    열심히 하시네요 ㅎㅎ

자바스크립트를 공부하다보면 클로저 함수라는 놈을 만나게 된다.


그런데 요놈, 한마디로 설명하기엔 내 머릿속에 정리가 잘 안되어 있어서, 이 참에 제대로 짚고 넘어가련다.


그래서 이번 포스트는 요놈, 클로저와 스코프 체인을 다뤄보려 한다.



스코프 체인(Scope chain)?

스코프 체인이 무엇인가 설명하기 전에, 먼저 글로벌 객체와 콜 객체를 알아야 한다.

글로벌 객체 & 콜 객체?

자바스크립트가 실행되면 내부적으로 글로벌 객체를 만든다.
요 글로벌 객체에는 글로벌 변수글로벌 함수가 담겨있다.
 
같은 맥락으로,
자바스크립트의 어떤 함수가 있는데, 요 함수가 실행되면 내부적으로 Call 객체를 만든다.
콜 객체에는 그럼 뭐가 있겠느냐구. 
함수 내에서 정의된 로컬 변수와 내부 함수가 있을 것이다.

콜 객체? 듣보인데? 

콜 객체가 이해가 안가면 Arguments 로 이해하면 좀 감이 온다.
우리가 함수 내부에서 arguments를 통해 인수에 접근할 수 있는 거슨, 바로 arguments가 이 콜 객체의 프로퍼티이기 때문이라는 사실.
요 객체에, 우리가 함수 내에서 정의한 로컬 변수도, 내부 함수도 다 프로퍼티로 저장 된다는 사실.

참고로, 콜 객체는 다른 말로 activation 객체라고도 하니, 구글링할 때 참고하자.




글엏담연. 

이게 대관절 스코프 체인이랑 무슨 관련이 있는건데?


'스코프' 라는 단어에서 우리는 무언가 '범위'를 가지고 장난치나보다, 하고 예상할 수 있다.


똑똑한 우리가 이미 잘 알고 있듯, 변수는 본인이 생성된 블록 범위 안에서만 유효하다.

스코프 체인의 '스코프'는, 바로 이 변수들과 함수들의 유효 범위와 비슷하다.





넘나 친절한 예시를 통해 더 친절하게 설명해보게따-.


var name = 'global'

function func1() {
  var name = 'call 1'
  console.log('func1() name: ', name)

  function func1_func1() {
    var name = 'call 1.1'
    console.log('func1_func1() name: ', name)

    function func1_func1_func1() {
      var name = 'call 1.1.1'
      console.log('func1_func1_func1() name: ', name)
    }

    func1_func1_func1()
  }

  func1_func1()
}

function func2() {
  var name = 'call 2'
  console.log('func2() name: ', name)

   //익명함수
   return function() {
    var name = 'call anonymous'
    console.log('anonymous func() name: ', name)
  }
}

console.log('global name: ', name)
func1()
var anony = func2()
anony()



위 코드의 마지막 줄에, 


func1_func1_func1()


라는 코드를 넣고 실행하면, 당연히 우리는


ReferenceError: func1_func1_func1 is not defined


라는 에러를 마주하게 된다.


왜?

func1_func_func1 함수의 유효 범위, 즉 스코프를 벗어난 엉뚱한 데에서 실행을 해버렸으니까.



이렇듯, 자바스크립트의 모든 변수와 함수들은 스코프를 가지고 있고,

이 스코프는 무엇으로 인해 정해지냐 하면-.

바로바로 아까 위에서 설명한 '글로벌 객체'와 '콜 객체'에 저장되어 있는 변수와 함수들에 의해 정해지는 것이다.



위의 예시 코드를 스코프 별로 구분하여 어떤 객체가 생성되는가 보면, 아래와 같다.


 scope

 내용

 생성 객체

 출력

script(global)

 var name = 'global'
 function func1() {...}
 function func2() {...}

 console.log('global name: ', name)
 func1()
 var anony = func2()
 anony()


글로벌 객체

 
{
  name: 'global',
  func1: function() {...},
  func2: function() {...},

  anony: function() {...}


global name:  global 

 func1()

 var name = 'call 1'
 console.log('func1() name: ', name)

 function func1_func1() {...}

 func1_func1()


콜 객체 1

{
  name: 'call 1',
  func1_func1: function() {...}


func1() name:  call 1 

func1_func1()


 var name = 'call 1.1'
 console.log('func1_func1() name: ', name)

 function func1_func1_func1() {...}

 func1_func1_func1()


콜 객체 1.1

{
  name: 'call 1.1',
  func1_func1_func1: function() {...}
}


 func1_func1() name:  call 1.1

 func1_func1_func1()

  var name = 'call 1.1.1'
  console.log('func1_func1_func1() name: ', name)


콜 객체 1.1.1

{
  name: 'call 1.1.1'
}


func1_func1_func1() name:  call 1.1.1 

 func2()

 var name = 'call 2'
 console.log('func2() name: ', name)

 return function() {...}


콜 객체 2

{
  name: 'call 2'
} 


익명 함수는 여기 없음!


 func2() name:  call 2

 anony()

 var name = 'call anonymous'
 console.log('anonymous func() name: ', name)


콜 객체 anonymous

{
  name: 'call anonymous'
}


 anonymous func() name:  call anonymous




친절미 넘치게 


보기 좋은 그림으로까지 또 보여주자면, 아래와 같은 그림을 만나볼 수 있다.





이거슬 글로 정리하자면,

글로벌 객체와, 콜 객체가 생성된 순서대로 연결된 것을 스코프 체인이라 한다.



아까 위에서 func1_func1_func1() 를 맨 아랫줄에 넣고 실행하면, 오류를 뱉는다고 했는데 그 이유는,

'func1_func1_func1'을 실행하라는 명령을 만나면, 해당 라인이 실행되고 있는 스코프( =글로벌객체)에 'func1_func1_func1'가 있는지를 찾아보는데,

온데간데 없기 때문에 에러를 뱉는 것이다.


반대로 'func1_func1' 함수 내에서 'func1_func1_func1' 함수가 실행 가능한 이유는,

그렇지만 'func1_func1' 함수의 스코프( =Call 1.1 객체)에는 'func1_func1_func1' 프로퍼티가 존재하므로 실행이 가능한 것이다.



당연하게 여겨왔던 것들에 대한 경이로움을 느끼는 순간이 아니라고 말할 수 없을 것이다아-.




요까지가 스코프 체인에 대한 설명이었다.


근데 설명을 보다가 요놈 익명함수가 좀 거슬렸을 거신데.

요게 바로 클로저 함수를 설명하기 위한 키 포인트이므로 아래로 ㄱㄱ





클로저(Closure) 함수

새로운 예시 코드를 통해 클로저 함수를 설명하도록 하게따-.


var name = 'global' function func2() { var name = 'call 2' console.log('func2() name: ', name) //익명함수 return function(newName) { if(newName != undefined) name = newName console.log('anonymous func() name: ', name) } } console.log('global name: ', name) var anony = func2() anony() anony('abc') anony()


이 친구의 출력 결과는, 아마도 


>> global name:  global

>> func2() name:  call 2

>> anonymous func() name:  global

>> anonymous func() name:  abc

>> anonymous func() name:  abc


를 예상했을 수 있겠지만, 안타깝게도 틀렸다.


이 친구도 흐름표를 통해 만나러 가즈아-!



 #

 scope

 내용

 글로벌/콜 객체

 출력

script(global)

 var name = 'global' 
 function func2() {...}

 console.log('global name: ', name)
 var anony = func2()
 anony()

 anony('abc')

 anony()


글로벌 객체

  

  name: 'global',
  func2: function() {...},

  anony: function() {...}


global name:  global 

 func2()

 var name = 'call 2'
 console.log('func2() name: ', name)

 return function() {...}


콜 객체 2

{ name: 'call 2' } 


 func2() name:  call 2

 3

 anony()

 if(newName != undefined) name = newName
 console.log('anonymous func() name: ', name)


콜 객체 2
{ name: 'call 2' } 


콜 객체 anony

{ name: *(콜 객체 2 name) }


 anonymous func() name:  call 2

 anony('abc')

 if(newName != undefined) name = newName
 console.log('anonymous func() name: ', name)


콜 객체 2
{ name: 'abc' }


콜 객체 anony

name: *(콜 객체 2 name) } 

  anonymous func() name:  abc

 anony()

 if(newName != undefined) name = newName

 console.log('anonymous func() name: ', name)


콜 객체 2
{ name: 'abc' } 


콜 객체 anony

name: *(콜 객체 2 name) } 


  anonymous func() name:  abc



  1.  글로벌 스코프에서 var anony = func2() 라인이 실행되므로, 글로벌 객체의 anony 프로퍼티에 익명함수가 대입된다.

  2.  var anony = func2() 라인이 실행되면서 func2 함수가 실행된다.
    이 때, 새로운 로컬 변수 name이 콜 객체 2에 저장된다.
    그리고 익명함수를 읽어 반환 하는데, 이 때 name 이라는 변수가 필요하다는 것을 알아차린다.
    그래서, 스코프 체인에서 가장 가까운 상위 스코프인 콜 객체 2의 name의 주소를 기억한다.
    (만약 콜 객체 2에 name이 없다면, 그 상위인 글로벌 객체의 name 주소를 기억할 것이다.)
    즉, 외부 함수로부터 생성된 콜 객체 2의 name 프로퍼티를 참조하는 것이다.

  3. anony() 라인이 실행되면, 아까 기억해둔 콜 객체2의 name 값을 가져와 출력하고,

  4. anony('abc') 가 실행되면, 또 아까 기억해둔 콜 객체2의 name 값을 바꾼 후 출력하고,

  5. 마지막 anony() 라인이 실행되면, 또다시 콜 객체2의 name 값을 가져와 출력한다.



따라서 정상적인 실행 결과는 아래와 같다.


>> global name:  global

>> func2() name:  call 2

>> anonymous func() name:  call 2

>> anonymous func() name:  abc

>> anonymous func() name:  abc




클로저 함수를 정리해서 한 마디로 설명하자면,


상위 스코프의 로컬 변수를 참조하는 함수 내의 함수

라고 설명할 수 있겠다.



보통은 함수 내에서 사용된 로컬 변수는, 해당 함수의 실행이 종료되면 파기되는 것이 맞다.


그런데, 이와 같이 클로저 함수에 의해서 계속 참조되고 있는 경우에는, 해당 로컬 변수를 파기하지 않고 계속 보관하는 것이다.










여기까지가 스코프 체인과 클로저 함수에 대한 원리(?)였다.


사실 포스트를 작성하면서 느낀거지만... 


어느 시점에 콜 객체가 생성되고, 또 그것들은 어디에 저장되며, 정확히 언제 파기되는 것인지 등등 더 제대로 알아야 할 것이 많음을 깨닫고 있다시와..ㅜ



그래서 다음엔 '호이스팅'을 좀 다뤄볼까 한다.


다루다 보면, 자연스레 알게될 것들이 많을 것 같다.




근데.. 언제? ...ㅎㅎ.......



  1. 소문듣고왔습니다 2018.05.24 09:50

    클로저 만드신분이세요? 감사합니다

  2. 양탕구리2 2018.05.30 14:05

    대단합니다

+ Recent posts