Skip to content

[week3_MINSU] 3주차 퀴즈#7

Open
Tnalxmsk wants to merge 1 commit intomainfrom
week3_MINSU
Open

[week3_MINSU] 3주차 퀴즈#7
Tnalxmsk wants to merge 1 commit intomainfrom
week3_MINSU

Conversation

@Tnalxmsk
Copy link
Collaborator

1. 다음 코드에서 var와 let을 교체했을 때의 출력과 차이를 각 키워드의 스코프 차이와 렉시컬 환경을 관련지어 설명해주세요

const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(function() { console.log(i); });
}
funcs.forEach(fn => fn());

출력

이유


2. 다음 코드의 문제점을 렉시컬 환경 관점에서 설명하고 어떤 부분을 개선하면 좋을지 설명해주세요.

function attachHandler(rootEl) {
  const big = new Array(1_000_000).fill({ msg: 'heavy' });

  const btn = document.createElement('button');
  btn.textContent = 'Show';
  rootEl.appendChild(btn);

  btn.addEventListener('click', function onClick() {
    console.log('sample:', big[0].msg);
  });

  // 페이지 전환 시 호출됨
  return function cleanup() {
    rootEl.innerHTML = '';
  };
}

const root = document.getElementById('root');
const cleanup = attachHandler(root);

// ...어느 시점
cleanup();

문제점

개선 방법


3. 다음 useState 함수의 내부 코드를 채워 주세요

다음 코드는 useState를 간단하게 구현한 예제입니다.

hooks는 각 useState 호출의 슬롯 저장소입니다.
hookIndex는 이번 렌더에서 몇 번째 훅을 읽는 중인지 가리키는 포인터입니다.
mount는 루트 컴포넌트 등록과 최초 실행을 위한 함수입니다.
rerender는 state 값의 변경이 발생하면 루트 컴포넌트를 다시 실행합니다.

(1)부터 (4)까지의 코드를 작성해주세요.

export const { useState, mount } = (function () {
  const hooks = [];
  let hookIndex = 0;
  let root = null;

  function useState(initialState) {
    const i = hookIndex++;
    if (hooks[i] === undefined) hooks[i] = initialState;

    const setState = (newState) => {
      const prev = /* (1) 현재 값 가져오기 */;
      const next = /* (2) newState가 함수라면 prev로 계산, 아니면 그대로 사용 */;
      /* (3) 최신 값 저장 */
      
      rerender();
    };

    return [/* (4) 현재 값 */, setState];
  }

  function mount(component) {
    root = component;
    rerender();
  }

  function rerender() {
    hookIndex = 0;
    root && root();
  }

  return { useState, mount };
})();

function App() {
  const [count, setCount] = useState(0);
  console.log('render:', count);

  if (count < 3) {
    setCount((prev) => prev + 1);
  }
}

// 초기 렌더
mount(App);

(1)

(2)

(3)

(4)

@mimizae
Copy link
Collaborator

mimizae commented Nov 11, 2025

Q1. 다음 코드에서 var와 let을 교체했을 때의 출력과 차이를 각 키워드의 스코프 차이와 렉시컬 환경을 관련지어 설명해주세요

  • 키워드가 var일 때
const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(function() { console.log(i); });
}
funcs.forEach(fn => fn());

출력은 아래와 같다

3
3
3

그 이유는 var가 함수 스코프 를 가지기 때문이다. var로 선언된 변수 i는 for 블록 내부에 한정되지 않고, 해당 블록을 감싸고 있는 함수 전체(혹은 전역 스코프)에 하나만 존재한다. 즉, 반복문이 여러 번 돌더라도 새로운 i가 매번 만들어지는 것이 아니라, 하나의 동일한 변수를 계속 갱신하는 것이다.

따라서 루프가 종료된 시점에서 i의 값은 3이 된다. funcs 배열에 담긴 각 익명 함수들은 모두 같은 i 변수를 참조하는 클로저를 형성한다. 이후 funcs.forEach(fn => fn())를 실행하면, 각 함수가 자신이 생성될 당시의 i 값이 아니라 최종적으로 남아 있는 하나의 i, 즉 3을 참조하여 3이 세 번 출력되는 것이다!!

  • 키워드가 let일 때
const funcs = [];
for (let i = 0; i < 3; i++) {
  funcs.push(function() { console.log(i); });
}
funcs.forEach(fn => fn());

출력 결과는 아래와 같다.

0
1
2

let이 블록 스코프 를 가지며, for문이 반복될 때마다 새로운 렉시컬 환경이 생성되기 때문이다. 즉, for문의 각 반복마다 i는 완전히 새로운 변수로 다시 선언되고, 그 시점의 값이 클로저에 캡처된다.

따라서 첫 번째 반복에서 생성된 함수는 i = 0을, 두 번째 반복에서는 i = 1을, 세 번째에서는 i = 2를 각각 자신의 독립적인 렉시컬 환경에 담게 된다. 이렇게 각 함수가 서로 다른 i를 참조하기 때문에, 나중에 함수를 실행하면 0, 1, 2가 순서대로 출력된다!!

정리하면, var는 반복문 전체에서 하나의 i만 존재하여 하나의 참조를 공유하고, let은 반복마다 새로운 스코프와 별개의 i를 생성한다는 점이 핵심적인 차이이다.

Q2. 다음 코드의 문제점을 렉시컬 환경 관점에서 설명하고 어떤 부분을 개선하면 좋을지 설명해주세요. 😵

렉시컬 환경 관점에서 보면 메모리 누수가 발생할 수 있는 잠재적인 문제가 있는 것 같다.

함수 attachHandler 내부에서는 big이라는 매우 큰 배열을 생성하고 있다.

이 배열은 100만 개의 객체를 가지고 있으며, 각각 { msg: 'heavy' } 라는 데이터를 담고 있다. 이후 코드에서 버튼을 만들고, 이 버튼에 클릭 이벤트 리스너를 등록하고 있다.

문제는 이 이벤트 리스너의 콜백 함수 onClick이 big 변수를 참조하고 있다는 점이다.

자바스크립트의 렉시컬 스코프 규칙에 따라, onClick 함수는 자신이 선언된 위치의 외부 렉시컬 환경을 기억한다.

이로 인해 attachHandler 함수가 종료된 이후에도, onClick은 여전히 big 배열이 포함된 환경을 참조하고 있기 때문에, 가비지 컬렉터가 big 배열을 해제하지 못한다.
즉, 버튼 이벤트 리스너가 남아 있는 한 big 배열은 계속 메모리에 유지된다!!!

cleanup 함수에서는 rootEl.innerHTML = ''을 통해 버튼을 제거하고 있지만, 이 조치만으로는 btn에 연결된 이벤트 리스너가 해제되지 않는다.

브라우저는 여전히 onClick 클로저를 참조하고 있으므로, big 배열이 계속 살아 있게 된다.
결과적으로 페이지를 전환하거나 컴포넌트를 교체할 때, big이 해제되지 않아 메모리 누수가 발생하는 것이다.

이 문제를 해결하기 위해서는, cleanup 시점에서 이벤트 리스너를 제거하여 클로저 참조를 끊어줘야 한다.

즉, btn.removeEventListener('click', onClick)을 호출해서 cleanup이 호출될 때 onClick 클로저의 참조가 끊어지게 하고 big 배열도 더 이상 접근할 수 없게 되어 가비지 콜렉터가 정상적으로 메모리를 회수할 수 있도록 한다.

 return function cleanup() {
    btn.removeEventListener('click', onClick); //  이렇게... 이벤트 해제!!
    rootEl.innerHTML = '';
  };
}

Q3. 다음 useState 함수의 내부 코드를 채워 주세요. (useState 구현 코드라니)

(1) hooks[i]
(2) typeof newState === function ? newState(prev) : newState
(3) hooks[i] = next;
(4) hooks[i]

구조 이해하기

  • hooks 배열 → 모든 useState 호출마다 하나의 슬롯이 생긴다.

예를 들어 컴포넌트에서 아래처럼 상태를 구성하면

const [count, setCount] = useState(0);
const [name, setName] = useState('Tom');

hooks 배열에는

hooks = [0, 'Tom'];

이렇게 저장되는 것!!

  • hookIndex → hookIndex는 이번 렌더링에서 몇 번째 훅을 읽고 있는가를 나타낸다

매 렌더링마다 hookIndex는 0으로 rerender 안에서 초기화되고, useState가 호출될 때마다 1씩 증가한다. 이걸 통해 훅 호출 순서 유지를 구현하는 것임!!!

function useState(initialState) {
    const i = hookIndex++;
    if (hooks[i] === undefined) hooks[i] = initialState;

    const setState = (newState) => {
      const prev = hooks[i]; // (1) hooks 배열의 i번째 슬롯이 바로 현재 state 값
      const next = typeof newState === 'function' ? newState(prev) : newState; // (2) setCount(prev => prev + 1) 형식의 업데이트 함수일 수도 있고, 그냥 setCount(10)처럼 값 그 자체일 수도 있음 → 삼항 연산자로 처리!!
      hooks[i] = next; // (3) next 값을 hooks의 같은 위치에 저장한다.
      
      rerender();
    };

    return [hooks[i], setState]; // (4)
  }

@sonnnnhe
Copy link
Collaborator

  1. 다음 코드에서 var와 let을 교체했을 때의 출력과 차이를 각 키워드의 스코프 차이와 렉시컬 환경을 관련지어 설명해주세요
const funcs = [];
for (var i = 0; i < 3; i++) {
 funcs.push(function() { console.log(i); });
}
funcs.forEach(fn => fn());

출력

  • var일 때
3
3
3
  • let일 때
0 
1
2

이유
for문의 변수 선언문에서 var 키워드로 선언한 변수 i는 전역 변수로 함수 레벨 스코프를 가지기 때문에 for문이 끝난 시점 3이 할당되어 있다. 이후 funcs.forEach(fn => fn()); 에서 참조되는 i는 모두 전역 변수 i를 참조하므로 3이 3번 출력되는 것이다.
만약 let을 사용한다면 이는 블록 레벨 스코프를 가지므로 for 문의 코드 블록이 반복 실행될 때마다 for 문 코드 블록의 새로운 렉시컬 환경이 생성된다. 즉, 독립적인 렉시컬 환경을 생성하여 식별자 값을 유지하므로 0 1 2가 출력된다.


  1. 다음 코드의 문제점을 렉시컬 환경 관점에서 설명하고 어떤 부분을 개선하면 좋을지 설명해주세요.
function attachHandler(rootEl) {
 const big = new Array(1_000_000).fill({ msg: 'heavy' });

 const btn = document.createElement('button');
 btn.textContent = 'Show';
 rootEl.appendChild(btn);

 btn.addEventListener('click', function onClick() {
   console.log('sample:', big[0].msg);
 });

 // 페이지 전환 시 호출됨
 return function cleanup() {
   rootEl.innerHTML = '';
 };
}

const root = document.getElementById('root');
const cleanup = attachHandler(root);

// ...어느 시점
cleanup();

코드 실행 과정 및 문제점

  1. attachHandler(rootEl) 함수 호출 시 새로운 실행 컨텍스트가 생성되고 식별자 rootEl, big, btn 등이 등록된 해당 함수의 렉시컬 환경이 생성된다.
  2. attachHandler(rootEl) 함수 코드 평가 과정이 끝나고 함수 실행 시 const big = new Array... 라인에서 매우 큰 배열이 메모리에 할당된다.
  3. 이후 btn.addEventListener(...) 라인의 함수 onClick은 자신의 스코프가 아닌 상위 스코프(attachHandler 함수의 렉시컬 환경)에 존재하는 big 변수를 참조한다. (onClick 함수 코드 평가 과정 생략)
  4. cleanup() 함수가 호출되에 rootEl의 모든 자식 노드는 DOM 트리에서 제거되지만, 즉 버튼이 화면에서 사라지지만 btn과 함수 onClick 사이의 연결은 끊어지지 않았기 때문에 onClick 리스너가 여전히 btn 노드에 의해 참조되고 있으므로 가비지 컬렉터가 해제하지 못하여 메모리 누수가 발생한다.

해결 방법
btn에 등록된 onClick 리스너를 제거하자!

// 리스너 함수를 분리
const onClick = () => {
  console.log('sample:', big[0].msg);
};

// 리스너로 등록
btn.addEventListener('click', onClick);

return function cleanup() {
  // 리스너 제거 - 클로저 참조 끊기
  btn.removeEventListener('click', onClick);
  rootEl.removeChild(btn);
};

  1. 다음 useState 함수의 내부 코드를 채워 주세요
    다음 코드는 useState를 간단하게 구현한 예제입니다.

hooks는 각 useState 호출의 슬롯 저장소입니다.
hookIndex는 이번 렌더에서 몇 번째 훅을 읽는 중인지 가리키는 포인터입니다.
mount는 루트 컴포넌트 등록과 최초 실행을 위한 함수입니다.
rerender는 state 값의 변경이 발생하면 루트 컴포넌트를 다시 실행합니다.

(1)부터 (4)까지의 코드를 작성해주세요.

(1) hooks[i]
(2) typeof newState === "function" ? newState(prev) : newState
(3) hooks[i] = next;
(4) hooks[i]

@qowjdals23
Copy link
Collaborator

qowjdals23 commented Nov 11, 2025

1. 다음 코드에서 var와 let을 교체했을 때의 출력과 차이를 각 키워드의 스코프 차이와 렉시컬 환경을 관련지어 설명해주세요

const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(function() { console.log(i); });
}
funcs.forEach(fn => fn());

출력

  • var 사용
3
3
3

이유:
var는 함수 스코프를 가지며, 블록 내부에서 독립적인 스코프를 생성하지 않는다.
따라서 루프가 끝난 뒤 i의 값은 3이 되며, 모든 함수가 동일한 렉시컬 환경을 공유한다.
이로 인해 모든 함수가 i의 최종값인 3을 출력한다.

  • let 사용
0
1
2

이유:
let은 블록 스코프를 가지며, 각 반복마다 새로운 렉시컬 환경을 생성한다.
각 함수는 서로 다른 i 값을 클로저로 캡처하므로 0, 1, 2가 순서대로 출력된다.


2. 다음 코드의 문제점을 렉시컬 환경 관점에서 설명하고 어떤 부분을 개선하면 좋을지 설명해주세요.

  • 문제점:

onClick 클로저가 big 배열을 참조하고 있는 것이 문제이다.
big은 attachHandler 함수의 지역 변수이지만, onClick 함수가 내부에서 참조하기 때문에 렉시컬 환경이 외부로 살아남게 된다.
자바스크립트의 렉시컬 환경은 함수가 정의된 시점의 변수 스코프를 함께 저장하는 구조이다.
onClick은 이벤트 리스너로 등록될 때, 자신이 정의된 환경을 그대로 캡처한다.
이때 big은 약 100만개의 원소를 가진 거대한 배열이고, 그 배열은 onClick의 [[Environment]] 내부 슬롯에 포함된 환경 레코드 (Environment Record)에 의해 계속 참조된다.

따라서

  1. attachHandler 실행이 끝나도 onClick 함수가 여전히 big 함수를 참조하기 때문에 big은 사라지지 않는다.
  2. 페이지 전환 등으로 cleanup() 이 호출되어 DOM을 비워도 이벤트 리스너(onClick)가 제거되지 않았으므로 여전히 big을 가리킨다
  3. 가비지 컬렉터가 big 을 수거하지 못해 브라우저 메모리 점유가 지속되는 메모리 누수로 이어진다.
    => 렉시컬 환경의 참조 체인이 유지되는 한 그 안의 대용량 객체는 해제되지 않는다.
  • 개선 방법:
    이벤트 리스너를 제거하여 onClick 클로저가 big을 계속 참조하지 않도록 해야 한다!
  // 페이지 전환 시 호출됨
  return function cleanup() {
    btn.removeEventListener('click', onClick); // 참조 해제
    rootEl.innerHTML = '';
  };
}


3. 다음 useState 함수의 내부 코드를 채워 주세요

(1) hooks[i]

(2) typeof newState ==='function' ? newState(prev) : newState;

(3) hooks[i] = next;

(4) hooks[i]

@Sohyunnnn
Copy link
Collaborator

1. 다음 코드에서 var와 let을 교체했을 때의 출력과 차이를 각 키워드의 스코프 차이와 렉시컬 환경을 관련지어 설명해주세요

const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(function() { console.log(i); });
}
funcs.forEach(fn => fn());

출력

3
3
3
0
1
2

이유

var은 함수 레벨 스코프를 가지며, 블록 단위로 스코프를 만들지 않음.

따라서 for 문 내부에서 선언된 i는 전역 렉시컬 환경에 등록되어 모든 함수가 동일한 i 식별자(하나의 환경 레코드)를 참조.

반복문이 끝나면 i의 최종 값은 3,

이후 모든 클로저가 하나의 i를 참조하므로 출력은 3,3,3

let 은 블록 레벨 스코프를 가지므로, 반복할때마다 새로운 렉시컬 환경이 만들어짐.

반복 시점에서 새로운 i가 생성되어 각각의 클로저가 자신이 생성될 당시의 환경 레코드의 i 값을 기억

즉, 클로저가 독립된 i를 캡쳐하기 때문에 0,1,2가 출력


2. 다음 코드의 문제점을 렉시컬 환경 관점에서 설명하고 어떤 부분을 개선하면 좋을지 설명해주세요.

function attachHandler(rootEl) {
  const big = new Array(1_000_000).fill({ msg: 'heavy' });

  const btn = document.createElement('button');
  btn.textContent = 'Show';
  rootEl.appendChild(btn);

  btn.addEventListener('click', function onClick() {
    console.log('sample:', big[0].msg);
  });

  // 페이지 전환 시 호출됨
  return function cleanup() {
    rootEl.innerHTML = '';
  };
}

const root = document.getElementById('root');
const cleanup = attachHandler(root);

// ...어느 시점
cleanup();

문제점

onClick 콜백 함수가 클로저를 통해 상위 스코프의 big 배열을 참조

cleanup()이 호출되어 제거되어도, btn의 이벤트 리스너가 여전히 big을 참조하는 렉시컬 환경이 남아 있기때문에 big 배열을 해제하지 못하여 메모리 누수 발생

개선 방법

big을 불필요하게 외부에서 참조하지 않도록, 필요한 데이터만 지역 변수로 넘겨 사용하거나 클로저 외부로 이동


3. 다음 useState 함수의 내부 코드를 채워 주세요

다음 코드는 useState를 간단하게 구현한 예제입니다.

hooks는 각 useState 호출의 슬롯 저장소입니다.

hookIndex는 이번 렌더에서 몇 번째 훅을 읽는 중인지 가리키는 포인터입니다.

mount는 루트 컴포넌트 등록과 최초 실행을 위한 함수입니다.

rerender는 state 값의 변경이 발생하면 루트 컴포넌트를 다시 실행합니다.

(1)부터 (4)까지의 코드를 작성해주세요.

export const { useState, mount } = (function () {
  const hooks = [];
  let hookIndex = 0;
  let root = null;

  function useState(initialState) {
    const i = hookIndex++;
    if (hooks[i] === undefined) hooks[i] = initialState;

    const setState = (newState) => {
      const prev = /* (1) 현재 값 가져오기 */;
      const next = /* (2) newState가 함수라면 prev로 계산, 아니면 그대로 사용 */;
      /* (3) 최신 값 저장 */

      rerender();
    };

    return [/* (4) 현재 값 */, setState];
  }

  function mount(component) {
    root = component;
    rerender();
  }

  function rerender() {
    hookIndex = 0;
    root && root();
  }

  return { useState, mount };
})();

function App() {
  const [count, setCount] = useState(0);
  console.log('render:', count);

  if (count < 3) {
    setCount((prev) => prev + 1);
  }
}

// 초기 렌더
mount(App);

(1) hooks[i]

(2) typeof newState === ‘function’ ? newState(prev) : newState

(3) hooks[i] = next

(4)hooks[i]

Copy link
Member

@jstar000 jstar000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q1.

  1. var
const funcs = [];

for (var i = 0; i < 3; i++) {
    funcs.push(function() { console.log(i); }); // 1️⃣
}

funcs.forEach(fn => fn());

반복문이 돌며 funcs 배열에 function() { console.log(i); }가 할당된다.
이때 i는 var 키워드로 선언된 변수며 var는 함수 레벨 스코프를 가지므로, 위 코드에서 for문 블록 안에 있더라도 결과적으로 전역 변수로 취급된다.
따라서 for문의 코드 블록이 반복 실행되더라도 for문 코드 블록의 새로운 Lexical Environment가 생성되지는 않는다.

funcs 배열에 할당된 함수를 실행시키면 모두 동일한 Lexical Environment의 식별자 i의 값을 참조하므로 3, 3, 3이 출력된다

  1. let
    for문의 변수 선언문에서 let 키워드로 선언한 변수를 사용하면 for문의 코드 블록이 반복 실행될 때마다 for문 코드 블록의 새로운 렉시컬 환경이 생성된다.
    +) 반복분의 코드 블록 내부에 함수 정의가 없는 반복문이 생성하는 새로운 렉시컬 환경은 반복 직후, 아무도 참조하지 않기 때문에 가비지 컬렉션의 대상이 된다.
Image

루프가 실행될 때,
i = 0일 때 function() { console.log(i); }가 생성되고, 이 함수는 블록 스코프 1의 i(=0)을 클로저로 캡처한다.
i = 1, 2일때도 마찬가지로 새로운 렉시컬 환경이 상성될 당시의 상태를 스냅샷처럼 저장한다.

따라서 funcs.forEach(fn => fn());가 실행되면 클로저의 [[Environment]] 내부 슬롯에 저장된 Lexical Environment의 참조값을 따라 해당 Lexical Environment에 등록된 i 식별자를 찾아, 그 식별자의 값을 출력한다.

반복문의 변수 선언부가 let/const일 때, 각 반복마다
블록 스코프 1: { i: 0 }
블록 스코프 2: { i: 1 }
블록 스코프 3: { i: 2 }
각 함수의 [[Environment]]가 서로 다른 렉시컬 환경을 가리킨다!

결과적으로 0, 1, 2가 출력된다.

Q2.

function attachHandler(rootEl) {
  const big = new Array(1_000_000).fill({ msg: 'heavy' });
  // [{msg: 'heavy'}, {msg: 'heavy'}, ...]

  const btn = document.createElement('button');
  btn.textContent = 'Show';
  rootEl.appendChild(btn);

  btn.addEventListener('click', function onClick() {
    console.log('sample:', big[0].msg); // onClick 함수가 클로저 함수
  });

  // 페이지 전환 시 호출됨
  return function cleanup() {
    rootEl.innerHTML = ''; // rootEl element 내부의 모든 HTML을 빈 문자열로 바꾼다, 즉 내부의 모든 자식 element를 DOM 트리에서 제거한다
  };
}

const root = document.getElementById('root');
const cleanup = attachHandler(root);

// ...어느 시점
cleanup();
  1. onClick 함수는 클로저 함수

클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고, 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다.

  • onClick 함수는 상위 스코프(attachHandler 함수)의 식별자 big을 참조하고 있다
  • onClick 함수는 attachHandler 함수의 생명 주기가 끝난 뒤에도 유지된다
    • rootEl.appendChild(btn); -> DOM 트리가 btn을 참조하고 있으므로 attachHandler 함수가 실행 컨텍스트 스택에서 pop된 후에도 btn은 Garbage Collection 대상이 아님, onClick은 btn에 등록된 이벤트 핸들러이므로 attachHandler 함수의 생명 주기가 끝난 뒤에도 유지됨
      => onClick 함수는 big을 참조하는 클로저, free variable은 big(free variable: 클로저에 의해 참조되는 상위 스코프의 변수)

+) DOM에서 제거 !== 객체 garbage collection

브라우저 메모리 (컴퓨터 RAM의 일부)
├── JS 엔진 (V8, SpiderMonkey 등) 관리 영역
│   ├── JS 객체
│   ├── 함수
│   ├── 변수
│   └── 클로저의 렉시컬 환경
│
└── 렌더링 엔진 (Blink, Webkit 등) 관리 영역
    ├── DOM 트리
    ├── CSS 렌더 트리
    └── 레이아웃 정보

JS 객체 등과 DOM 트리 모두 브라우저 메모리에 저장되지만, JS 객체를 관리하는 주체와 DOM 트리를 관리하는 주체가 다르다.

'JS 메모리'를 JavaScript 엔진이 관리하는 메모리 영역, 'DOM 메모리'를 렌더링 엔진이 관리하는 메모리 영역이라고 하자.
DOM 트리에서 특정 element를 삭제해도, 그 element는 JS 메모리에 남아있을 수 있다

const btn = document.createElement('button'); // btn 객체가 JS 메모리에 생성됨
rootEl.appendChild(btn); // DOM 메모리에 노드로 추가됨
rootEl.innerHTML = '';  // DOM 트리(DOM 메모리)에서 제거됨
console.log(btn);       // 하지만 JS 메모리에는 btn 객체가 여전히 존재!(rootEl에서 append한게 떨어진거지 btn 객체 자체가 사라지지는 않음)
  1. 코드 문제점 찾아보기
  • 버튼의 이벤트 리스너로 onClick 함수(클로저)가 등록됨, 이 클로저는 big을 참조하고 있는 상태

  • onClick의 생명 주기가 끝나지 않는다면 big도 Garbage Collection의 대상이 되지 않는다(교재 p.385: '객체를 포함한 모든 값은 누군가에 의해 참조되지 않을 때 비로소 가비지 컬렉터에 의해 메모리 공간의 확보가 해제되어 소멸한다')

  • attachHandler 함수 안에서 버튼 객체가 선언되고, DOM 트리에 추가되고, onClick 이벤트 리스너가 등록된 후 attachHandler 함수가 종료됨.

    • attachHandler 함수 종료 후에는 btn에 접근할 수는 없음(btn은 attachHandler의 지역변수이므로)
    • 하지만 cleanup 함수 실행 전까지는 DOM 트리가 btn 객체를 참조하고 있기 때문에 btn 객체 자체는 JS 메모리에 여전히 존재
  • cleanup()이 실행됨
    -rootEl.innerHTML = '' 코드가 실행돼 버튼을 포함해 DOM 트리의 모든 자식 노드가 삭제됨(DOM 메모리에서 삭제됨). 따라서 더이상 렌더링 엔진은 btn을 참조하지 않음. 이제 JS 엔진에 btn 변수를 참조하는 누군가가 없다면 btn 변수도 Garbage Collection의 대상이 됨.

    • JS 엔진에도 더이상 btn을 참조하는 누군가가 없으므로 이 시점에서 btn이 GC(Garbage Collection) 대상이 돼서 JS 메모리에서 제거된다?
  • 참조

    • 참조: 어떤 객체에 접근할 수 있는 연결(링크)
    • btn과 onClick의 참조관계
      • btn.addEventListener('click', onClick): btn이 이벤트 리스너 목록을 가지고 있고, 그 목록 안에서 onClick 함수에 대한 참조를 저장한다.
      • 따라서 btn이 onClick을 참조한다 -> 따라서 btn이 메모리에 존재하면 onClick은 GC 대상이 아니다
  • GC

    • 자바스크립트의 GC
    • 이 객체가 windowdocument에서 출발해 도달 가능한가? 참조 방향은 위에서 아래로의 단방향 탐색
    • GC는 '이 객체가 누굴 참조하냐'가 아니라, '이 객체를 누가 참조하냐'를 따져서, 해당 객체로 가는 길이 끊겼다면 unreachable 상태로 판단하고 GC의 대상으로 여김
    • 만약 Garbage Collector가 '루트에서 onClick까지 도달 가능하다'고 판단하면 onClick은 GC 대상이 아님. 하지만 이 코드에서 onClick은 btn 내부의 리스너 리스트에만 저장되어 있으므로 btn이 GC되면 이벤트 리스너 리스트도 사라지므로 onClick도 아무도 참조하지 않는 상태가 됨. 따라서 GC 대상이 됨. 그럼 onClick도 아무도 참조하지 않으므로 onClick이 GC?

어디서 꼬인 걸까요...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants