인터프린트언어인 javascirpt에서 헷갈리는 클로저/호이스팅
1. 스코프 (Scope)
스코프(Scope)는 변수의 유효범위를 의미합니다. 자바스크립트 엔진이 참조할 변수를 찾아내기 위해 규칙을 정해둔 것이라고 보면 됩니다. 변수가 선언된 위치에 따라 다른 코드에서 해당 변수를 참조할 수 있는지 없는지가 결정됩니다.
글로벌 스코프 (Global Scope) 코드 어디서든 참조할 수 있는 영역입니다.
// 전역 변수 선언 (글로벌 스코프)
let globalMessage = "나는 어디서든 보입니다.";
function printMessage() {
// 함수 내부에서도 전역 변수에 접근 가능
console.log("함수 내부:", globalMessage);
}
printMessage(); // 출력: 함수 내부: 나는 어디서든 보입니다.
if (true) {
// 블록 내부에서도 전역 변수에 접근 가능
console.log("블록 내부:", globalMessage);
}
console.log("전역 공간:", globalMessage); // 출력: 전역 공간: 나는 어디서든 보입니다.지역 스코프 (Local Scope) 특정 지역(함수 내부 또는 블록 내부)에서만 참조할 수 있는 영역입니다.
// 1) 함수 스코프 예시
function localFunction() {
let functionVar = "함수 내부에서만 유효합니다.";
console.log(functionVar); // 정상 작동
}
localFunction();
// console.log(functionVar);
// 에러 발생 -> ReferenceError: functionVar is not defined (외부 접근 불가)
// 2) 블록 스코프 예시 (if문, for문 등)
if (true) {
let blockVar = "if문 블록 내부에서만 유효합니다.";
const blockConst = "나도 마찬가지입니다.";
var varVariable = "나는 블록 스코프를 무시합니다."; // var는 함수 스코프를 따름
console.log(blockVar); // 정상 작동
}
console.log(varVariable); // 정상 작동 (var는 블록 외부에서도 접근 가능)
// console.log(blockVar);
// 에러 발생 -> ReferenceError: blockVar is not defined (let은 외부 접근 불가)보충: 렉시컬 스코프 (Lexical Scope) 자바스크립트는 함수를 어디서 호출했는지가 아니라, 어디서 선언했는지에 따라 상위 스코프를 결정하는 '렉시컬 스코프'를 따릅니다. 이 개념은 뒤에 나오는 클로저의 근간이 됩니다.
let text = "글로벌 텍스트";
function targetFunction() {
// targetFunction은 전역(글로벌 스코프)에 선언되었습니다.
// 따라서 이 함수의 상위 스코프는 전역 스코프이며, 전역 변수 text를 참조합니다.
console.log(text);
}
function callerFunction() {
let text = "지역 텍스트";
// targetFunction을 지역 변수 text가 있는 이곳에서 호출합니다.
targetFunction();
}
callerFunction(); // 출력 결과: "글로벌 텍스트"2. 호이스팅 (Hoisting)
호이스팅(Hoisting)은 코드가 실행되기 전, 자바스크립트 엔진이 선언문을 해당 스코프의 최상단으로 끌어올리는 것처럼 동작하는 현상입니다. 실제 코드가 이동하는 것은 아니며, 컴파일 단계에서 메모리에 변수와 함수를 먼저 등록하기 때문에 발생합니다.
1) 변수 호이스팅 (Variable Hoisting)
변수는 선언 방식(var, let, const)에 따라 호이스팅 시 동작 방식이 완전히 달라집니다.
var선언과 초기화(undefined)가 동시에 이루어집니다. 따라서 선언문 이전에 변수에 접근해도 에러가 나지 않고undefined를 반환합니다.let****,const선언은 호이스팅되지만 초기화는 이루어지지 않습니다. 선언문이 나오기 전까지는 TDZ(Temporal Dead Zone, 일시적 사각지대)에 갇히게 되므로 접근 시 참조 에러(ReferenceError)가 발생합니다.
예시 코드
// var의 경우
console.log(varValue); // undefined (에러가 발생하지 않음)
var varValue = "var 변수";
// let, const의 경우
console.log(letValue); // ReferenceError: Cannot access 'letValue' before initialization
let letValue = "let 변수";2) let, const에서 TDZ (Temporal Dead Zone, 일시적 사각지대)란?
TDZ(Temporal Dead Zone, 일시적 사각지대)는 스코프의 시작 지점부터 변수의 선언 지점까지 변수를 참조할 수 없는 구간을 의미합니다.
자바스크립트에서 let과 const로 선언된 변수는 이 TDZ의 영향을 받으며, 선언되기 전에 변수에 접근하려고 하면 ReferenceError(참조 에러)가 발생합니다.
TDZ가 발생하는 원인: 변수의 생성 단계
자바스크립트에서 변수는 총 3가지 단계(선언 ➔ 초기화 ➔ 할당)를 거쳐 생성됩니다.
-
선언 단계 (Declaration phase): 자바스크립트 엔진에 변수 객체를 등록합니다.
-
초기화 단계 (Initialization phase): 메모리 공간을 확보하고 변수에
undefined를 할당합니다. -
할당 단계 (Assignment phase): 사용자가 입력한 실제 값을 변수에 저장합니다.
var키워드: 호이스팅이 일어날 때 [선언 단계]와 [초기화 단계]가 동시에 이루어집니다. 따라서 선언문 이전에 호출해도undefined로 접근이 가능합니다.let****,const키워드: 호이스팅 시 [선언 단계]만 이루어집니다. [초기화 단계]는 실제 코드에서let/const선언문에 도달했을 때 수행됩니다.
예시 코드
// 스코프 시작
console.log(value); // ReferenceError: Cannot access 'value' before initialization
// -------------------- 여기까지가 value 변수의 TDZ (일시적 사각지대) --------------------
let value = "정상 초기화 완료"; // 여기서 [초기화 단계] 및 [할당 단계]가 일어남
console.log(value); // 출력: "정상 초기화 완료" (TDZ 종료 후이므로 정상 작동)3) let/const를 사용하는 추세 (var로 인한 코드 더티 현상)
var 키워드는 다음과 같은 치명적인 단점들로 인해 스파게티 코드를 유발하고 디버깅을 어렵게 만듭니다.
- 블록 스코프 무시:
var는 함수 스코프만 따르기 때문에if문이나for문 안에서 선언한 변수가 블록 외부에서도 살아남아 전역 변수를 오염시킵니다. - 중복 선언 허용: 이미 존재하는 변수를 다시 선언해도 에러를 내지 않고 값을 덮어써 버립니다.
이러한 문제를 방지하기 위해 엄격한 블록 스코프를 지원하고 코드의 예측 가능성을 높여주는
let과const를 사용하는 것이 모던 자바스크립트의 표준이 되었습니다. 정적 타입을 지원하는 타입스크립트(TypeScript) 환경에서도var사용은 금기시되며, 컴파일 단계에서 변수 생명주기를 안전하게 관리하기 위해let과const사용을 강제하고 있습니다.
4) 함수 호이스팅 (Function Hoisting)
함수는 정의하는 방식(선언식 vs 표현식)에 따라 호이스팅 결과가 완전히 다릅니다.
- 함수 선언식: 함수 전체가 호이스팅되므로 선언문 이전에도 호출이 가능합니다.
- 함수 표현식: 변수에 함수를 할당하는 형태이기 때문에 함수가 아닌 변수 호이스팅 규칙을 따릅니다.
예시 코드
// 함수 선언식
sayHello(); // "Hello!" (정상 작동)
function sayHello(){
console.log("Hello!");
}
// 함수 표현식
sayHi(); // TypeError: sayHi is not a function (var 변수 규칙을 따르므로 현재 undefined 상태)
var sayHi = function(){
console.log("Hi!");
};(만약 변수 표현식을 *let*이나 *const*로 선언했다면 TypeError 대신 *ReferenceError*가 발생합니다.)
3. 클로저 (Closure)
클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여, 만일 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 스코프에 접근할 수 있는 함수를 말합니다.
1) 대표적인 코드
클로저는 상태를 안전하게 은닉하고 유지해야 할 때(예: 카운터) 가장 많이 사용됩니다.
function createCounter(){
let count = 0; // 외부함수의 변수 (은닉화됨)
return function(){
count++; // 내부함수가 외부함수의 count 변수를 기억하고 제어함
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2 (createCounter 호출이 끝났음에도 count 상태가 유지됨)2) 헷갈릴 수 있는 코드
비동기 처리나 반복문 안에서 var를 사용할 때 클로저와 호이스팅 메커니즘이 얽혀 의도치 않은 결과가 나오는 대표적인 상황입니다.
// 헷갈리기 쉬운 예시 (var 사용)
for (var i = 0; i < 3; i++) {
setTimeout(function(){
console.log(i); // 기대값: 0, 1, 2 -> 실제 결과: 3, 3, 3
}, 100);
}
// 원인: var i는 함수 스코프(여기서는 전역)이므로 100ms 뒤에 실행될 때 이미 i는 3이 되어 있습니다.
// 올바른 해결책 (let 사용)
for (let j = 0; j < 3; j++) {
setTimeout(function(){
console.log(j); // 결과: 0, 1, 2
}, 100);
}
// 원인: let은 블록 스코프를 가지므로, 반복문이 돌 때마다 새로운 렉시컬 환경이 생성되어 각각의 j 값을 클로저가 기억합니다.3) 예외 상황인 코드
모든 내부 함수가 클로저를 형성하는 것은 아닙니다. new Function 문법을 사용해 함수를 생성하면 렉시컬 환경이 아닌 무조건 전역 스코프를 참조하게 되므로 클로저가 생성되지 않는 예외가 발생합니다.
let value = "global";
function getFunc(){
let value = "local";
// new Function은 자신이 서술된 곳의 로컬 스코프를 기억하지 못합니다.
let func = new Function('return value;');
return func;
}
const run = getFunc();
console.log(run()); // "global" (local 환경을 기억하지 못하고 전역의 value를 참조함)
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.