← Blog
Computer Science / 네트워크

JavaScript 핵심 개념 — 클로저 · this · 이벤트 루프 · 프로토타입

면접에서 "말로 설명한다"는 가정으로 작성. 답을 외우기보다 "왜"와 "트레이드오프"를 설명할 수 있는 것이 목표입니다.


1-1. 클로저(Closure)

함수가 선언될 당시의 렉시컬 환경(스코프)을 기억하여, 외부 스코프 밖에서 실행되어도 그 변수에 접근할 수 있는 특성. 즉 함수 + 그 함수가 선언된 환경의 조합입니다.

function counter() {
  let count = 0;
  return () => ++count; // count를 기억함
}
const c = counter();
c(); // 1
c(); // 2
  • 활용: 데이터 은닉(private 변수), 상태 유지, 콜백/이벤트 핸들러에서 값 캡처.
  • 주의: 반복문에서 var로 클로저를 만들면 모든 클로저가 같은 변수를 참조해 버그 → let(블록 스코프)으로 해결.

1-2. 호이스팅(Hoisting)

변수·함수 선언이 스코프 최상단으로 끌어올려진 것처럼 동작하는 현상. 실제로 코드가 이동하는 게 아니라, 실행 컨텍스트 생성 단계에서 선언이 먼저 메모리에 등록되기 때문입니다.

  • var: 선언이 호이스팅되고 undefined초기화됨
  • let/const: 호이스팅은 되지만 초기화 전 접근 시 에러 → TDZ(Temporal Dead Zone)
  • 함수 선언문: 함수 전체가 호이스팅됨 / 함수 표현식은 변수 규칙을 따름

화살표 함수는 호이스팅이 안 되나? (정확한 표현)

자주 틀리는 포인트. **"화살표 함수가 호이스팅 안 된다"가 아니라 "할당받는 변수(const/let)가 TDZ에 걸린다"**가 정확합니다. 차이를 만드는 건 "화살표 함수냐"가 아니라 **"함수 선언문이냐, 변수 할당이냐"**입니다.

// 함수 선언문 → 함수 전체 호이스팅
sayHi(); // ✅ 동작
function sayHi() {}

// 함수 표현식(화살표) → const 변수라 TDZ
greet(); // ❌ ReferenceError (Cannot access before initialization)
const greet = () => {};

// var면 화살표 무관하게 동일하게 실패 (undefined로 초기화되므로)
foo(); // ❌ TypeError: foo is not a function
var foo = () => {};
형태호이스팅선언 전 호출
function f(){}함수 전체✅ 가능
const f = () => {}선언만 (TDZ)❌ ReferenceError
var f = () => {}선언만 (undefined)❌ TypeError

면접 멘트: "화살표 함수는 함수 표현식이라 변수에 할당되는 형태이고, const/let의 TDZ 때문에 선언 전 접근이 불가능합니다."


1-3. this 바인딩

this는 함수가 어떻게 호출되는가에 따라 결정됩니다.

  1. 일반 함수 호출 → 전역 객체(strict mode에서는 undefined)
  2. 메서드 호출 → 메서드를 소유한 객체
  3. 생성자 호출(new) → 새로 생성된 인스턴스
  4. call/apply/bind → 명시적으로 지정한 객체
  5. 화살표 함수 → 자신의 this가 없고, **선언된 위치의 상위 스코프 this**를 그대로 사용(렉시컬)

call / apply / bind 차이

셋 다 this를 명시적으로 지정합니다.

  • call(thisArg, arg1, arg2): 인자를 나열해 즉시 호출
  • apply(thisArg, [args]): 인자를 배열로 받아 즉시 호출
  • bind(thisArg): 호출하지 않고 this가 고정된 새 함수를 반환

1-4. 이벤트 루프 / 마이크로태스크 vs 매크로태스크

JS는 싱글 스레드지만 이벤트 루프 덕분에 비동기를 처리합니다. 콜 스택이 비면 이벤트 루프가 큐에서 작업을 꺼내 실행합니다. 비동기 콜백은 두 개의 분리된 큐로 관리됩니다.

매크로태스크(Macrotask): setTimeout, setInterval, I/O, UI 렌더링, 이벤트 콜백, MessageChannel, setImmediate(Node)

마이크로태스크(Microtask): Promise.then/catch/finally, async/await(await 이후 코드), queueMicrotask, MutationObserver, process.nextTick(Node, 최우선)

핵심 규칙 (우선순위)

콜 스택이 비면 → 마이크로태스크 큐를 전부 비운다 → 매크로태스크를 하나만 처리한다 → 다시 마이크로태스크 전부 → 매크로태스크 하나 … 반복.

마이크로태스크가 항상 우선이고, 한 사이클에 큐를 통째로 비웁니다.

console.log('1');
setTimeout(() => console.log('2'), 0);      // 매크로
Promise.resolve()
  .then(() => console.log('3'))             // 마이크로
  .then(() => console.log('4'));            // 마이크로
console.log('5');
// 출력: 1 → 5 → 3 → 4 → 2

setTimeout 0ms인데도 23,4보다 늦게 나오는 게 포인트.

async/await

async function f() {
  console.log('a');
  await null;        // 여기서 함수 양보
  console.log('b');  // await 이후는 마이크로태스크
}
console.log('start'); f(); console.log('end');
// 출력: start → a → end → b

마이크로태스크 기아(starvation)

마이크로태스크가 계속 새로 추가되면 큐를 "전부" 비우는 동안 매크로태스크 차례가 영영 오지 않을 수 있습니다.

마이크로태스크매크로태스크
대표 예Promise.then, awaitsetTimeout, I/O
한 사이클 처리량전체하나만
우선순위높음낮음

1-5. 프로토타입(Prototype)

JS는 프로토타입 기반 언어입니다. 모든 객체는 [[Prototype]] 내부 슬롯으로 다른 객체(프로토타입)를 가리키며, 프로퍼티를 찾을 때 없으면 프로토타입 체인을 따라 올라가며 탐색합니다. 이를 통해 상속과 메서드 공유를 구현합니다.


1-6. 얕은 복사 vs 깊은 복사

  • 얕은 복사: 한 단계만 복사, 중첩 객체는 참조 공유 ({...obj}, Object.assign, slice)
  • 깊은 복사: 모든 단계를 새로 복사, 참조 끊김 (structuredClone(), JSON.parse(JSON.stringify()) — 단 함수·undefined·순환참조 처리 불가)

1-7. var / let / const

varletconst
스코프함수블록블록
재선언OXX
재할당OOX
호이스팅undefined 초기화TDZTDZ

1-8. 디바운스 vs 스로틀

둘 다 이벤트가 너무 자주 발생할 때 함수 실행 횟수를 줄이는 기법. 전략이 다릅니다.

  • 디바운스: 이벤트가 멈추고 일정 시간이 지나야 실행 → "마지막 한 번". 호출마다 타이머를 리셋. ex) 엘리베이터
  • 스로틀: 일정 시간 간격마다 최대 한 번 실행 → "진행 중에도 주기적으로". 쿨다운 동안 무시. ex) 버스, 지하철
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);                     // 리셋
    timer = setTimeout(() => fn(...args), delay);
  };
}
function throttle(fn, interval) {
  let blocked = false;
  return (...args) => {
    if (blocked) return;                     // 쿨다운 중 무시
    fn(...args);
    blocked = true;
    setTimeout(() => (blocked = false), interval);
  };
}
이벤트:    X X X X X X X X X X   (멈춤)
디바운스:                       🔴      ← 멈춘 뒤 마지막 1번
스로틀:    🔴     🔴     🔴     🔴      ← 일정 간격마다
상황선택
검색 자동완성 / 입력 검증 / resize디바운스
스크롤 추적 / 무한 스크롤 / 버튼 연타 방지 / 마우스 이동스로틀

결정적 차이: 이벤트가 계속되는 도중에 실행되는가. 디바운스는 멈춰야 실행, 스로틀은 진행 중에도 간격마다 실행.

플러스 멘트: 실무에선 lodash _.debounce/_.throttle을 쓰며 leading(첫 호출 즉시)·trailing(마지막 호출) 옵션으로 세밀하게 제어합니다.


1-9. 이벤트 위임(Event Delegation)

자식 각각에 핸들러를 다는 대신, 부모에 하나만 등록하고 버블링을 이용해 처리하는 패턴.

이벤트 전파 3단계

1. 캡처링 : window → document → div → button   (위→아래)
2. 타깃   : button (실제 클릭 요소)
3. 버블링 : button → div → document → window   (아래→위)  ← 기본 핸들러는 여기서 동작
el.addEventListener('click', handler);              // 버블링(기본)
el.addEventListener('click', handler, { capture: true }); // 캡처링

패턴과 핵심 보정

const ul = document.querySelector('ul');
ul.addEventListener('click', (e) => {
  const li = e.target.closest('li');     // 실제 클릭 지점에서 가장 가까운 li
  if (!li || !ul.contains(li)) return;   // 여백 등 무시
  console.log(li.textContent);
});
프로퍼티의미
e.target이벤트가 실제로 시작된 요소 (li 안 span일 수도)
e.currentTarget핸들러가 등록된 요소 (여기선 ul)
  • 장점: ① 메모리 절약(핸들러 1개) ② 동적 추가 요소 자동 대응 ③ 관리 단순
  • 자식 엘리먼트 때문에 e.target 대신 closest()로 보정해야 안전.

주의

  • 버블링되지 않는 이벤트(focus, blur, mouseenter, mouseleave)는 위임이 안 됨 → focusin/focusout(버블링 O)이나 캡처링 사용.
  • e.stopPropagation()(전파 중단) vs e.preventDefault()(기본 동작만 차단)는 다른 것.

플러스 멘트: React의 합성 이벤트(SyntheticEvent)도 내부적으로 루트에 이벤트를 위임하는 방식으로 동작합니다.