본 글은 Modern JavaScript Deep-dive을 요약한 글입니다.
자세한 내용은 본 책을 읽으시기 바랍니다.

 

Document Object Model은 HTML 문서의 계층적 구조와 정볼르 표현하며 이를 제어할 수 있는 API, 즉 프로퍼티와 메서드를 제공하는 트리 자료 구조다.

39.1 노드

39.1.1 HTML 요소와 노드 객체

요소는 HTML 문서를 구성하는 개별적인 요소를 의미한다.

!https://blog.kakaocdn.net/dn/Fsxjc/btsJNW7W4mo/tOf1CHdybjFoTHPL3B9iV1/img.png

HTML 요소는 렌더링 엔진에 의해 파싱되어 DOM을 구성하는 요소 노드 객체로 변환된다. 이때 HTML 요소의 어트리뷰트는 어트리뷰트 노드로, HTML 요소의 텍스트 콘텐츠는 텍스트 노드로 변환된다.

HTML 요소는 중첩 관계를 갖는다. 시작 태그와 종료 태그에는 텍스트 뿐만 아니라 다른 HTML 요소도 포함할 수 있다. 이때 중첩 관계에 의해 계층적인 부자 관계가 형성된다. 이러한 HTML 요소 간의 부자 관계를 반영하여 객체화한 모든 노드 객체들을 트리 자료 구조로 구성한다.

트리 자료 구조

트리 자료 구조는 부모 노드와 자식 노드로 구성되어 노드 간의 계층적 구조를 표현하는 비선형 자료 구조를 말한다. 최상위 노드는 부모 노드가 없으며, 루트 노드라 한다. 자식 노드가 없는 노드를 리프 노드라 한다. 노드 객체들로 구성된 트리 자료구조를 DOM이라 한다.

39.1.2 노드 객체의 타입

DOM은 노드 객체의 계층적인 구조롤 구성된다. 노드 객체는 총 12개의 종류가 있다. 이 중에서 중요한 노드 타입은 다음과 같이 4가지다.

문서 노드

문서 노드는 DOM 트리의 최상위에 존재하는 루트 노드로서 document 객체를 가리킨다. HTML 문서 전체를 가리키는 객체로서 전역 객체 window의 document 프로퍼티에 바인딩되어 있다. 브라우저 환경의 모든 자바스크립트 코드는 script 태그에 의해 분리되어 있어도 하나의 전역 객체 window를 공유한다. 즉, HTML 문서당 document 객체는 유일하다.

document 객체는 DOM 트리의 루트 노드이므로 DOM 트리의 노드들에 접근하기 위한 집인점 역할을 담당한다.

요소 노드

HTML 요소 간의 중첩에 의해 부자 관계를 가지며, 이 부자 관계를 통해 정보를 구조화 한다.

어트리뷰트 노드

어트리뷰트가 지정된 HTML 요소의 요소 노드와 연결되어 있다. 단 부모 노드와 연결되어 있지 않고 요소 노드에만 연결되어있다. 그러므로 어트리뷰트 노드는 요소 노드의 형제 노드는 아니다.

텍스트 노드

문서의 정보를 표현한다고 할 수 있다. 텍스트 노드는 요소 노드의 자식 노드이며, 자식 노드를 가질 수. ㅓㅂㅅ는 리프 노드다.

39.1.3 노드 객체의 상속 구조

DOM을 구성하는 노드 객체는 자신의 구조와 정보를 제어할 수 있는 DOM API를 사용할 수 있다. DOM을 구성하는 노드 객체는 ECMAScript 사양에 정의된 표준 빌트인 객체가 아니라 브라우저 환경에서 추가적으로 제공하는 호스트 객체다. 하지만 노드 객체도 자바스크립트 객체이므로 프로토타입에 의한 상속 구조를 갖는다.

이를 프로토 타입 체인 관점에서 살펴 본다면 input 요소 노드 객체는 프로토타입 체인에 있는 모든 프로토타입의 프로퍼티나 메서드를 상속받아 사용할 수 있다.

노드 타입에 상관없이 모든 노드 객체가 공통으로 갖는 기능도 있고, 노드 타입에 따라 고유한 기능도 있다. 예를 들어 이벤트 관련된 기능은 EventTarget 인터페이스가 제공한다. 또한 모든 노드 객체는 트리 자료구조의 노드로서 공통적으로 트리 탐색 기능이나 노드 정보 제공 기능이 필요한데 이 같은 노드 관련 기능 Node 인터페이스가 제공한다.

하지만 요소 노드 객체는 HTML 요소의 종류에 따라 고유한 기능도 있다. 따라서 필요한 기능을 제공하는 인터페이스가 HTML 요소의 종류에 따라 각각 다르다. 이처럼 노드 객체는 공통된 기능일수록 프로토타입 체인의 상위에, 개별적인 고유 기능일수록 프로토타입 체인의 하위에 구축하여 상속 구조를 갖느다.

DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체의 종류, 즉, 노드 타입에 따라 필요한 기능을 프로퍼티와 메서드의 집합인 DOM API로 제공한다. 이 DOM API를 통해 HTML의 구조나 내용 또는 스타일 등을 동적으로 조작할 수 있다.

39.2 요소 노드 취득

HTML의 구조나 내용 또는 스타일 등을 동적으로 조작하려면 먼저 요소 노드를 취득해야 한다. 이처럼 요소 노드 취득은 HTML 요소를 조작하는 시작점이다.

39.2.1 id를 이용한 요소 노드 취득

Document.prototype.getElemaneById 메서드는 인수로 전달한 id 어트리뷰트 값을 갖는 하나의 요소 노드를 탐색하여 반환한다. id 값은 HTML 문서 내에서 유일한 값이어야 하며 여러 개의 값을 가질 수 없다. 단 중복된 id 값을 갖는 HTML 요소가 여러 개 존재하더라도 에러가 발생하지 않고 전달된 id 값을 갖는 첫 번째 요소 노드만 반환한다. 존재하지 않을 경우 null을 반환한다. HTML요소에 id 어트리뷰트를 부여하면 id 값과 동일한 이름의 전역 변수가 암묵적으로 선언되고 해당 노드 객체가 할당되는 부수 효과가 있다. 단, 동일한 이름의 전역 변수가 이미 선언되어있으면 이 전역 변수에 노드 객체가 재할당되지 않는다.

39.2.2 태그 이름을 이용한 요소 노드 취득

Document.prototype/Element.prototype.getElementsByTagName 메서드는 인수로 전달한 태그 이름을 갖는 요소노드들을 탐색하여 반환한다. 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션 객체인 HTMLCollection 객체를 반환한다. 이는 유사 배열 객체이면서 이터러블이다.

39.2.3 class를 이용한 요소 노드 취득

Document.prototype/Element.prototype.getElementsByClassName 메서드는 인수로 전달한 class 어트리뷰트 값을 갖는 모든 요소 노드들을 탐색하여 반환한다. 공백으로 구분하여 여러 개의 class를 지정할 수 있다.

39.2.4 CSS 선택자를 이용한 요소 노드 취득

CSS 선택자는 스타일을 적용하고자 하는 HTML 요소를 특정할 때 사용하는 문법이다.

// 전체 선택자: 모든 요소를 선택
* {...}

// 태그 선택자 : 모든 p 태그를 모드 선택
p {...}

// id 선택자 : id 값이 'foo'인 요소를 모두 선택 
#foo {...}

// class 선택자 : class 값이 'foo'인 요소를 모두 선택
.foo {...}

// 어트리부트 선택자 : input 요소 중에 type 어트리뷰트 값이 'text'인 요소를 모두 선택
input[type=text] {...}

// 후손 선택자 : div 요소의 후손 중 p 요소를 모두 선택
div p {...}

// 자식 선택자 : div 요소의 자식 요소 중 p 요소를 모두 선택
div > p {...}

// 인접 형제 선택자 : p 요소의 형제 요소 중에 p 요소 바로 뒤에 위치하는 ul 요소를 선택
p + ul {...}

// 일반 형제 선택자 : p 요소의 형제 요소 중에 p 요소 뒤에 위치하는 ul 요소를 모두 선택
p ~ ul {...}

// 가상 클래스 선택자 : hover 상태인 a 요소를 모두 선택
a:hover {...}

// 가상 요소 선택자 : p 요소의 콘텐츠의 앞에 위치하는 공간을 선택 일반적으로 content 프로퍼티와 함께 사용
p::before {...}

Doucument.prototype/Elment.prototype.querySelector 메서드는 인수로 전달한 CSS 선택자를 만족시키는 하나의 요소 노드를 탐색하여 반환한다. querySelectorAll 메서드는 여러 개의 요소 노드 객체를 갖는 DOM 컬렉션인 NodeList 객체를 반환한다.

39.2.5 특정 요소 노드를 취득할 수 있는지 확인

Element.prototype.matches 메서드는 인수로 전달한 CSS 선택자를 통해 특정 요소 노드를 취득 할 수 있는 지 확인한다. 그렇기에 이벤트 위임을 사용할 때 유용하다.

39.2.6 HTMLCollection과 NodeList

DOM 컬렉션 객체인 HTMLColletction과 NodeList 는 DOM API가 여러 개의 결과 값을 반환하기 위한 DOM 컬렉션 객체다. 모두 유사 배열 객체이면서 이터러블이다. 따라서 for..of 문으로 순회할 수 있으며 스프레드 문법을 사용하여 간단히 배열로 변환할 수 있다.

중요한 특징은 노드 객체의 상태 변화를 실시간으로 반영하는 살아있는 객체라는 것이다. 단, NodeList는 대부분의 경우 노드 객체의 상태 변화를 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-Live 객체로 동작하지만 경우에 따라 live 객체로 동작할 때가 있다.

HTMLCollection

getElementByTagName/ByClassName 메서드가 반환하는 HTMLColletction는 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는 DOM 컬렉션 객체다.

<body>
<ul id="fruits">
	<li class="red">Apple</li>
		<li class="red">Banana</li>
			<li class="red">Orange</li>
</ul>
</body>

<script>
	const $elem = document.getElementsByClassName('red');
	
	for (let i =0 ; i<$elems.length; i++) {
		$elems[i].className = 'blue'
	}
	
</script>

위 예제는 class 값을 red인 요소 노드를 모두 취득하고, 취득한 모든 요소 노드를 담고 있는 HTMLColletction 객체를 for 문으로 순회하여 className 프로퍼티를 사용하여 모든 요소의 class 값을 blue로 변경한다. 하지만 이는 예상대로 동작하지 않는다. 두번째 li 요소만 class 값이 변경되지 않는다.

  1. 첫번쨰 반복 (i === 0) 첫번째 li 요소는 class 값이 red에서 blue로 변경되었으므로 getElementsByClassName 메서드의 인자로 전달한 red와 더는 일치하지 않기 때문에 $elem에서 실시간으로 제거 된다.
  2. 두번째 반복 (i === 1) 첫번째 반복에서 첫 번째 li 요소는 $elem에서 제거되었다. 따라서 $elem[1]은 세 번째 li 요소다. 이 요소도 blue로 변경되고 $elem에서 실시간으로 제거 된다.
  3. 세번째 반복(i === 2) 앞의 과정을 통해 첫 번째 세 번째 li 요소가 $elem에서 제거되었다. 따라서 두 번째 li 요소 노드만 남았다. 이때 length는 1이므로 for문의 조건식에 false로 평가되어 반복이 종료된다.

이러한 HTMLColletction의 문제는 for 문을 역방향으로 순회하는 방벙이나 while문을 사용하여 회피할 수 있다. 더 간단한 해결책은 부작용을 발생시키는 원인인 HTMLColletction를 사용하지 않는 것이다. 유용한 배열의 고차함수를 사용하면 된다.

NodeList

HTMLColletction 객체의 문제점을 해결하기위해 Doucument.prototype/Elment.prototype.querySelector 대신 querySelectorAll 메서드를 사용하는 방법도 있다. 이는 NodeList를 반환하는데 실시간을 노드 객체의 상태 변경을 반영하지 않는 객체다.

querySelectorAll 이 반환하는 NodeList 객체는 forEact, item, entries, keys, values 메서드를 제공한다. NodeList 객체는 과거의 정적 상태를 유지하는 non-live 객체로 동작한다. 하지만 childNodes 프로퍼티가 반환하는 NodeList 객체는 HTMLCollection 객체와 같이 실시간으로 노드 객체의 상태 변경을 반영하는 live 객체로 동작하므로 주의 필요하다.

따라서 노드 객체의 상태 변경과 상관없이 안전하게 DOM 컬렉션을 사용하려면 HTMLCollection이나 NodeList 객체를 배열로 반환하여 사용하는 것을 권장한다. 객체를 배열로 변환하면 배열의 유용한 고차함수를 사용할 수 있다는 장점도 있다. 둘다 유사 배열 객체이기에 스프레드 문법으로 간단하게 배열로 변환 가능 하다.

39.3 노드 탐색

취득한 요소 노드를 기점으로 DOM 트리의 노드를 옮겨 다니며 부모, 형제, 자식 노드 등을 탐색해야할 때가 있다. 이를 위해 DOM 트리 상의 노드를 탐색할 수 있도록 Node, Element 인터페이스는 트리 탐색 프로퍼티를 제공한다.

노드 탐색 프로퍼티는 모두 접근자 프로퍼티다. setter 없이 getter만 존재하여 참조만 가능한 읽기 전용 접근자 프로퍼티다. 값을 할당하면 아무런 에러 없이 무시된다.

39.3.1 공백 테스트 노드

****HTML 요소 사의 스페이스, 탭, 줄바꿈, 등의 공백 문자는 텍스트 노드를 생성한다.

따라서 노드를 탐색할 때는 공백 문자가 생성한 공백 텍스트 노드에 주의해야 한다. 하지만 인위적으로 공백 문자를 제거하면 가독성이 좋지 않으므로 권장하지 않는다.

39.3.2 자식 노드 탐색

  • Node.prototype.childNodes : 요소 노드 포함, 텍스트 노드 포함한 NodeList객체 반환,
  • Element.prototype.children : 요소 노드 포함, 텍스트 노드 포함하지 않은 HTMLCollection 객체 반환
  • Node.prototype.firstChild : 첫 번째 자식 텍스트or요소 노드 반환
  • Node.prototype.lastChild : 마지막 자식 텍스트or요소 노드 반환
  • Element.prototype.firstElementChild : 첫 번째 자식 요소 노드 반환
  • Element.prototype.;astElementChild : 마지막 자식 요소 노드 반환

39.3.3 자식 노드 존재 확인

자식 노드의 존재를 확인하려면 Node.prototype.hasChildNodes 메서드를 사용한다. 자식 노드가 존재하면 true, 존재하지 않으면 false를 반환한다. 단, 텍스트 노드를 포한하여 자식 노드의 존재를 확인한다. 그렇기에 텍스트 노드가 아닌 자식 노드를 존재하는 지 확인하려면 children.length or childElementCount 프로퍼티를 사용한다.

39.3.4 요소 노드의 텍스트 노드 탐색

요소 노드의 텍스트 노드는 요소 노드의 자식 노드다. 따라서 요소 노드의 텍스트 노드는 firstChild 프로퍼티로 접근할 수 있다.

39.3.5 부모 노드 탐색

부모 노드를 탐색하려면 Node.prototype.parentNode 프로퍼티를 사용한다. 텍스트 노드는 DOM 트리의 최종단 노드인 리프 노드 이므로 부모 노드가 텍스트 노드인 경우는 없다.

39.3.6 형제 노드 탐색

부모 노드가 같은 형제 노드를 탐색하려면 다음과 같은 노드 탐색 프로퍼티를 사용한다. 단, 어트리뷰트 노드는 요소 노드와 연결되어 있지만 부모 노드가 같은 형제 노드가 아니기 때문에 반환되지 않는다.

  • Node.prototype.previousSibling Node.prototype.nextSibling : 요소 노드나 텍스트 노드를 반환한다.
  • Node.prototype.previousElementSibling, Node.prototype.nextElementSibling : 요소 노드만 반환한다.

39.4 노드 정보 취득

  • Node.prototype.nodeType : 요소 노드, 텍스트 노드, 문서 노드 타입을 각각 상수 1, 3, 9로 반환한다.
  • Node.prototype.nodeName : 노드의 이름을 문자열로 반환한다. 각각 예를들어(LI, #text, #document)로 반환한다.

39.5 요소 노드의 텍스트 조작

39.5.1 nodeValue

지금부터 살펴볼 Node.prototype.nodeValue 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티다. 따라서 참조와 할당 모두 가능하다. 노드 객체의 nodeValue 프로퍼티를 참조하면 노드 객체의 값을 반환한다. 노드 객체의 값이란 텍스트 노드의 텍스트다.따라서 문서 노드나 요소 노드의 nodeValue 프로퍼티를 참조하면 null을 반환한다.

텍스트 노드의 nodeValue 프로퍼티에 값을 할당하면 텍스트 노드의 값, 즉 텍스트를 변경할 수 있다. 따라서 요소 노드의 텍스트를 변경하려면 다음과 같은 순서의 처리가 필요하다.

  1. 텍스트를 변경할 요소 노드를 취득한다.
  2. 취득한 요소 노드의 텍스트 노드를 탐색한다. 요소노드의 자식 노드 이므로 firstChild 프로퍼티를 사용하여 탐색한다.
  3. 탐새한 텍스트 노드의 nodeValue 프로퍼티를 사용하여 텍스트 노드의 값을 변경한다.

39.5.2 textContent

Node.protype.textContent 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티로서 요소 노드의 텍스트와 모든 자손 노드의 텍스트를 모두 취득하거나 변경한다. 또한 요소 노드의 childNodes 프로퍼티가 반환한 모든 노드들의 텍스트 노드의 값, 즉 텍스트를 모두 반환한다. 이때 HTML 마크업은 무시된다.

 <body>
    <div id="foo">Hello <span>world!</span></div>
  </body>
  <script>
    console.log(document.getElementById('foo').textContent); // Hello world!
  </script>

요소 노드의 textContent 프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가된다. 이때 할당한 문자열에 HTML 마크업이 포함되어 있더라도 문자열 그대로 인식되어 텍스트로 취급된다.

 <body>
    <div id="foo">Hello <span>world!</span></div>
  </body>
  <script>
    document.getElementById('foo').textContent = 'Hi <span>there!</span>';
  </script>

  • textContent 프로퍼티와 유사한 동작을 하는 innerText 프로퍼티가 있다. 하지만 다음과 같은 이유로 사용하지 않는 것이 좋다.
    • css에 순종적이다. 예를 들어 innerText 프로퍼티는 CSS에 의해 비표시로 지정된 요소 노드의 텍스트를 반환하지 않는다.
    • css을 고려해야 하므로 textContent보다 느리다.

39.6 DOM 조작

DOM 조작에 의해 DOM에 새로운 노드가 추가 되거나 삭제되면 리플로우와 리페인트가 발생하는 원인이 되므로 성능에 영향을 준다.

39.6.1 innerHTML

요소 노드의 innerHTML 프로퍼티를 참조하면 요소 노드의 콘텐츠 영역 내에 포함된 모든 HTML 마크업을 문자열로 반환한다.

요소 노드의 innerHTML 프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열에 포함되어 있는 HTML 마크업이 파싱되어 요소 노드의 자식 노드로 DOM에 반영된다. 요소 노드의 innerHTML 프로퍼티에 할당한 HTML 마크업 문자열은 렌더링 엔진에 의해 파싱되어 요소 노드의 자식으로 DOM에 반영된다.

단점

  1. 사용자로 입력 받은 데이터를 그대로 innerHTML 프로퍼티에 할당하는 것은 크로스 사이트 스크립팅 공격에 취약하므로 위험하다.
  2. 요소 노드의 innerHTML 프로퍼티에 HTML 마크업 문자열을 할당하는 경우 요소 노드의 모든 자식 노드를 제거하고 할당한 HTML 마크업 문자열을 파싱하여 DOM를 변경한다.
  3. 새로운 요소를 삽입할 때 삽입될 위치를 지정할 수 없다.

39.6.2 insertAdjacentHTML 메서드

Element.prototype.insertAdjactentHTML 메서드는 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다. 두 번째 인수로 전달한 HTML 마크업 문자열을 파싱하고 그 결과로 생성된 노드를 첫 번째 인수로 전달한 위치에 삽입하여 DOM에 반영한다. 첫 번째 인수로는 아래 와 같이 네가지 다.

insertAdjactentHTML 메서드는 기존 요소에는 영향을 주지 않고 새롭게 삽입될 요소 만을 파싱하여 자식요소로 추가하므로 innerHTML 프로퍼티보다 효율적이고 빠르다. 그러나 크로스 사이트 스크립팅 공격에 취약하다는 점은 동일하다.

39.6.3 노드 생성과 추가

DOM은 노드를 직접 생성/삽입/삭제/치환하는 메서드도 제공한다.

요소 노드 생성

Document.prototype.createElement(tagName) 메서드의 매개변수 tagName에는 태그 이름을 나타내는 문자열을 인수로 전달한다.

const $li = document.createElement('li');

createElement 메서드로 생성한 요소 노드는 기존 DOM에 추가되지 않고 홀로 존재하는 상태다. 따라서 이후에 생성된 요소 노드를 DOM에 추가하는 처리가 별도로 필요하다.

텍스트 노드 생성

Document.prototype.createTextNode(text) 메서드의 매개변수 text에는 텍스트 노드의 값으로 사용할 문자열을 인수로 전달한다. createElement 메서드와 마찬가지로 동 떨어진 상태가 되기에 생성된 텍스트 노드를 요소 노드에 추가하는 처리가 별도로 필요하다.

텍스트 노드를 요소 노드의 자식 노드로 추가

Node.prototype.appendChild(childNode) 메서드는 매개변수 childNode에게 인수로 전달한 노드를 appendChild 메서드를 호출한 노드의 마지막 자식 노드로 추가한다.

$li.appendChild(textNode);

요소 노드에 자식 노드가 하나도 없는 경우에는 텍스트 노드를 생성하여 자식 노드로 추가하는 것보다는 textContent 프로퍼티를 사용하는 편이 더욱 간편하다. 단, 자식 노드가 있는 경우 textContent 프로퍼티에 문자열을 할당하면 요소 노드의 모든 자식 노드가 제거되고 추가되므로 주의가 필요하다.

요소 노드를 DOM에 추가

Node.prototype.appendChild 메서드를 사용하여 텍스트 노드와 부자 관계로 연결한 요소 노드를 #fruits 요소 노드의 마지막 자식 요소로 추가한다. 기존의 DOM에 요소 노드를 추가하는 처리는 이 과정 뿐이다. DOM에 한번 추가 될때 마다 DOM은 한번 변경된다. 이때 리플로우와 리페인트가 실행된다.

39.6.4 복수의 노드 생성과 추가

3개의 요소 노드를 생성하여 DOM에 3번 추가하면 DOM에 3번 변경된다. 이때 리플로우와 리페인트가 3번 실행된다. DOM를 변경하는 것은 높은 비용이 드는 처리이므로 가급적 횟수를 줄이는 편이 성능에 유리하다.

DOM을 여러 번 변경하는 문제를 회피하기 위해 컨테이너 요소를 사용해 보자. DOM에 추가해야할 3개의 요소 노드를 컨텐이너 요소에 자식 노드로 추가하고, 컨테이너 요소를 요소에 자식으로 추가한다면 DOM은 한 번만 변경된다.

위 방법은 성능에 유리하기는 하지만 불필요한 컨테이너 요소가 DOM에 추가되는 부작용이 있다. 이러한 문제는 DocumentFragment 노드를 통해 해결 할 수 있다.

DocumentFragment는 문서, 요소, 어트리뷰트, 텍스트 노드와 같은 노드 객체의 일종으로, 부모 노드가 없어서 기존 DOM과는 별도로 존재한다는 특징이 있다. 그렇기에 DocumentFragment 노드에 자식 노드를 추가하여도 기존 DOM에는 어떠한 변경도 발생하지 않고 DocumentFragment 노드를 DOM에 추가하면 자신은 제거되고 자신의 자식 노드만 추가된다.

39.6.5 노드 삽입

마지막 노드로 추가

Node.prototype.appendChild 메서드는 인수로 전달받은 노드를 자신을 호출한 노드의 마지막 자식 노드로 DOM에 추가한다.

지정한 위치에 노드 삽입

Node.prototype.insertBefore(newNode,childNode) 메서드는 첫 번째 인수로 전달받은 노드를 두 번째 인수로 전달받은 노드 앞에 삽입한다. 두 번째 인수로 전달받은 노드가 null 이면 첫 번째 인수로 전달받은 노드를 insertBefore 메서드를 호출한 노드의 마지막 자식 노드로 추가된다.

39.6.6 노드 이동

DOM에 이미 존재하는 노드를 appendChild 또는 insertBefore 메서드를 사용하여 DOM에 다시 추가하면 현재 위치에서 노드를 제거하고 새로운 위치에 추가한다. ⇒ 즉 노드가 이동한다.

39.6.7 노드 복사

Node.prototype.cloneNode([deep: true | false]) 메서드는 노드의 사본을 생성하여 반환한다. 매개변수 deep에 true를 인수로 전달하면 노드를 깊은 복사하여 모든 자손 노드가 포함된 사본을 생성하고, false를 인수로 전달하거나 생략하면 노드를 얕은 복사하여 노드 자신만의 사본을 생성한다.

<!DOCTYPE html>
<html>
<body>
   <ul id="fruits">
     <li>Apple</li>
  </ul>
</body>
<script>
  const $fruits = document.getElementById('fruits');
  const $apple = $fruits.firstElementChild;
  
  const $shallowClone = $apple.coneNode();
  
  $shallowClone.textContent="Banana";
  
  $fruits.appendChild($shallowClone);
  
  const $deepClone = $fruits.cloneNode(true);
  $fruits.appendChild($deepClone);
</script>
</html>

39.6.8 노드 교체

Node.prototype.replaceChild(newChild,oldChild) 메서드는 자신을 호출한 노드의 자식 노드를 다른 노드로 교체한다. 첫 번째 매개변수 newChild에 교체할 새로운 노드를 인수로 전달하고, 두 번째 매개변수 oldChild에는 이미 존재하는 교체될 노드로 인수로 전달한다.

<!DOCTYPE html>
<html>
<body>
   <ul id="fruits">
     <li>Apple</li>
  </ul>
</body>
<script>
  const $fruits = document.getElementById('fruits');
  
  const $newChild = document.createElement('li');
  
  $newchild.textContent = 'Banana';
  
  $fruits.replace($newChild, $fruits.firstElementChild);
</script>
</html>

39.6.9 노드 삭제

Node.prototype.removeChild(child) 메서드는 child 매개변수에 인수로 전달한 노드를 DOM에서 삭제한다.

39.7 어트리뷰트

39.7.1 어트리뷰트 노드와 attributes 프로퍼티

HTML 문서의 구성요소인 HTML 요소는 여러 개의 어트리뷰트를 가질수 있다.

<input id='user' type='text' value='ungmo2'>

글로벌 어트리뷰트와 이벤트 핸들러 어트리뷰트는 모든 HTML 요소에서 공통적으로 사용할 수 있지만 특정 HTML 요소에만 한정적으로 사용 가능한 어트리뷰트도 있다.

HTML 문서가 파싱될 때 HTML 요소의 어트리뷰트는 어트리뷰트 노드로 변환되어 요소 노드와 연결된다. 이때 HTML 어트리뷰트 당 하나의 어트리뷰트 노드가 생성된다. 이때 모든 어트리뷰트 노드의 참조는 유사 배열 객체이자 이터러블 NamedNodeMap 객체에 담겨서 요소 노드의 어트리뷰트 프로퍼티에 저장된다.

따라서 요소 노드의 모든 어트리뷰트 노드는 요소 노드의 Element.prototype.attribute 프로퍼티로 취득할 수 있다. attribute 프로퍼티는 읽기 전용 접근자 이며, 요소 노드의 모든 어트리뷰트 노드의 참조가 담긴 NamedNodeMap 객체를 반환한다.

39.7.2 HTML 어트리뷰트 조작

Element.prototype.getAttribute/setAttribute 메서드를 사용하면 attribute 프로퍼티를 통하지 않고 요소 노드에서 메서드를 통해 직접 HTML 어트리뷰트 값을 취득하거나 변경할 수 있어서 편리하다.

또 해당하는 attribute가 DOM 요소 노드에 존재하는지 확인하려면 hasAttribute 메서드를 사용하고, 특정 attribute를 삭제하고 싶다면 removeAttribute 메서드를 사용하면 된다.

39.7.3 HTML 어트리뷰트 vs. DOM 프로퍼티

요소 노드 객체에는 HTML 어트리뷰트에 대응하는 프로퍼티가 존재한다. 이 DOM 프로퍼티들은 HTML 어트리뷰트 값을 초기값으로 가지고 있다. 이처럼 HTML 어트리뷰트는 다음과 같이 중복 관리 되고 있는 것처럼 보인다.

  1. 요소 노드의 attributes 프로퍼티에서 관리하는 어트리뷰트 노드
  2. HTML 어트리뷰트에 대응하는 요소 노드의 프로퍼티 ⇒ DOM 프로퍼티

HTML 어트리뷰트는 DOM에서 중복관리 되고 있을까 ? ⇒ NO!!

HTML 어트리뷰트의 역할은 HTML 요소의 초기 상태를 지정하는 것이다. 즉, HTML 어트리뷰트 값은 HTML 요소의 초기 상태를 의미하며 이는 변하지 않는다.

요소 노드는 상태를 가지고 있다. 예를 들어, input 요소 노드나 checkbox 요소 노드가 가지고 있는 상태는 사용자의 입력에 의해 변화하는 살아있는 것이다.

그렇기에 사용자의 입력에 의해 변경된 최신 상태를 관리 해야하는 것은 물론 HTML 어트리뷰트로 지정한 초기 상태도 관리해야 한다.

이처럼 요소 노드는 2개의 상태, 즉 초기 상태와 최신 상태를 관리해야 한다. 요소 노드의 초기상태는 어트리뷰트 노드가 관리하며, 요소 노드의 최신 상태는 DOM 프로퍼티가 관리한다.

어트리뷰트 노드

HTML 어트리뷰트로 지정한 HTML 요소의 초기 상태는 어트리뷰트 노드에서 관리한다. 사용자에 입력에 의해 상태가 변경되어도 변하지 않고 HTML 어트리뷰트로 지정한 HTML 요소의 초기 상태를 그대로 유지한다. setAttribute 메서드는 어트리뷰트 노드에서 관리하는 HTML 요소에 지정한 어트리뷰트 값, 즉 초기 상태 값을 변경한다.

DOM 프로퍼티

사용자가 입력한 최신 상태는 HTML 어트리뷰트에 대응하는 요소 노드의 DOM 프로퍼티가 관리한다. DOM 프로퍼티는 사용자의 입력에 의한 상태 변화에 반응하여 언제나 최신 상태를 유지 한다. DOM 프로퍼티에 값을 할당하는 것은 HTML 요소의 최신 상태 값을 변경하는 것을 의미한다.

HTML 어트리뷰트와 DOM 프로퍼티의 대응관계

  • 대부분의 HTML 어트리뷰트는 HTML 어트리뷰트 이름과 동일한 DOM 프로퍼티와 1:1로 대응한다.
  • 단, 다음과 같이 HTML 어트리뷰트와 DOM 프로퍼티가 언제나 1:1로 대응하는 것은 아니며, 키가 반드시 일치하는 것도 아니다.
    • id 어트리뷰트와 id프로퍼티는 1:1대응하며, 동일한 값으로 연동한다.
    • input 요소의 value 어트리뷰트는 value 프로퍼티와 1:1 대응한다. 하지만 value 어트리뷰트는 초기상태를, value 프로퍼티는 최신상태를 갖는다.
    • class 어트리뷰트는 className, classList 프로퍼티와 대응한다.
    • for 어트리뷰트는 htmlFor 프로퍼티와 1:1 대응한다.
    • td 요소의 colspan 어트리뷰트는 대응하는 프로퍼티가 존재하지 않는다.
    • textContent 프로퍼티는 대응하는 어트리뷰트가 존재하지 않는다.
    • 어트리뷰트 이름은 대소문자를 구별하지 않지만 대응하는 프로퍼티 키는 카멜 케이스를 따른다.

DOM 프로퍼티 값의 타입

  • getAttribute 메서드로 취득한 어트리뷰트 값은 언제나 문자열이다.
  • 하지만 DOM 프로퍼티로 취득한 최신 상태 값은 문자열이 아닐 수 있다. (checkbox의 경우 boolean 타입이다.)

39.7.4 data 어트리뷰트와 dataset 프로퍼티

data 어트리뷰트와 dataset 프로퍼티를 사용하면 HTML 요소에 정의한 사용자 정의 어트리뷰트와 자바스크립트 간에 데이터를 교환할 수 있다. data- 접두사 다음에 임의의 이름을 붙여 사용한다.

data 어트리뷰트의 값은 HTMLElement.dataset 프로퍼티로 취득할 수 있다. dataset 프로퍼티는 HTML 요소의 모든 data 어트리뷰트의 정보를 제공하는 DOMStringMap 객체를 반환한다. DOMStringMap 객체는 data 어트리뷰트의 data- 접두사 다음에 붙인 임의의 카멜 케이스로 변환한 프로퍼티를 가지고 있다.

39.8 스타일

39.8.1 인라인 스타일 조작

HTMLElement.prototype.style 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티로서 요소 노드의 인라인 스타일을 취득할 수 있거나 조작할 수 있다.

style 프로퍼티를 참조하면 CSSStyleDeclaration 타입의 객체를 반환한다. 다양한 CSS 프로퍼티에 대응하는 프로퍼티를 가지고 있으며, 이 프로퍼티에 값을 할당하면 해당 CSS 프로퍼티가 인라인 스타일로 HTML 요소에 추가되거나 변경된다.

css 프로퍼티는 케밥 케이스를 따른다. 이에 대응하는 CSSStyleDeclaration 객체의 프로퍼티는 카멜 케이스를 따른다.

39.8.2 클래스 조작

. 으로 시작하는 클래스 선택자를 사용하여 CSS class를 미리 정의 한 다음, HTML 요소의 class 어트리뷰트 값을 변경하여 HTML 요소의 스타일을 변경할 수도 있다. 이때 class 어트리뷰트에 대응하는 요소 노드의 DOM 프로퍼티를 사용한다. 단, class 어트리뷰트에 대응하는 DOM 프로퍼티는 class 가 아니라 className과 classList다. (자바스크립트의 class는 예약어 이기 때문.)

className

Element.prototype.className 프로퍼티는 setter와 getter 모두 존재 하는 접근자 프로퍼티로서 HTML 요소의 class 어트리뷰트 값을 취득하거나 변경한다. 이를 참조하면 class 어트리뷰트 값을 문자열로 반환하고, 문자열을 할당하면 class 어트리뷰트 값을 할당한 문자열로 변경한다.

<!DOCTYPE html>
<html>
  <head>
    <style>
      .box {
        width: 100px;
        height: 100px;
        background-color: yellow;
      }
      .red {
        color: red;
      }
      .blue {
        color: blue;
      }
    </style>
  </head>
  <body>
    <div class="box red">Hello World</div>
    <script>
      const $box = document.querySelector('.box');

      // .box 요소의 class 어트리뷰트 값을 취득
      console.log($box.className); // "box red"

      // .box 요소의 class 어트리뷰트 값 중에서 "red"만 "blue" 로 변경
      $box.className = $box.className.replace('red', 'blue');
    </script>
  </body>
</html>

classList

element.prototype.classList 프로퍼티는 class 어트리뷰트의 정보를 담은 DOMTokenList 객체를 반환한다.

  <script>
      const $box = document.querySelector('.box')

      // .box 요소의 class 어트리뷰트 정보를 담은 DOMTokenList 객체를 취득
      console.log($box.classList);
      // DOMTokenList(2) [length: 2, value: "box blue", 0: "box", 1: "blue"]

      // .box 요소의 class 어트리뷰트 값 중에서 "red"만 "blue" 로 변경
      $box.classList.replace('red', 'blue');
    </script>

DOMTokenList 객체는 class 어트리뷰트의 정보를 나타내는 컬렉션 객체로서 유사 배열 객체이면서 이터러블이다. 다음과 같이 유용한 메서드들을 제공한다.

  • add(...className) : 인수로 전달한 1개 이상의 문자열을 class 어트리뷰트 값으로 추가한다.
  • remove(...className) : 인수로 전달한 1개 이상의 문자열과 일치하는 클래스를 class 어트리뷰트에서 삭제한다.
  • item(index) : 인수로 전달한 index에 해당하는 클래스를 class 어트리뷰트에서 반환한다.
  • contains(className) : 인수로 전달한 문자열과 일치하는 클래스가 class 어트리뷰트에 포함되어 있는지 확인한다.
  • replace(oldClassName, newClassName) : 첫 번째 인수로 전달한 문자열을 두 번째 인수로 전달한 문자열로 변경한다.
  • toggle(className[force]) : 인수로 전달한 문자열과 일치하는 클래스가 존재하면 제거하고, 존재하지 않으면 추가한다.
  • 이 밖에도 DOMTokenList 객체는 forEach, entries, keys, value, supports 메서드를 제공한다.

39.8.3 요소에 적용되어 있는 CSS 스타일 참조

style 프로퍼티는 인라인 스타일만 반환한다. 따라서 클래스를 적용한 스타일이나 상속을 통해 암묵적으로 적용된 스타일은 프로퍼티로 참조할 수 없다. 적용되어 있는 모든 CSS 스타일을 참조 해야할 경우 getComputedStyle 메서드를 사용한다.

window.getComputedStyle(element[, pseudo]) 메서드는 첫 번째 인수로 전달한 요소 노드에 적용되어 있는 평가된 스타일을 CSSStyleDeclaration 객체에 담아 반환한다. 두 번째 인수로 :after, :before 와 같은 의사 요소를 지정하는 문자열을 전달할 수 있다.

  • 평가된 스타일 : 요소 노드에 적용되어 있는 모든 스타일 (링크, 임베딩, 인라인, 자바스크립트에서 적용한, 상속된, 기본 스타일 등) 이 조합되어 최종적으로 적용된 스타일을 말한다.

39.9 DOM 표준

W3C, WHATWG이라는 두 단체가 나름대로 협력하면서 공통된 표준을 만들어 왔다. 그러나 서로 다른 결과물을 내놓기 시작했고 2018년 4월 부터 구글, 애플, 마이크로소프트, 모질라로 구성된 4개의 주류 브라우저 벤더사가 주도하는 WHATWG이 단일 표준을 내놓기로 합의했다.

현재 4가지 버전이 존재한다.

'Javascript' 카테고리의 다른 글

Deep-dive .38 : 브라우저의 렌더링  (0) 2024.08.08
Deep-dive .37 : Set과 Map  (0) 2024.08.06
Deep-dive .35 : 스프레드 문법  (0) 2024.07.31
Deep-dive .34 : 이터러블  (0) 2024.07.30
Deep-dive .26 : ES6 함수의 추가기능  (1) 2024.07.24
본 글은 Modern JavaScript Deep-dive을 요약한 글입니다.
자세한 내용은 본 책을 읽으시기 바랍니다.

 

 구글의 V8 자바스크립트 엔진으로 빌드된 자바스크립트 런타임 환경인 Node.js의 등장으로 자바스크립트는 웹 브라우저를 벗어나 서버 사이드 애플리케이션 개발에서도 사용할 수 있는 범용 개발 언어가 되었다. 하지만 자바스크립트가 가장 많이 사용되는 분야는 웹 브라우저 환경에서 동작하는 웹페이지/애플리케이션의 클라이언트 사이드다.

 대부분의 프로그래밍 언어는 운영체제나 가상 머신 위에서 실행되지만 웹 애플리케이션의 클라이언트 사이드 자바스크립트는 브라우저에서 HTML,CSS와 함께 실행된다. 따라서 브라우저 환경을 고려할 때 더 효율적인 클라이언트 사이드 자바스크립트 프로그래밍이 가능하다.

 이를 위해 브라우저 HTML, CSS, 자바스크립트로 작성된 텍스트 문서를 어떻게 파싱하여 브라우저에 렌더링 하는지 알아보자.

38.1 요청과 응답

 브라우저의 핵심 기능은 필요한 리소스를 서버에 요청하고 서버로부터 응답받아 브라우저에 시각적으로 렌더링하는 것이다. 즉 렌더링에 필요한 리소스는 모두 서버에 존재하므로 필요한 리소스를 서버에 요청하고 서버가 응답한 리소스를 파싱하여 렌더링하는 것이다.

 서버에 요청을 전송하기 위해 브라우저는 주소창을 제공한다. 브라우저 주소창에 URL을 입력하고 엔터 키를 누르면 URL의 호스트 이름이 DNS를 통해 IP 주소로 변환되고 이 IP 주소를 갖는 서버에게 요청을 전송한다.

 루트 요청에는 명확히 리소스를 요청하는 내용이 없지만 일반적으로 서버는 루트 요청에 대해 암묵적으로 Index.html을 응답하도록 기본 설정 되어있다.

 따라서 서버는 루트 요청에 대해 서버의 루트 폴더에 존재하는 정적 파일 index.html을 클라이언트로 응답한다. 만약 index.html이 아닌 다른 정적 파일을 요청하려면 요청할 정적 파일의 경로와 파일 이름을 URL 호스트 뒤에 패스에 기술하여 서버에 요청한다. 그러면 서버는 루트 폴더의 assets/data 폴더 내에 있는 정적 파일 data.json을 응답할 것이다.

 반드시 브라우저의 주소창을 통해 서버에게 정적 파일만을 요청할 수 있는 것은 아니다. 자바스크립트을 통해 동적으로 서버에 정적/동적 데이터를 요청할 수도 있다.

요청과 응답은 개발자 도구의 Network 패널에서 확인할 수 있다. 그러나 여기서 살펴보면 index.html 뿐만 아니라 CSS, 자바스크립트, 이미지. 폰트 파일들도 응답된 것을 확인할 수 있다. 이는 브라우저의 렌더링 엔진이 HTML을 파싱하는 도중에 외부 리소스를 로드하는 태그, 즉 css => link 태그, 이미지 => img 태그, 자바스크립트 => script 태그 등을 만나며 HTML의 파싱을 일시 중단하고 해당 리소스 파일을 서버로 요청하기 때문이다.

38.2 HTTP 1.1과 HTTP 2.0

HTTP는 웹에서 브라우저와 서버가 통신하기 위한 프로토콜이다. HTTP는 1991년에 문서화되었고 1996년에 1.0, 99년에 1.1, 2015년에 2.0이 발표되었다.

  1. HTTP 1.1은 기본적으로 커넥션 당 하나의 요청과 응답만 처리한다. 따라서 HTML 문서 내에 포함된 여러 개의 리소스 요청 즉 여러 태그들에 의한 리소스 요청이 개별적으로 전송되고 응답 또한 개별적으로 전송된다. 이처럼 리소스의 동시 전송이 불가능한 구조이므로 요청할 리소스의 개수에 비례하여 응답 시간도 증가하는 단점이 있다.
  2. HTTP 2.0은 커넥션당 여러 개의 요청과 응답, 즉 다중 요청과 응답이 가능하다. 따라서 여러 리소스의 동시 전송이 가능하므로 1.1에 비해 페이지 로드 속도가 약 50% 정도 빠르다고 알려져 있다.

38.3 HTML 파싱과 DOM 생성

 브라우저의 요청에 의해 서버가 응답한 HTML 문서는 문자열로 이루어진 순수한 텍스트다. 이를 브라우저에 시각적인 픽셀로 렌더링 하려면 브라우저가 이해할 수 있는 자료구조로 변환하여 메모리에 저장해야 한다.

 브라우저 렌더링 엔진은 다음과 같은 과정을 통해 응답 받은 HTML 문서를 파싱하여 브라우저가 이해할 수 있는 자료구조인 DOM을 생성한다.

  1. 서버에 존재하던 HTML 파일이 브라우저의 요청에 의해 응답된다. 이때 서버는 브라우저가 요청한 HTML 파일을 읽어 들여 메모리에 저장한 다음 메모리에 저장된 바이트를 인터넷을 경유하여 응답한다.
  2. 브라우저는 서버가 응답한 HTML 문서를 바이트 형태로 응답받는다. 그리고 응답된 바이트 형태의 HTML 문서는 meta 태그의 charset 어트리뷰트에 의해 지정된 인코딩 방식을 기준으로 문자열로 변환된다.
  3. 문자열로 변환된 HTML 문서를 읽어 들여 문법적 의미를 갖는 코드의 최소 단위인 토큰들로 분해한다.
  4. 각 토큰들을 객체로 변환하여 노드들을 생성한다. 노드는 이후 DOM을 구성하는 기본 요소가 된다.
  5. HTML 문서는 HTML 요소들의 집합으로 이루어지며 HTML 요소는 중첩 관계를 갖는다. 이러한 HTML 요소 간의 부자 관계를 반영하여 모든 노드들을 트리 자료구조로 구성한다. 이 노드들로 구성된 트리 구조를 DOM이라 부른다.

즉, DOM은 HTML 문서를 파싱한 결과물이다.

38.4 CSS 파싱과 CSSOM 생성

 렌더링 엔진은 HTML을 처음부터 한 줄씩 순차적으로 파싱하여 DOM을 생성해 나간다. 이처럼 렌더링 엔진은 DOM을 생성해 나가다가 CSS를 로드하는 link 태그나 style 태그를 만나면 DOM 생성을 일시 중단한다.

 그리고 link 태그의 href 어트리뷰트에 지정된 CSS 파일을 서버에 요청하여 로드한 CSS 파일이나 style 태그 내의 CSS를 HTML과 동일 한 파싱 과정을 거치며  해석하여 CSSOM을 생성한다. 이후 CSS 파싱을 완료하면 HTML 파싱이 중단된 지점부터 다시 HTML 파싱하기 시작하여 DOM 생성을 재개한다.

 CSSOM은 CSS의 상속을 반영하여 생성된다.

38.5 렌더 트리 생성

 렌더링 엔진은 서버로부터 응답된 HTML과 CSS을 파싱하여 각각 DOM과 CSSOM을 생성한다. 그리고 DOM과 CSSOM은 렌더링을 위해 렌더 트리로 결합된다.

 렌더 트리는 렌더링을 위한 트리 구조의 자료 구조다. 따라서 브라우저 화면에 렌더링 되지 않는 노드(meta, script 태그 등)와 CSS에 의해 비표시(display: none 등)되는 노드들은 포함하지 않는다. 즉, 브라우저 화면에 렌더링되는 노드만으로 구성된다.

 이후 완성된 렌더 트리는 각 HTML 요소의 레이아웃을 계산하는 데 사용되며 브라우저 화면에 픽셀을 렌더링 하는 페인팅 처리에 입력된다.

지금까지 살펴본 브라우저의 렌더링 과정은 반복해서 실행될 수 있다. 다음과 같은 경우 반복해서 레이아웃 계산과 페인팅이 재차 실행된다.

  • 자바스크립트에 의한 노드 추가 또는 삭제
  • 브라우저 창의 리사이지에 의한 뷰포트 크기 변경
  • HTML 요소의 레이아웃에 변경을 발생시키는 width/height, margin, padding, border, display, position, top/rignt/bottom/left 등의 스타일 변경

레이아웃 계산과 페인틴을 다시 실행하는 리렌더링은 비용이 많이 드는, 즉 성능에 악영향을 주는 작업이다. 따라서 가급적 리렌더링이 빈번하게 발생하지 않도록 주의할 필요가 있다.

38.6 자바스크립트 파싱과 실행

 DOM은 HTML 문서의 구조와 정보뿐만 아니라 HTML 요소와 스타일 등을 변경할 수 있는 프로그래밍 인터페이스로서 DOM API를 제공한다. 즉, 자바스크립트 코드에서 DOM API를 사용하면 이미 생성된 DOM을 동적으로 조작할 수 있다.

 

 CSS 파싱 과정과 마찬가지로 렌더링 엔진은 HTML을 한 줄씩 순차적으로 파싱하며 DOM을 생성해 나가다가 script 태그를 만나면 DOM 생성을 일시 중단한다. 그리고 script 태그의 src 어트리뷰트에 정의된 자바스크립트 파일을 서버에 요청하여 자바스크립트 코드를 파싱하기 위해 자바스크립트 엔진에 제어권을 넘긴다. 이후 자바스크립트 파싱과 실행이 종료되면 렌더링 엔진으로 다시 제어권을 넘겨  HTML 파싱이 중단된 지점부터 다시 HTML 파싱을 시작하여 DOM 생성을 재개한다.

 

 자바스크립트 파싱과 실행은 자바스크립트 엔진이 처리한다. 자바스크립트 엔진은 자바스크립트 코드를 파싱하여 CPU가 이애할 수 있는 저수준 언어로 변환하고 실행하는 역할을 한다. 렌더링 엔진으로부터 제어권을 넘겨받은 자바스크립트 엔진은 자바스크립트 코드를 파싱하기 시작한다. 엔진은 코드를 해석하여 AST 추상적 구문 트리를 생성한다. 그리고 AST를 기반으로 인터프리터가 실행할 수 있는 중간 코드인 바이트코드를 생성하여 실행한다.

 

토크나이징

 단순한 문자열인 자바스크립트 소스코드를 어휘 분석하여 문법적으로 의미를 갖는 코드의 최소 단위인 토큰들로 분해한다. 렉싱이라고 부르기도 하지만 미묘한 차이가 존재한다.

파싱

토큰들의 잡합을 구문 분석하여 AST 추상적 구문트리를 생성한다. AST는 토큰에 문법적 의미와 구조를 반영한 트리 구조의 자료구조다. 이를 인터프리터나 컴파일러만 사용하는 것은 아니고 AST를 사용하면 프랜스파일러를 구현할 수도 있다.

바이트코드 생성과 실행

파싱의 결과물로서 생성된 AST는 인터프리터가 실행할수 있는 중간 코드인 바이트코드로 변환되고 인터프리터에 의해 실행된다. V8엔진의 경우 자주 사용되는 코드는 터보팬이라 불리는 컴파일러에 의해 최적화된 머신 코드로 컴파일되어 성능을 최적화하고 사용 빈도가 적어지면 다시 디옵티마이징 하기도 한다.

38.7 리플로우와 리페인트

만약 자바스크립트 코드에 DOM이나 CSSOM을 변경하는 DOM API가 사용된 경우 DOM이나 CSSOM이 변경된다. 이때 변경된 DOM과 CSSOM은 다시 렌더 트리로 결합되고 변경된 레더 트리를 기반으로 레이아웃과 페인트 과정을 거쳐 브라우저의 화면에 다시 렌더링한다. 이를 리플로우, 리페인트라 한다.

  • 리플로우 : 레이아우 계산을 다시 하는 것. 노드 추가/삭제, 요소의 크기/위치 변경, 윈도우 리사이징 등 레이아웃에 영향을 주는 변경이 발생한 경우에 한하여 실행된다.
  • 리페인트 : 재결합된 렌더 트리를 기반으로 다시 페인트를 하는 것을 말한다.

따라서 리플로우와 리페인트가 반드시 순차적으로 동시에 발생하는 것은 아니다. 레이아웃에 영향일 없는 변경은 리플로우 없이 리페인트만 실행된다.

38.8 자바스크립트 파싱에 의한 HTML 파싱 중단

 렌더링 엔진과 자바스크립트 엔진은 병렬적으로 파싱을 실행하지 않고 직렬적으로 파싱을 수행한다. 이처럼 동기적으로, 즉 위에서 아래 방향으로 순차적으로 HTML, CSS, 자바스크립트를 파싱하고 실행한다. 이것은 script 태그의 위치에 따라 HTML 파싱이 블로킹되어 DOM 생성이 지연될 수 있다는 것을 의미한다. 따라서 script 태그의 위치는 중요한 의미를 갖는다.

 

 DOM API가 HTML 요소를 취득하기 이전에 HTML 요소가 파싱되지 않은 경우 정상적으로 동작하지 않기 때문에 script 태그는 body d요소의 가장 아래에 자바스크립트를 위치시키는 것이 좋은 아이디어다. 다음과 같은 이유가 존재한다.

  • DOM이 완성되지 않은 상태에서 자바스크립트가 DOM를 조작하면 에러가 발생할 수 있다.
  • 자바스크립트 로딩/파싱/실행으로 인해 HTML 요소들의 렌더링에 지장받는 일이 발생하지 않아 페이지 로딩 시간이 단축된다.

38.9 script 태그의 async/defer 어트리뷰트

 자바스크립트 파싱에 의한 DOM 생성이 중단되는 문제를 근본적으로 해결하기 위해 HTML5부터 script 태그에 async와 defer 어튜리뷰트가 추가되었다. src 어트리뷰트를 통해 외부 자바스크립트 파일을 로드하는 경우에만 사용할 수 있다. 즉 인라인 자바스크립트에는 사용할 수 없다.

 async와 defer 어트리뷰트를 사용하면 HTML 파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 동시에 진행된다. 하지만 자바스크립트의 실행 시점에서 차이가 존재한다.

 

async 어트리뷰트

 HTML 파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 동시에 진행된다. 단, 자바스크립트의 파싱과 실행은 자바스크립트 파일의 로드가 완료된 직후 진행되며, 이때 HTML 파싱이 중단된다.

 여러 개의 script 태그에 async 어트리뷰트를 지정하면 script 태그의 순서와는 상관없이 로드가 완료된 자바스크립트부터 먼저 실행되므로 순서가 보장되지 않는다.

 

defer 어트리뷰트

 async 어트리뷰트와 마찬가지로 HTML 파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 동시에 진행된다. 단, 자바스크립트의 파싱과 실행은 HTML 파싱이 완료된 직후, 즉 DOM 생성이 완료된 직후 진행된다. 따라서 DOM 생성이 완료된 이후에 실행되어야할 자바스크립트에 유용하다.

 

'Javascript' 카테고리의 다른 글

Deep-dive .39 : DOM  (1) 2024.09.27
Deep-dive .37 : Set과 Map  (0) 2024.08.06
Deep-dive .35 : 스프레드 문법  (0) 2024.07.31
Deep-dive .34 : 이터러블  (0) 2024.07.30
Deep-dive .26 : ES6 함수의 추가기능  (1) 2024.07.24
본 글은 Modern JavaScript Deep-dive을 요약한 글입니다.
자세한 내용은 본 책을 읽으시기 바랍니다.

 

37.1 Set

Set 객체는 중복되지 않은 유일한 값들의 집합이다. 배열과 유사 하지만 다음과 같은 차이가 있다.

 

구분 배열  Set 객체
동일한 값을 중복하여 포함할 수 있다. O X
요소 순서에 의미가 있다. O X
인덱스로 요소에 접근할 수 있다. O X

 

Set은 수학적 집합을 구현하기 위한 자료구조다. 이를 통해 교집합, 합집합, 차집합, 여집합 등을 구현할 수 있다.

37.1.1 Set 객체의 생성

Set 객체는 Set 생성자 함수로 생성한다. 인수를 전달하지 않으면 빈 객체가 생성된다.

const set = new Set();
console.log(set); // Set(0) {}

Set 생성자 함수는 이터러블을 인수로 전달받아 Set 객체를 생성한다. 이때 이터러블의 중복된 값은 Set 객체에 요소로 저장되지 않는다.

const set1 = new Set([1,2,3,3]);
console.log(set1); // Set(3){1,2,3}

중복을 허용하지 않는 Set 객체의 특성을 활용하여 배열에서 중복된 요소를 제거할 수 있다.

const uniq = array => [...new Set(array)];
console.log(uniq([2,1,2,3,4,3,4,])) // [2,1,3,4]

37.1.2 요소 개수 확인

Set 객체의 요소 개수를 확인할 때는 Set.prototype.size 프로퍼티를 사용한다.

const { size } = new Set([1,2,3,3]);
console.log(size); // 3

size 프로퍼티는 setter 함수 없이 getter 함수만 존재하는 접근자 프로퍼티다. 따라서 프로퍼티에 숫자를 할당하여 Set 객체의 요소 개수를 변경할  수 없다.

37.1.3 요소 추가

Set 객체에 요소를 추가할 때는 Set.prototype.add 메서드를 사용한다.

const set = new Set();
console.log(set); // Set(0) {}

set.add(1);
console.log(set); // Set(1) {1}

 add 메서드는 새로운 요소가 추가된 Set 객체를 반환한다. 따라서 연속적으로 호출할 수 있다. 또한 Set 객체에 중복된 요소의 추가는 허용되지 않는다. 이때 에러가 발생하지 않고 무시된다.

 

일치 비교 연산자를 사용하면 NaN과 NaN을 다르다고 평가한다. 하지만 Set 객체는 이둘을 같다고 평가형 중복 추가를 허용하지 않는다. +0과 -0은 일치 비교 연산자 ===와 마찬가지로 같다고 평가하여 중복 추가를 허용하지 않는다.

 

 Set 객체는 객체나 배열과 같이 자바스크립트의 모든 값을 요소로 저장할 수 있다.

const set = new Set();

set.add(1)
        .add('a')
            .add(true)
                .add(undefined)
                    .add(null)
                        .add({})
                            .add([])
                                .add(()=>{};)

console.log(set); // Set(8){1,'a',true,undefined,null,{},[],()=>{}}

37.1.4 요소 존재  여부 확인

Set 객체에 특정 요소가 존재하는지 확인하려면 Set.prototype.has 메서드를 사용한다. 메서드는 특정 요소의 존재 여부를 나타내는 불리언 값을 반환한다.

const set = new Set([1,2,3,]);

console.log(set.has(2)); // true

37.1.5 요소 삭제

Set 객체의 특정 요소를 삭제하려면 Set.prototype.delete 메서드를 사용한다. 메서드는 삭제 성공 여부를 나타내는 불리언 값을 반환한다. 삭제하려는 요소값을 인수로 전달해야 하는데 Set 객체에는 순서에 의미가 없기 때문이다. 다시 말해, 배열과 같이 인덱스를 갖지 않는다.

const set = new Set([1,2,3]);

set.delete(2);
console.log(set); Set(2){1,3}

만약 존재하지 않는 Set 객체의 요소를 삭제하려 하면 에러 없이 무시된다. delete 메서드는 삭제 성공 여부를 나타내는 불리언 값을 반환한다. 따라서 연속적으로 호출할 수 없다.

37.1.6 요소 일괄 삭제

Set 객체의 모든 요소를 일괄 삭제하려면 Set.prototype.clear 메서드를 사용한다. 언제나 undefiend를 반환한다.

const set = new Set([1,2,3]);

set.clear();
conosole.log(set); // Set(0){}

37.1.7 요소 순회

Set 객체의 요소를 순회하려면 Set.prototype.forEach 메서드를 사용한다. Array.prototype.forEach 메서드와 유사하게 콜백 함수와 forEach 메서드의 콜백 함수 내부에서 this로 사용될 객체를 인수로 전달한다. 이때 다음과 같이 3개의 인수를 전달받는다.

  • 첫 번째 인수: 현재 순회 중인 요소값
  • 두 번째 인수: 첫번째 인수와 동일
  • 세 번재 인수: 현재 순회 중인 Set 객체 자체

첫 번째 인수와 두 번째 인수는 같은 값이다. 이처럼 동작하는 이유는 Array.prototype.forEach 메서드와 인터페이스를 통일하기 위함이며 다른 의미는 없다. Array.prototype.forEach 메서드의 콜백 함수는 두 번째 인수를 현재 순회 중인 요소의 인덱스를 전달 받는다. 하지만 Set 객체는 순서에 의미가 없어 배열과 같이 인덱스를 갖지 않는다.

const set = new Set([1,2,3]);

set.forEach((v, v2, set) => console.log(v, v2, set));

/*
1 1 Set(3){1,2,3}
2 2 Set(3){1,2,3}
3 3 Set(3){1,2,3}
*/

Set 객체는 이터러블이다. 따라서 for ... of 문으로 순회할 수 있으며, 스프레드 문법과 배열 디스트러처링의 대상이 될 수도 있다. Set 객체를 순회하는 순서는 요소가 추가된 순서를 따른다. ECMAScript 사양에 규정되어 있지는 않지만 다른 이터러블의 순회와 호환성을 유지하기 위함이다. 

37.1.8 집합 연산

Set 객체는 수학적 집합을 구현하기 위한 자료구조다. 따라서 Set 객체를 통해 교집합, 합집합, 차집합 등을 구현할 수 있다.

 

교집합

교집합 ANB는 집합 A와 집합 B의 공통 요소로 구성된다.

Set.prototype.intersection = function (set){
	return new Set([...this].filter(v=> set.has(v)));
};

const setA = new Set([1,2,3,4]);
const setB = new Set([2,4]);

// A와 B의 교집합
console.log(setA.intersection(setB)); // Set(2) {2,4}


합집합

합집합 AUB는 집합 A와 집합 B의 중복 없는 요소로 구성된다.

Set.prototype.union = function (set) {
	return new Set([...this, ...set])
}

const setA = new Set([1,2,3,4]);
const setB = new Set([4,5,6]);

// A와 B의 합집합
console.log(setA.union(setB)); // Set(6) {1,2,3,4,5,6}

 

차집합

차집합 A-B는 집합A에만 존재하지만 집합B에는 존재하지 않는 요소로 구성된다.

Set.prototype.difference = function (set) {
	return new Set([...this].filter(v=> !set.has(v)));
};

const setA = new Set([1,2,3,4])
const setB = new Set([2,4,])

// A에 대한 B의 차집합
console.log(setA.difference(setB)); // Set(2) {1,3}

 

부분 집합과 상위 집합

집합 A가 집합 B에 포함되는 경우 집합 A는 집합 B의 부분 집합이며, 집합 B는 집합 A의 상위 집합이다

// this가 subset의 상위집합 인지 확인한다.
Set.prototype.isSuperset = function (subset) {
	const superset = [...this];
    return [...subset.every(v => supersetArr.includes(v))]
}

const setA = new Set([1,2,3,4])
const setB = new Set([2,4,])

console.log(setA.isSuperset(setB)); // true

37.2 Map

Map 객체는 키와 값의 쌍으로 이루어진 컬렉션이다. 객체와 유사하지만 다음과 같이 차이가 존재한다.

구분 객체 Map 객체
키로 사용할 수 있는 값 문자열 또는 심벌 값 객체를 포함한 모든 값
이터러블 X O
요소 개수 확인 Object.keys(obj).legnth map.size

37.2.1 Map 객체의 생성

Map 객체는 Map 생성자 함수로 생성한다. 인수를 전달하지 않으면 빈 객체가 생성된다.

const map = new MapO();
console.log(map); // Map(0){}

Map 생성자 함수는 이터러블을 인수로 전달받아 Map 객체를 생성한다. 이때 인수로 전달된 이터러블은 키와 값의 쌍으로 이루어진 요소로 구성되어야 한다.

const map1 = new Map([['key1', 'value1'],['key2','value2']]);
console.log(map1); // Map(2){"ket1"=>"value1", "key2"=>"value2"}

const map2 = new Map([1,2]); // TypeError: Iterator value 1 is not an entry object

Map 생성자 함수의 이수로 전달한 이터러블에 중복된 키를 갖는 요소가 존재하면 값이 덮어써진다. 따라서 Map 객체에는 중복된 키를 갖는 요소가 존재할 수 없다.

37.2.2 요소 개수 확인

Map 객체의 요소 개수를 확인할 때는 Map.prototype.size 프로퍼티를 사용한다.

const map = new Map([['key1','value1'],['key2','value2']]);
console.log(size); // 2

size 프로퍼티는 setter 함수 없이 getter 함수만 존재하는 접근자 프로퍼티다. 따라서 size 프로퍼티에 숫자를 할당하여 Map 객체의 요소 개수를 변경할 수 없다.

37.2.3 요소 추가

Map 객체에 요소를 추가할 때는 Map.prototype.set 메서드를 사용한다.

const map = new Map();
map.set('key1','value1')

console.log(map); // Map(1){"key1"=>"value1"}

set 메서드는 새로운 요소가 추가된 Map 객체를 반환한다. 따라서 set 메서드를 호출한 후에 다시 연속적으로 호출할 수 있다.

Map 객체에는 중복된 키를 갖는 요소가 존재할 수 없기 때문에 중복된 키를 갖는 요소를 추가하면 값이 덮어 써진다. 이때 에러가 발생하지 않는다.

일치 비교 연산자를 사용하면 NaN과 NaN을 다르다고 평가한다. 하지만 Set 객체는 이둘을 같다고 평가형 중복 추가를 허용하지 않는다. +0과 -0은 일치 비교 연산자 ===와 마찬가지로 같다고 평가하여 중복 추가를 허용하지 않는다.

 

객체는 문자열 또는 심벌 값만 키로 사용할 수 있다. 하지만 Map 객체는 키 타입에 제한이 없다. 따라서 객체르 포함한 모든 값을 키로 사용할 수 있다. 이는 Map 객체와 일반 객체의 가장 두드러지는 차이점이다.

const map = new Map();

const lee = {name:'Lee'}
const kim = {name:'kim'}

// 객체도 키로 사용할 수 있다.
map.set(lee,'developer')
		.set(kim,'designer');
        
console.log(map);
// Map(2) {{name:'Lee'}=> "developer", {name:'kim'}=> "designer"}

37.2.4 요소 취득

Map 객체에서 특정 요소를 취득하려면 Map.prototype.get 메서드를 사용한다. get 메서드의 인수로 키를 전달하면 Map 객체에서 인수로 전달한 키를 갖는 값을 반환한다. 만약 전달한 키를 갖는 요소가 존재하지 않으면 undefined를 반환한다.

const map = new Map();

const lee = {name:'Lee'}
const kim = {name:'kim'}

map.set(lee,'developer')
		.set(kim,'designer');
        
console.log(map.get(lee)); // developer

37.2.5 요소 존재 여부 확인

Map 객체에 특정 요소가 존재하는지 확인하려면 Map.prototype.has 메서드를 사용한다. 메서드는 특정 요소의 존재 여부를 나타내는 불리언 값을 반환한다.

const lee = {name:'Lee'}
const kim = {name:'kim'}

const map = new Map([[lee],'developer'],[kim],'designer');


console.log(map.get(lee)); // true

37.2.6 요소 삭제

Map 객체의 요소를 삭제하려면 Map.prototype.delete 메서드를 사용한다. 메세더는 삭제 성공 여부를 나타내는 불리언 값을 반환한다.

const lee = {name:'Lee'}
const kim = {name:'kim'}

const map = new Map([[lee],'developer'],[kim],'designer');

map.delete(kim)
console.log(map); // Map(1){name:'Lee"=>'developer'}

만약 존재하지 않는 키로 Map 객체의 요소를 삭제하려 하면 에러 없이 무시된다. delete 메서드는 삭제 성공 여부를 나타내는 불리언 값을 반환한다 그렇기에 연속적으로 호출할 수 없다.

37.2.7 요소 일괄 삭제

Map 객체의 요소를 일괄 삭제하려면 Map.prototype.clear 메서드를 사용한다. clear 메서드는 언제나 undefined를 반환한다.

const lee = {name:'Lee'}
const kim = {name:'kim'}

const map = new Map([[lee],'developer'],[kim],'designer');

map.clear()
console.log(map); // Map(0){}

37.2.8 요소 순회

Map 객체의 요소를 순회하려면 Map.prototype.forEach 메서드를 사용한다. Array.prototype.forEach와 유사하게 콜백 함수와 forEach 함수 내부에서 this로 사용될 객체를 인수로 전달한다. 다음과 같이 3개의 인수를 전달 받는다.

  • 첫 번째 인수: 현재 순회 중인 요소값
  • 두 번째 인수: 현재 순회 중인 요소키
  • 세 번째 인수: 현재 순회 중인 Map 객체 자체
const lee = {name:'Lee'}
const kim = {name:'kim'}

const map = new Map([[lee],'developer'],[kim],'designer');

map.forEach((v, k, map)=>console.log(v, k, map));
/*
developer {name: "Lee"} Map(2){
	{name:'Lee'}=>"developer",
    {name:'kim'}=>"designer"
}
designer {name: "kim"} Map(2){
	{name:'Lee'}=>"developer",
    {name:'kim'}=>"designer"
}
*/

Map 객체는 이터러블이다. 따라서 for...of 문으로 순회할 수 있으며 스프레드 문법과 배열 디스트럭처링 할당의 대상이 될 수도 있다.

Map 객체는 이터러블 이면서 동시에 이터레이터인 객체르 반환하는 메서드를 제공한다.

Map 메서드 설명
Map.prototype.keys Map 객체에서 요소키를 값으로 갖는 이터러블이면서 동시에 이터레이터인 객체를 반환한다.
Map.prototype.values Map 객체에서 요소값을 값으로 갖는 이터러블이면서 동시에 이터레이터인 객체를 반환한다.
Map.prototype.entries Map 객체에서 요소키와 요소값을 값으로 갖는 이터러블이면서 동시에 이터레이터인 객체를 반환한다.

Map 객체를 순회하는 순서는 요소가 추가된 순서를 따른다. ECMAScript사양에 규정되어 있지는 않지만 다른 이터러블의 순회와 호환성을 유지하기 위함이다.

const lee = {name:'Lee'}
const kim = {name:'kim'}

const map = new Map([[lee],'developer'],[kim],'designer');

// Map.prototype.key는 Map 객체에서 요소키를 값으로 갖는 이터레이터를 반환한다.
for(const key of map.key()){
	console.log(key); // {name: "Lee"} {name: "Kim"}
}

// Map.prototype.values는 Map 객체에서 요소값을 값으로 갖는 이터레이터를 반환한다.
for(const value of map.values()){
	console.log(value); // developer designer
}

// Map.prototype.entries는 Map 객체에서 요소키와 요소값을 값으로 갖는 이터레이터를 반환한다.
for(const entry of map.entries()){
	console.log(entry); // [{name: "Lee"}, developer] [{name: "Kim"}, designer]
}

 

'Javascript' 카테고리의 다른 글

Deep-dive .39 : DOM  (1) 2024.09.27
Deep-dive .38 : 브라우저의 렌더링  (0) 2024.08.08
Deep-dive .35 : 스프레드 문법  (0) 2024.07.31
Deep-dive .34 : 이터러블  (0) 2024.07.30
Deep-dive .26 : ES6 함수의 추가기능  (1) 2024.07.24
본 글은 Modern JavaScript Deep-dive을 요약한 글입니다.
자세한 내용은 본 책을 읽으시기 바랍니다.

 

ES6에서 도입된 스프레드 문법 ... 은 하나로 뭉쳐 있는 여러 값들의 잡함을 펼쳐서 개별적인 값들의 목록으로 만든다. 스프레드 문법을 사용할 수 있는 대상은 for ... of 문으로 순회할 수 잇는 이터러블에 한정된다.

console.log(...[1, 2, 3]); // 1 2 3

console.log(...'hello'); // h e l l o

console.log(... new Map([['a', '1'], ['b','2']])); // ['a', '1'] ['b','2']
console.log(... new Set([1, 2, 3])); // 1 2 3

 이터러블인 배열을 펼쳐서 요소들을 개별적인 값들의 목록 1 2 3으로 만든다. 즉, 스프레드 문법의 결과는 값이 아니다. 이는 스프레드 문법 ...이 피연산자를 연산하여 값을 생성하는 연산자가 아님을 의미한다. 따라서 스프레드 문법의 결과는 변수에 할당할 수 없다.

 다음과 같이 쉼표로 구분한 값의 목록을 사용하는 문맥에서만 사용할 수 있다.

  • 함수 호출문의 인수 목록
  • 배열 리터럴의 요소 목록
  • 객체 리터럴의 프로퍼티 목록

35.1 함수 호출문의 인수 목록에서 사용하는 경우

요소들의 집합인 배열을 펼쳐서 개별적인 값들의 목록으로 만든 후, 이를 함수의 인수 목록으로 전달해야 하는 경우가 있다.

const arr = [1, 2, 3];

const max = Math.max(arr); // NaN

만약 Math.max 메서드에 숫자가 아닌 배열을 인수로 전달하면 최대값을 구할 수 없으므로 NaN를 반환한다. 이 같은 문제를 해결하기 위해 배열을 평쳐서 요소들을 개별적인 값들의 목록으로 만든 후, Math.max 메서드의 인수로 전달해야 한다. 스프레드 문법이 제공되기 이전에는 Function.prototype.apply를 사용했다.

 

 스프레드 문법을 사용하면 더 간결하고 가독성이 좋다.

const arr = [1, 2, 3]

cosnt max = Math.max(...arr); // 3

스프레드 문법은 앞에서 살펴본 Rest 파라미터와 혀애가 동일하여 혼동할 수 있으므로 주의할 필요가 있다. Rest 파라미터는 함수에 전달된 인수들의 목록을 배열로 전달받기 위해 매개변수 이름 앞에 ... 을 붙이는 것이다. 스프레드 문법은 여러 개의 값이 하나로 뭉쳐있는 배열과 같은 이터러블을 펼쳐서 개별적인 값들의 목록을 만드는 것이다. 따라서 이둘은 서로 반대의 개념이다.

35.2 배열 리터럴 내부에서 사용하는 경우

 스프레드 문법을 배열 리터럴에서 사용하면 ES5에서 사용하던 기존의 방식보다 더욱 간결하고 가독성 좋게 표현할 수 있다.

35.2.1 concat

스프레드 문법을 사용하면 별도의 메서드를 사용하지 않고 배열 리터럴만으로도 2개의 배열을 1개의 배열로 연결할 수 있다.

const arr = [...[1, 2], ...[3, 4]]
console.log(arr); // [1, 2, 3, 4]

35.2.2 splice

ES5에서 어떤 배열의 중간에 다른 배열의 요소들을 추가하거나 제거하려면 spilce 메서드를 사용한다. 이때 spilce 메서드의 세 번째 인수로 배열을 전달하면 배열 자체가 추가된다. 그렇기에 세번째 인수 [2, 3]를 2,3 으로 해체하여 전달해야 한다. 스프레드 문법을 사용하면 다음과 같이 간결하고 가독성 좋게 표현할 수 있다.

const arr1 = [1 , 4];
const arr2 = [2 , 3];

arr1.spilce(1, 0, arr2);
console.log(arr1); // [1, 2, 3, 4]

35.2.3 배열 복사

ES5에서 배열을 복사하려며 splice 메서드를 사용한다. 스프레드 문법을 사용시 더 간결하게 표현할 수 있다. 이때 원본 배열의 각 요소는 얕은 복사하여 새로운 복사본을 생성한다.

const origin = [1, 2];

const copy = [...origin];
console.log(copy); // [1, 2]
console.log(copy === origin); // false

35.2.4 이터러블을 배열로 반환

ES5에서 이터러블을 배열로 반환하려면 Function.prototype.apply 또는 Function.prototype.calll 메서드를 사용해여 splice 메서드를 호출해야 한다. 스프레드 문법을 사용하면 좀더 간편하게 이터러블을 배열로 변환할 수 있다.

function sum(){
	return [...argumnets].reduce((pre, cur)=> pre + cur, 0);
}

console.log(sum(1,2,3)); // 6

위 예제보다 더 나은 방법은 Rest 파라미터를 사용하는 것이다.

const sum = (...args) => args.reduce((pre, cur)=> pre + cur, 0);

console.log(sum(1,2,3)); // 6

단, 이터러블이 아닌 유사 배열 객체는 스프레드 문법의 대상이 될 수 없다.

35.3 객체 리터럴 내부에서 사용하는 경우

Rest 프로퍼티와 함께 TC39프로세스의 stage4 단계에 제안되어 있는 스프레드 프로퍼티를 사용하면 객체 리터럴의 프로퍼티 목록에서도 스프레드 문법을 사용할 수 있다. 스프레드 프로퍼티가 제안되기 이전에는 ES6에서 도입된 Object.assign 메서드를 사용하여 여러 개의 객체를 병합하거나 특정 프로퍼티를 변경 또는 추가했다.

 스프레드 프로퍼티는 Object.assign 메서드를 대체할 수 있는 간편한 문법이다.

// 객체 병합. 프로퍼티가 중복되는 경우 뒤에 위치한 프로퍼티가 우선권을 갖는다.
const merged = {...{x: 1, y: 2}, ...{y: 10, z: 3}};
console.loog(merged); // {x:1 , y: 10, z: 3}

// 특정 프로퍼티 변경
const changed = {...{x: 1, y: 2}, y: 100};
// changed = {...{x: 1, y: 2}, ...y: 100}
console.log(changed) //{x: 1, y: 100

// 프로퍼티 추가
const added = {...{x: 1, y: 2},z: 3};
// added = {...{x: 1, y: 2}, ...{z: 3}}
console.log(added) // {x: 1, y: 2, z: 3}

'Javascript' 카테고리의 다른 글

Deep-dive .38 : 브라우저의 렌더링  (0) 2024.08.08
Deep-dive .37 : Set과 Map  (0) 2024.08.06
Deep-dive .34 : 이터러블  (0) 2024.07.30
Deep-dive .26 : ES6 함수의 추가기능  (1) 2024.07.24
Deep-dive .25 : 클래스  (0) 2024.07.09
본 글은 Modern JavaScript Deep-dive을 요약한 글입니다.
자세한 내용은 본 책을 읽으시기 바랍니다.

34.1 이터레이션 프로토콜

ES6에서 도입된 이터레이션 프로토콜은 순회 가능한 데이터 컬렉션을 만들기 위해 ECMAScript 사양에 정의하여 미리 약속한 규칙이다. ES6에서는 순회 가능한 데이터 컬렉을 이터레이션 프로토콜을 준수하는 이터러블로 통일하여 for..of 문, 스프레드 문법, 배열 디스트럭처링 할당의 대상으로 사용할 수 있도록 일원화했다.

이터레이션 프로토콜에는 이터러블 프로토콜과 이터레이터 프로토콜이 있다.

  • 이터러블 프로토콜 : Symbol.iterator를 프로퍼티 키로 사용한 메서드를 직접 구현하거나 프로토타입 체인을 통해 상속 받은 메서드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환한다. 이터러블 프로토콜을 준수한 객체를 이터러블이라 한다. 이터러블은 for...of 문으로 순회할 수 있으며 스프레드 문법과 배열 디스트럭처링 할당의 대상으로 사용할 수 있다.
  • 이터레이터 프로토콜 : 이터러블의 Symbol.iterator 메서드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환한다. 이터레이터는 next 메서드를 소유하며 이를 호출하면 이터러블을 순회하며 value와 done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환한다. 이러한 규약을 이터레이터 프로토콜이라 하며, 이터레이터 프로토콜을 준수한 객체를 이터레이터라 한다. 이터레이터는 이터러블의 요소를 탐색하기 위한 포인터 역할을 한다.

34.1.1 이터러블

이터러블 프로토콜을 준수한 객체를 이터러블이라 한다. 즉 이터러블은 Symbol.iterator를 프로퍼티 키로 사용한 메서드를 직접 구현하거나 프로토타입 체인을 통해 상속받은 객체를 말한다. 이터러블은 for...of문으로 순회할 수 있으며, 스프레드 문법과 배열 디스트럭처링 할당의 대상으로 사용할 수 있다.

const array = [1, 2, 3];

// 배열은 Array.prototype의 Symbol.iterator 메서드를 상속받는 이터러블이다.
console.log(Symbol.iterator in array); // true

// 이터러블인 배열은 for ... of 문으로 순회 가능하다.
for (const item of array) {
	console.log(item);
}

// 이터러블인 배열은 스프레드 문법의 대상으로 사용할 수 있다.
console.log([... array]); // [1, 2, 3]

// 이터러블인 배열은 배열 디스트럭처링 할당의 대상으로 사용할 수 있다.
const [a, ...rest] = array;
console.log(a, rest); // 1, [2, 3]

 

Symbol.iterator 메서드를 직접 구현하지 않거나 상속받지 않는 일반 객체는 이터러블 프로토콜을 준수한 이터러블이 아니다. 따라서 일반 객체는 for...of문으로 순회할 수 없으며 스프레드 문법과 배열 디스트러처링 할당의 대상으로 사용할 수 없다. 단 TC39 프로세스의 stage 4 단계에 제안되어 있는 스프레드 프로퍼티 제안은 일반 객체에 스프레드 문법의 사용을 허용한다.

const obj = {a : 1, b : 2};

// 스프레드 프로퍼티 제안은 객체 리터럴 내부에서 스프레드 문법의 사용을 허용한다.
console.log({...obj}); // {a: 1, b: 2}

34.1.2 이터레이터

이터러블의 Symbol.iterator 메서드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환한다. 이터러블의 Symbol.iterator 메서드가 반환한 이터레이터는 next 메서드를 갖는다.

const array = [1, 2, 3];

// Symbol.iterator 메서드는 이터레이터를 반환한다.
const iterator = array[Symbol.iterator]();

// Symbol.iterator 메서드가 반환한 이터레이터는 next 메서드를 갖는다.
console.log('next' in iterator); // true

 

이터레이터의 next 메서드는 이터러블의 각 요소를 순회하기 위한 포인터의 역할을 한다. 즉, next 메서드를 호출하면 이터러블을 순차적으로 한 단계씩 순회하며 순회 결과를 나타내는 이터레이터 리절트 객체를 반환한다.

const array = [1, 2, 3];

// Symbol.iterator 메서드는 이터레이터를 반환한다.
const iterator = array[Symbol.iterator]();

// Symbol.iterator 메서드가 반환한 이터레이터는 next 메서드를 갖는다.
console.log('next' in iterator); // true

// next 메서드를 호출하면 이터러블을 순회하며 순회 결과를 나타내는 이터러이터 리절트 객체를 반환한다.
// 이터레이터 리절트 객체는 value와 done 프로퍼티를 갖는 객체다.
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

이터레이터의 next 메서드가 반환하는 이터레이터 리절트 객체의 value 프로퍼티는 현재 순회 중인 이터러블의 값을 나타내며 done 프로퍼티는 이터러블의 순회 완료 여부를 나타낸다.

34.2 빌트인 이터러블

 자바스크립트는 이터레이션 프로토콜을 준수한 객체인 빌트인 이터러블을 제공한다.

빌트인 이터러블 Symbol.iterator 메서드
Array Array.prototype[Symbol.iterator]
String String.prototype[Symbol.iterator]
Map Map.prototype[Symbol.iterator]
Set Set.prototype[Symbol.iterator]
TyedArray TyedArray.prototype[Symbol.iterator]
arguments arguments.prototype[Symbol.iterator]
DOM 컬렉션 NodeList.prototype[Symbol.iterator]
HTMLCollection.prototype[Symbol.iterator]

34.3 for .. of 문

for ... of 문은 이터러블을 순회하면서 이터러블의 요소를 변수에 할당한다.

for (변수 선언문 of 이터러블) {...}

 

for ... in 문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 true인 프로퍼티를 순회하며 열거한다. 이때 프로퍼티 키가 심벌인 프로퍼티는 열거하지 않는다.

 

for ... of 문은 내부적으로 이터레이터의 next 메서드를 호출하여 이터러블을 순회하며 next 메서드가 반환한 이터레이터 리절트 객체의 value 프로퍼티 값을 for...of 문의 변수에 할당한다. 그리고 이터레이터 리절트 객체의 done 프로퍼티 값이 false이면 이터러블의 순회를 계속하고 true 이며 이터러블의 순회를 중단한다.

for(const item of [1,2,3]){
	console.log(item); // 1 2 3
}

34.4 이터러블과 유사 배열 객체

유사 배열 객체는 마치 배열처럼 인덱스로 프로퍼티 값에 접근할 수 있고 length 프로퍼티를 갖는 개체를 말한다. 유사 배열 객체는 length 프로퍼티를 갖기 때문에 for 문으로 순회할 수 있고, 인덱스를 나타내는 숫자 형식의 문자열을 프로퍼티 키로 가지므로 마치 배열 처럼 프로퍼티 값에 접근할 수 있다.

// 유사 배열 객체
const arrayLike = {
	 0: 1,
     1: 2,
     2: 3,
     length: 3
}

// 유사 배열 객체는 length 프로퍼티를 갖기 때문에 for문으로 순회할 수 있다.
for (let i = 0; i < arrayLike.length; i ++){
	// 유사 배열 객체는 마치 배열처럼 인덱스로 프로퍼티 값에 접근할 수 있다.
    console.log(arrayLike[i]); // 1 2 3
}

 

 유사 배열 객체는 이터러블이 아닌 일반 객체다. 따라서 유사 배열 객체는 Symbol.iterator 메서드가 없기 때문에 for...of 문으로 순회할 수 없다. 단, arguments, NodeList, HTMLCollection은 유사 배열 객체이면서 이터러블이다. 정확히 말하면 ES6에서 이터러블이 도입디면서 유사 배열 객체인 arguments, NodeList, HTMLCollection 객체에 Symbol.iterator 메서드를 구현하여 이터러블이 되었다. 배열도 마찬가지로 ES6에서 이터러블이 도입되면서 Symbol.iterator 메서드를 구현하여 이터러블이 되었다.

 하지만 모든 유사 배열 객체가 이터러블인 것은 아니다. 다만 ES6에서 도입된 Array.from 메서드를 사용하여 배열롤 간단히 변환할 수 있다.

34.5 이터레이션 프로토콜의 필요성

for..of 문, 스프레드 문법, 배열 디스트럭처링 할당 등은 다양한 데이터 소스를 사용할 수 있다. ES6에서는 순회 가능한 데이터 컬렉션을 이터레이션 프로토콜을 준수하는 이터러블로 통일하여 for...of 문, 스프레드 문법, 배열 디스트럭처링 할당의 대상으로 사용할 수 있도록 일원화 했다.

 이터러블은 for...of 문, 스프레드 문법, 배열 디스트럭처링 할당과 같은 데이터 소비자에 의해 사용되므로 데이터 공급자의 역할을 한다고 볼 수 있다. 만약 데이터 공급자가 각자의 순회 방식을 갖는다면 데이터 소비자는 다양한 데이터 공급자의 순회 방식을 모두 지원해야 한다. 이는 효율적이지 않다. 하지만 다양한 데이터 공급자가 이터레이션 프로토토콜을 준수하도록 규정하면 데이터 소비자는 이터레이션 프로토콜만 지원하도록 구현하면 된다.

 이처럼 이터레이션 프로토토콜은 다양한 데이터 공급자가 하나의 순회 방식을 갖도록 규정하여 데이터 소비자가 효율적으로 다양한 데이터공급자를 사용할 수 있도록 데이터 소비자와 데이터 공급자를 연결하는 인터페이스의 역할을 한다.

34.6 사용자 정의 이터러블

34.6.1 사용자 정의 이터러블 구현

이터레이션 프로토콜을 준수하지 않은 일반 객체도 이터레이션 프로토콜을 준수하도록 구현하면 사용자 장의 이터러블이 된다.

// 피보나치 수열을 구현한 사용자 정의 이터러블
const fibonacci = {
	// Symbol.iterator를 구현하여 프로토콜을 준수한다.
	[Symbol.iterator](){
    	let [pre, cur] = [0,1]
        const max = 10;
        
        // next 메서드는 이터레이터 리절트 객체를 반환해야 한다.
        return {
        	next() {
            	[pre,cur] = [cur, pre + cur];
                return {value : cur, done : cur >= max}
            }
        }
    }
}

34.6.2 이터러블을 생성하는 함수

수열의 최대값을 외부에서 전달할 수 있도록 수정하여 고정된 값을 전달한 값을 변경되게 하자.

// 피보나치 수열을 구현한 사용자 정의 이터러블
const fibonacci = function(max) {
	// Symbol.iterator를 구현하여 프로토콜을 준수한다.
	[Symbol.iterator](){
    	let [pre, cur] = [0,1]
        
        // next 메서드는 이터레이터 리절트 객체를 반환해야 한다.
        return {
        	next() {
            	[pre,cur] = [cur, pre + cur];
                return {value : cur, done : cur >= max}
            }
        }
    }
}

for (const num of fibonacciFunc(10)){
	console.log(num); // 1 2 3 5 8
}

34.6.3 이터러블이면서 이터레이터인 객체를 생성하는 함수

 앞선 예제의 피보나치 함수는 이터러블을 반환한다. 만약 이터레이터를 생성하려면 이터러블의 Symbol.iterator 메서드를 호출해야한다. 이터러블 이면서 이터레이터인 객체를 생성하면 Symbol.iterator 메서드를 호출하지 않아도 된다.

 다음 객체는 Symbol.iterator 메서드와 next 메서드를 소유한 이터러블 이면서 이터레이터다. Symbol.iterator 메서드에서 this를 반환하므로 next 메서드를 갖는 이터레이터를 반환한다.

{
	[Symbol.iterator]() {return this;},
    next() {
    	return{value: any, done: boolean}
    }
}

 

피보나치 함수를 이터러블이면서 이터레이터 객체를 생성하여 반환하는 함수로 변경해보자.

// 피보나치 수열을 구현한 사용자 정의 이터러블
const fibonacci = function(max) {
    	let [pre, cur] = [0,1]
        
        // Symbol.iterator 메서드와 next 메서드를 소유한 이터러블 이면서 이터레이터인 객체를 반환
        // 
        return {
        	[Symbol.iterator](){return this;},
        	next() {
            	[pre,cur] = [cur, pre + cur];
                return {value : cur, done : cur >= max}
            }
        }
}
// iter는 이터러블이면서 이터레이터다.
let iter = fibonacci(10);

for (const num of fibonacciFunc(10)){
	console.log(num); // 1 2 3 5 8
}

console.log(iter.next()); // {value : 1, done : false}
console.log(iter.next()); // {value : 2, done : false}
console.log(iter.next()); // {value : 3, done : false}
console.log(iter.next()); // {value : 5, done : false}
console.log(iter.next()); // {value : 8, done : false}
console.log(iter.next()); // {value : 13, done : true}

34.6.4 무한 이터러블과 지연 평가

// 무한 이터러블을 생성하는 함수
const fibonacci = function() {
    	let [pre, cur] = [0,1]
        
        return {
        	[Symbol.iterator](){return this;},
        	next() {
            	[pre,cur] = [cur, pre + cur];
                // 무한을 구현해야하므로 done 프로퍼티 생략
                return {value : cur}
            }
        }
}

for (const num of fibonacci()){
	if(num > 10000) break;
	console.log(num); // 1 2 3 5 8 ... 4181 6765
}

// 배열 디스트럭처링 할당을 통해 무한 이터러블에서 3개의 요소만 취득한다.
const [f1, f2, f3] = fibonacci();
console.log(f1, f2, f3); // 1 2 3

 이터러블은 데이터 공급자 역할을 하는데 배열이나 문자열 등은 모든 데이터를 메모리에 미리 확보한 다음 데이터를 공급한다. 하지마 위 예제의 이터러블은 지연 평가를 통해 데이터를 생성한다. 

 지연 평가는 데이터가 필요한 시점 이전까지는 미리 데이터를 생성하지 않다가 데이터가 필요한 시점이 되면 그때야 비로소 데이터를 생성하는 기법이다. 즉, 평가가 필요할때 까지 평가를 늦추는 기법이 지연 평가다.

 위 예제의 피포나치 함수는 무한 이러터블을 생성한다. 하지만 데이터 소비자인 for...of 문이나 배열 디스트럭처링 할당 등이 실행되기 이전까지는 데이터를 생성하지 않는다. for ... of 문의 경우 이터러블을 순회할 때 내부에서 이터레이터의 next  메서드를 호출하는데 바로 이때 데이터가 생성된다. 즉, 데이터가 필요할 때가지 데이터의 생성을 지연하다가 데이터가 필요한 순간 데이터를 생성한다.

이처럼 지연 평가를 사용하면 불필요한 데이터를 미리 생성하지 않고 필요한 데이터를 필요한 순간에 생성하므로 빠른 실행 속도를 기대할 수 있고 불필요한 메모리를 소비하지 않으며 무한도 표현할 수 있다는 장점이 있다.

'Javascript' 카테고리의 다른 글

Deep-dive .37 : Set과 Map  (0) 2024.08.06
Deep-dive .35 : 스프레드 문법  (0) 2024.07.31
Deep-dive .26 : ES6 함수의 추가기능  (1) 2024.07.24
Deep-dive .25 : 클래스  (0) 2024.07.09
Deep-dive .24 : 클로저  (0) 2024.07.09
본 글은 Modern JavaScript Deep-dive을 요약한 글입니다.
자세한 내용은 본 책을 읽으시기 바랍니다.

 

26.1 함수의 구분

  ES6 이전의 모든 함수는 일반 함수로서 호출할 수 있는 것은 물론 생성자 함수로서 호출할 수 있다. 주의할 것은 ES6 이전에 일반적으로 메서드라고 부르던 객체에 바인딩된 함수도 callable이며 constructor라는 것이다.

 객체에 바인딩된 함수를 생성자 함수로 호출하는 경우는 흔치는 않겠지만 문제가 존재한다. 성능면에서 문제가 존재하는데, 객체에 바인딩된 함수가 constructor라는 것은 객체에 바인딩된 함수가 prototype 프로퍼티를 가지며, 프로토타입 객체도 생성한다는 것을 의미하기 때문이다. 

 함수에 전달되어 보조 함수의 역할을 수행하는 콜백 함수도 마찬가지다. 콜백 함수도 constructor이기 때문에 불필요한 프로토타입 객체를 생성한다. 이처럼 ES6 이전의 모든 함수는 사용 목적에 따라 명확한 구분이 없으므로 호출 방식에 특별한 제약이 없고 생성자 함수로 호출되지 않아도 프로토타입 객체를 생성한다.

 이러한 문제를 해결하기위해 ES6에서는 함수를 사용 목적에 따라 세 가지 종류로 명확히 구분했다.

ES6 함수의 구분 constructor prototype super arguments
일반함수 O O X O
메서드 X X O O
화살표 함수 X X X X

26.2 메서드

ES6 사양에서 메서드는 메서드 축약 표현으로 정의된 함수만을 의미한다.

const obj = {
	x: 1,
    // foo는 메서드다
    foo() {return this.x}
    // bar에 바인딘된 함수는 메서드가 아닌 일반 함수다.
    bar : function(){return this.x}
}

console.log(obj.foo()); // 1
console.log(obj.bar()); // 1

 ES6 사양에서 정의한 메서드는 인스턴스를 생성할 수 없는 non-constructor다. 따라서 ES6 메서드는 생성자 함수로 호출할 수 없다.

 ES6 메서드는 인스턴스를 생성할 수 없으므로 prototype 프로퍼티가 없고 프로토타입도 생성하지 않는다. 참고로 표준 빌트인 객체가 제공하는 프로토타입 메서드와 정적 메서드는 모두 non-constructor다.

 ES6 메서드는 자신을 바인딩한 객체를 가리키는 내부 슬롯 [[HomeObject]]를 갖는다. super 참조는 이를 사용하므로 ES6 메서드는 super 키워드를 사용할 수 있다.

const base = {
	name: 'Lee',
    sayHi(){
    	return `Hi! ${this.name}`;
    }
};

const derived = {
	__proto__: base,
    sayHi() {
    	return`${super.sayHi()}. how are you doing?`
    }
}

 이처럼 ES6 메서드는 본연의 기능(super)을 추가하고 의미적으로 맞지 않는 기능(constructor)은 제거했다. 따라서 메서드를 정의할 때 프로퍼티 값을 익명 함수 표현식을 할당하는 이전의 방식은 사용하지 않는다.

26.3 화살표 함수

화살표 함수는 표현만 간략한 것이 아니라 내부 동작도 기존의 함수보다 간략하다. 특히 화살표 함수는 콜백 함수 내부에서 this가 전역 객체를 가리키는 문제를 해결하기 위한 대안으로 유용하다.

26.3.1 화살표 함수 정의

함수 정의

화살표 함수는 함수 선언문으로 정의할 수 없고 함수 표현식으로 정의해야한다.

const multiply = (x,y) => x*y;

 

매개변수 정의

  • 매개변수가 여러 개일 경우 소괄호 안에 매개변수를 선언한다.
  • 매개변수가 한 개인 경우 소괄호를 생략할 수 있다.
  • 매개변수가 없는 경우 소괄호를 생략할 수 없다.

함수 몸체 정의

  • 함수 몸체가 하나의 문으로 구성된다면 함수 몸체를 감싸는 중괄호를 생략할 수 있다.
  • 함수 몸체를 감싸는 중괄호를 생략한 경우 함수 몸체 내부의 문이 표현식이 아닌 문이라면 에러가 발생한다. 따라서 함수 몸체가 하나의 문으로 구성된다 해도 함수 몸체의 문이 표현식이 아닌 무이라면 중괄호를 생략할 수 없다.
  • 객체 리터럴을 반환하는 경우 소괄호로 감싸주어야 한다.
  • 함수 몸체가 여러 개의 무능로 구성된다면 몸체를 감싸는 중괄호를 생략할 수 없다.
  • 화살표 함수도 일급 객체이므로 고참함수에 인수로 전달할 수 있다. 이경우 일반적인 함수 표현식보다 표현이 간결하고 가독성이 좋다.

26.3.2 화살표 함수와 일반 함수의 차이

  1. 화살표 함수는 인스턴스를 생성할 수 없는 non-constructor다. 그렇기에 인스턴스를 생성할 수 없으므로 prototype 프로퍼티가 없고 프로토타입도 생성하지 않는다.
  2. 중복된 매개변수 이름을 선언할 수 없다. 일반 함수는 중복된 매개변수 이름을 선언해도 에러가 발생하지 않는다.(단, strict mode에서는 에러 발생)
  3. 화살표 함수는 함수 자체의 this, argumnets, super, new.target 바인딩을 갖지 않는다. 이를 참조하면 스코프 체인을 통해 상위 스코프의 this, argumnets, super, new.target을 참조한다.
    만약 화살표함수와 화살표 함수가 중첨되어 있다면 스코프 체인 상에서 가장 가까운 상위 함수 중에서 화살표 함수가 아닌 함수의  this, argumnets, super, new.target을 참조한다.

26.3.3 this

화살표 함수의 this는 일반 함수의 this와 다르게 동작한다. 즉, 콜백 함수 내부의 this가 외부 함수의 this와 다르기 때문에 발생하는 문제를 해결하기 위해 의도적으로 설계된 것이다.

 this 바인딩은 함수의 호출 방식, 즉 함수가 어떻게 호출되었는지에 따라 동적으로 결정된다. 함수를 호출할 때 어떻게 호출되었는지에 따라 this에 바인딩할 객체가 동적으로 결정된다.

 이때 주의할 것은 일반 함수로서 호출되는 콜백 함수의 경우다. 고차 함수의 인수로 전달되어 고차 함수 내부에서 호출되는 콜백 함수도 중첩 함수라고 할 수 있다.

class Prefixer{
	constructor(prefix){
    	this.prefix = prefix;
    }
    
    add(arr){
    	// 1
    	return arr.map(function (item){
        	return this.prefix + item; // 2
        })
    }
}

 프로토타입 메서드 내부인 1에서 this는 메서드를 호출한 객체를 가리킨다. 그런데 map의 인수로 전달한 콜백 함수의 내부인 2에서 this는 undefined를 가리킨다. 이는 map 메서드가 콜백 함수를 일반함수로서 호출하기 때문이다.

 

 일반 함수로서 호출되는 모든 함수 내부의 this는 전역 객체를 가리킨다. 그런데 클래스 내부의 모든 코드는 strict mode가 암묵적으로 적용된다. 따라서 map 메서드의 콜백 함수에도 strict mode가 적용된다.

 strice mode에서 일반 함수로서 호출된 모든 함수 내부의 this에는 전역 객체가 아니라 undefined가 바인딩되므로 일반 함수로서 호출되는 map 메서드의 콜백 함수 내부의 this에는 undefined가 바인딩된다.

 이때 발생하는 문제가 바로 "콜백 함수 내부의 this 문제"다. 즉, 콜백 함수의 this와 외부 함수의 this가 서로 다른 값을 가리키고 있기 때문에 TypeError가 발생한 것이다.

 

ES6에서는 화살표 함수를 사용하여 이를 해결할 수 있다.

class Prefixer{
	constructor(prefix){
    	this.prefix = prefix;
    }
    
    add(arr){
    	return arr.map(item => this.prefix + item);
    }
}

const prefix = new Prefixer('-webkit-')
console.log(prefix.add(['transition', 'user-select']));
// ['-webkit-transition','-webkit-user-select']

 화살표 함수는 함수 자체의 this 바인딩을 갖지 않는다. 따라서 화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 그대로 참조한다. 이를 lexical this라 한다. 렉시컬 스코프와 같이 화살표 함수의 this가 함수가 정의된 위치에 의해 결정된다는 것을 의미한다.

 

화살표 함수를 제외한 모든 함수는 this 바이딩이 반드시 존대한다. 하지만 화살표 함수는 함수 자체의 this 바인딩이 존재하지 않는다. 따라서 화살표 함수 내부의 this를 참조하면 일반적인 식별자처럼 스코프 체인을 통해 상위 스코프에서 this를 탐색한다.

  • 화살표 함수와 화살표 함수가 중첩되어 있다면 스코프 체인 상에서 가장 가까운 상위 함수 중에서 화살표 함수가 아닌 함수의 this를 참조한다.
  • 화살표 함수가 전역 함수라면 화살표 함수의 this는 전역 객체를 가리킨다.
  • 프로퍼티에 할당한 화살표 함수도 스코프 체인 상에서 가장 가까운 상위 함수 중에서 화살표 함수가 아닌 함수의 this를 참조한다.
  • 화살표 함수는 함수 자체의 this 바인딩을 갖지 않기 때무에 Function.prototype.apply,Function.prototype.call,Function.prototype.bind 메서드를 사용해도 화살표 함수 내부의 this를 교체할 수 없다.

 메서드를 화살표 함수로 정의하는 것은 피해야한다. 여기서 말하는 메서드는 ES6 메서드가 아닌 일반적인 의미의 메서드를 말한다.

// Bad
const person = {
	name: 'Lee',
    sayHi: () => console.log(`Hi ${this.name}`)
}

// sayHi 프로퍼티에 항당된 화살표 함수 내부의 this는 상위 스코프인 전역의 this가 가리키는 전역개체를
// 가리키므로 window.this를 가리키므로 빈 문자열을 갖게 된다.
// 전역객체 window에는 빌트인 프로퍼티 name이 존재한다.
person.sayHi(); // Hi

// Good
const person = {
	name: 'Lee',
    sayH() {console.log(`Hi ${this.name}`)
    }
}

 위 예제의 경우 sayHi 프로퍼티에 할당한 화살표 함수 내부의 this는 메서드를 호출하는 객체인 person을 가리키지 않고 상위 스코프인 전역의 this가 가리키는 전역 객체를 가리킨다. 따라서 화살표 함수로 메서드를 정의하는 것은 바람직하지 않다. 프로토타입 객체의 프로퍼티에 화살표 함수를 할당하는 경우도 동일한 문제가 발생한다.

// Bad
function Person(name) {
	this. name: 'Lee',
}

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

const person = new Person('Lee');
person.sayHi(); // Hi

// Good
function Person(name) {
	this. name: 'Lee',
}
Person.prototype.sayHi= function() {console.log(`Hi ${this.name}`)}

 ES6 메서드 축약 표현을 사용하거나 프로토타입에 동적으로 할당할 때는 축약 표현이 불가능하니 일반 함수를 할당하는 것이 좋다.

 

클래스 필드 정의 제안을 사용하여 클래스 필드에 화살표 함수를 정의할 수도 있다.

// Bad
class Person {
	name = 'Lee';
    sayHi = () => console.log(`Hi ${this.name}`)
}

const person = new Person();
person.sayHi(); Hi Lee

// Good
class Person2 {
	constructor() {
    	this.name = 'Lee';
        // 클래스가 생성한 인스턴스(this)의 sayHi 프로퍼티에 화살표 함수를 할당한다.
        // 따라서 sayHi 프로퍼티는 인스턴스 프로퍼티다.
        sayHi() {console.log(`Hi ${this.name}`)}
    }
}
const person2 = new Person2();
person2.sayHi(); // Hi Lee

 sayHi 클래스 필드에 할당한 화살표 함수의 상위 스코프는 사실 클래스 외부다. 하지만 this는 클래스 외부의 this를 참조하지 않고 클래스가 생성할 인스턴스를 참조한다. 따라서 sayHi 클래스 필드에 할당한 화살표 함수 내부에 참조한 this는 constructor 내부의 this 바인딩과 같다. constructor 내부의 this 바인딩은 클래스가 생성한 인스턴스를 가리키므로 sayHi 클래스 필드에 할당한 화살표 함수 내부의 this 또한 클래스가 생성한 인스턴스를 가리킨다.

하지만 클래스 필드에 정의한 화살표 함수는 인스턴스 메서드가 되므로 메서드를 정의할 때는 ES6 메서드 축약 표현으로 정의하는 것이 좋다.

26.3.4 super

화살표 함수는 함수 자체의 super 바인딩을 갖지 않는다. 따라서 화살표 내부에서 super를 참조하면 this와 마찬가지로 상위 스코프의 super를 참조한다.

class Base{
	constructor(name){
    	this.name = name;
    }
    
    sayHi() {
    	return `Hi! ${this.name}`'
    }
}

class Derived extends Base {
	// 화살표 함수의 super는 상위 스코프의 constructor의 super를 가리킨다.
    sayHi = () => `${super.sayHi()} how are you doing?`
}

26.3.5 arguments

화살표 함수는 자체의 argumnets 바인딩을 갖지 않는다. 화살표 함수 내부에서 aruments를 참조하면 this와 마찬가지로 상위 스코프의 arguments를 참조한다.

(function () {
	// 화살표 함수 foo의 arguments는 상위 스코프인 즉시 실행 함수의 arguments를 가리킨다.
    const foo = () => console.log(arguments); // [Arguments] {'0':1, '1':2}
    foo(3,4);
}(1,2));

// 화살표 함수 foo의 arguments는 상위 스코프인 즉시 실행 함수의 arguments를 가리킨다.
// 하지만 전역에는 arguments 객체가 존재하지 않는다. arguments 객체는 함수 내부에서만 유효하다.
const foo = () => console.log(arguments);
foo(1,2); // ReferenceError: arguements is not defined

 화살표 함수에서는 arguments 객체를 사용할 수 없다. 상위 스코프의 arguments 객체를 참조할 수는 있지만 화살표 함수 자신에게 전달된 인수 목록을 확인할 수 없고 상위 함수에게 전달된 인수 목록을 참조하므로 그다지 도움되지 않는다. 따라서 화살표 함수로 가변 인자 함수를 구현해야할 때는 반드시 Rest 파라미더틀 사용해야 한다.

26.4 Rest 파라미터

26.4.1 기본 문법

 Rest 파라미터는 매개변수 이름 앞에 세개의 점 ...을 붙여서 정의한 매개변수를 의미한다. Rest 파라미터는 함수에 전달된 인수들의 목록을 배열로 전달받는다.

  • 일반 매개변수와 Rest 파라미터는 함께 사용할 수 있다. 이때 함수에 전달된 인수들은 매개변수와 Rest 파라미터에 순차적으로 할당된다.
  • 먼저 선언된 매개변수에 할당된 인수를 제외한 나머지 인수들로 구성된 배열이 할당된다. 따라서 Rest 파라미터는 반드시 마지막 파라미터이어야 한다.
  • Rest 파라미터는 단 하나만 선언할 수 있다.
  • Rest 파라미터는 함수 정의시 선언한 매개변수 개수를 나타내는 함수 객체의 length 프로퍼티에 영향을 주지 않는다.

26.4.2 Rest 파라미터와 arguments 객체

ES6에서는 rest 파라미터를 사용하여 가변 인자 함수의 인수 목록을 배열로 직접 전달 받을 수 있다. 이를 통해 유사 배열 객체인 arguments 객체를 배열로 전환하는 번거로움을 피할 수 있다.

fucntion sum(... args){
	// Rest 파라미터 args에는 배열[1,2,3,4,5]가 할당된다.
    return args.reduce((pre, cur)=> pre + cur, 0);
}

console.log(sum(1,2,3,4,5)); // 15

26.3 매개변수 기본값

 함수를 호출할 때 매개변수의 개수만큼 인수를 전달하는 것이 바람직하지만 그렇지 않은 경우에도 에러가 발생하지 않는다. 이는 자바스크립트 엔진이 매개변수의 개수와 인수의 개수를 체크하지 않기 때문이다.

 인수가 전달되지 않은 매개변수의 값은 undefine다. 따라서 매개변수에 인수를 전달되었는지 확인하여 인수가 전달되지 않은 경우 매개변수에 기본값을 할당할 필요가 있다.

 ES6에서 도입된 매개변수 기본값을 사용하면 함수 내에게 수행하던 인수 체크 및 초기화를 간소화 할 수 있다.

function sum(x=0, y=0) {
	return x + y;
}

console.log(sum(1)); // 1

 앞서 살펴본 Rest 파라미터에는 기본값을 지정할 수 없다.

function foo(...rest = []) {
	console.log(rest);
}
// SyntaxError: Rest parameter may not have a default initalizer

 

매개변수 기본값은 매개변수 개수를 나타내는 함수 객체의 length 프로퍼티와 arguments 객체에 아무런 영향을 주지 않는다.

'Javascript' 카테고리의 다른 글

Deep-dive .35 : 스프레드 문법  (0) 2024.07.31
Deep-dive .34 : 이터러블  (0) 2024.07.30
Deep-dive .25 : 클래스  (0) 2024.07.09
Deep-dive .24 : 클로저  (0) 2024.07.09
Deep-dive .23 : 실행 컨텍스트  (2) 2024.07.02
본 글은 Typescript Programming을 요약한 글입니다.
자세한 내요은 본 책을 읽으시길 바랍니다.

 

 12장에서는 타입스크립트 응용 프로그램을 빌드하고 제품화하는 방법을 살펴본다.

  • 타입스크립트 응용 프로그램을 빌드하는 데 필요한 준비물
  • 서버에서 타입스크립트 응용 프로그램을 빌드하고 실행하기
  • 브라우저에서 타입스크립트 응용 프로그램을 빌드하고 실행하기
  • 타입스크립트를 빌드하고 NPM으로 발행하기

12.1 타입스크립트 프로젝트 빌드하기

 12.1.1 프로젝트 레이아웃

이 책에서는 타입스크립트 소스 코드를 최상위의 src/ 디렉터리에 저장하고, 컴파일한 결과 역시 최상위 dist/ 디렉터리에 저장할 것을 권장한다. 소스 코드와 생성된 코드를 두 개의 최상위 디렉터리에 분리할 수 있어서 다른 도구들과 통합하기가 편해진다. 또한 빌드 과정에서 만들어지는 부산물을 버전 관리 대상에서 제외하기 쉽다는 장점도 있다.

12.1.2 부산물

타입 파일 확장자 tsconfig.json 플래그 기본적으로 생성?
자바스크립트 .js {"emitDeclarationOnly": false} O
소스맵 .js.msp {"sourceMap": true} X
타입 선언 .d.ts {"declaration": true} X
선언 맵 .d.ts.map {"declarationMap": true} X
  1. 자바스크립트 파일 : TSC는 타입스크립트를 자바스크립트로 변환한다.
  2. 소스맵 : 생성된 자바스크립트 코드를 원래 타입스크립트 파일의 행과 열로 연결하는 데 필요한 특별 파일이다. 디버깅에 도움을 주고 자바스크립트 예외의 스택 추적값에서 가리키는 행과 열을 타입스크립트 파일에 매핑해준다.
  3. 타입선언 : 생성된 타입을 다른 타입스크립트 프로젝트에서 이용할 수 있도록 해준다.
  4. 선언 맵 : 타입스크립트 프로젝의 컴파일 시간을 단축하는 데 사용된다.

12.1.3 컴파일 대상 조정

자바스크립트는 매년 규격 명세가 새롭게 릴리스되면서 빠르게 진화할 뿐만 아니라 프로그래머 입장에서는 자신이 구현한 프로그램이 어떤 자바스크립트 번전을 지원하는 플랫폼에서 실행될지 보장할 수 없다. 더 나아가 많은 자바스크립트 프로그램은 단일 형태, 서버와 클라이언트 모두에서 실행할 수 있다.

  • 백엔드 자바스크립트 프로그램을 자신이 제어할 수 있는 서버에서 실행할때는 정확한 어떤 버전으로 실행할지 결정할 수 있다.
  • 백엔드 자바스크립트 프로그램을 오픈 소스로 릴리스한다면 소비자의 플랫폼에서 어떤 버전으로 실행할지 알 수 없다.
  • 자바스크립트를 브라우저에서 실행할 때는 사람들이 어떤 브라우저를 사용할지 알 수 없다.
  • 단일 구조의 자바스크립트 라이브러리를 릴리스한다면 가능한 한 낮은 버전의 Nodejs와 자바스크립트 엔진 및 버전을 동시에 지원해야한다.

모든 환경과 버전을 지원하는 것은 아니지만 코드는 가능하면 최신 버전으로 작성하는 것이 좋다. 이렇게 해도 구 버전 플랫폼에서 동작하게 할 수 있는데 두 가지 방법이 존재한다.

  1. 트랜스파일 : 최신 버전의 자바스크립트를 대상 플랫폼에서 지원하는 가장 낮은 자바스크립트 버전으로 변환한다.
  2. 폴리필 : 실행하려는 자바스크립트 런타임이 포함하지 않는 최신 기능을 폴리필로 제공한다.

TSC는 트랜스파일하는 기능을 기본으로 지원하지만 폴리필은 자동으로 해주지 않는다. TSC에서 대상 환경에 관한 정보를 설정하는 옵션은 세 가지다.

  • Target : 트랜스파일하려는 자바스크립트 버전을 설정한다.
  • module : 대상 모듈 시스템을 설정한다.
  • lib : 타입스크립트 대상 환경에서 어떤 자바스크립트 기능을 지원하는지 알려준다.

응용 프로그램을 실행할 환경의 자바스크립트 버전을 target에 설정하고, 어떤 기능을 쓸지는 lib에 설정한다. module 값은 대상 환경이 NodeJS냐 브라우저냐에 따라 달라지며, 브라우저 환경에서는 어떤 모듈 로더를 쓰는지를 고려해 정해야 한다.

 

target

TSC의 내장 트랜스파일러는 대부분의 자바스크립트 기능을 예전 자바스크립트 버전으로 변환할 수 있다. 트랜스파일 대상을 설정하려면 tsconfig.json 파일을 열어서 target 필드에 원하는 값을 채워 넣는다.

 

lib

 트랜스파일할 때 한 가지 유의사항이 있다. 대상 환경에서 새로운 기능을 지원하지 않으면 폴리필로 직접 제공해야 한다는 사실이다. 이런 폴리필을 우리가 직접 구현할 필요는 없다. core-js 같은 폴리필 라이브러리에서 필요한 기능을 설치하거나, @babel/polyfill을 설치한 . 후 바벨을 이용해 컴파일하면 타입스크립트가 타입을 확인하면서 필요한 폴리필을 자동으로 설치해준다.

 폴리필을 추가했으면 tsconfig.json의 lib 필드를 수정해서 해당 기능이 반드시 지원됨을 TSC에 알리도록 한다. 브라우저에서 실행할 때는 window, document 등 자바스크립트를 브라우저에서 실행할 때 필요한 API들을 사용할 수 있도록 DOM 타입 선언도 활성화해야 한다.

{
	"compilerOptions": {
    	"lib": [
        	"es2015",
            "es2016.array.includes",
            "dom"
        ]
    }
}

12.1.4 소스 맵 활성화

소스 맵은 트랜스파일된 코드를 원본 코드와 이어주는 정보를 제공한다. 대부분의 개발 도구, 에러 리포팅/로깅 프레임워크, 빌드 도구는 소스 맵의 존재를 이미 알고 있다. 보통 빌드 파이프라인은 처음 코드와는 상당히 다른 형태의 코드들을 중간중간 생성하기 때문에 파이프라인 곳곳에서 소스 맵을 활용하여 최종 자바스크립트의 디버깅을 휠씬 수월하게 처리해준다.

 개발 환경에서는 물론, 실제 제품에서도 브라우저와 서버 환경에 소스 맵을 함께 배포하면 좋다. 단점이 있다면 하나, 보안이 요구되는 상황이라면 고객용 브라우저 환경에는 소스 맵을 포함시키지 않는 편이 좋다.

12.1.5 프로젝트 참조

 응용 프로그램의 크기가 커지면 TSC가 타입을 확인하고 코드를 컴파일하는 데 더 오래 걸린다. 이 문제의 해법으로, TSC는 점진적 컴파일과 함께 프로젝트 참조라는 기능을 제공하여 컴파일 시간을 획기적으로 줄인다. 프로젝트 참조는 다음처럼 사용한다.

 1. 타입스크립트 프로젝트를 여러 프로젝트로 분리한다. 함께 수정될 가능성이 큰 코드들을 같은 디렉터리에 저장하는 방식으로 쪼갠다.

 2. 각 프로젝트 디렉터리에 최소한 다음 정보를 포함하는 tsconfig.json을 만든다.

{
	"compilerOptions": {
    	"composite": true,
        "declearation": true,
        "declarationMap": true,
        "rootDir": "."
    },
    "include": [
    	"./**/*.ts"
    ],
    "references": [
    	{
        	"path": "../myReferencedProject",
            "prepend": true
        }
    ],
}
  • composite: TSC에 이 디렉터리는 큰 타입스크립트 프로젝트의 서브 프로젝트임을 알려준다.
  • declaration: TSC에 이 프로젝트의 .d.ts 선언 파일을 생성하라고 지시한다. 각각의 프로젝트는 다른 프로젝트의 선언 파일과 생성된 자바스크립트 파일들에는 접근할 수 있지만, 원본 타입스크립트 파일에는 접근하지 못한다. 이렇게 하여 TSC가 타입을 다시 검사하거나 다시 컴파일해야하는 코드를 선택하는 기준이 만들어진다. 프로젝트 참조가 큰 프로젝트의 빌드 효율을 높여주는 핵심 원리다.
  • declarationMap: TSC에 생성된 타입 선언의 소스 맵을 빌드하라고 지시한다.
  • reference: 이 서브 프로젝트가 의존하는 다른 서브 프로젝트들의 목록이다.
    • 각 참조의 path는 tsconfig.json 파일이 담긴 디렉터리를 가리키거나, TSC 설정파일을 직접 가리켜야 한다.
    • prepend는 참조하는 서브 프로젝트에서 생성한 자바스크립트와 소스 맵을 이 서브 프로젝트에서 생성한 소스와 맵에 이어 붙인다.
  • rootDir: 이 서브 프로젝트가 루트 프로젝트(.)에 상대적으로 컴파일되어야 함을 명시한다.

 3. 아직 다른 서브 프로젝트에서 참조하지 않은 모든 서브 프로젝트를 참조하는 루트 tsconfig.json을 만든다.

{
	"files": [],
    "references": [
    	{
        	"path": "./myProject"
        },
        {
        	"path": "./mySecondProject"
        }
    ]
}

 

 4. 프로젝트를 컴파일할 때 프로젝트 참조를 활용하도록 bulid 플래그를 지정한다.

tsc --bulid # 또는 줄여서 tsc -b

12.1.6 에러 모니터링

타입스크립트는 컴파일 타임의 에러만 경고하므로 사용자가 런타임에 겪을 수 있는 에러를 컴파일 타임에 방지할 수 방법을 찾아야한다. Sentry,Bugsnag 같은 에러 모니터링 도구를 이용하면 런타임 예외를 보고하고 분석하는 데 도움이 된다.

12.2 서버에서 타입스크립트 실행

타입스크립트 코드를 NodeJS 환경에서 실행하려면 tsconfig.json의 module 플래그를 commonjs로 설정하고, 코드를 ES2015 자바스크립트로 컴파일한다. 그러면 ES2015의 import를 require로, export를 module.exports로 변환하여 NodeJS에서 추가 번들 없이 실행할 수 있도록 컴파일해준다. 소스맵도 NodeJS 프로세스에 제공해야 한다.

12.3 브라우저에서 타입스크립트 실행

브라우저에서 타입스크립트를 실행하려면 서버에서 보다 해야할 과정이 많다.

 

 1. 컴파일 하려는 모듈을 선택한다. 라이브러리를 발행할 때는 umd를 사용해 다양한 모듈 번들러와의 호환성을 극대화해야한다. 발행 계획이 없다면 사용하는 모듈 번들러에 맞는 포맷으로 컴파일하면 되니 해당 번들러의 문서를 확인해 본다.

 

 2. 모든 타입스크립트 파일으 한 개의 자바스크립트 파일이나 자바스크립트 파일 집합으로 컴파일하도록 빌드 파이프라인을 설정한다. TSC는 빌드 플러그인이나 웹팩이 제공하는 정도로 영리한 코드 분할을 제공하지 않으므로 자체 기능으로는 부족하다 사실을 느낄 것이다. 그렇기에 처음부터 강력한 빌드 도구를 사용하느 것이 바람직하다.

  • 빌드 도구가 프로젝트 의존성 그래프를 더 정확하게 분석할 수 있도록, 코드를 모듈로 유지하고 코드에서 임의의 의존성을 피한다.
  • 동적 임포트를 이용하여 게으르게 로딩하면 첫 페이지 렌더링 속도를 높일 수 있다.
  • 자동 코드 분할 기능을 사용하면 페이지 로딩이 불필요하게 느려지지 않는다.
  • 페이지 로딩 시간을 측정하는 수단을 마련한다.
  • 제품 빌드를 가능한한 개발 빌드와 같은 형태로 유지한다.
  • 빠진 브라우저 기능을 폴리필로 제공하는 대책을 마련한다.
    • 모든 번들에 폴리필로 제공하는 표준 집합을 마련하거나
    • 브라우저가 지원하는 기능이 무엇이냐에 따라 동적으로 필요한 폴리필을 마련할 수 있다.

12.4 타입스크립트 코드를 NPM으로 발행하기

타입스크립트를 다른 누군가가 사용하도록 컴파일 할때는 다음 규칙들을 따르는 것이 좋다.

  • 자신의 코드를 쉽게 디버깅할 수 있도록 소스 맵을 생성한다.
  • 다른 사람이 우리의 코드르 쉽게 빌드하고 실행할 수 있도록 ES5로 컴파일한다.
  • 어떤 모듈 타입으로 컴파일할지 주의 깊게 결정한다.
  • 다른 타입스크립트 사용자가 우리 코드의 타입을 얻을 수 있도록 타입 선언을 생성한다.

다음과 같은 과정을 거친다.

  1. 먼저 tsc로 타입스크립트를 자바스크립트로 컴파일하고 대응하는 타입선언을 생성한다.
  2. 이어서 NPM에 발행하지 않을 타입스크립트 코드 목록을 .npmignore 파일에 기재하여 패키지가 너무 커지지 않도록한다. 그리고 .gitignore 파일에는 부산물을 제외하게끔 설정해서 깃 저장소가 불필요한 파일로 오염되는 일을 방지한다.
  3. 마지막으로 프로젝트의 package.json에 "types" 필드를 추가해서 타입 선언이 제공될 것임을 알려준다. 스크립트도 추가해서 패키지의 자바스크립트, 타입선언, 소스 맵이 항상 원본 타입스크립트와 같은 최신 버전이 되도록 만든다.

12.5 세 슬래시 지시어

이 지시어는 특별한 포맷의 타입스크립트 주석으로, TSC에 명령을 하달한다. types는 타입 전용 전체 모듈 임포트를 생략할 때 사용하고, amd-module은 생성된 AMD 모듈의 이름을 정할 때 사용한다.

12.5.1 types 지시어

 모듈에서 무언가를 임포트한 코드를 자바스크립트로 컴파일 할 때 타입스크립트가 항상 import나 require 호출을 생성하는 것은 아니다. export한 대상들이 모듈에서 오직 타입 위치에서만 쓰인다면, 타입스크립트는 해당 import 문에 해당하는 자바스크립트 코드를 전혀 생성하지 않는다. 임포트된 대상들이 타입 수준에서만 존재한다고 생각하기 때문이다. 이 기능을 임포트 생략이라고 부른다.

12.5.2 amd-module 지시어

 타입스크립트 코드를 AMD 모듈 포맷으로 컴파일할 때 타입스크립트는 기본값으로 익명 AMD 모듈을 생성한다. 이때 코드에서 amd-module 세 슬래시 지시어를 추가해주면 AMD 모듈에 이름을 지어줄 수 있다.

// <amd-module name="LogService" /> 1
export let LogService = { // 2
	log() {
    	// ...
    }
}
  1. amd-module 지시어에 name 속성을 설정했다.
  2. 나머지 코드는 그대로이다.

TSC로 AMD 모듈 포맷으로 컴파일하면 다음의 자바스크립트 코드가 생성된다.

// <amd-module name="LogService" /> 1
definde('LogeService', ['require','exports'], function(require, exports){ // LogeService라는 이름이 생겼다
	// .... 
})

AMD로 모듈로 컴파일할 떄는 코드를 번들링하거나 디버깅하기 쉽도록 amd-module 지시어를 사용하자.

본 글은 Modern JavaScript Deep-dive을 요약한 글입니다.
자세한 내용은 본 책을 읽으시기 바랍니다.

 

25.1 클래스는 프로토타입의 문법적 설탕인가?

 자바스크립트는 프로토타입 기반 객체지향 언어이다. 이는 클래스가 필요 없는 객체지향 프로그래밍 언어이다. ES5에서는 클래스 없이도  생성자 함수와 프로토타입을 통해 객체지향 언어의 상속을 구현할 수 있다.

 ES6에서 도입된 클래스는 기존 프로토타입 기반 객체지향 프로그래밍보다 자바나 C#과 같은 클래스 기반 객체지향 프로그래밍에 익숙한 프로그래머가 더욱 빠르게 학습할 수 있도록 클래스 기반 객체지향 프로그래밍 언어와 매우 흡사한 새로운 객체 생성 메커니즘을 제시한다.

 단, 클래스와 생성자 함수는 모두 프로토타입 기반의 인스턴스를 생성하지만 정확히 동일하게 동작하지는 않는다. 클래스는 생성자 함수보다 엄격하며 생성자 함수에서는 제공하지 않는 기능도 제공한다. 다음과 같이 몇 가지 차이가 있다.

  1. 클래스를 new 연산자 없이 호출하면 에러가 발생한다. 하지만 생성자 함수를 new 연산자 없이 호출하면 일반 함수로서 호출된다.
  2. 클래스는 상속을 지원하는 extends와 super 키워드를 제공한다. 하지만 생성자 함수는 지원하지 않는다.
  3. 클래스는 호이스팅이 발생하지 않는 것처럼 동작한다. 하지만 함수 선언문으로 정의된 생성자 함수는 함수 호이스팅이, 함수 표현식으로 정의된 생성자 함수는 변수 호이스팅이 발생한다.
  4. 클래스 내의 모든 코드에는 암묵적으로 strict mode가 지정되어 실행되며 해제할 수 없다. 하지만 생성자 함수는 mode가 암묵적으로 지정되지는 않는다.
  5. 클래스의 constructor, 프로토타입 메서드, 정적 메서드는 모두 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 false다. 열거되지 않는다.

클래스는 생성자 함수 기반의 객체 생성 방식보다 견고하고 명료하다. 또한 키워드는 통한 상속 관계 구현을 더욱 간결하고 명료하게 한다. 따라서 클래스를 프로토타입 기반 객체 생성 패턴의 단순한 문법적 설탕이라고 보기보다는 새로운 객체 생성 메커니즘으로 보는 것이 좀 더 합당하다.

25.2 클래스 정의

 클래스는 class 키워드를 사용하여 정의한다. 파스칼 케이스를 사용하는 것이 일반적이다. 함수와 마찬가지로 표현식으로 클래스를 정의할 수도 있다. 이름을 가질 수도 있고, 갖지 않을 수도 있다.

// 익명 클래스 표현식 
const Person = class {};

// 기명 클래스 표현식
const Person = class MyClass {};

 클래스를 표현식으로 정의할 수 있다는 것은 익급객체라는 것을 의미한다. 일급 객체로서 다음과 같은 특징을 갖는다.

  1. 무명의 리터럴로 생성할 수 있다. 즉 런타임에 생성이 가능하다.
  2. 변수나 자료구조(객체나 배열)에 저장할 수 있다.
  3. 함수의 매개변수에게 전달할 수 있다.
  4. 함수의 반환값으로 사용할 수 있다

25.3 클래스 호이스팅

클래스는 함수로 평가된다. 클래스 선언문으로 정의한 클래스는 함수 선언문과 같이 소스코드 평가 과정, 즉 런타임 이전에 먼저 평가되어 함수 객체를 생성한다. 이때 클래스가 평가되어 생성된 함수 객체는 생성자 함수로서 호출할 수 있는 함수, 즉 constructor다. 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다.

 클래스 선언문은 마치 호이스팅이 발생하는 것처럼 보이나 그렇지 않다.

const Person = '';

{
	// 호이스팅이 발생하지 않는다면 ''이 출력
    console.log(Person)
    // ReferenceError: Cannot access 'Person' before initalization
    
    // 클래스 선언문
    class Person {}
}

 클래스 선언문도 호이스팅이 발생한다. 단, 클래스는 let, const 키워드로 선언한 변수처럼 호이스팅된다. 따라서  클래스 선언문 이전에 일시적 사각지대에 빠지기 때문에 호이스팅이 발생하지 않는 것처럼 동작한다.

25.4 인스턴스 생성

 클래스는 생성자 함수이며 new 연산자와 함께 호출되어 인스턴스를 생성한다. 클래스는 인스턴스를 생성하는 것이 유일한 존재이므로 반드시 new 연산자와 함께 호출해야 한다.

class Person {}

const me = new Person();

const you = Person();
// TypeError: Class ...

 클래스 표현식으로 정의된 클래스의 경우 다음 예제와 같이 클래스를 가리키는 식별자를 사용해 인스턴스를 생성하지 않고 기명 클래스 표현식의 클래스 이름을 사용해 인스턴스를 생성하면 에러가 발생한다. 클래스 이름은 외부 코드에서 접근 불가능하기 때문이다.

cosnt Person = class MyClass{};

const you = new MyClass(); // ReferenceError: ...

25.5 메서드

클래스 몸체에는 0개 이상의 메서드만 선언할 수 있다. 이는 constructor, 프로토타입 메서드, 정적 메서드 세가지의 종류가 존재한다.

25.5.1 constructor

 constructor는 인스턴를 생성하고 초기화하기 위한 특수 메서드다. constructor는 이름을 변경할 수 없다.

class Person{
	// 생성자
    constructor(name){
    	// 인스턴스 생성 및 초기화
        this.name = name;
    }
}

 클래스는 인스턴를 생성하기 위한 생성자 함수다. 

 클래스도 함수 객체 고유의 프로퍼티를 갖고 있다. 함수와 동일하게 프로토타입과 연결되어 있으며 자신의 스코프체인을 구성한다. 모든 함수 객체가 가지고 있는 prototype 프로퍼티가 가리키는 프로토타입 객체의 constructor 프로퍼티는 클래스 자신을 가리키고 있다. 즉, new 연산자와 함께 클래스를 호출하면 클래스는 인스턴스를 생성한다.

생성자 함수와 마찬가지로 constructor 내부에서 this에 추가한 프로퍼티는 인스턴스 프로퍼티가 된다. constructor 내부의 this는 생성자 함수와 마찬가지로 클래스가 생성한 인스턴스를 가리킨다. 그런데 클래스가 평가되어 생성된 함수 객체나 클래스가 생성한 인스턴스 어디에도 constructor 메서드가 보이지 않는다. 이는 constructor가 단순한 메서드가 아니라는 것을 의미한다. 

 constructor는 메서드로 해석되는 것이 아니라 클래스 정의가 평가되면 constructor의 기술된 동작을 하는 함수 객체가 생성된다. 즉 평가되어 생성함 함수 객체 코드의 일부가 된다.

 

constructor는 생성자 함수와 유사하지만 몇 가지 차이가 있다.

  1. constructor는 클래스 내에서 최대 한 개만 존재할 수 있다.
  2. constructor를 생략하면 클래스에 빈 constructor가 암묵적으로 정의된다.

프로퍼티가 추가되어 초기화된 인스턴스를 생성하려면 constructor 내부에서 this에 인스턴스 프로퍼티를 추가한다.

class Person{
	constructor(){
    // 고정값으로 인스턴스 초기화
    this.name = 'Lee';
    this.address = 'Seoul';
    }
}

 

인스턴스를 생성할 때 클래스 외부에서 인스턴스 프로퍼티의 초기값을 전달하려면 다음과 같이 constructor에 매개변수를 선언하고 인스턴스를 생성할 때 초기값을 전달한다.

class Person{
	constructor(name, address){
    // 인수로 인스턴스 초기화
    this.name = name;
    this.address = address;
    }
}

const me = new Person('Lee', 'Seoul')

 이처럼 constructor 내에서는 인스턴스의 생성과 동시에 프로퍼티 추가를 통해 인스턴스의 초기화를 실행한다. 따라서 인스턴스를 초기화하라면 constructor를 생략해서는 안된다. 

 

constructor는 별도의 반환문을 갖지 않아야한다. new 연산자와 함께 클래스가 호출되면 암묵적으로 this, 즉 인스턴스를 반환하기 때문이다. 만약 다른 객체를 명시적으로 반환하면 return 문에 명시한 객체가 반환된다. 하지만 명시적으로 원시값을 반환하면 원시값 반환은 무시되고 암묵적으로 this가 반환된다.

class Person{
	constructor(name){
    this.name = name;
    
    // 명시적으로 원시값을 반환하면 무시되고 암묵적으로 this가 반환된다.
    return 100;
    }
}

const me = new Person('Lee')
console.log(me) // Person {name : 'Lee'}

이처럼 constructor 내부에서 명시적으로 this가 아닌 다른 값을 반환하는 것은 클래스의 기본 동작을 훼손한다.

25.5.2 프로토타입 메서드

클래스 몸체에서 정의한 메서드는 생성자 함수에 의한 객체 생성 방식과는 다르게 클래스의 prototype 프로퍼티에 메서드를 추가하지 않아도 기본적으로 프로토타입 메서드가 된다.

class Person{
	constructor(name){
    	this.name = name;
    }
    
    sayHi(){
    	console.log(`Hi! My name is ${this.name}`);
    }
}

const me = new Person('Lee');
me.sayHi();

 생성자 함수와 마찬가지로 클래스가 생성한 인스턴스는 프로토타입 체인의 일원이 된다.

// me 객체의 프로토타입은 Person.prototype이다
Object.getPrototypeOf(me) === Person.prototype; // true
me instanceof Person // true

// Person.prototype의 프로토타입은 Object.prototype이다
Object.getPrototypeOf(Person.prototype) === Object.prototype; // true
me instanceof Object // true

// me 객체의 constructor는 Person 클래스다.
me.constructor === Person // true

이처럼 클래스 몸체에서 정의한 메서드는 인스턴스의 프로토타입에 존재하는 프로토타입 메서드가 된다. 인스턴스는 프로토타입 메서드를 상속받아 사용할 수 있다. 프로토타입 체인은 기존의 모든 객체 생성 방식뿐만 아니라 클래스에 의해 생성된 인스턴스에도 동일하게 적용된다.

25.5.3 메서드

클래스에서는 메서드에 static 키워드를 붙이면 정적 메서드가 된다.

class Person{
	constructor(name){
    	this.name = name
    }
    
    static sayHi() {
    	console.log('Hi!')
    }
}

이처럼 정적 메서드는 클래스에 바인딩된 메서드가 된다. 클래스는 클래스 정의가 평가되는 시점에 함수 객체가 되므로 인스턴스와 달리 별다른 생성 과정이 필요없다. 따라서 정적 메서드는 클래스 정의 이후 인스턴스를 생성하지 않아도 호출할 수 있다. 
 또한 정적 메서드는 인스턴스로 호출할 수 없다. 정적 메서드가 바인딩된 클래스는 인스턴스의 프로토타입 체인 상에 존재하지 않기 때문이다.

// 정적 메서드는 클래스로 호출한다. 인스턴스 없이 가능하다.
Person.sayHi();

// 인스턴스에서 호출할 수 없다
const me = new Person('Lee');
me.sayHi() // TypeError: ...

25.5.4 정적 메서드와 프로토타입 메서드의 차이

 정적 메서드와 프로토타입 메서드의 차이는 다음과 같다.

  1. 정적 메서드와 프로토타입 메서드는 자신이 속해 있는 프로토타입 체인이 다르다.
  2. 정적 메서드는 클래스로 호출하고 프로토타입 메서드는 인스턴스로 호출한다.
  3. 정적 메서드는 인스턴스 프로퍼티를 참조할 수 없지만 프로토타입 메서드는 인스턴스 프로퍼티를 참조할 수 없다.
class Square{
	// 정적 메서드
    static area(width, height){
    	return width * height
    }
}

console.log(Sqaure.area(10,10)); // 100

class Square2{
	constructor(width, height){
    	this.width = width;
        this.height = height;
    }
	// 프로토타입 메서드
   	area(){
    	return this.width * this.height
    }
}

const square = new Square2(10,10);
console.log(square.area()); // 100

 프로토타입 메서드는 인스턴스로 호출해야 하므로 프로토타입 메서드 내부의 this는 프로토타입 메서드를 호출한 인스턴스를 가리킨다. 정적 메서드는 클래스로 호출해야 하므로 정적 메서드 내부의 this는 인스턴스가 아닌 클래스를 가리킨다. 즉, 프로토타입 메서드와 정적 메서드 내부의 this 바인딩이 다르다.

 따라서 메서드 내부에서 인스턴스 프로퍼티를 참조할 필요가 있다면 this를 사용해야 하며, 이러한 경우 프로토타입 메서드로 정의해야한다.

Math.max(1,2,3) // 3
Number.isNaN(NaN) // true
JSON.stringify({a : 1}) // "{"a": 1}"
Object.is({},{}); // false
Reflect.has({a: 1}, 'a') // true

표준 빌트인 객체인 Math, Number, JSON, Object, Reflect 등은 다양한 정적 메서드를 가지고 있다. 이들 정적 메서드는 애플리케이션 전역에서 사용할 유틸리티 함수다.이처럼 클래스 또는 생성자 함수를 하나의 네임스페이스로 사용하여 정적 메서드를 모아 놓으면 이름 충돌 가능성을 줄여주고 관련 함수들을 구조화할 수 있는 효과가 있다. 이처럼 정적 메서드는 애플리케이션 전역에서 사용할 유틸리티 함수를 전역 함수로 정의하지 않고 메서드로 구조화할 때 유용하다.

25.5.5 클래스에서 정의한 메서드의 특징

  1. function 키워드를 생략한 메서드 축향 표현을 사용한다.
  2. 객체 리터럴과는 다르게 클래스에 메서드를 정의할 때는 콤마가 필요가 없다.
  3. 암묵적으로 strict mode로 실행된다.
  4. for...in 문이나 Object.keys 메서드 등으로 열겨할 수 없다. 즉, 프로퍼티의 열거 가능 여부를 나타내며, 불리언 값을 갖는 프로퍼티 어트리뷰트 [[Enumberable]]의 값이 false다.
  5. 내부 메서드 [[Constructor]]를 갖지 않는 non-constructor다. 따라서  new 연산자와 함꼐 호출할 수 없다.

25.6 클래스의 인스턴스 생성 과정

 new 연산자와 함께 클래스를 호출하면 생성자 함수와 마찬가지로 클래스의 내부 메서드 [[Construct]]가 호출된다. 클래스는 new 연산자 없이 호출할 수 없다. 생성자 함수의 인스턴스 과정과 비슷하게 인스턴스가 생성된다.

1. 인스턴스 생성과 this 바인딩

 new 연산자와 함게 클래스를 호출하면 constructor의 내부 코드가 실행되기에 앞서 암묵적으로 빈객체가 생성된다. 이 빈 객체가 바로 클래스가 생성한 인스턴스다. 생성한 인스턴스의 프로토타입으로 클래스의 prototype 프로퍼티가 가리키는 객체가 설정된다. 암묵적으로 생성된 빈 객체, 즉 인스턴스는 this에 바인딩된다.

2. 인스턴스 초기화

 constructor의 내부 코드가 실행되어 this에 바인딩되어 있는 인스턴스를 초기화한다. 즉, this에 바인딩되어 있는 인스턴스에 프로퍼티를 추가하고 constructor가 인수로 전달받은 초기값으로 인스턴스의 프로퍼티 값을 초기화한다. constructor가 생략되면 이 과정도 생략된다.

3. 인스턴스 반환

 클래스의 모든 처리가 끝나면 완성된 인스턴스에 바인딩된 this가 암묵적으로 반환된다.

25.7 프로퍼티

25.7.1 인스턴스 프로퍼티

인스턴스 프로퍼티는 constructor 내부에서 정의해야한다. constructor 내부에서 this에 인스턴스 프로퍼티를 추가한다. 이로써 클래스가 암묵적으로 생성한 빈 객체, 즉 인스턴스에 프로퍼티가 추가되어 인스턴스가 초기화된다.

class Person{
	constructor(name) {
    	// 인스턴스 프로퍼티
        this.name = name; 
    }
}

const me = new Person('Lee');

console.log(me.name); // Lee

 constructor 내부에서 this에 추가한 프로퍼티는 언제나 클래스가 생성한 인스턴스의 프로퍼티가 된다. 접근 제한자를 지원하지 않기에 인스턴스 프로퍼티는 언제나 public하다.

25.7.2 접근자 프로퍼티

 접근자 프로퍼티는 자체적으로 값 ([[value]] 내부슬롯)을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티다.

class Person{
	constructor(firstName, lastNmae){
    	this.firstName = firstName;
        this.lastName = lastName;
    }
    
    // 접근자 프로퍼티
    // getter
    get fullName(){
    	return `${this.firstName} ${this.lastName}`
    }
    
    // setter
    set fullName(name){
    	[this.firstName, this.lastName] = name.split(' ')
    }
}

const me = new Person('Ungmo', 'Lee');

// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
me.fullNmae = 'Heegun Lee'
console.log(me); // {firstName: "Heegun", lastName: "Lee"}

// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
console.log(me.fullName); // Heegun Lee

// fullName은 접근자 프로퍼티다.
// 접근자 프로퍼티는 get, set, enumerable, configurable 프로퍼티 어트리뷰트를 갖는다.
console.log(Object.getOwnPropertyDescriptor(Person.prototype, 'fullName'))

 접근자 함수는 getter 함수와 setter 함수로 구성되어 있다. getter는 인스턴스 프로퍼티에 접근할 때마다 프로퍼티 값을 조작하거나 별도의 행위가 필요할 때 사용한다. 메서드 앞에 get 키워드를 사용해 정의한다. setter는 인스턴스 프로퍼티에 값을 할당할 때마다 프로퍼티 값을 조작하거나 별도의 행위가 필요할 때 사용한다. 메서드 앞에 set 키워드를 사용해 정의한다.

 getter와 setter 이름은 인스턴스 프로퍼티처럼 사용된다. getter를 호출하는 것이 아닌 프로퍼티처럼 참조하는 형식으로 사용하며, 참조 시에 내부적으로 getter가 호출된다. setter도 프로퍼티 처럼 값을 할당하는 형식으로 동일하다

 getter는 이름 그대로 무언가를 취득할 떄 사용하므로 반드시 무언가를 반환해야하고 setter는 무언가를 프로퍼티에 할당해야 할 때 사용하므로 반드시 매개변수가 있어야 한다. 단 하나의 값만 할당받기에 단 하나의 매개변수만을 지닐 수 있다.

 기본적으로 클래스의 접근자 프로퍼티는 프르토타입의 프로퍼티가 된다.

25.7.3 클래스 필드 정의 제안

클래스 필드는 클래스 기반 객체지향 언어에서 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어다.

  1. 자바스크립트의 클래스에서 인스턴스 프로퍼티를 선언하고 초기화하려면 반드시 constructor 내부에서 this에 프로퍼티를 추가해야 한다.
  2. 또한 인스턴스 프로퍼티를 참조하려면 반드시 this를 사용하여 참조해야 한다.
  3. 클래스 기반 객체지향 언어의 this는 언제나 클래스가 생성할 인스턴스를 가리킨다.
  4. 자바스크립트의 클래스 몸체에는 메서드만 선언할 수 있다. 그러나 클래스 필드 정의의 표준 사양 제안이 승급되는 것이 확실하기에 최신 node와 브라우저에는 이를 구현해놓았다.

클래스 몸체에서 클래스 필드를 정의하는 경우 this에 클래스 필드를 바인딩해서는 안된다.

class Person {
	this.name = ''; // SyntaxError ...
}

 

클래스 필드를 참조하는 경우 자바스크립트에서는 this를 반드시 사용해야한다.

class Person {
	// 클래스 필드
    name = 'Lee'
    
    constructor() {
    	console.log(name) // ReferenceError ...
    }
}

 

클래스 필드에 초기값을 할당하지 않으면 undefined를 갖는다. 인스턴스를 생성할 때 외부의 초기값으로 클래스 필드를 초기화해야 할 필요가 있다면 constructor에서 클래스 필드를 초기화해야 한다. 이처럼 인스턴스를 생성할 때 클래스 필드를 초기화할 필요가 있다면 constructor 밖에서 클래스 필드를 정의할 필요가 없다. 이때 this, 즉 클래스가 생성한 인스턴스에 클래스 필드에 해당하는 프로퍼티가 없다면 자동으로 추가되기 때문이다.

class Person {
    // 클래스 필드 초기화
    constructor(name) {
    	this.name = name;
    }
}

 

함수는 일급 객체이므로 함수를 클래스 필드에 할당할 수 있다. 따라서 클래스 필드를 통해 메서드를 정의할 수도 있다.

class Person {
	// 클래스 필드
    name = 'Lee'
    
    // 클래스 필드에 메서드 할당
    getName = function () {
    	return this.name
    }
    // 화살표 함수로 정의 가능
    // getName = () => this.name;
}

 

 이처럼 클래스 필드에 함수를 할당하는 경우, 인스턴스 메서드가 된다. 모든 클래스 필드는 인스턴스 프로퍼티가 되기 때문이다. 그렇기에 이를 권장하지 않는다.

25.5.4 private 필드 정의 제안

 자바스크립트는 캡슐화를 완전하게 지원하지 않는다. 접근 제한지를 지원하지 않기에 인스턴스 프로퍼티는 인스턴스를 통해 클래스 외부에서 언제나 참조할 수 있다. 즉, 언제나 public이다. 다행히도 private 필드를 정의할 수 있는 새로운 표준 사양이 제안되어 승급이 확실시 되는 상황이라 최신 브라우저와 node에 이미 구현되어 있다.

 private 필드의 선두에는 #을 붙여준다. 참조할 때도 #을 붙여주어야 한다. private 필드는 반드시 클래스 몸체에 정의해야한다. constructor에 정의하면 에러가 발생한다.

class Person {
	// private 필드 정의
    #name = '';
	
    constructor(name){
    	// private 필드 참조
        this.#name = name;
    }
}

 

public 필드는 어디서든 참조할 수 있지만 private 필드는 클래스 내부에서만 참조할 수 있다.

접근 가능성 public private
클래스 내부 O O
자식 클래스 내부 O X
클래스 인스턴스를 통한 접근  O X

이처럼 클래스 외부에서 private 필드에 직접 접근할 수 있는 방법은 없다. 다만 접근자 프로퍼티를 통해 간접적으로 접근하는 방법이 유효하다. 

25.7.5 static 필드 정의 제안

static을 이용한 정적 필드 (static public, staitc private 필드, staitc private 메서드) 역시 새로운 표준 사양으로 제안되어 최신 브라우저와 node에 이미 구현되어 있다.

class MyMath{
	// static public 필드 정의
    static PI = 22 / 7 ;
    
    // static private 필드 정의
    static #num = 10;
    
    // static 메서드
    static increment(){
    	return ++MyMath.#num;
    }
}

25.8 상속에 의한 클래스 확장

25.8.1 클래스 상속과 생성자 함수 상속

 프로토타입 체인을 통해 다른 객체의 자산을 상속받는 프로토타입 기반 상속과 달리 상속에 의한 클래스 확장은 기존 클래스를 상속받아 클래스를 확장하여 정의하는 것이다. 

 

클래스와 생성자 함수는 인스턴스를 생성할 수 있는 함수라는 점에서 매우 유사하다. 하지만 클래스는 상속을 통해 기존 클래스를 확장할 수 있는 문법이 기본적으로 제공되지만 생성자 함수는 그렇지 않다. bird 클래스와 lion 클래스는 상속을 통해 Animal 클래스의 속성을 그대로 사용하고 자신만의 고유한 속성을 추가하여 확장했다. 이처럼 상속에 의한 클래스 확장은 코드 재사용 관점에서 매우 유용하다.

class Animal {
	construcor(age, weight){
    	this.age = age;
        this.weight = weight;
    }
    
    eat() {return 'eat';}
    
    move() {return 'move';}
}

// 상속을 통해 Animal 클래스를 확장한 Bird 클래스

class Bird extends Animal{
	fly() { return 'fly';}
}

 

클래스는 상속을 통해 다른 클래슬르 확장할 수 있는 문법인 extends 키워드가 기본적으로 제공된다. 하지만 생성자 함수는 클래스와 같이 상속을 통해 다른 생성자 함수를 확장할 수 있는 문법이 제공되지 않는다.

25.8.2 extends 키워드

 상속을 통해 클래스를 확장하려면 extends 키워드를 사용하여 상속받을 클래스를 정의한다.

  • 상속을 통해 확장된 클래스를 서브 클래스라 부르고,
  • 서브 클래스에게 상속된 클래스를 수퍼클래스라 부른다. 

 extends 키워드의 역할은 수퍼 클래스와 서브 클래스 간의 상속 관계를 설정하는 것이다. 클래스도 프로토타입을 통해 상속 관계를 구현한다. 수퍼 클래스와 서브 클래스는 클래스 간의 프로토타입 체인도 생성한다. 이를 통해 프로토타입 메서드, 정적 메서드 모두 상속 가능하다.

25.8.3 동적 상속

 extends 키워드는 클래스뿐만 아니라 생성자 함수를 상속받아 클래스를 확장할 수도 있다. 단, 키워드 앞에는 클래스가 와야한다. extends 키워드 다음에는 클래스 뿐만아니라 [[Constructor]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있다. 이를 통해 동적으로 상속받을 대상을 결정할 수 있다.

function Base1() {}

class Base2 {}

let condition = true;

// 조건에 따라 동적으로 상속 대상을 결정하는 서브 클래스
class Derived extends (condition ? Base1 : Base2) {}

25.8.4 서브클래스의 constructor

 클래스에 constructor를 생략하면 비어있는 constructor가 암묵적으로 정의된다.

 서브클래스에서 constructor를 생략하면 클래스에 다음과 같은 constructor가 암묵적으로 정의된다. args는 new 연산자와 함께 클래스를 호출 할 때 전달한 인수의 리스트다. super()는 수퍼클래스의 constructor를 호출하여 인스턴스를 생성한다.

constructor(...args){super(...args); }

 

수퍼 클래스와 서브 클래스 모두 constructor 생략하면 다음과 같이 암묵적으로 빈 객체가 생성된다. 프로퍼티를 소유하는 인스턴스를 생성하려면 constructor 내부에서 인스턴에 프로퍼티를 추가해야 한다.

// 수퍼 클래스
class Base {
	constructor(){}
}

// 서브 클래스
class Derived extends Base{
	constructor(...args){super(...args);}
}

25.8.5 super 키워드

super 키워드는 함수처럼 호출할 수도 있고 this와 같이 식별자처럼 참조할 수 있는 특수한 키워드다.

  • super를 호출하면 수퍼클래스의 constructor를 호출한다.
  • super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.

super 호출

 super를 호출하면 수퍼클래스의 constructor를 호출한다. 수퍼 클래스의 constructor 내부에서 추가한 프로퍼티를 그대로 갖는 인스턴스를 생성한다면 서브클래스의 constructor를 생략할 수 있다. 이때 new 연산자와 함께 서브클래스를 호출하면서 전달한 인수는 모두 서브클래스에 암묵적으로 정의된 constructor의 super 호출을 통해 수퍼클래스의 constructor에 전달된다. 

 

 서브클래스에서 추가한 프로퍼티를 갖는 인스턴스를 생성한다면 서브클래스의 constructor를 생략할 수 없다. 이때 new 연산자와 함꼐 서브클래스를 호출하면서 전달한 인수 중에서 수퍼클래스의 constructor에 전달할 필요가 있는 인수는 서브클래스의 constructor에서 호출하는 super를 통해 전달한다.

class Base{
	constructor(a,b){
    this.a = a;
    this.b = b;
    }
}

class Derived extends Base {
	constructor(a,b,c){
    super(a,b)
    this.c =c;
    }
}

const derived = new Derived(1,2,3)
console.log(derived; // Derived {a: 1 , b: 2, c: 3}

이처럼 인스턴스 초기화를 위해 전달한 인수는 수퍼클래스와 서브클래스에 배분되고 상속 관계의 두 클래스는 서로 협력하여 인스턴스를 생성한다.

 

super를 호출할 때 주의 상황은 다음과 같다.

  1. 서브클래스에서 constructor를 생략하지 않은 경우 서브클래스의 constructor에서는 반드시 super를 호출해야 한다.
  2. 서브클래스의 constructor에서 super를 호출하기 전에 this를 참조할 수 없다.
  3. super는 반드시 서브클래스의 constructor에서만 호출된다.

super 참조

 매서드 내에서 super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.

 

 1. 서브클래스의 프로토타입 메서드 내에서 super.sayHi는 수퍼클래스의 프로토타입 메서드 sayHi를 가리킨다.

class Base{
	constructor(name){
    	this.name = name;
    }
    
    sayHi() {
    	return `Hi! ${this.name}`;
    }
}

class Derived extends Base {
	sayHi() {
    	return `${super.sayHi()}, How are you doing?`;
    }
}

const derived = new Derived('Lee')
console.log(derived.sayHi()); // Derived {a: 1 , b: 2, c: 3}

 super 참조를 통해 수퍼클래스의 메서드를 참조하려면 super가 수퍼클래스의 메서드가 바인딩된 객체, 즉 수퍼클래스의 protortpe 프로퍼티에 바인딩된 프로토타입을 참조할 수 있어야 한다.

class Base{
	constructor(name){
    	this.name = name;
    }
    
    sayHi() {
    	return `Hi! ${this.name}`;
    }
}

class Derived extends Base {
	sayHi() {
    	const __super = Object.getPrototypeOf(Derived.prototype)
    	return `${__super.sayHi.call(this)}, How are you doing?`;
    }
}

const derived = new Derived('Lee')
console.log(derived.sayHi()); // Derived {a: 1 , b: 2, c: 3}

 super는 자심을 참조하고 있는 메서드가 바인딩되어 있는 객체의 프로토타입을 가리킨다. 따라서 super.sayhi는 Base.prototype.sayHi를 가리킨다. 단 호출할 때 call 메서드를 사용해 this를 전달해야 한다.

 

 call 메서드를 사용해 this를 전달하지 않고 그대로 호출하면 Base.prototype.sayHi 메서드 내부의 this는 Base.prototype을 가리킨다. Base.prototype.sayHi 메서드는 프로토타입 메서드이기 때문에 내부의 this는 Base.prototype가 아닌 인스턴스를 가리켜야 한다. name 프로퍼티는 인스턴스에 존재하기 때문이다.

 이처럼 super 참조가 동작하기 위해서는 super를 참조하고 있는 메서드가 바인딩되어 있는 객체의 프로토타입을 찾을 수 있어야한다. 이를 위해 메서드는 내부슬롯 [[HomeObject]]를 가지며, 자시늘 바인딩하고 있는 객체를 가리킨다. 

주의할 것은 ES6의 메서드 축약 표현으로 정의된 함수만이 [[HomeObject]]를 갖는다는 것이다. 이를 가지는 함수만이 super 참조를 할 수 있다. 단 수퍼클래스의 메서드를 참조하기 위해 사용하므로 서브클래스의 메서드에서 사용해야한다. 또한 클래스 뿐만 아니라 객체 리터럴에서도 사용할수 있다.

 

2. 서브클래스의 정적 메서드 내에서 super.sayHi는 수퍼클래스의 정적 메서드 sayHi를 가리킨다.

25.8.6 상속 클래스의 인스턴스 생성 과정

1. 서브클래스의 super 호출

자바스크립트 엔진은 클래스를 평가할 때 수퍼클래스와 서브클래스를 구분하기 위해 "base"또는 "derived"를 값으로 갖는 내부 슬롯 [[ConstructorKind]]를 갖는다. 이를 통해 수퍼클래스와 서브클래스는 new 연산자와 함께 호출되었을 때 동작이 구분된다.

 다른 클래스를 상속받지 않는 클래스는 new 연산자와 함께 호출되었을 때 암묵적으로 빈 객체, 인스턴스를 생성하고 이를 this에 바인딩한다. 하지만 서브클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임한다. 이것이 바로 서브클래스의 constructor에서 반드시 supe를 호출해야 하는 이유다.

서브클래스가 new 연산자와 함께 호출되면 서브클래스 constructor 내부의 super 키워드가 함수처럼 호출된다. 만약 서브클래스 constructor 내부에 super 호출이 없으면 에러가 바생한다. 실제로 인스터스를 생성하는 주체는 수퍼클래스이므로 수퍼클래스의 constructor를 호출하는 super가 호출되지 않으면 인스턴스를 생성할 수 없기 때문이다.

 

2. 수퍼클래스의 인스턴스 생성과 this 바인딩

 수퍼클래스의 constructor 내부의 코드가 실행되기 이전에 암묵적으로 빈 객체를 생성한다. 이 빈 객체가 바로 클래스가 생성한 인스턴스다. 암묵적으로 생성된 빈 객체, 즉 인스턴스는 this에 바인딩된다. 이때 인스턴스는 수퍼클래스가 생성하는 것이다. 하지만 new 연산자와 함께 호출된 클래스가 서브클래스라는 것이 중요하다. 인스턴스는 new.target이 가리키는 서브클래스가 생성한 것으로 처리된다.

 따라서 생성된 인스턴스의 프로토타입은 new target, 즉 서브클래스의 prototype 프로퍼티가 가리키는 객체다.

class Rectangle {
	constructor(width, height) {
    
    	// 암묵적으로 빈객체가 생성되고 this가 바인딩된다.
        console.log(this) // ColorRectangle {}
        
        // new 연산자와 함께 호출된 함수, new.target은 ColorRectangle이다.
        console.log(new.target) //ColorRectangle
        
        // 생성된 인스턴스의 프로토타입으로 ColorRectangle.prototype이 설정된다.
        console.log(Object.getPrototypeOf(this) === ColorRectangle.prototype); // true
    
    ...
}

 

3. 수퍼클래스의 인스턴스 초기화

this에 바인딩되어 있는 인스턴스에 프로퍼티를 추가하고 constructor가 인수로 전달받은 초기값으로 인스턴스의 프로퍼티를 초기화한다.

 

4. 서브클래스 constructor로의 복귀와 this 바인딩

이때 super가 반환한 인스턴스가 this에 바인딩된다. 서브클래스는 별도의 인스턴스를 생성하지 않고 super가 반환한 인스턴스를 this에 바인딩하여 그대로 사용한다. 이처럼 super가 호출되지 않으면 인스턴스가 생성되지 않으며, this 바인딩도 할 수 없다. 서브클래스의 constructor에서 super를 호출하기 전에는 this를 참조할 수 없는 이유가 바로 이 때문이다.

 

5. 서브클래스의 인스턴스 초기화

super 호출 이후, 서브클래스의 constructor에 기술되어 있는 인스턴스 초기화가 실행된다.

 

6. 인스턴스 반환

클래스의 모든 처리가 끝나면 완성된 바인딩된 this가 암묵적으로 반환된다.

class ColorRectangle extends Rectangle {
	constructor(width, height, color) {
    	super(width, height);
		
        // super가 반환한 인스턴스가 this에 바인딩된다.
        console.log(this); // ColorRectangle
        
        // 인스턴스 초기화
        this.color = color;
        
        // 완성된 인스턴스가 바인딩된 this에 암묵적으로 반환된다.
        console.log(this); // ColorRectangle {width: 2, height: 4, color: "red"}
     }
     
     ...
}

25.8.7 표준 빌트인 생성자 함수 확장

extends 키워드 다음에는 클래스뿐만 아니라 [[Constructor]] 내부 메서드를 갖느 함수 객체로 평가될 수 있는 모든 표현식에 사용할 수 있다. String, Number, Array 같은 표준 빌트인 객체 역시 이러한 내부 메서드를 갖는 함수 이므로 extends 키웓드를 사용하여 확장할 수 있다.

이때 주의할 것은 Array.prototype의 메서드 중에서 map, filter와 같이 새로운 배열을 반환하는 메서드가 MyArray 클래스의 인스턴스를 반환한다는 것이다. 만약 새로운 배열을 반환하는 메서드가 MyArray 클래스의 인스턴스를 반환하지 않고 Array의 인스턴스를 반환하면 MyArray 클래스의 메서드와 메서드 체이닝이 불가능하다.

// 메서드 체이닝
// [1,2,3] => [1,1,3] => [1,3] => 2
console.log(myArray.filter(v => v % 2).uniq().average());

myArray.filter가 반환하는 인스턴스는 MyArray 클래스가 생성한 인스턴스, 즉 MyArray 타입이다. 따라서 myArray.filter가 반환하는 인스턴스로 uniq 메서드를 연이어 호출할 수 있다.

'Javascript' 카테고리의 다른 글

Deep-dive .34 : 이터러블  (0) 2024.07.30
Deep-dive .26 : ES6 함수의 추가기능  (1) 2024.07.24
Deep-dive .24 : 클로저  (0) 2024.07.09
Deep-dive .23 : 실행 컨텍스트  (2) 2024.07.02
Deep-dive .22 : this  (0) 2024.07.02

+ Recent posts