관리 메뉴

Jerry

#7. 호이스팅, 전역 컨텍스트, 클로저 본문

CS/Terminology

#7. 호이스팅, 전역 컨텍스트, 클로저

juicyjerry 2025. 3. 6. 14:39

"자바스크립트를 사용하다 보면 변수와 함수가 예상과 다르게 동작하는 경우를 본 적이 있을 겁니다.

예를 들어 변수 선언 전에 console.log(a);를 호출했을 때 오류가 발생하지 않고 undefined가 출력되는 걸 본 적이 있나요?

이런 현상이 발생하는 이유는 바로 호이스팅 때문입니다."

 

 

 

오늘은 자바스크립트의 핵심 개념인 호이스팅전역 컨텍스트, 클로저에 대해 알아보는 시간을 가지겠습니다.

 

 

1. 도입: 왜 중요한 개념인가?

JavaScript에서 호이스팅, 전역 컨텍스트, 클로저는 함수 및 변수의 동작을 이해하는 데 핵심적인 개념이다.

 

- 코드 실행 순서 파악

- 스코프 관리

- 메모리 효율 향상

 

과 같이 중요한 역할을 한다.

이를 잘 활용하면 버그를 줄이고 유지 보수성을 높일 수 있다.

 

 

 

2. 개념 설명 및 예제

① 호이스팅 (hoisting)

- JavaScript에서 변수와 함수 선언이 코드 실행 전에 메모리에 미리 할당되는 현상

- 선언부가 코드의 최상단으로 끌어올려진 것처럼 동작

- let과 const는 호이스팅되지만 초기화되지 않아 참조 전에 사용하면 에러 발생

 

console.log(a); // undefined
var a = 10;

foo(); // "Hello, Hoisting!"
function foo() {
  console.log("Hello, Hoisting!");
}

- var a는 호이스팅되지만 undefined로 초기화

- 함수 foo는 선언 자체가 끌어올려져 정상 호출

- 이는 var는 선언과 초기화가 동시에 진행되지만, 함수 선언식은 메모리에 전체 함수가 저장되기 때문

 

 

 

let과 const 차이

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;

- 위 두 키워드 let과 const는 TDZ(Temporal Dead Zone) 때문에 초기화 전에 접근 불가

- TDZ란 변수가 선언되었지만 초기화가 완료되기 전까지 접근할 수 없는 구간

-  이 구간에 접근하면 ReferenceError가 발생

 

 

 

var를 사용하지 않는 이유

변수 중복 선언 가능

-> 예기치 않은 덮어쓰기 발생

 

함수 스코프

-> 블록 스코프가 없어 예상치 못한 동작 가능

var가 블록 스코프가 없는 이유
if (true) {
  var x = 10;
}
console.log(x); // 10 (블록 스코프를 따르지 않음)​

- var는 함수 스코프만 따르기 때문에 블록 {} 내부에서 선언해도 외부에서 접근할 수 있다.

 

 

호이스팅 시 undefined로 초기화

-> 원치 않는 undefined 버그 발생 가능

 

 

TDZ 없음

-> 선언 전 사용 가능해 코드 안정성 떨어짐

 

 

 

② 전역 실행 컨텍스트 (Global  Execution Context)

- 코드가 실행될 때 기본적으로 생성되는 실행 컨텍스트

- 전역 스코프에 정의된 변수와 함수들은 어디에서든 접근 가능

 

 

Q. 전역 실행 컨텍스트가 어떻게 생길까?

- 자바스크립트 코드 실행되면 가장 먼저 전역 실행 컨텍스트가 생성

- 이때, 전역 객체(window 또는 global)와 this 바인딩, 변수 환경, 렉시컬 환경 등이 함께 초기화됨

- var로 선언한 변수는 window 객체의 속성이 되고

- 함수도 전역 객체의 메서드로 등록됨

- let과 const는 전역 객체의 속성이 되지 않음

console.log(this); // 브라우저 환경: window, Node.js 환경: global
console.log(window); // window 객체 (브라우저)
console.log(global); // global 객체 (Node.js)

var globalVar = "Hello";
console.log(window.globalVar); // "Hello" (브라우저)
console.log(global.globalVar); // "Hello" (Node.js)

- 전역 컨텍스트에서 정의된 변수는 window 객체(브라우저) 또는 global 객체(Node.js)에서 접근 가능

 

 

 

 

let과 const는 전역 객체의 속성이 되지 않음

이 차이가 생기는 이유는 "ES6의 스코프 관리 방식" 때문

var name = "Alice";
console.log(window.name); // "Alice"

var는 변수 환경(Variable Environment)에 등록되며, window 객체에 자동으로 추가되어
이 때문에 예기치 않은 값 덮어쓰기 같은 위험이 발생할 수 있음

 

let age = 25;
console.log(window.age); // undefined

let과 const는 전역 실행 컨텍스트의 "렉시컬 환경(Lexical Environment)"에 등록되며, 전역 객체의 속성이 되지 않음
이 덕분에 var처럼 전역 네임스페이스 오염을 일으키지 않도록 보호할 수 있음

 

 


실행 컨텍스트(Execution Context)란?

- JavaScript 코드가 실행되는 환경을 의미하며, 실행될 코드에 대한 정보를 관리

- 크게 전역 컨텍스트와 함수 컨텍스트 존재

- 자바스크립트 파일 실행하는 순간 하나의 전역 컨텍스트 생성

- 자바스크립트 코드의 각 함수 실행할 때마다 함수 컨텍스트 생성

function func1() {
    function func2() {
        console.log('func2 호출');
    }
    func2();
    console.log('func1 호출');
}
func1();

 

 

 

 

위 예제에 대한
콜 스택에 실행 컨텍스트 쌓이는 과정

 

 

 

 

 

 

실행 컨텍스트 구성 요소

1. 변수 환경 (Variable Environment)

- 선언된 변수 및 함수 정보 저장

 

 

2. 렉시컬 환경 (Lexical Environment)  

- 스코프와 클로저를 이해하는 핵심 개념

- 변수와 함수의 선언 위치(렉시컬 스코프)에 따라 변수 접근 범위가 결정되는 구조

- 스코프 및 this 바인딩 관리

- * 환경 레코드(Environment Record)와 ** 외부 렉시컬 환경(Outer Lexical Environment Reference)로 나뉨


* 현재 실행 컨텍스트에서 선언된 변수와 함수가 저장(var, let, const, 함수 선언문 등이 포함)

** 상위 스코프(부모 스코프)의 렉시컬 환경을 참조(스코프 체인(Scope Chain) 이 형성)

스코프란?
- 변수와 함수가 접근할 수 있는 범위
전역 스코프
- 코드 어디서든 접근 가능

함수 스코프
- 함수 내부에서만 접근 가능(var)

블록 스코프
- {} 내부에서만 접근 가능 (let, const 사용 시)

 

 

 

예시) 렉시컬 환경의 동작 방식

실행 시 렉시컬 환경 구조
1. 전역 실행 컨텍스트 생성

- outer 함수가 window(전역 객체)에 등록


2. outer 함수 실행 컨텍스트 생성

- 환경 레코드: { a: 10, inner: function }

- 외부 렉시컬 환경: 전역 환경


3. inner 함수 실행 컨텍스트 생성
- 환경 레코드: { b: 20 }
- 외부 렉시컬 환경: outer 함수의 환경

=> 결과적으로, inner 함수는 a에 접근 가능하지만, outer는 b에 접근 불가!

function outer() {
  let a = 10;

  function inner() {
    let b = 20;
    console.log(a); // ✅ 내부 함수에서 외부 변수 접근 가능 (스코프 체인)
  }

  inner();
}

outer();

 

 

스코프 체인(Scope Chain)
- 자바스크립트에서 변수를 찾는 과정
- 자바스크립트에서 변수를 찾을 때 현재 스코프 → 부모 스코프 → 전역 스코프 순서로 검색

var globalVar = "나는 전역 변수";

function outer() {
  var outerVar = "나는 outer 함수의 변수";

  function inner() {
    var innerVar = "나는 inner 함수의 변수";

    console.log(innerVar); // ✅ inner 함수 내부에서는 접근 가능
    console.log(outerVar); // ✅ 부모(outer) 함수의 변수 접근 가능
    console.log(globalVar); // ✅ 전역 변수 접근 가능
  }

  inner();
  console.log(innerVar); // ❌ ReferenceError: innerVar is not defined
}

outer();
console.log(outerVar); // ❌ ReferenceError: outerVar is not defined​


- inner() 함수는 outer() 내부에 있으므로 outerVar를 참조할 수 있다.
- outer() 함수는 전역 스코프에 선언되었으므로, globalVar를 참조할 수 있다.
- 하지만 outer() 내부에서 innerVar를 참조하려 하면 에러가 발생한다.
→ innerVar는 inner() 내부에서만 접근 가능하기 때문!



 

 

 

 

3. this 바인딩

- 실행 컨텍스트 내에서 this가 가리키는 대상

전역 컨텍스트  |  this → window (브라우저) / global (Node.js)
함수 호출         | this → undefined (strict mode), 전역 객체 (non-strict mode)객체
메서드 호출     |  this → 해당 객체
화살표 함수     | this → 부모 컨텍스트의 this

* 화살표 함수는 자신만의 this를 가지지 않으며, 선언된 스코프 this 값 그대로 사용


const obj = {
  name: "Alice",
  getName: function () {
    console.log(this.name);
  }
};
obj.getName(); // "Alice"

const arrowFunc = () => console.log(this);
arrowFunc(); // window (브라우저 환경)​

 

 

 


전역 변수의 문제점

- 전역 컨텍스트에서 선언한 변수는 왜 이렇게 위험할까?

- var, let, const 없이 선언하면(전역 컨텍스트에서 변수 선언하면) 전역 객체에 속하게 되어, 원치 않는 전역 오염이 발생할 수 있음

  • 예상치 못한 값 변경
console.log(a); // undefined
var a = 10;
if (true) {
  var x = 10;
}
console.log(x); // 10 (블록 {} 내부에서 선언했지만 외부에서도 접근 가능)
  • 네임스페이스 오염
네임스페이스
- 이름 충돌을 방지 하기 위해 사용되는 개념
- 여러 개의 변수, 함수, 클래스 등이 같은 이름을 가질 수 있도록 구분된 영역
var username = "Alice";
console.log(window.username); // "Alice"
  • 메모리 누수 가능성 증가
// 전역 변수는 앱이 종료될 때까지 메모리에 남아 있기 때문에,
// 필요 없는 데이터도 계속 유지되어 메모리 누수가 발생할 가능성이 커짐

var bigArray = new Array(1000000).fill("🚀");

function doSomething() {
  console.log("Processing...");
}

doSomething();
// bigArray가 더 이상 필요 없지만, 전역 변수라 메모리에서 사라지지 않음

- 이런 문제를 방지하려면 지역 변수로 선언하거나, 필요 없을 때 명시적으로 null로 설정하는 것이 좋아.

 

 

// 이벤트 리스너로 인한 메모리 누수
var element = document.getElementById("btn");

function handleClick() {
  console.log("버튼 클릭!");
}

element.addEventListener("click", handleClick);

// 전역 변수로 element를 계속 유지하면, DOM이 제거되어도 메모리에서 해제되지 않음
// document.body.removeChild(element); (버튼이 사라져도 여전히 메모리에 남아 있음)


// 해결 방법: 이벤트 리스너를 제거하거나, 필요 없는 전역 변수를 null로 설정!
element.removeEventListener("click", handleClick);
element = null;

 

  • 다른 코드와 충돌 가능성 증가

var config = { theme: "dark" };

function init() {
  var config = { theme: "light" }; // 지역 변수로 사용해야 하는데, 같은 이름의 전역 변수를 덮어씀
}

init();
console.log(config.theme); // "dark"가 기대되지만, 실수로 변경될 가능성이 있음

- 이런 문제를 방지하려면 전역 변수를 최대한 줄이고, 모듈화하여 관리하는 것이 중요

 

  • 디버깅 어려움 증가
// 전역 변수가 많아지면, 어떤 함수가 값을 변경했는지 추적하기 어려움
// 특히 비동기 코드에서 전역 변수를 조작하면, 예측할 수 없는 동작이 발생할 수 있음

var counter = 0;

setTimeout(() => {
  counter += 1;
  console.log(counter);
}, 1000);

counter += 2;
console.log(counter); // 예상치 못한 값 출력 가능

- 클로저, 모듈 패턴, 또는 상태 관리를 사용해 전역 변수를 최소화

- 함수 내부 변수, 모듈화, IIFE (즉시 실행 함수) 등을 사용하면 더 안전한 코드가 될 수 있음

 

 

 

 

 

③ 클로저 (Closure)

- 내부 함수가 외부 함수의 변수에 접근할 수 있도록 하는 개념

- 내부 함수 반환 후에도 외부 함수 변수 유지 가능

function outer() {
  let count = 0;
  return function inner() {
    count++;
    console.log(count);
  };
}

const counter = outer();
counter(); // 1
counter(); // 2
counter(); // 3

내부 함수 inneroutercount 변수에 접근하며, outer 실행 후에도 count 값을 기억

 

 

 

 

 

특정 기능에서 내부 상태를 유지해야 하는 경우가 있습니다.
만약 카운트를 유지하면서 값을 증가시키는 함수를 만들고 싶다면, 전역 변수 없이 어떻게 구현할 수 있을까요?"






 

클로저 활용 예제: 데이터 은닉

function createCounter() {
  let count = 0;
  return {
    increment: function () {
      count++;
      console.log(count);
    },
    getCount: function () {
      return count;
    }
  };
}

const counterObj = createCounter();
counterObj.increment(); // 1
console.log(counterObj.getCount()); // 1

위처럼 클로저를 활용하면 데이터 은닉을 통해 변수에 직접 접근하지 못하도록 보호할 수 있음

- 내부 함수 inner()는 outer()의 변수 count에 접근할 수 있다.

- 하지만 외부에서 count를 직접 변경할 수는 없다!
- 이런 방식으로 데이터를 은닉할 수 있다.

 

 

 

 

 

클로저 활용 예제: 이벤트 핸들러 유지

function attachEventHandler() {
  let clickCount = 0;
  document.getElementById("btn").addEventListener("click", function () {
    clickCount++;
    console.log("Button clicked", clickCount, "times");
  });
}
attachEventHandler();

 

 

 

 

 

API 요청 횟수 제한 (Throttle 적용)

function throttle(func, delay) {
  let lastCall = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func(...args);
    }
  };
}

window.addEventListener("resize", throttle(() => {
  console.log("Resizing...");
}, 500));



 

 

 

 

3. 마무리 & 실무에서 활용 방법

호이스팅 이해로 버그 예방

- var 대신 let과 const를 사용하여 예기치 않은 undefined 오류 방지
전역 컨텍스트 최소화

- 전역 변수 사용을 줄이고, 모듈 패턴 또는 IIFE(즉시 실행 함수) 활용
클로저 활용

- 데이터 은닉을 통한 보안 강화
- 이벤트 핸들러에서 상태 유지

- API 요청 횟수 제한(디바운싱 & 스로틀링)

 

데이터 은닉(Data Hiding)
객체 지향 프로그래밍(OOP)에서 중요한 개념
객체의 내부 상태나 데이터를 외부에서 직접 접근하지 못하도록 하는 것
주로 클로저를 활용하여, 외부에서는 데이터를 수정하거나 읽을 수 없게 하여 캡슐화를 달성

데이터 은닉의 이유
1. 보안성 향상
- 데이터가 외부에서 수정되지 않도록 막을 수 있음
- 이를 통해 불필요한 데이터 변경이나 악의적인 수정으로부터 보호할 수 있습니다.
- 예를 들어, 사용자의 계좌 정보와 같은 중요한 데이터는 외부에서 직접 접근할 수 없어야 하며, 이 데이터는 오직 특정한 함수나 메서드를 통해서만 변경되거나 조회될 수 있도록 제한

2. 인터페이스 단순화
- 내부 구현을 외부에 숨길 수 있기 때문에, 외부에서 필요한 메서드만 제공하고 나머지 복잡한 구현은 감추어 효율적인 사용이 가능
- 예를 들어, 사용자가 객체의 상태를 수정하는 방법에 대해 알 필요 없이, setBalance()와 같은 메서드만 제공해 내부적으로 데이터 검증과정을 처리하도록 할 수 있음

3. 상태 관리 용이
- 데이터가 은닉되면 상태 변경을 추적하고 관리하기가 용이
- 객체의 속성을 변경할 때마다 로직을 통제할 수 있으므로, 예기치 않은 동작을 방지
- 예를 들어, 객체의 상태가 바뀔 때마다 이벤트를 발생시키거나 유효성 검사를 수행할 수 있음

4. 불변성 유지
- 데이터 은닉을 사용하면 불변성을 유지할 수 있음
- 외부에서 직접 수정할 수 없게 하여, 데이터가 의도치 않게 변경되는 문제를 예방할 수 있음
- 예를 들어, 객체 내부에서만 상태를 변경하고 외부에서는 그 상태를 읽기만 하도록 제한

 

디바운싱(Debouncing)
- 일정 시간이 지나야 함수를 실행하는 방식
- 연속된 이벤트 중 마지막 호출만 실행
- 검색창 자동완성, 입력 필터링에서 사용

function debounce(func, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => func(...args), delay);
  };
}

// API 요청 함수
function fetchData(query) {
  console.log(`API 요청: ${query}`);
}

const debouncedFetch = debounce(fetchData, 1000);

debouncedFetch("apple");  // 요청 X
debouncedFetch("banana"); // 요청 X
debouncedFetch("carrot"); // 1초 후 'API 요청: carrot' 실행​

- 입력이 멈춘 후 1초가 지나야 실행됨
→ 불필요한 요청을 방지할 수 있어
- 연속 입력 시 이전 요청이 취소됨

 

스로틀링(Throttling)
- 일정 간격으로만 함수를 실행하는 방식
- 이벤트가 계속 발생해도 일정한 간격으로 실행
- 주로 스크롤 이벤트, 버튼 클릭 방지, 네트워크 요청 제한에 사용
function throttle(func, limit) {
  let lastCall = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      lastCall = now;
      func(...args);
    }
  };
}

// API 요청 함수
function fetchData(query) {
  console.log(`API 요청: ${query}`);
}

const throttledFetch = throttle(fetchData, 2000);

throttledFetch("apple");  // 'API 요청: apple' 실행
throttledFetch("banana"); // 2초 내 요청 무시
throttledFetch("carrot"); // 2초 후 'API 요청: carrot' 실행​

- 2초에 한 번만 실행됨 → 과도한 요청 방지.
- 연속 입력해도 일정한 간격으로만 실행됨.

 

 

 

 

 

 

 

 

결론: 전역 변수를 최소화하자!
  • 렉시컬 환경을 이해하고, 전역 변수를 줄이자.
  • 전역 객체(window, global)에 변수를 직접 할당하지 말자.
  • 객체, 클로저, 모듈 시스템(ES6 import/export)을 활용하자.
  • 이벤트 리스너, 타이머를 관리하여 메모리 누수를 방지하자.
  • 비동기 코드에서 전역 변수를 사용하면 예측 불가능한 결과가 발생할 수 있으니 주의하자!

 

 

 

 

 

 

 

 

 

 

반응형

'CS > Terminology' 카테고리의 다른 글

#9. this 용법  (0) 2025.03.11
#8. SSR vs CSR  (0) 2025.03.10
#6. REST API vs GraphQL  (0) 2025.03.05
#4. REST API란?  (0) 2025.02.25
npm init -y 란?  (1) 2021.08.11