Event

카테고리 JavaScript

이벤트 드리븐 프로그래밍

브라우저는 처리해야할 특정 사건이 발생하면 이를 감지하여 **이벤트(event)를 발생(trigger)**시킨다. 사용자가 어떤 행동을 하였을 때 브라우저가 이를 감지하여 함수를 호출하고 싶을 때 이벤트 드리븐 프로그래밍 방식을 사용한다.

즉, 이벤트란? 브라우저가 특정 사건이 발생한 것을 감지하고 함수를 호출하기 위해 필요한 것이다.

즉, 함수를 언제 호출할지 알 수 없으므로 개발자가 명시적으로 함수를 호출하는 것이 아니라 브라우저에게 함수 호출을 위임하는 것이다.

  • 이벤트 발생시 호출될 함수 ⇒ 이벤트 핸들러
  • 이벤트 발생시 브라우저에게 이벤트 핸들러의 호출을 위임 ⇒ 이벤트 핸들러 등록

Window, Document, HTMLElement 타입의 객체는 onclick과 같이 특정 이벤트에 대응하는 다양한 이벤트 핸들러 프로퍼티를 가지고 있다. 이 이벤트 핸들러 프로퍼티에 함수를 할당하면 해당 이벤트가 발생했을 때, 할당한 함수가 브라우저에 의해 호출된다.

이와 같이 프로그램의 흐름을 이벤트 중심으로 제어하는 프로그래밍 방식을 이벤트 드리븐 프로그래밍이라고 한다.

이벤트 타입

이벤트 타입은 이벤트의 종류를 나타내는 문자열이다. 대표적으로 마우스 이벤트(click, mouseup, mousemove, mouseover…), 키보드 이벤트(keydown, keypress…), 포커스 이벤트(focus, focusin…) 등 다양한 이벤트가 있다.

  • 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식으로 등록할 수 있다.

이벤트 핸들러 등록 방식

이벤트 핸들러 프로퍼티 방식

이벤트 핸들러 프로퍼티에 함수를 바인딩하면 이벤트 핸들러가 등록된다.

  • 이벤트 핸들러는 대부분 이벤트를 발생시킬 이벤트 타깃에 바인딩한다. 하지만 반드시 이벤트 타깃에 이벤트 핸들러를 바인딩해야하는 것은 아니다.
  • 이벤트 핸들러 프로퍼티에 하나의 이벤트 핸들러만 바인딩할 수 있다는 단점이 있다.

addEventListener 메서드 방식

EventTarget.prototype.addEventListener 메서드를 사용하여 이벤트 핸들러를 등록할 수 있다.

  • 이벤트 타입을 전달시 on 접두사를 붙이지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');

// 이벤트 핸들러 프로퍼티 방식
$button.onclick = function () {
console.log('button click');
};

// addEventListener 메서드 방식
$button.addEventListener('click', function () {
console.log('button click');
});
</script>
</body>
</html>
  • 이벤트 핸들러 프로퍼티 방식과 달리 addEventListener 메서드에는 이벤트 핸들러를 인수로 전달한다.
  • 위 예제대로 두가지 방식을 같이 사용하여도 서로에게 영향을 주지 않으므로 클릭 이벤트 발생 시 2개의 이벤트 핸들러가 모두 호출된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');

// addEventListener 메서드는 동일한 요소에서 발생한 동일한 이벤트에 대해
// 하나 이상의 이벤트 핸들러를 등록할 수 있다.
$button.addEventListener('click', function () {
console.log('[1]button click');
});

$button.addEventListener('click', function () {
console.log('[2]button click');
});
</script>
</body>
</html>
  • 동일한 HTML 요소에서 발생한 동일한 이벤트에 대해 이벤트 핸들러 프로퍼티 방식은 하나 이상의 이벤트 핸들러를 등록할 수 없지만, addEventListener 메서드는 하나 이상의 이벤트 핸들러를 등록할 수 있다.

이벤트 핸들러 제거

addEventListener 메서드로 등록된 이벤트를 제거하기 위해 removeEventListener 메서드를 사용한다.

  • 단, addEventListener 메서드에게 전달한 인수와 removeEventListener 메서드에게 전달한 인수가 같아야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<body>
<button>Click me!</button>
<script>
const $button = document.querySelector('button');

const handleClick = () => console.log('button click');

// 이벤트 핸들러 등록
$button.addEventListener('click', handleClick);

// 이벤트 핸들러 제거
// addEventListener 메서드에 전달한 인수와 removeEventListener 메서드에
// 전달한 인수가 일치하지 않으면 이벤트 핸들러가 제거되지 않는다.
$button.removeEventListener('click', handleClick, true); // 실패
$button.removeEventListener('click', handleClick); // 성공
</script>
</body>
</html>
  • 이벤트 핸들러로 전달한 등록 이벤트 핸들러가 동일해야 하므로 무명 함수를 이벤트 핸들러로 등록한 경우 제거할 수 없다. 이 경우 이벤트 핸들러를 제거하기 위해서는 이벤트 핸들러 참조를 변수나 자료구조에 저장하고 있어야 한다.

이벤트 객체

이벤트 발생 시 이벤트에 관련한 다양한 정보를 담고 있는 이벤트 객체가 동적으로 생성된다. 생성된 이벤트 객체는 이벤트 핸들러의 첫번째 인수로 전달된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<body>
<p>클릭하세요. 클릭한 곳의 좌표가 표시됩니다.</p>
<em class="message"></em>
<script>
const $msg = document.querySelector('.message');

// 클릭 이벤트에 의해 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달된다.
function showCoords(e) {
$msg.textContent = `clientX: ${e.clientX}, clientY: ${e.clientY}`;
}

document.onclick = showCoords;
</script>
</body>
</html>
  • 이벤트 객체는 이벤트 핸들러의 첫번째 인수로 전달되어 매개변수 e에 암묵적으로 할당된다. 이는 브라우저가 이벤트 핸들러 호출할 때, 이벤트 객체를 인수로 전달하기 때문이다.

이벤트 타입에 따라 생성되는 이벤트 객체의 고유한 프로퍼티가 달라진다.

이벤트 객체의 공통 프로퍼티

공통 프로퍼티 설명 타입
type 이벤트 타입 string
target 이벤트를 발생시킨 DOM 요소 DOM 요소 노드
currentTarget 이벤트 핸들러가 바인딩된 DOM 요소 DOM 요소 노드
eventPhase 이벤트 전파 단계 (0: 이벤트없음, 1: 캡처링 단계, 2: 타깃 단계, 3: 버블링 단계) number
bubbles false면 버블링하지 않는다. boolean
cancelable preventDefault 메서드 호출하여 이벤트 기본 동작 취소 가능한지 여부 false면 취소할 수 없다. boolean
defaultPrevented preventDefault 메서드 호출하여 이벤트 취소했는지 여부 boolean
isTrusted 사용자 행위에 의해 발생한 이벤트인지 여부, click메서드, dispatchEvent 메서드를 통해 인위적으로 발생시킨 이벤트인 경우 false boolean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<body>
<input type="checkbox">
<em class="message">off</em>
<script>
const $checkbox = document.querySelector('input[type=checkbox]');
const $msg = document.querySelector('.message');

// change 이벤트가 발생하면 Event 타입의 이벤트 객체가 생성된다.
$checkbox.onchange = e => {
console.log(Object.getPrototypeOf(e) === Event.prototype); // true

// e.target은 change 이벤트를 발생시킨 DOM 요소 $checkbox를 가리키고
// e.target.checked는 체크박스 요소의 현재 체크 상태를 나타낸다.
$msg.textContent = e.target.checked ? 'on' : 'off';
};
</script>
</body>
</html>
  • 사용자 입력에 의해 checked 프로퍼티 값 변경되면 change 이벤트 발생하고 Event 타입의 이벤트 객체가 생성된다.
  • 이벤트 객체의 target 프로퍼티는 이벤트를 발생시킨 객체를 나타낸다. 즉, change 이벤트를 발생시킨 DOM 요소인 $checkbox 이다.
  • 이벤트 객체의 currentTarget 프로퍼티는 이벤트 핸들러가 바인딩된 DOM 요소를 가리킨다.
  • 일반적으로 이벤트 객체의 target 프로퍼티와 currentTarget 프로퍼티는 동일한 객체를 가리키지만 나중에 이벤트 위임에서는 서로 다른 객체를 가리킬 수 도 있다.

이벤트 전파

DOM 트리 상에 존재하는 DOM 요소 노드에서 발생한 이벤트는 DOM 트리를 통해 전파된다.

  • 생성된 이벤트 객체는 이벤트를 발생시킨 DOM 요소인 이벤트 타깃을 중심으로 DOM 트리를 통해 전파된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<body>
<ul id="fruits">
<li id="apple">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
<script>
const $fruits = document.getElementById('fruits');

// #fruits 요소의 하위 요소인 li 요소를 클릭한 경우
$fruits.addEventListener('click', e => {
console.log(`이벤트 단계: ${e.eventPhase}`); // 3: 버블링 단계
console.log(`이벤트 타깃: ${e.target}`); // [object HTMLLIElement]
console.log(`커런트 타깃: ${e.currentTarget}`); // [object HTMLUListElement]
});
</script>
</body>
</html>
  • 위 예제는 ul 요소에 이벤트 핸들러 바인딩하고 ul 요소의 하위 요소인 li 요소를 클릭하여 이벤트를 발생 시켜보자. event.target은 li 요소이고, event.currentTarget은 ul 요소이다.

과정

  1. li 요소 클릭시 클릭 이벤트 발생하여 클릭 이벤트 객체를 생성한다. 클릭된 li 요소가 이벤트 타깃이 된다. 이 때 이벤트 객체는 window에서 시작해서 이벤트 타깃 방향으로 전파된다. (캡처링 단계)
  2. 이벤트 객체는 이벤트를 발생시킨 이벤트 타깃에 도달한다. (타깃 단계)
  3. 이벤트 객체는 이벤트 타깃에서 시작해서 window 방향으로 전파된다. (버블링 단계)
  • 이벤트 핸들러 어트리뷰트/프로퍼티 방식으로 등록한 이벤트 핸들러는 타깃단계와 버블링 단계의 이벤트만 캐치할 수 있지만, addEventListener 메소드로 등록한 이벤트 핸들러는 타깃단계, 버블링단계, 캡처링 단계의 이벤트도 선별적으로 캐치할 수 있다. 캡처링 단계의 이벤트 캐치하려면 addEventListener 메소드의 3번째 인수로 true를 전달해야한다.

즉, 이벤트는 이벤트를 발생시킨 이벤트 타깃을 물론 상위 DOM 요소에서도 캐치할 수 있다.

다음 이벤트들은 버블링을 통해 전파되지 않는다. 왜냐하면 event.bubbles 값이 false

  • focus/blur
  • load/unload/abort/error
  • mouseenter/mouseleave

이러한 이벤트를 사용하여 캡처링으로 이벤트를 캐치하는 것보다 대안의 이벤트들을 사용하여 버블링을 통해 캐치하는 것이 더 합리적이다.

이벤트 위임

이벤트 위임은 여러 개의 하위 DOM 요소에 각각 이벤트 핸들러를 등록하는 대신 하나의 상위 DOM 요소에 이벤트 핸들러를 등록하는 방법을 말한다. 이벤트 위임을 통해 상위 DOM 요소에 이벤트 핸들러 등록하면 여러 개의 하위 DOM 요소에 이벤트 핸들러를 등록할 필요가 없다. 또한 동적으로 하위 DOM 요소 추가하더라도 일일이 추가된 DOM 요소에 이벤트 핸들러를 등록할 필요가 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE html>
<html>
<head>
<style>
#fruits {
display: flex;
list-style-type: none;
padding: 0;
}

#fruits li {
width: 100px;
cursor: pointer;
}

#fruits .active {
color: red;
text-decoration: underline;
}
</style>
</head>
<body>
<nav>
<ul id="fruits">
<li id="apple" class="active">Apple</li>
<li id="banana">Banana</li>
<li id="orange">Orange</li>
</ul>
</nav>
<div>선택된 내비게이션 아이템: <em class="msg">apple</em></div>
<script>
const $fruits = document.getElementById('fruits');
const $msg = document.querySelector('.msg');

// 사용자 클릭에 의해 선택된 내비게이션 아이템(li 요소)에 active 클래스를 추가하고
// 그 외의 모든 내비게이션 아이템의 active 클래스를 제거한다.
function activate({ target }) {
// 이벤트를 발생시킨 요소(target)가 ul#fruits의 자식 요소가 아니라면 무시한다.
if (!target.matches('#fruits > li')) return;

[...$fruits.children].forEach($fruit => {
$fruit.classList.toggle('active', $fruit === target);
$msg.textContent = target.id;
});
}

// 이벤트 위임: 상위 요소(ul#fruits)는 하위 요소의 이벤트를 캐치할 수 있다.
$fruits.onclick = activate;
</script>
</body>
</html>
  • 이벤트 객체의 currentTarget 프로퍼티는 언제나 $fruits 요소를 가리키지만 이벤트 객체의 target 프로퍼티는 실제로 이벤트를 발생시킨 DOM 요소를 가리킨다.

만약 $fruits 요소의 하위 요소에서 클릭 이벤트가 발생했다면 이벤트 객체의 currentTarget 프로퍼티와 target 프로퍼티는 다른 DOM 요소를 가리킨다.

댓글 공유

DOM

렌더링 엔진에 의해 HTML 문서를 브라우저가 이해할 수 있는 자료구조인 DOM을 생성한다. DOM이란, HTML문서의 계층적인 구조와 정보를 표현하며 이를 제어할 수 있는 API, 즉 프로퍼티와 메서드를 제공하는 트리 자료구조이다.

노드

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

노드 객체들로 구성된 트리 자료구조를 DOM이라한다.

노드도 자바스크립트 객체이므로 프로토타입에 의한 상속 구조를 갖는다.

주의사항

상속관계를 아는 것보다 어떤 DOM API를 사용하여 동적으로 변경하고 조작할 수 있는지를 알아야한다.

querySelector, querySelectorAll 메서드가 다소 느리긴 하더라도 CSS 선택자로 요소 노드 취득시 구체적인 조건과 일관된 방식으로 요소 노드 취득할 수 있으므로 id 어트리뷰트가 있는 요소는 getElementById 메서드를 사용하고 그 외의 경우에는 querySelector, querySelectorAll 메서드를 사용하자.

HTMLCollection과 NodeList

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

  • HTMLCollection과 NodeList는 노드 객체의 상태 변화를 실시간으로 반영하는 살아있는 객체이다.
  • HTMLCollection은 언제나 live 객체로 동작한다.
  • NodeList는 대부분의 경우 상태 변화를 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-live 객체로 동작하지만 경우에 따라 live 객체로 동작한다.

결론

HTMLCollection, NodeList 객체를 사용하지 말아라. 유사 배열 객체이면서 이터러블인 NodeList, HTMLCollection 객체를 배열로 변환하면 부작용을 제거할 수 있다. 유용한 배열 고차함수 forEach, map, filter 등을 사용할 수 있다.

DOM 조작

DOM 조작은 새로운 노드를 생성하여 DOM에 추가하거나 기존 노드 삭제 또는 교체 하는 것을 말한다. 이 경우 리플로우와 리페인트가 발생한다.

innerHTML

시작 태그와 종료 태그 사이의 모든 마크업을 문자열로 반환한다. HTML 마크업도 포함된 문자열을 반환하는 것이 textContent 프로퍼티와 차이점이다.

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<body>
<div id="foo">Hello <span>world!</span></div>
</body>
<script>
// #foo 요소의 콘텐츠 영역 내의 HTML 마크업을 문자열로 취득한다.
console.log(document.getElementById('foo').innerHTML);
// "Hello <span>world!</span>"
</script>
</html>
  • 요소 노드의 innerHTML 프로퍼티에 문자열 할당하면 요소 노드의 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가된다. 이 때 HTML 마크업이 포함되어 있으면 파싱되어 요소 노드의 자식 노드로 DOM에 반영된다.

복수의 노드 생성과 추가 ⭐️

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<body>
<ul id="fruits"></ul>
</body>
<script>
const $fruits = document.getElementById('fruits');

['Apple', 'Banana', 'Orange'].forEach(text => {
// 1. 요소 노드 생성
const $li = document.createElement('li');

// 2. 텍스트 노드 생성
const textNode = document.createTextNode(text);

// 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);

// 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($li);
});
</script>
</html>
  • 위 예제는 3개의 요소 노드가 생성하여 DOM에 3번 추가하여 DOM이 3번 변경된다. 이러한 방법은 리플로우를 많이 발생 시키므로 피해야한다.

⇒ 컨테이너 요소를 미리 생성한 후 DOM에 추가할 3개 요소 노드를 컨테이너 요소의 자식 노드로 추가한 뒤 컨테이너 요소를 #fruits 요소에 자식으로 추가한다. 하지만 이 또한, 불필요한 컨테이너 요소(div)가 DOM에 추가되어 바람직 하지 않다.

어트리뷰트

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

HTML 문서의 요소는 여러 개의 어트리뷰트를 가질 수 있다. ex) class, checked, aria-label…

  • 모든 HTML 요소에 공통적인 것부터 해당 요소만 사용할 수 있는 어트리뷰트가 있다.
  • HTML 요소의 어트리뷰트는 어트리뷰트 노드로 변환되어 요소 노드와 연결된다. 이는 NamedNodeMap 객체에 담겨 요소 노드의 attributes 프로퍼티에 저장된다.
  • attributes 프로퍼티는 getter만 존재하는 접근자 프로퍼티이다.

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

  • HTML 어트리뷰트는 초기값 (변하지 않는다.)
  • DOM 프로퍼티는 HTML 프로퍼티를 초기값으로 가지고 변경될 수 있다.

첫렌더링 까지 어트리뷰트 노드의 어트리뷰트 값과 요소 노드의 value 프로퍼티에 할당된 값은 HTML 어트리뷰트 값과 동일하다. 하지만 첫 렌더링 이후 사용자가 input 요소에 무언가 입력시 바뀌게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="ungmo2">
<script>
const $input = document.getElementById('user');

// attributes 프로퍼티에 저장된 value 어트리뷰트 값
console.log($input.getAttribute('value')); // ungmo2

// 요소 노드의 value 프로퍼티에 저장된 value 어트리뷰트 값
console.log($input.value); // ungmo2
</script>
</body>
</html>
  • input 요소 노드는 상태를 가지고 있고 사용자의 입력에 의한 변경된 최신 상태를 관리해야 할뿐더러 HTML 어트리뷰트로 지정한 초기 상태도 관리해야한다.

즉, 요소 노드의 초기 상태는 어트리뷰트 노드가 관리하며 요소 노드의 최신 상태는 DOM 프로퍼티가 관리한다.

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

대부분은 HTML 어트리뷰트는 HTML 어트리뷰트 이름과 동일한 DOM 프로퍼티와 1:1로 대응한다. 반드시는 아니다.

  • id 어트리뷰트와 id 프로퍼티는 1:1 대응, 동일한 값으로 연동한다.
  • input 요소의 value 어트리뷰트는 value 프로퍼티와 1:1 대응, value 어트리뷰트는 초기상태를, value 프로퍼티는 최신 상태를 갖는다.
  • class 어트리뷰트에 대응하는 DOM 프로퍼티는 className, classList 프로퍼티이다.

DOM 프로퍼티 값의 타입

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

ex) checkbox요소의 checked 어트리뷰트 값은 문자열 이지만 checked 프로퍼티 값은 불리언이다.

댓글 공유

브라우저란?

브라우저는 HTML, CSS, Javascript로 작성된 텍스트 문서를 서버에게 요청하여 응답을 받아 의미있는 단위인 토큰으로 파싱하여 시각적으로 렌더링 해주는 역할을 담당한다.

브라우저 렌더링 과정

1. 요청과 응답

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

이렇게 요청을 보내면 서버는 서버의 루트 폴더에 존재하는 정적파일로 응답을 보낸다. 기본적으로 index.html

2. HTML 파싱과 DOM 생성

브라우저 요청에 의해 서버가 응답한 HTML 문서는 문자열로 이루어진 순수한 텍스트이다. 의미없는 문자열 데이터를 브라우저가 이해할 수 있는 자료구조(객체)로 변환하여 메모리에 저장해야한다.

그래서 HTML 문서를 파싱하여 브라우저가 이해할 수 있는 자료구조인 DOM을 생성한다.

  1. 서버는 요청의 응답하기 위해 요청한 HTML 파일을 읽어 메모리에 저장한 뒤 메모리에 저장된 바이트(2진수)를 인터넷을 경유하여 응답한다.
  2. 브라우저는 이를 받아 meta 태그의 charset 방식에 따라(UTF-8) 문자열로 변환한다.
  3. 문자열로 변환된 HTML문서를 토큰화한다.
  4. 각 토큰을 객체로 변환하여 노드를 생성한다. 노드는 DOM을 구성하는 기본 요소이다. ex) 문서 노드, 요소 노드 등
  5. HTML 문서는 중첩관계를 통해 부자관계가 형성된다. 이러한 부자관계를 반영하여 모든 노드들을 트리 자료구조로 구성한다. 이러한 노드들로 구성된 트리 자료구조를 DOM이라 부른다.

3. CSS 파싱과 CSSOM 생성

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

그 결과 CSS 파일을 서버에 요청하여 응답받은 CSS 파일이나 style 태그 내의 CSS를 HTML과 동일한 과정으로 토큰화 생성 → CSSOM 생성 과정을 거친다. 이후 파싱이 완료되면 HTML 파싱이 중단된 지점부터 다시 HTML을 파싱하기 시작한다.

4. 렌더 트리 생성

앞선 과정에서 생성된 DOM과 CSSOM은 렌더링을 위해 렌더 트리로 결합된다. 이 때 브라우저 화면에 렌더링되지 않는 노드(meta태그, script 태그 등)와 CSS에 의해 표시되지 않는(display:none) 노드들은 포함하지 않는다.

지금까지의 렌더링 과정은 여러번 반복되서 실행될 수 있다. 렌더링이 반복 실행되는 원인은 다음과 같다.

  • 자바스크립트에 의한 노드 추가 또는 삭제
  • 브라우저 창의 리사이징에 의한 viewport 크기 변경
  • HTML 요소의 레이아웃(위치와 크기)을 변경시키는 width, height, margin, padding, border, display, position 등의 스타일 변경

이러한 리렌더링은 비용이 많이 들고 성능에 악영향을 주므로 리렌더링이 적게 발생하도록 하여야한다.

5. 자바스크립트 파싱과 실행

HTML 파싱의 결과물 DOM은 HTML 문서의 구조와 정보뿐 아니라 HTML 요소와 스타일을 변경할 수 있는 프로그래밍 인터페이스로서 DOM API를 제공한다.

즉, DOM API를 사용하여 이미 생성된 DOM을 동적으로 조작할 수 있다.

CSS 파싱과정과 마찬가지로 script 태그 만나면 DOM 생성을 일시 중단한다.

이후 자바스크립트 파일을 서버에 요청하여 응답받은 파일이나 script 태그내의 코드를 파싱하기 위해 자바스크립트 엔진에 제어권을 넘긴다. (렌더링 엔진 → 자바스크립트 엔진으로 제어권 이동)

이후 자바스크립트 파싱과 실행이 종료되면 렌더링 엔진으로 다시 제어권 넘겨 HTML 파싱 중단된 시점부터 다시 DOM 생성을 재개한다.

자바스크립트 엔진은 자바스크립트 코드를 파싱하기 시작한다. 자바스크립트를 해석하여 AST(추상적 구문 트리)를 생성한다. 그리고 AST를 기반으로 인터프리터가 실행할 수 있는 중간 코드인 바이트 코드를 생성하여 실행한다.

6. 리플로우와 리페인트

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

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

렌더링 엔진과 자바스크립트 엔진은 서로 제어권을 이동시키면서 병렬적으로 파싱하지 않고 직렬적으로 파싱을 수행한다. 브라우저는 이처럼 동기적으로, 즉 순차적으로 HTML, CSS, 자바스크립트를 파싱하고 실행한다.

script 태그를 만나면 제어권이 이동하기 때문에 HTML 문서 내의 script 태그의 위치는 중요한 의미를 갖는다.

대표적인 문제로는 HTML이 생성되기 전에 자바스크립트 코드가 HTML요소를 동적으로 조작하려고 하면 정상적으로 동작하지 않을 수 있다. 이에 대한 해결책으로는 아래와 같다.

  • body 태그 제일 하단에 script 태그(자바스크립트)를 위치 시키는 것

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

앞서 알아본 문제를 근본적으로 해결하기 위해서 HTML5부터 script 태그에 asyncdefer 어트리뷰트가 추가되었다.

두 어트리뷰트는 src 어트리뷰트를 통해 외부의 자바스크립트 파일을 로드하는 경우에만 사용할 수 있다.

1
2
<script async src="extern.js"></script>
<script defer src="extern.js"></script>

이 둘을 사용하면 HTML 파싱과 외부 자바스크립트 파일의 로드가 비동기적으로 진행된다. 단 두 어트리뷰트의 실행 시점의 차이가 있다.

  1. async 어트리뷰트

자바스크립트의 파싱과 실행은 자바스크립트 파일의 로드가 완료된 직후 진행된다. 이 때 HTML 파싱이 중단된다.

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

즉, 순서보장이 필요한 script 태그는 async 어트리뷰트 지정하지 않아야 한다.

  1. defer 어트리뷰트

자바스크립트의 파싱과 실행은 HTML 파싱이 완료된 직후, 즉 DOM 생성이 완료된 직후(DOMContentLoaded 이벤트가 발생한다) 진행된다.

모듈은 기본적으로 defer이다. async는 잘 사용하지 않지만 폰트의 경우 용량도 크고 순서가 크게 상관이 없으니 최대한 빨리 가져오기 위해 async를 사용하기도 한다.

댓글 공유

모듈 다시내보내기

카테고리 JavaScript

📌 모듈 다시 내보내기 기능

import한 모듈을 즉시 다시 내보내기 기능을 구현할 수 있다.

사용하는 이유

진입점인 index.js 파일이 있다고 가정하자. 이 진입점을 내보내서 다른 개발자들이 안의 모듈들을 사용하려고 할 때, 다른 개발자가 모듈의 내부 구조를 건드리게 하면 안된다.

그러기 위해서 공개할 모듈만 index.js 파일에 넣어 내보내기를 하고 나머지는 숨기는 것이 현명하다.

때문에 내보낼 기능들을 분산하여 구현한 뒤 index.js에서 해당 기능들을 가져와서 다시 내보내기 하면 원하는 목적을 이룰 수 있다.

사용방법

1
2
3
4
5
6
7
8
9
10
11
12
13
// index.js
// currency, currencyKR 모듈을 다시 내보냅니다.
export * from "./currency/currency.js";

// fetchBooks 모듈을 다시 내보냅니다.
export * from "./fetchBooks.js";
as
import, export 할 때 as 라는 문법을 사용할 수 있다.

as를 사용하면 이름을 바꿔서 모듈을 가져올 수 있다.

// module.js
import { currencyKR as currency } from "../../utils/index.js";

currencyKR 기능을 currency 라는 이름으로 바꿔서 import 해주었다.

🏓 소감

module에 대해서는 쓸 때마다 헷갈렸었는데, 이렇게 모르는 것이 생길 때마다 공부하여 정리하다보면 익숙해지면서 알게 될 것 같다.

댓글 공유

이터레이션 프로토콜

ES6에서 도입된 이터레이션 프로토콜은 순회 가능한 데이터 컬렉션을 만들기 위한 규약이다.

즉, ES6 이후부터는 순회 가능한 데이터 컬렉션을 이터레이션 프로토콜 규약을 준수하는 이터러블로 통일하였다.

이터러블 프로토콜

Symbol.iterator를 프로퍼티 키로 메서드 직접 구현하거나 프로토타입 체인을 통해 상속받은 Symbol.iterator 메서드를 호출 시 이터레이터 프로토콜을 준수한 이터레이터를 반환한다.

위의 규약을 준수한 객체를 이터러블이라고 한다. 이터러블은 다음이 가능하다.

  • for…of 문 순회
  • spread 문법
  • 배열 디스트럭처링 할당의 대상

일반객체는 Symbol.iterator 메서드를 구현하거나 상속받지 않으므로 이터러블이 아니다.

이터레이터 프로토콜

이터러블의 Symbol.iterator 메서드 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환한다. 이터레이터는 next 메서드를 소유하고 next 메서드 호출 시 이터러블을 순회하며 value와 done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환한다. 이터레이터는 이터러블 요소를 탐색하기 위한 포인터 역할을 한다.

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

이터러블은 for..of문, 스프레드 문법, 배열 디스트럭처링 할당과 같은 데이터 소비자에 의해 사용되므로 데이터 공급자의 역할을 한다고 볼 수 있다.

다양한 데이터 공급자(Array, String, Map 등)가 이터레이션 프로토콜이라는 하나의 규약만 준수하도록 규정하면 데이터 소비자(for..of, 스프레드 문법 등)는 이터레이션 프로토콜만 지원하도록 구현하면 된다.

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

결론

어떤 객체가 Symbol.iterator 메서드를 호출하여 이터레이터 프로토콜을 준수한 이터레이터를 반환한다.

위와 같은 규약을 이터러블 프로토콜이라 하며 이터러블 프로토콜을 준수한 객체를 이터러블이라 한다.

next 메서드를 소유한 이터레이터가 next 메서드 호출하면 이터러블 순회하면서 이터레이터 리절트 객체 반환한다.

위와 같은 규약을 이터레이터 프로토콜이라 하며 이터레이터 프로토콜을 준수한 객체를 이터레이터라고한다.

  • 이터레이터 : next 메서드 소유하여 호출시 이터레이터 리절트 객체 반환
  • 이터러블 : Symbol.iterator 메서드를 호출하여 이터레이터 반환

댓글 공유

ES6 이후 메서드

ES6 이전의 함수는 사용 목적에 따라 일반함수로 호출이 가능했고 생성자 함수로 호출이 가능했다. 즉, ES6 이전의 모든 함수는 callable이면서 constructor다.

1
2
3
[1, 2, 3].map(function (item) {
return item * 2;
}); // [ 2, 4, 6 ]
  • 심지어 콜백함수도 constructor이기 때문에 불필요한 프로토타입 객체를 생성한다.

이처럼 ES6 이전의 모든 함수는 사용 목적에 따라 명확한 구분이 없어 호출방식에 제약이 없고 생성자 함수로 호출되지 않아도 프로토타입 객체를 생성한다. 이는 혼란스러워 실수를 유발하고 성능에도 좋지 않다.

  • ES6 이후부터는 오류에 엄격하도록 만들고 있다. ⇒ 오류를 발생시킬 여지가 있는 것들은 그 부분을 개선해주고 있다.

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

인스턴스 생성할 수 없는 non-constructor이다.

  • 인스턴스 생성 불가
  • prototype 프로퍼티 없다
  • 프로토타입 생성 불가

ES6 메서드는 자신을 바인딩한 객체를 가리키는 내부 슬롯 [[HomeObject]]를 갖는다.

함수 사용 목적에 따라 구분한 ES6에서의 함수

이를 해결하기 위해 ES6에서는 함수를 사용 목적에 따라 3가지로 구분하였다.

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

화살표 함수

function 키워드 함수보다 표현만 간단한 것이 아니라 내부 동작도 간략한다.

주로 콜백 함수 내부에서 this가 전역 객체를 가리키는 문제를 해결하기 위해 사용한다.

화살표 함수 정의

함수 선언문 안되고 함수 표현식으로만 정의 가능하다.

1
2
3
4
5
const multiply = (x, y) => x * y;

const multiply = (x) => x * y; // 매개변수 1개면 소괄호 생략 가능

const multiply = () => x * y; // 매개변수 없으면 소괄호 생략 불가

화살표 함수 몸체 정의

함수 몸체 감싸는 {} 중괄호를 생략한 경우 표현식이 아닌 문일 경우 에러를 발생 시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
// concise body
const power = x => x ** 2;
power(2); // -> 4

// 위 표현은 다음과 동일하다.
// block body
const power = x => { return x ** 2; };

const arrow = () => const x = 1; // SyntaxError: Unexpected token 'const'

// 위 표현은 다음과 같이 해석된다.
const arrow = () => { return const x = 1; };

객체 리터럴을 반환하는 경우 소괄호로 감싸줘야한다.

1
2
3
4
5
6
7
const create = (id, content) => ({ id, content });
create(1, "JavaScript"); // -> {id: 1, content: "JavaScript"}

// 위 표현은 다음과 동일하다.
const create = (id, content) => {
return { id, content };
};

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

  1. 화살표 함수는 non-constructor로, 인스턴스 생성할 수 없다.
  • 인스턴스 생성 불가
  • prototype 프로퍼티 없다
  • 프로토타입 생성 불가
  1. 화살표 함수는 중복된 매개변수 이름 선언시 에러 발생
1
2
const arrow = (a, a) => a + a;
// SyntaxError: Duplicate parameter name not allowed in this context
  1. 화살표 함수는 this, arguments, super, new.target 바인딩 갖지 않는다.

따라서 화살표 함수 내부에서 위의 것들을 참조할 때 스코프 체인상 가장 가까운 상위 함수 중 화살표 함수가 아닌 함수의 this, arguements, super, new.target을 참조한다.

주의사항 : 메서드를 화살표 함수로 정의하지 않기

단, 메서드를 화살표 함수로 정의하면 안된다.

1
2
3
4
5
6
7
8
9
10
// Bad
const person = {
name: "Lee",
sayHi: () => console.log(`Hi ${this.name}`),
};

// sayHi 프로퍼티에 할당된 화살표 함수 내부의 this는 상위 스코프인 전역의 this가 가리키는
// 전역 객체를 가리키므로 이 예제를 브라우저에서 실행하면 this.name은 빈 문자열을 갖는
// window.name과 같다. 전역 객체 window에는 빌트인 프로퍼티 name이 존재한다.
person.sayHi(); // Hi

메서드 정의할 때는 메서드 축약 표현을 사용하는 것이 좋다.

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

person.sayHi(); // Hi Lee
  • 메서드 내부에서 this는 메서드를 호출한 인스턴스에 바인딩된다.
1
2
3
4
5
6
7
8
9
10
// Bad
function Person(name) {
this.name = name;
}

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

const person = new Person("Lee");
// 이 예제를 브라우저에서 실행하면 this.name은 빈 문자열을 갖는 window.name과 같다.
person.sayHi(); // Hi
  • 프로토타입 객체의 프로퍼티에 화살표 함수를 사용하는 것도 문제가 된다.
  • 프로퍼티 동작 추가 시에는 ES6 메서드 정의 사용할 수 없으므로 일반함수로 정의해야한다.

댓글 공유

상속

카테고리 JavaScript

자바스크립트에서 상속을 구현하기 위해 프로토타입을 기반으로 구현한다고 이전시간에 배워보았다.

그럼 상속을 구현하는 방법이 과연 이 방법뿐일까? 한번 알아보자.

직접상속

Object.create에 의한 직접 상속 (정적메서드)

Object.create 메서드는 명시적으로 프로토타입을 지정하여 새로운 객체를 생성한다.

1
2
Object.create(생성할 객체의 프로토타입, 생성할 객체의 프로퍼티키와 프로퍼티 디스크립터 객체로 이뤄진 객체)
Object.create(prototype[, propertiesObject])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 프로토타입이 null인 객체를 생성한다. 생성된 객체는 프로토타입 체인의 종점에 위치한다.
// obj → null
let obj = Object.create(null);
console.log(Object.getPrototypeOf(obj) === null); // true
// Object.prototype을 상속받지 못한다.
console.log(obj.toString()); // TypeError: obj.toString is not a function

// obj → Object.prototype → null
// obj = { x: 1 };와 동일하다.
obj = Object.create(Object.prototype, {
x: { value: 1, writable: true, enumerable: true, configurable: true },
});
// 위 코드는 다음과 동일하다.
// obj = Object.create(Object.prototype);
// obj.x = 1;
console.log(obj.x); // 1
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

// 빈객체만 생성하는 것이 아니다.
const obj1 = Object.create(obj, {
x: { value: 10 },
y: { value: 20, writable: true },
});
console.log(obj1); // {x: 10, y: 20}

const myProto = { x: 10 };
// 객체리터럴로 생성된 객체를 직접 상속받는다.
// obj → myProto → Object.prototype → null
obj = Object.create(myProto);
console.log(obj.x); // 10
console.log(Object.getPrototypeOf(obj) === myProto); // true
console.log(Object.getPrototypeOf(obj) === Object.prototype); // false

// 생성자 함수
function Person(name) {
this.name = name;
}

// obj → Person.prototype → Object.prototype → null
// obj = new Person('Lee')와 동일하다.
obj = Object.create(Person.prototype);
obj.name = "Lee";
console.log(obj.name); // Lee
console.log(Object.getPrototypeOf(obj) === Person.prototype); // true

Object.create 메서드의 첫번째 인수로 전달한 객체를 프로토타입으로 하는 프로토타입 체인에 속하는 객체를 생성한다. 즉, 객체를 생성하면서 직접적인 상속을 구현하는 것이다.

  • new 연산자 없이 객체 생성 가능
  • 프로토타입을 지정하면서 객체 생성 가능
  • 객체 리터럴로 생성된 객체도 상속받을 수 있다.

Object.prototype의 빌트인 메서드를 직접 호출하는 것은 위험하다. 그 이유는 Object.create 메서드를 사용하여 프로토타입 체인 종점에 위치하는 객체를 생성할 수도 있기 때문이다.

1
2
3
4
5
6
7
8
// 프로토타입이 null인 객체, 즉 프로토타입 체인의 종점에 위치하는 객체를 생성한다.
const obj = Object.create(null);
obj.a = 1;

console.log(Object.getPrototypeOf(obj) === null); // true

// obj는 Object.prototype의 빌트인 메서드를 사용할 수 없다.
console.log(obj.hasOwnProperty("a")); // TypeError: obj.hasOwnProperty is not a function

그러므로 Object.prototype의 빌트인 메서드는 call,apply 등을 통해 간접적으로 호출하는 것을 권장한다.

하지만 위 방법은 너무 길다…

그래서 새로나온 문법을 사용하자

1
Object.hasOwn(obj, property);

위 방법을 사용하여 같은 기능을 구현할 수 있다.

정적 프로퍼티/메소드

생성자 함수로 인스턴스를 생성하지 않아도 참조,호출할 수 있는 프로퍼티, 메소드를 말한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 생성자 함수
function Person(name) {
this.name = name;
}

// 프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};

// 정적 프로퍼티
Person.staticProp = "static prop";

// 정적 메서드
Person.staticMethod = function () {
console.log("staticMethod");
};

const me = new Person("Lee");

// 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출한다.
Person.staticMethod(); // staticMethod

// 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출할 수 없다.
// 인스턴스로 참조/호출할 수 있는 프로퍼티/메서드는 프로토타입 체인 상에 존재해야 한다.
me.staticMethod(); // TypeError: me.staticMethod is not a function

생성자 함수로 생성한 인스턴스로 정적 프로퍼티와 메소드를 참조, 호출할 수 없다.

생성자 함수가 생성한 인스턴스는 프로퍼티, 메소드를 참조할 때 프로토타입 체인 내에서만 할 수 있다.

정적 프로퍼티,메소드는 인스턴스의 프로토타입 체인 내에 없기 때문에 참조, 호출할 수 없다.

댓글 공유

자바스크립트는 public,private,protected 등의 키워드가 없어서 객체지향 프로그래밍이 아니라는 오해를 받지만 자바스크립트는 클래스 기반 객체지향 프로그래밍 언어보다 효율적이며 더 강력한 프로토타입 기반의 객체지향 프로그래밍 언어이다.

객체지향 프로그래밍

객체지향 프로그래밍이란, 특성을 나타내는 속성들의 집합을 통해 실체를 나타내어 구별하거나 인식하는 개념을 프로그래밍에 접목시킨 프로그래밍 패러다임이다. 여기서 프로그램에 필요한 속성만을 간추려 추상화 한것이다.

1
2
3
4
5
6
7
8
9
10
11
고양이 = {
동물
귀여움
털이있음
다리가 4개임...
}

const person = {
name:'Lee',
address:'Seoul'
};

프로토타입과 상속

자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거한다.

1
2
3
4
5
6
7
8
9
10
11
12
// 불필요한 메서드 중복 소유
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
return Math.PI * this.radius ** 2;
};
}

const circle1 = new Circle(1); // 얘도 getArea 가지고
const circle2 = new Circle(2); // 쟤도 getArea 가지네?

console.log(circle1.getArea === circle2.getArea); // false

위 예시는 Circle 생성자 함수가 인스턴스를 생성할 때마다 동일한 동작을 하는 getArea 메서드를 중복 생성하는데, 이 경우 모든 인스턴스가 메서드를 중복 소유하여 메모리 낭비를 야기시킬 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 프로토타입 상속을 기반으로 불필요한 중복 제거
function Circle(radius) {
this.radius = radius;
}

// 생성자 함수가 생성한 모든 인스턴스가 getArea 메서드를 공유하여 사용할 수 있도록 프로토타입에 추가
Circle.prototype.getArea = function () {
return Math.PI * this.radius ** 2;
};

const circle1 = new Circle(1);
const circle2 = new Circle(2);

console.log(circle1.getArea === circle2.getArea); // true

프로토타입 객체

프로토타입 객체(줄여서 프로토타입)은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체로서 다른 객체의 공유 프로퍼티(메서드 포함)를 제공한다.

  • 객체간 상속을 구현하기 위해 사용
  • 프로토타입을 상속받은 하위 객체는 상위 객체의 프로퍼티를 자유롭게 사용

앞으로 나올 개념은 그림이 없으면 이해하기 어렵다. 이 블로그는 나의 생각을 정리해 둔 것이므로 이해를 돕기 위해서 모던 자바스크립트 Deep Dive 책을 참고하도록하자. 혹은 Poiema Web 사이트를 참고하자.

객체 생성 방식에 따라 객체가 생성될 때, 프로토타입이 결정되고 [[Prototype]] 내부 슬롯의 값으로 저장된다.

함수객체의 prototype 프로퍼티

함수 객체만이 갖는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.

함수 정의 방식에 따라 일반함수 정의할 경우 constructor이고 ES6에서 정한 메서드 축약표현, 화살표 함수로 함수 정의할 경우 non-constructor이다.

non-constructor인 화살표 함수, 메서드 축약표현으로 정의한 메서드는 prototype 프로퍼티를 소유하지 않는다. ⇒ 프로토타입도 생성하지 않는다.

프로토타입 객체(프로토타입), 생성자 함수의 prototype 프로퍼티, 프로토타입의 constructor 프로퍼티, 생성자 함수가 생성한 객체(인스턴스)의 proto 등 헷갈리는 용어들이 많이 나오니 혼동하지 않도록 한다.

프로토타입의 constructor 프로퍼티와 생성자 함수

모든 프로토타입은 constructor 프로퍼티를 갖는다. constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다.

프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재하며 생성자 함수가 생성되는 시점과 더불어 프로토타입도 생성된다.


프로토타입의 생성 시점

생성자 함수가 생성되는 시점과 더불어 프로토타입도 생성된다.

사용자 정의 생성자 함수는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다.

1
2
3
4
5
6
7
8
9
10
11
12
// 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성
console.log(Person.prototype);

// 생성자 함수
function Person(name) {
this.name = name;
}

// 만약 함수 표현식이라면 에러(변수호이스팅)
const foo = function Person(name) {
this.name = name;
};
  1. 함수 선언문은 함수 호이스팅이 일어나 런타임 이전에 자바스크립트 엔진에 의해 실행된다.

  2. 이 때 함수 객체가 생성되고 더불어 생성된 프로토타입은 Person 생성자 함수의 prototype의 프로퍼티에 바인딩된다.

이 때 생성된 프로토타입은 오직 constructor 프로퍼티만 갖는 객체이다.

생성된 프로토타입의 프로토타입은 항상 Object.prototype이다.

반면, 빌트인 생성자 함수의 경우 전역 객체가 생성되는 시점에 생성된다.

즉, 객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화 되어 존재한다. 이 후 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당된다. 그 결과 생성된 객체는 프로토타입의 상속을 받는다.


객체 생성 방식과 프로토타입의 결정

객체는 다양한 방식으로 생성되어 세부적인 객체 생성 방식의 차이는 있지만 모두 추상연산 OrdinaryObjectCreate에 의해 생성된다는 공통점이 있다.

추상연산 OrdinaryObjectCreate는 필수적으로 자신이 생성할 객체의 프로토타입을 인수로 전달받는다.

즉, 프로토타입은 추상연산 OrdinaryObjectCreate에 전달되는 인수에 의해 결정된다. 인수는 객체가 생성되는 시점에 객체 생성 방식에 의해 결정된다.

  1. 객체 리터럴로 생성된 객체의 프로토타입
1
Object.prototype;
  1. Object 생성자 함수로 생성된 객체의 프로토타입
1
Object.prototype;
  1. 사용자 정의 생성자 함수로 생성된 객체의 프로토타입
1
생성자함수.prototype;

생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체이다. 이 프로토타입은 constructor 프로퍼티만 가지고 있다.


프로토타입 체인

자바스크립트는 객체의 프로퍼티에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. 이를 프로토타입 체인이라고 한다.

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

// 프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};

const me = new Person("Kim");
console.log(me.hasOwnProperty("name")); // true

Person 생성자 함수에 의해 생성된 me 인스턴스는 Object.prototype의 메서드인 hasOwnProperty 메서드를 호출할 수 있다. 이것은 Person.prototype뿐만 아니라 Object.prototype도 상속 받았다는 것을 알 수 있다.

단, me 객체의 프로토타입은 Person.prototype이다.

그리고 Person.prototype의 프로토타입은 Object.prototype이다.

프로토타입 최상위에 있는 객체는 언제나 Object.prototype(프로토타입 체인의 종점)이다. 그러므로 Object.prototype의 [[Prototype]] 내부 슬롯은 null 이다.

식별자와 프로퍼티 차이

식별자는 함수의 중첩관계로 이루어진 스코프의 계층적 구조로 이뤄진 스코프 체인에서 검색하여 찾는다.

반면에 객체의 프로퍼티는 해당 객체의 프로퍼티가 없다면 프로토타입 체인을 따라 [[Prototype]] 내부 슬롯에 바인딩된 프로토타입으로 이동하면서 프로퍼티를 검색한다.

스코프 체인과 프로토타입 체인은 별도로 동작하지 않고 서로 협력하여 식별자와 프로퍼티를 검색하는데 사용된다.

소감

프로토타입에 대해 설명하기가 무척 어렵고 난해했는데 그림을 보며 공부하니 이해가 수월하였다. TIL을 정리하면서 저작권때문에 그림을 첨부할 순 없었지만 머릿속으로 다시 그려보면서 정리하니 기억에 오래 남을 것 같다.

이제 러버덕을 하면서 말로 설명해보는 시간을 가져야겠다.

댓글 공유

프로퍼티 어트리뷰트

오늘은 프로퍼티 어트리뷰트에 대해서 알아보자. 한국말로 번역하면 속성 속성이다. 하지만 자바스크립트에서 속성은 두가지로 구분하여 사용하여야 한다. 이번 시간에는 속성 중 하나인 프로퍼티에 대해 알아보자

내부슬롯 internal slot

내부슬롯과 내부메서드는 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 프로퍼티와 메서드이다.

내부슬롯은 개발자가 직접 접근할 수는 없다.

하지만, [[Prototype]] 내부슬롯은 proto를 통해 간접적으로 접근할 수 있다.

프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체

자바스크립트 엔진은 프로퍼티 생성 시 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.

1
2
3
4
let a = { name: "kim", age: 28 };
Object.getOwnPropertyDescriptors(a).age.value = 3000;

console.log(a); // {name: 'kim', age: 28}

프로퍼티 어트리뷰트란 내부슬롯이다. 따라서 직접 접근할 순 없지만, Object.getOwnPropertyDescriptor 메서드로 간접적으로 접근할 수 있다.

다만 간접적으로 접근만 가능한 것이므로 위 메서드로 값을 변경하거나 할 수는 없다.

⇒ 프로퍼티 생성될 때 [[value]]는 프로퍼티 값으로 초기화 되고 나머지는 true로 초기화된다.

1
2
3
4
5
const person = {
name : 'Lee'
}

console.log(Object.getOwnPropertyDescriptor(person, 'name'); // {value: 'Lee', writable:true,... configurable: true}
  • 프로퍼티 디스크립터 객체를 반환한다. 존재하지 않거나 상속받은 프로퍼티라면 undefined 반환

데이터 프로퍼티와 접근자 프로퍼티

1. 데이터 프로퍼티

키와 값으로 구성된 일반적인 프로퍼티

  • 프로퍼티의 값 [[value]]
  • 값의 갱신 가능여부 [[writable]] : false이면 프로퍼티의 [[value]]의 값을 변경할 수 없는 읽기 전용 프로퍼티
  • 열거 가능여부 [[enumerable]] : for…in 혹은 Object.keys 메서드로 열거 가능 여부
  • 재정의 가능여부 [[configurable]] : false이면 해당 프로퍼티 삭제, 프로퍼티 어트리뷰트 값의 변경 금지

2. 접근자 프로퍼티

자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티

  • [[get]] : 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 프로퍼티 어트리뷰트 [[get]]의 값 즉, getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환된다.
  • [[set]] : 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 프로퍼티 어트리뷰트 [[set]]의 값 즉, setter 함수가 호출되고 그 결과가 프로퍼티 값으로 저장된다.
  • [[enumerate]]
  • [[configurable]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const person = {
// 데이터 프로퍼티
firstName: "Donald",
lastName: "Duck",

// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`;
},

// setter 함수
set fullName(name) {
[this.firstName, this.lastName] = name.split(" ");
},
};

// 데이터 프로퍼티를 통한 프로퍼티 값의 참조
console.log(person.firstName + " " + person.lastName); // Donald Duck

// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
// 접근자 프로퍼티 fullName값을 저장하면 setter 함수가 호출
person.fullName = "Daisy Duck";
console.log(person); // {firstName: "Daisy", lastName: "Duck"}

// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출
console.log(person.fullName); // Daisy Duck

// firstName은 데이터 프로퍼티
let descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor);
// {value:"Daisy", writable: true, enumerable: true, configurable: true}

// fullName은 접근자 프로퍼티
descriptor = Object.getOwnPropertyDescriptor(person, "fullName");
console.log(descriptor); // {get: f, set: f, enumerable: true, configurable: true}

접근자 프로퍼티와 데이터 프로퍼티 구분 방법

1
2
3
4
5
6
7
// 일반 객체의 __proto__는 접근자 프로퍼티
Object.getOwnPropertyDescriptor(Object.prototype, "__proto__");
// {get: f, set: f, enumerable: true, configurable: true}

// 함수 객체의 prototype은 데이터 프로퍼티
Object.getOwnPropertyDescriptor(function () {}, "prototype");
// {value: {...}, writable: true, enumerable: false, configurable: false}

위 예시는 접근자 프로퍼티의 경우 다음과 같은 프로퍼티 어트리뷰트가 나오고 데이터 프로퍼티의 경우 출력되는 프로퍼티 어트리뷰트가 다른 것을 확인할 수 있다.

프로퍼티 정의

프로퍼티를 정의한다. 라는 것은 새로운 프로퍼티 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나 기존 프로퍼티 어트리뷰트를 재정의하는 것을 말한다.

데이터 프로퍼티 정의

1
2
3
4
5
6
7
8
9
10
11
12
const person = {};

Object.defineProperty(person, "firstName", {
value: "yiju",
writable: true,
enumerable: true,
configurable: true,
});

Object.defineProperty(person, "lastName", {
value: "Kim",
});
  • Object.defineProperty 메서드로 프로퍼티 어트리뷰트 정의할 수 있다. 인수로는 객체의 참조, 데이터 프로퍼티의 키(문자열), 프로퍼티 디스크립터 객체를 전달한다.
  • 디스크립터 객체 누락시키면 false가 기본값

궁금증

왜 프로퍼티 동적으로 생성해줄 때, 프로퍼티 어트리뷰트 생략하는데 프로퍼티 어트리뷰트값이 true인데, define 메서드를 사용할 때 생략하면 기본값 false?

1
2
3
4
5
6
7
const obj = {};
obj['name'] = 'Kim';
Object.defineProperty(obj,'age',{
value:19,
});

왜 다른가?

동적으로 생성시에 편의를 위해서 true로 설정된다. 그렇지 않은 경우 Object.defineProperty() 메서드를 사용할 때, 생략하는 어트리뷰트는 false로 설정된다.

writable : false

1
2
3
// [[writable]] 값이 false 인 경우 해당 프로퍼티의 [[value]]의 값 변경할 수 없다.
// 에러 발생하지 않고 무시
person.lastName = "soondae";

enumerable : false

1
2
// [[enumerable]] 값이 false 인 경우 해당 프로퍼티는 for...in 문이나 Object.keys 등으로 열거할 수 없다.
console.log(Object.keys(person)); // ['firstName']

configurable : false

1
2
3
4
5
6
7
// [[configurable]] 값이 false 인 경우 해당 프로퍼티를 삭제할 수 없다.
// 삭제해도 에러없이 무시된다.
delete person.lastName; // false

// 또한, 해당 프로퍼티를 재정의 할 수도 없다.
// Object.defineProperty(person, 'lastName', { enumerable : true });
// Uncaught TypeError: Cannot redefine property: firstName

접근자 프로퍼티 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object.defineProperty(person, "fullName", {
// getter 함수
get() {
return `${this.firstName} ${this.lastName}`;
},
// setter 함수
set(name) {
[this.firstName, this.lastName] = name.split(" ");
},
enumerable: true,
configurable: true,
});

person.fullName = "yoonju hong";
console.log(person); // {firstName: 'yoonju', lastName: 'Kim'}
  • lastName은 writable이 false여서 값이 안바뀌는 것을 볼 수 있다.

객체 변경 방지

객체 변경을 방지하는 메서드를 제공한다. 종류에 따라 금지하는 강도가 다르다.

직속 프로퍼티만 방지(얕은 방지)

구분 메서드 프로퍼티 추가 삭제 값 읽기 값 쓰기 프로퍼티 어트리뷰트 재정의
객체 확장 금지 Object.preventExtensions X O O O O
객체 밀봉 Object.seal X X O O X (configurable : false)
객체 동결 Object.freeze X X O X (writable : false) X (configurable : false)

객체 확장 금지 확인 메서드 - Object.isExtensible( ) false면 확장 금지된 객체

객체 밀봉 확인 메서드 - Object.isSealed( ) true면 밀봉된 객체

객체 동결 메서드 - Object.isFrozen( ) true면 동결된 객체

깊은 방지를 구현하려면 재귀적으로 객체 값을 갖는 모든 프로퍼티에 대해 Object.freeze 메서드를 호출해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function deepFreeze(target) {
// 객체가 아니거나 동결된 객체는 무시하고 객체이고 동결되지 않는 객체만 동결
if (target && typeof target === "object" && !Object.isFrozen(target)) {
Object.freeze(target);
Object.keys(target).forEach((key) => deepFreeze(target[key]));
}
return target;
}

const person = {
name: "Lee",
address: { city: "Seoul" },
};

// 깊은 객체 동결
deepFreeze(person);

console.log(Object.isFrozen(person));
console.log(Object.isFrozen(person.address));

person.address.city = "Busan";
console.log(person);

댓글 공유

함수

카테고리 JavaScript

Function 함수

오늘은 자바스크립트의 함수에 대해 공부해보자.

자바스크립트에서 함수란, 코드블록으로 일련의 문(statement)을 감싸서 하나의 실행 단위로 정의한 것이다.

함수를 목적에 맞게 사용하기 위해서는 함수이름, 매개변수, 인자 등을 알맞게 설정해줘야한다. 그렇지 않게 사용하는 것은 지양한다.

목적

함수를 사용하는 목적은 필요할 때마다 호출하여 일련의 코드들을 재사용하기 위해 사용한다.

1
2
3
4
5
6
7
8
9
10
function returnRank(name, tall) {
// 이름과 키라는 매개변수를 통해 순위를 반환해주는 함수
return rank;
}

// 100번째 줄
returnRank("yiju", 188);

// 300번째 줄
returnRank("kim", 160);

위와 같이 하나의 로직을 여러 곳에서 재사용하고 싶을 때 함수를 사용하면 함수 이름으로 가독성도 높아지고 100번째 줄이나 300번째 줄에서 코드가 문제가 발생했을 경우 returnRank 함수가 선언된 부분만 유지보수를 해주면 되기 때문에 유지보수가 간편해진다.

단, 함수는 목적에 맞게 가급적 작게 만들고 매개변수도 3개를 넘지 않도록 만들 것을 지향한다.


정의

함수를 정의하는 방식은 4가지 방식이 있다.

  • 함수 선언문
  • 함수 표현식
  • Function 생성자 함수
  • 화살표 함수(ES6)

기본적인 함수 선언문과 함수 표현식에 대해 알아보자

자바스크립트에서 변수를 선언하면 암묵적으로 정의가 이뤄지기 때문에 선언과 정의가 모호하다.(MDN에서도 모호..) 위에서 함수 선언문이 평가되면 식별자가 암묵적으로 생성되고 함수 객체가 할당된다. 그렇기 때문에 “함수는 정의된다”로 표현한다. C언어에서 정의는 변수에 값을 할당하여 변수의 실체를 명확히 하는 것이다. 즉, 메모리 주소가 연결되면 정의라고 판단)

함수 리터럴

리터럴은 문자나 약속된 기호를 사용하여 값을 생성하는 표기법이다. 함수로 함수 리터럴을 사용하여 값을 생성할 수 있다. 함수 리터럴은 다음으로 구성되어 있다.

1
2
3
function add(x, y) {
return x + y;
}
  • function 키워드
  • 함수 이름 (add) 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자
  • 매개변수 목록 (x,y)
  • 함수 몸체({})

함수 선언문

1
2
3
4
5
6
7
// 함수 선언문
function add(x, y) {
return x + y;
}

// 함수 호출 (함수이름과 똑같은 식별자를 사용)
add(2, 5);
  • 함수 선언문은 표현식이 아닌 문이다.
  • 함수 선언문은 함수 이름을 생략할 수 없다.

앞서 언급했듯이 함수 이름은 함수 몸체 내에서만 참조 가능하다. 그렇다면 함수를 호출하려면 어떻게 할 수 있을까?

함수 선언문으로 사용되면 자바스크립트 엔진이 암묵적으로 함수 이름과 똑같은 식별자를 생성하고 함수 객체를 할당한다.

함수 표현식

1
2
3
4
5
6
7
// 함수 표현식
var add = function (x, y) {
return x + y;
};

// 함수 호출 (함수 이름으로 호출)
add(2, 10);

자바스크립트 함수는 객체타입의 값이기 때문에 변수에 할당하거나 프로퍼티의 값으로 될 수 있고 배열과 같은 자료구조의 요소가 될 수 있다.

이러한 성질 때문에 자바스크립트 함수는 일급객체다.

  • 함수 표현식은 함수 이름 생략하는 것이 일반적이다.
  • 함수 표현식은 표현식인 문이다. 즉, 값처럼 사용할 수 있다. ex)변수할당

중의적 코드 : 기명 함수 리터럴

함수 선언문은 함수 이름을 생략할 수 없으며 표현식이 아닌 문이므로 변수에 할당할 수 없다. 그러면 아래의 코드는 어떻게 동작할지 예상해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 기명 함수 리터럴 단독 사용 문맥 => 함수 선언문으로 해석
function foo() {
console.log("foo");
}

foo(); // foo

// 변수에 할당하는 문맥 => 함수 리터럴 표현식으로 해석
var add = function add(x, y) {
return x + y;
};

console.log(add(2, 3)); // 5

// 피연산자로 사용되는 문맥 => 함수 리터럴 표현식으로 해석
(function bar() {
console.log("bar");
});

bar(); // ReferenceError : bar is not defined

위 코드에서는 함수 선언문이 변수에 할당된 것처럼 보인다. 이게 어떻게 가능할까?

우리는 블록문 {}에서 앞서 중의적 표현과 문맥에 대해 다룬 적이 있다.

자바스크립트 엔진이 {}를 객체 리터럴로 인지할 것인지, 아니면 코드 블록문으로 인지할 것인지는 문맥에 따라 다르게 결정된다.

이와 같이 기명 함수 리터럴도 중의적인 코드이므로 문맥에 따라 해석이 달라질 수 있다. 다음은 자바스크립트 엔진이 기명 함수 리터럴을 해석하는 방식이다.

  1. 기명 함수 리터럴을 단독으로 사용하면 함수 선언문으로 해석

  2. 함수 리터럴이 값으로 평가되어야 하는 문맥, 예시처럼 변수에 할당하거나 피연산자로 사용하면 함수 리터럴 표현식으로 해석한다.

() 그룹 연산자 안에서 기명 함수 리터럴은 함수 리터럴 표현식으로 해석된다.

함수 리터럴에서 함수 이름은 함수 몸체 내부에서만 참조가능하기 때문에 외부에서 함수이름으로 호출시 에러 발생

함수 선언문의 경우 함수이름으로 암묵적으로 식별자를 생성하여 객체를 할당해주기 때문에 함수 이름으로 호출 가능


함수 선언문 vs 함수 표현식

함수 선언문과 함수 표현식은 생성 시점이 다르다.

1
2
3
4
5
6
7
8
9
10
11
12
console.log(add); // ƒ add(x, y) {return x + y;}
console.log(sub); // undefined

// 함수 선언문
function add(x, y) {
return x + y;
}

// 함수 표현식
var sub = function (x, y) {
return x - y;
};
  • 함수 선언문 : 모든 선언문이 런타임 이전에 JS엔진에 먼저 실행된다. 즉, 암묵적으로 함수 이름과 동일한 식별자를 생성하여 함수 객체를 할당한다. 함수 호이스팅

  • 함수 표현식 : 변수 선언 부분은 변수 호이스팅이 발생하여 undefined로 초기화되고 변수 할당문은 런타임에서 평가되어 함수 객체로 할당된다.


함수호출

일반 객체와 함수가 다른점은 함수는 호출을 할 수 있다는 것이다. 호출과 참조는 다르다. 호출은 실행흐름을 바꾸기도 하며 코드문들의 결과값을 반환해준다.

() 함수 호출 연산자를 사용하여 함수를 호출할 수 있다. 그러면 실행흐름이 함수로 옮겨지고 return 키워드를 만나게되면 그 즉시 함수 실행을 종료하고 return 키워드 우측 표현식에 대한 값을 반환한다.

return 키워드 다음 행의 문들은 무시된다. return 키워드 다음에 개행하여 코드를 작성하면 자동 세미콜론 삽입 기능에 의해 큰 오류를 발생시킬 수 있다.

인수

함수를 호출할 때 매개변수에 들어갈 인수를 전달해줘야한다. 하지만 인수가 매개변수보다 적거나 많더라도 오류를 발생시키지 않는다.

  • 매개변수보다 인수를 적게 전달할 경우 부족한 매개변수는 undefined 처리된다.
  • 매개변수보다 인수가 많아도 오류 발생 시키지 않는다. 단, 모든 인수는 arguments 객체의 프로퍼티로 보관된다.
1
2
3
4
5
function add(x,y = 0,z){
return x+y+z;
}

add(2, ,5); // X

중간에 y값의 매개변수를 생략하고 싶어서 공백으로 둬서 호출할 수 없다.

만약 인자를 순서를 신경쓰지 않고 전달해주고 싶다면 객체를 인자로 전달해주면 된다.

1
2
3
4
5
6
$.ajax({
method: "POST",
url: "/user",
data: { id: 1, name: "Lee" },
cache: false,
});

단, 함수 내부에서 객체를 변경하게 되면 참조값으로 복사되었기 때문에 함수 외부의 객체가 변경되는 부수효과가 발생할 수 있으니 주의해야한다.


순수함수와 비순수함수

외부 상태에 의존하거나 참조하거나 즉, 부수효과가 없는 함수를 순수함수라고 하고 그렇지 않은 함수를 비순수함수라고 한다.

1
2
3
4
5
var x = 10;

function add(a, b) {
return a + b + x;
}

위 함수는 a,b 매개변수가 함수 내부로 전달되어도 외부 변수 x의 값이 변경되면 결과값이 달라지기 때문에 순수함수가 아니다.

순수함수는 오직 매개변수만을 통해 함수 내부로 전달된 인수에게만 의존하여 값을 생성해 반환한다. 전달받은 인수는 변경하지 않는다.

하지만 우리가 순수함수만으로 프로그래밍을 하는 것은 불가능하다. 함수형 프로그래밍은 반복문, 조건문을 제거하여 복잡성을 해결하고 전역 변수 사용을 억제 및 생명주기 최소화하여 상태변경을 최소화하는 것을 목표로 하기 때문에 순수함수만을 사용하는 것은 옳지 않다.

콜백함수

함수의 매개변수를 통해 함수 내부로 전달되는 함수를 콜백함수라고 한다. 또한 콜백함수를 매개변수를 통해 받은 함수는 고차함수라고 한다.

콜백함수는 함수 외부에서 고차함수 내부로 주입하기 때문에 자유롭게 교체가 가능하고 경우에 따라 변경되는 로직을 가진 외부 함수를 내부로 전달할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 외부에서 전달받은 f를 n만큼 반복호출
function repeat(n, f) {
for (var i = 0; i < n; i++) {
f(i); // i를 전달하면서 f를 호출
}
}

var logAll = function (i) {
console.log(i);
};

repeat(5, logAll); // 0 1 2 3 4

var logOdds = function (i) {
if (i % 2) console.log(i);
};

repeat(5, logOdds); // 1 3
  • 콜백함수를 전달할 때에는 콜백함수를 호출하지 않고 함수 전달만 해야한다.

콜백함수는 비순수함수인가?

“앞서 함수 내부에서 외부 상태를 직접 참조하지 않더라도 매개변수를 통해 객체를 전달받으면 비순수함수가 된다.”

=> 맞다 콜백함수도 비순수 함수이다. 나중에 addEventListener 함수도 콜백함수를 매개변수로 받아 DOM을 조작하는 것도 배울 텐데 DOM을 조작한다는 것 자체가 비순수함수가 되는 것이다. 다시말해 순수함수만으로 코드를 짜는것은 불가능하다.


소감

모던 자바스크립트 12장 함수 파트를 읽으면서 함수 정의와 목적에 대해 배웠고 그동안 무의식적으로 사용했던 함수 이름을 통한 호출에 대해 이유를 알게되어 재밌었다.

코드 맥락에 맞게 자바스크립트 엔진이 해석하는게 달라질 수 있다는 점과 콜백함수와 순수, 비순수 함수에 대해 제대로 알고 넘어갈 수 있어 좋았다.

댓글 공유

loco9939

author.bio


author.job