JavaScript는 ES6 이후 큰 변화가 있었습니다.
기존의 var
키워드를 사용한 함수 레벨 스코프에서 let
과 const
가 도입되며 블록 레벨 스코프를 지원하게 되었죠.
함수 레벨 스코프 vs 블록 레벨 스코프
📌 함수 레벨 스코프(Function-level Scope)
기존 JavaScript는 함수 단위로 변수를 관리했습니다. 즉, var
로 선언된 변수는 함수 내부에서만 유효하며, {}
같은 블록 내부에서는 영향을 받지 않았습니다.
var i = 10;
for (var i = 0; i < 5; i++) {
console.log(i); // 0 1 2 3 4
}
console.log(i); // 5
블록 레벨 스코프를 지원하는 프로그래밍 언어에서는 for 문에서 반복을 위해 선언된 i 변수가 for 문의 코드 블록 내에서만 유효한 지역 변수지만, var 키워드로 선언된 변수는 블록 레벨 스코프를 인정하지 않기 때문에 i 변수는 전역 변수가 됩니다.
따라서 전역 변수 i는 중복 선언되고 그 결과 의도치 않은 전역 변수의 값이 재할당됩니다.
💡 이 문제를 해결하기 위해 ES6에서 let
과 const
를 도입하여 블록 레벨 스코프를 지원하게 되었습니다.
📌 블록 레벨 스코프(Block-level Scope)
let
과 const
키워드는 블록{}
내부에서만 유효합니다.
즉, if
, for
, while
등의 코드 블록 내부에서 선언된 변수는 해당 블록을 벗어나면 접근할 수 없습니다.
let j = 10;
for (let j = 0; j < 5; j++) {
console.log(j); // 0 1 2 3 4
}
console.log(j); // 10 (for 블록 내부의 j와 다름)
이제 for
문에서 선언된 j
는 블록 {}
내부에서만 유효하며, 블록을 벗어나면 원래의 j=10
을 유지합니다.
🎯 함수 레벨 vs 블록 레벨 스코프 비교
스코프 | 변수 키워드 | 유효 범위 | 특징 |
함수 레벨 스코프 | var | 변수가 선언된 함수 내부 전체 | 블록 {} 내부에서도 유지됨 |
블록 레벨 스코프 | let, const | 변수가 선언된 {} 내부에서만 유효 | 블록을 벗어나면 제거됨 |
렉시컬 스코프 (Lexical Scope)
JavaScript는 렉시컬 스코프(Lexical Scope, 정적 스코프)를 따릅니다.
즉, 함수를 어디서 호출했는지가 아니라, 함수를 어디서 정의했는지에 따라 상위 스코프를 결정합니다.
- 함수가 실행될 때가 아니라, 함수가 정의될 때 상위 스코프가 결정됩니다.
- 따라서 함수를 어디서 호출했는지는 상위 스코프 결정에 영향을 주지 않습니다.
var x = 1;
function first() {
var x = 10;
second();
}
function second() {
console.log(x);
}
first(); // 1
second(); // 1
위의 예제의 실행 결과는 second 함수의 상위 스코프가 무엇인지에 따라 결정됩니다.
1. 함수를 어디서 호출했는지
2. 함수를 어디서 정의했는지
첫 번째 방식은 동적 스코프(dynamic scope)로 second 함수의 상위 스코프는 first함수의 지역 스코프와 전역 스코프일 것입니다.
두 번째 방식은 정적 스코프(static scope)로 second 함수의 상위 스코프는 전역 스코프일 것입니다.
이처럼 함수의 상위 스코프는 함수 정의가 실행될 때 정적으로 결정됩니다. 함수 정의(함수 선언문 또는 함수 표현식)가 실행되어 생성된 함수 객체는 이렇게 결정된 상위 스코프를 기억합니다.
위 예제에서 second()
내부에는 x
가 없으므로 상위 스코프에서 x를 찾습니다. 렉시컬 스코프를 따르므로, second 함수의 상위 스코프는 전역 스코프가 되어 console.log(x);
는 전역 변수 x = 1
를 출력합니다.
만약, 동적 스코프를 따랐다면 second()
를 first()
내부에서 호출했으므로, 상위 스코프가 first()
의 지역 스코프가 되어 x = 10
이 출력되었을 것입니다.
클로저(Closure)란?
JavaScript를 사용하다 보면 외부 함수의 변수를 내부 함수에서 참조할 수 있는 상황을 자주 접하게 됩니다.
예를 들어, 함수 실행이 끝난 후에도 내부 함수가 이미 종료된 외부 함수의 변수에 접근하는 경우가 있죠.
이러한 개념을 클로저(Closure)라고 합니다.
MDN에서는 클로저를 다음과 같의 정의하고 있습니다.
" A closure is the combination of a function and the lexical environment within which that function was declared."
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
간단한 예시로 살펴보면,
const x = 1;
// ①
function outer() {
const x = 10;
const inner = function() { console.log(x); }; // ②
return inner;
}
// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer(); // ③
innerFunc(); // ④ 10
1️⃣ outer() 함수가 호출되면서, 내부에서 inner 함수가 생성됩니다.
2️⃣ inner 함수는 console.log(x);를 포함하며, x 변수에 접근하고 있습니다.
3️⃣ outer() 함수는 inner 함수를 반환한 후 실행이 종료됩니다.
- 즉, outer 함수의 실행 컨텍스트가 제거되면서, 일반적으로 x = 10도 메모리에서 사라져야 합니다.
4️⃣ 하지만 innerFunc()를 실행하면 여전히 x = 10이 유지되어 10이 출력됩니다.
== inner 함수는 outer 함수가 종료된 후에도 x = 10을 기억하고 있습니다.
이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있습니다. 이러한 중첩 함수를 클로저(closure)라고 부릅니다.
📌 클로저가 중요한 이유
클로저는 단순히 "외부 함수의 변수를 기억하는 내부 함수" 이상의 역할을 하고 있습니다.
데이터 은닉 (Encapsulation)
우리는 JavaScript에서 객체를 사용하여 데이터를 관리할 수 있지만,
객체의 속성을 외부에서 직접 수정하는 것을 막고 싶을 때가 있습니다.
const user = {
name: "Jeong",
password: "1234"
};
console.log(user.password); // "1234" (외부에서 쉽게 접근 가능)
user.password = "hacked"; // 외부에서 쉽게 수정 가능
console.log(user.password); // "haked"
이처럼 객체 속성을 아무나 수정할 수 있다면 보안이 취약해집니다.
클로저를 사용하면 데이터 은닉을 통해 외부 접근을 차단할 수 있습니다.
function createUser(name, password) {
let _password = password; // private 변수처럼 동작
return {
getName: function () {
return name;
},
verifyPassword: function (input) {
return input === _password;
}
};
}
const user = createUser("Jeong", "secure123");
console.log(user.getName()); // "Jeong"
console.log(user.verifyPassword("wrong")); // false
console.log(user.verifyPassword("secure123")); // true
console.log(user._password); // undefined (외부에서 접근 불가능)
✅ password는 createUser 내부에 선언된 변수이므로, 외부에서 직접 접근할 수 없습니다.
✅ verifyPassword 메서드만을 통해서만 비밀번호 확인이 가능하도록 설계되었습니다.
✅ 클로저를 활용하여 데이터를 보호하면서도, 필요한 기능만 외부에 제공할 수 있습니다.
상태 유지
일반적인 함수는 호출될 때마다 초기화되기 때문에,
어떤 값을 유지하면서 업데이트해야 할 경우 매번 외부 변수에 값을 저장해야 합니다.
let counter = 0;
function increment() {
counter++;
console.log(counter);
}
increment(); // 1
increment(); // 2
increment(); // 3
하지만 이 방식은 counter가 전역 변수라서, 예상치 않은 곳에서 변경될 위험이 있습니다.
function createCounter() {
let count = 0; // 외부에서 접근 불가능한 변수
return {
increment: function () {
count++;
console.log(count);
},
decrement: function () {
count--;
console.log(count);
},
reset: function () {
count = 0;
console.log("Counter reset to 0");
}
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
counter.reset(); // "Counter reset to 0"
✅ 클로저를 사용하여 count를 외부에서 직접 변경할 수 없도록 보호합니다.
✅ 함수를 실행할 때마다 count가 새로 초기화되지 않고 유지됩니다.
✅ 이 방식은 리액트의 useState() 같은 상태 관리에서도 사용됩니다.
이 외에도 사용하는 관점에 있어 유용하지만,
너무 많은 클로저를 생성하면 메모리 누수가 발생할 수 있으므로 적절하게 활용해야 합니다.
'FrontEnd > JavaScript' 카테고리의 다른 글
[JavaScript] 스코프(scope) : 전역/지역 스코프, 식별자 결정과 스코프 체인 (0) | 2025.02.23 |
---|