프론트엔드

자바스크립트 깨알상식 (간단)

고은수 2025. 1. 26. 18:19

어디가서 아는척 해보자 ^0^

1. 자바스크립트 배열은 사실 배열이 아님

 
우리가 아는 배열은 이렇게 각 요소가 동일한 데이터 크기를 가지고 빈틈없이 연속적으로 이어져있다. 이러한 구조는 데이터가 정렬되어 있으면 '인덱스 x 요소 크기'만큼만 이동하면 되므로 요소에 접근이 매우 빠른 장점이 있다. 하지만, 정렬되지 않은 경우에 특정 요소를 검색하는 경우 배열의 모든 요소에서 그 요소를 찾을 때까지 순서대로 검색해야 하므로 시간이 많이 걸린다(O(n)). 또한 배열에 요소를 삽입하거나 삭제할 때 뒤 인덱스의 요소들을 이동시켜야 해서 비용이 많이 든다. 이러한 배열을 밀집 배열이라고 한다.

자바스크립트의 배열은 사실 위와 같은 배열이 아니라 해시 테이블 구조의 객체이다. 각 요소는 단순한 값이 아니라 value, writable, enumerable, configurable의 속성 설명자들을 포함하고 이 플래그들은 같은 메모리 블록에서 1비트(boolean)로 저장된다. 여기서 각 요소를 위한 메모리 공간은 동일한 크기를 갖지 않아도 되며 연속적으로 이어져있지 않아도 된다. 이런 배열을 희소 배열이라고 한다.
이러한 구조로 하나의 배열에 다양한 타입의 데이터를 저장할 수 있고 크기도 동적으로 조절할 수 있다. 또한 특정 요소의 검색, 삽입, 삭제도 전통적인 배열보다 효율적이다. 하지만 해시 테이블로 구현된 객체이므로 인덱스로 요소에 접근하는 경우 일반적인 배열보다 성능적인 면에서 느릴 수밖에 없고, 각 요소마다 추가적인 메타데이터를 저장해야 하고 연속된 메모리 공간을 사용하지 않아 메모리를 효율적으로 사용하지 못하는 단점이 있다.
모던 자바스크립트 엔진은 배열을 일반 객체와 구별하여 좀 더 배열처럼 동작하도록 최적화하여 구현했다. 배열의 모든 요소가 동일한 타입이라면 연속된 메모리 공간에 값을 직접 저장하고, 배열의 인덱스가 연속적이고 빈 공간이 없는 경우 엔진은 이를 감지하여 전통적인 배열과 유사한 방식으로 저장한다. 또한 자주 함께 접근되는 요소들은 메모리상에서 가깝게 배치한다.
(V8 엔진 뜯어보기)

 

V8 엔진 뜯어보기

개요지난 글 브라우저 내부 동작을 알아보자(+Web Worker)에서 브라우저 렌더링 과정을 알아보며 Blink 엔진과 V8 엔진이 어떻게 상호작용하는지 알아보았다. Blink 엔진이 파싱, 레이아웃, 페인팅, 컴

skdltn210.tistory.com

const arr = []

console.time('array')

for (let i = 0; i < 100000000; i++){
    arr[i] = i;
}

console.timeEnd('array')

const obj = {}

console.time('obj')

for (let i = 0; i < 100000000; i++){
    obj[i]=i;
}

console.timeEnd('obj')

배열이 좀 더 빠름

2. getElementById vs querySelector

getElementById가 더 빠르다!
브라우저는 따로 해시 테이블 구조의 자료구조를 만들어서 모든 id를 여기에 저장한다.

const idHashTable = {
    'container': /* 참조 */ ElementReference,
    'paragraph1': /* 참조 */ ElementReference,
    'paragraph2': /* 참조 */ ElementReference
}

id는 문서에서 유일해야 하므로, 충돌이 발생하지 않고 O(1) 시간 복잡도로 요소에 접근할 수 있다. 반면 querySelector는 CSS 선택자를 파싱 하여, 모든 DOM을 순회하면서 각 노드에 대해 선택자 조건을 평가하여 일치하는 노드들을 찾는다. 이 과정의 시간 복잡도는

  1. CSS 선택자 파싱 : O(m), m은 선택자의 길이
  2. DOM 트리 순회 : O(n), n은 DOM 노드 수
  3. 선택자 조건 평가 : 각 노드마다 O(m)

O(n*m)이 된다.
근데 사실 매우 복잡한 구조가 아니라면 querySelector를 써도 사용자가 체감할 정도로 성능차이가 나진 않을 것 같다. 코드의 가독성, 유지 보수성과 성능을 잘 고려하여 둘 중에 잘 골라서 쓰자.

3. onClick vs addEventListener

onClick은 DOM 요소의 직접적인 프로퍼티로 저장된다. 이 구조에서 onClick은 단 하나의 함수 참조만 저장할 수 있고 새로운 핸들러가 할당되면 이전 핸들러는 가비지 컬렉션의 대상이 된다.

DOMElement {
    // ... 다른 프로퍼티들 ...
    onclick: Function | null,  // 단일 함수 참조
    // ... 다른 프로퍼티들 ...
}

addEventListener는 각 요소마다 이벤트 리스너들의 목록을 관리한다. 같은 이벤트에 대해 여러 핸들러를 등록할 수 있고 캡처링과 버블링 단계 등 세부적인 제어도 가능하다.
각 이벤트 리스너는

  • 실제 핸들러 함수
  • 이벤트 옵션(캡처링, 버블링 등)
  • 기타 메타데이터

같은 정보를 포함하는 객체로 저장된다.

DOMElement {
    // ... 다른 프로퍼티들 ...
    __eventListeners: {
        'click': [
            {
                handler: Function,
                capture: boolean,
                once: boolean,
                passive: boolean
            },
            // 더 많은 리스너 객체들...
        ],
    }
}

즉, addEventListener가 더 많은 메모리를 사용하고, 더 복잡한 가비지 컬렉션을 수행한다. 하지만 대부분의 경우 여러 이벤트 핸들러를 관리하고, 코드의 관심사를 분리하고, 다양한 설정을 할 수 있기 때문에 addEventListener를 사용하는 것이 권장된다.
 

이벤트 위임

버튼이 있는 컴포넌트를 많이 만든다고 생각해 보자. 새로운 컴포넌트가 생성될 때마다 그 컴포넌트에 이벤트리스너를 다는 건 메모리가 낭비된다.

parentElement.addEventListener('click', event => {
    const targetComponent = event.target.closest('.component');
    if (targetComponent) {
        ...
    }
});

이벤트 위임을 사용하여 전역으로 이벤트 리스너를 달고 . closest를 활용하여 가장 가까운 컴포넌트를 선택하도록 해서 메모리 사용량을 줄일 수 있다. 다만 DOM을 탐색하여 접근해야 하므로 해당 컴포넌트를 찾는 비용이 발생한다. 
 

근데 React에서는 왜 onClick 씀?

React의 onClick은 원조 onClick과 다르다. 내부적으로 이벤트 위임을 자동으로 처리하여 실제로 모든 이벤트 핸들러가 document 레벨에서 관리된다. 또한 가상 DOM과 통합되어 있어 이벤트 처리와 DOM 업데이트를 최적화된 방식으로 처리한다.

4. this

자바스크립트에서 this는 지 맘대로 가리킨다. 간단하게 알아보자. 

실행 컨텍스트와 클로저 이해해보기를 보고 오면 이해하기 쉽다.

 

1. this는 모든 곳에 존재함

this // Window {0: global, 1: Window, 2: Window, window: Window, ... }

function foo() {
  console.log(this);
}

foo(); // Window {0: global, 1: Window, 2: Window, window: Window, ... }

전역 스코프에서 this를 호출하면 전역 객체인 Window를 가리킨다. 함수를 호출할 때도 사실 window.foo()로 호출되기 때문에 함수 내부에서도 this는 기본적으로 전역 객체를 가리킨다.
 

2. this 바인딩은 함수 호출 방식에 의해 동적으로 결정됨

function foo() {
    console.log(this);
}

const obj = {
    list : [1,2,3],
    foo,
}

obj; // { list: (3) [1, 2, 3], foo: ƒ foo(), ... }

아까랑 this가 달라졌다. 위에서는 객체의 메소드로 foo 함수를 등록하여 foo는 obj의 메소드가 되었다. 이때 this는 메소드를 소유한 객체(obj)를 가리킨다.

function foo() {
    console.log(this);
}

const obj = {
    list : [1,2,3],
    foo,
}

const obj2 = {
    list : [4,5,6]
}

foo.call(obj2); // { list: (3) [4, 5, 6], ... }

 
또한 call, apply, bind 같은 메소드를 사용하여 this를 원하는 객체로 직접 바인딩할 수 있다.
 

3. 화살표 함수 안의 this는 선언될 때 결정됨

const foo = {
    list : ["a", "b"],
    getList() {
        setTimeout( function() {console.log(this.list);},2000);
        return this;
    }
}

foo.getList();
// undefined

비동기 상황일때 setTimeout 내부의 일반 함수는 자신만의 this를 가지고, 이는 전역 객체를 가리킨다. 즉 this.listundefined가 된다.

콜백 함수는 완전히 새로운 실행 컨텍스트에서 실행된다. 이때 함수는 foo 객체의 메소드로서 호출되는 게 아니라 독립적인 함수로 호출되는데, 독립적인 함수로 호출될 때는 기본적으로 this가 전역 객체(Window)를 가리키게 된다.
const foo = {
    list : ["a", "b"],
    getList() {
        setTimeout(() => {console.log(this);},2000);
        return this;
    }
}

foo.getList(); 
// { getList: getList() { setTimeout(() => {…}, list: (2) ['a', 'b'], ... }

화살표 함수는 자신만의 this를 생성하지 않고 자신이 선언된 시점의 this를 그대로 사용하고 이를 렉시컬 스코프라고 한다. 즉, 누가 이 함수를 호출했는가?가 아니라 이 함수가 어디에서 정의되었는가?가 중요하다. 여기서는 getList 메소드의 this(foo 객체)를 그대로 사용하게 된다.

참고

https://www.yes24.com/product/goods/92742567

 

모던 자바스크립트 Deep Dive - 예스24

『모던 자바스크립트 Deep Dive』에서는 자바스크립트를 둘러싼 기본 개념을 정확하고 구체적으로 설명하고, 자바스크립트 코드의 동작 원리를 집요하게 파헤친다. 따라서 여러분이 작성한 코드

www.yes24.com

https://www.youtube.com/watch?v=fllhA9yGSYE