일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 백준
- HTTP
- 코드스테이츠
- 코어 자바스크립트
- 정재남
- 리트코드
- 회고
- 리액트
- python
- LeetCode
- 리덕스
- js
- 손에 익히며 배우는 네트워크 첫걸음
- til
- 타입스크립트
- 제로초
- 타임어택
- 2주 프로젝트
- javascript
- 파이썬
- 렛츠기릿 자바스크립트
- programmers
- SQL 고득점 Kit
- 자바스크립트
- 토익
- 4주 프로젝트
- 알고리즘
- codestates
- 프로그래머스
- 타입스크립트 올인원
- Today
- Total
Jerry
#7. 호이스팅, 전역 컨텍스트, 클로저 본문
"자바스크립트를 사용하다 보면 변수와 함수가 예상과 다르게 동작하는 경우를 본 적이 있을 겁니다.
예를 들어 변수 선언 전에 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
- 내부 함수 inner는 outer의 count 변수에 접근하며, 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 |