[JS Study #03] 스코프와 클로저, JS의 실행 맥락 이해하기
🚀 시작하며
코드를 짜다 보면 let
, const
, var
가 도대체 어떤 범위에서 동작하는지 헷갈릴 때가 많습니다.
또 함수 안에 함수, 또는 비동기 콜백 안에서 값을 찾기 어려운 경험도 있습니다.
이 글에서는 제가 헷갈렸던 스코프와 클로저에 대해서 생각하고 고민하며 개념을 정리하려고 합니다.
스코프(Scope)란?
스코프는 변수가 어디까지 유효한지 검사하는 범위를 말합니다. 쉽게 말해서 변수가 살아있는 영역이며, 어디서 이 변수에 접근할수 있는가를 말할 수 있겠습니다.
스코프의 종류
구분 | 설명 | 예시 |
---|---|---|
전역 스코프(Global Scope) | 코드 전체에서 접근 가능 | var a = 1 |
함수 스코프(Function Scope) | 함수 안에서 선언된 변수는 그 함수 안에서만 유효 | function f() {var x = 1;} |
블록 스코프(Block Scope) | {} 중괄호 내부에서만 유효(let , const ) |
if (true){ let x = 1;} |
-
예제
function test(){ if (true){ var a = 1; let b = 2; } console.log(a); //1 console.log(b); //ReferenceError! }
여기서 ReferenceError가 나는 이유는
var
는 함수 전체에 걸쳐 살아있고let
과const
는 블록{}
안에서만 살아있습니다.
❓왜 var
는 함수 스코프고, let
과 const
는 블록 스코프일까??
자바스크립트가 처음 만들어졌을 때는 함수 단위로 스코프를 나누는 var
만 존재했습니다.
당시 자바스크립트는 간단한 스크립트 언어였고 복잡한 블록 구조나 클래스 설계를 고려하지 않았기 때문에 함수 하나 단위로만 스코프를 나누는 단순한 구조인 것이죠.
이때, 문제가 발생합니다.
for (var i = 0; i < 3; i++){
setTimeout(() => console.log(i),100);
}
>>> 3, 3, 3
이런 반복문 같은 곳에서 선언된 변수를 예로 들었을 때, var
는 함수 전체에서 유효하기 때문에 블록 내 반복마다 다른 i
를 만들지 못해서 스코프를 블록처럼 생각을하고 코드를 짰을 때, 버그가 나는 경우가 증가하게 됩니다.
ECMAScript6 (ES6, 2015) 이후 let
과 const
도입
ES6이후 자바스크립트는 블록 스코프를 도입합니다.
그 이유는 크게 3가지 정도인데, 첫 번째, 실제 코드 구조와 개발자의 구상이 어긋 나는 것을 해결하기 위함입니다.
두 번째는 함수가 아닌 조건문, 반복문, try-catch 블록 등에서 변수 제한이 필요하기 때문입니다.
세번째는 const
의 불변성과 스코프 격리를 통해 코드 안정성을 확보하기 위해서 입니다.
스코프 체인(Scope Chain)
변수를 찾을 때 가장 가까운 스코프부터 바깥으로 차례차례 탐색하는 것을 말합니다.
const x = 1;
function outer() {
const x = 2;
function inner() {
console.log(x); //2
}
inner();
}
outer();
inner()
가 x
를 찾고 자신의 스코프에 없으니, outer()
에서 찾습니다.
렉시컬 스코프(Lexical Scope)란?
렉시컬(Lexical)이란 “코드를 작성한 위치”를 의미합니다.
렉시컬 스코프는 함수가 어디서 정의되었는지에 따라 상위 스코프가 정해진다는 개념입니다.
const a = 1;
function outer() {
const a = 2;
function inner() {
console.log(a);
}
return inner;
}
const func = outer();
func(); // 2
이 예제에서 inner()
함수는 어디서 실행되었든 상관없이 자신이 “정의된 위치”인 outer()
스코프에서 a = 2
를 찾습니다. → 즉, 실행 위치가 아니라, 정의된 위치가 중요하다는 것!
클로저(Closure)란?
클로저는 함수가 선언될 당시의 스코프를 기억하는 함수입니다. 즉, 함수가 종료되어도, 그 함수가 선언된 외부 스코프에 접근이 가능합니다.
function outer(){
const name = "언박스";
return function inner(){
console.log(`안녕, ${name}`);
};
}
const greet = outer();
greet();
>>> 안녕, 언박스
name
은 outer()
에 선언된 변수이고, outer()
는 이미 종료됐지만, inner()
는 name
을 기억하는 것이 클로저인것이죠.
클로저 = 기억하는 함수
function counter() {
let count = 0;
return function () {
count++;
console.log(count)
};
}
const clickCount = counter();
clickCounter(); //1
clickCounter(); //2
clickCounter(); //3
const
는 외부에서 접근이 불가능 해서 counter()
함수 밖에서 그대로 count
에 접근하려고 하면 불가능합니다. 그러나 내부 함수는 계속 기억하고 있기 때문에 상태가 유지됩니다.
실전에서 클로저가 중요한 이유
🔁 1. 반복문과 클로저 (setTimeout 루프 문제)
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 항상 3 출력
}, 100);
}
var
는 함수 스코프라서 동일한 i
를 공유합니다.
➡️ 마지막 값 3만 출력됨
이를 해결하기 위해서는 클로저로 i
를 캡쳐해야 합니다.
for (var i = 0; i < 3; i++) {
(function(j){
setTimeout(() => console.log(j), 100);
})(i);
}
또는 ES6 let
을 사용하여 해결합니다. ➡️ 매 루프마다 새로운 i
for (let i = 0; i <3; i++){
setTimeout(()=> console.log(i),100);
}
🖱️ 2. 이벤트 핸들러 안에서 상태가 고정되는 이유
function bindClick(msg) {
return function () {
console.log("Clicked: " + msg);
}
}
const button = document.querySelector("button");
button.addEventListener("click", bindClick("헬로우"));
bindClick("헬로우")
는 실행 시점이 아니라 정의 시점의 msg
를 기억하고 있기 때문에 동작합니다.
- 여러 버튼에 다른 메시지를 주고 싶을 때 유용
💡 클로저는 상태를 숨기고 유지하는 데 탁월
- 함수 외부에서는 접근 불가능
- 내부 함수에서는 여전히 접근 가능
클로저의 사용
-
React 등 함수형 컴포넌트
useState
,useCallback
,useMemo
전부 클로저 기반입니다. -
반복문 비동기 처리
setTimeout
에서i
가 계속 바뀌는 문제를 해결합니다. -
상태 은닉
외부에서 접근 못하게 숨기고, 내부 함수로만 제어할 수 있습니다.(캡슐화)
클로저와 메모리 이슈
클로저는 필요한 값을 계속 기억하기 때문에 참조가 해제되지 않으면 메모리 누수가 발생할 수 있습니다.
function leaky() {
const bigData = new Array(1000000).fill("🧠");
return function() {
console.log(bigData[0]);
}
}
const keep = leaky();
// keep가 계속 존재 → bigData가 GC 대상에서 제외됨
→ 따라서 불필요한 클로저는 명시적으로 제거하거나 참조를 끊어야 함
오해하기 쉬운 개념
-
함수가 실행될 때마다 클로저가 만들어지는가?
❌, 함수 안에 또 다른 함수(내부 함수)가 선언되고, 그 내부 함수가 외부 변수에 접근할 때만 클로저가 됩니다.
-
클로저는 메모리 누수의 원인
누수의 원인일 수 있습니다. 참조가 계속 유지되면 가비지 컬렉션이 수거하지 못해 메모리에 계속 남아있습니다. 그래서 이를 정리하지 않으면 누수가 발생하는 것 입니다.
가비지 컬렉션(Garbage Collection)
더 이상 쓰이지 않는 메모리를 자동으로 정리해주는 자바스크립트 엔진의 기능
가비지 컬렉션 덕에 우리가 메모리를 직접 할당하거나 해제하지 않아도 됩니다.
가비지 컬렉션의 정의를 보니 더 궁금해져서 추가로 정리 하겠습니다.
가비지 컬렉션(Garbage Collection)
가비지 컬렉션은 앞에서 말했듯이 더 이상 쓰지 않는 메모리를 자동으로 정리해주는 자바스크립트의 엔진 기능입니다.
배경: 가비지 컬렉션의 필요
예전 프로그래밍 언어에서는 개발자가 직접 메모리 할당과 해제를 제어했습니다. 그래서 만약 실수로 해제하는 것을 잊는 다면 메모리 누수가 발생하게 되고, 해제를 해놓고 접근하게 되면 세그멘테이션 폴트 Segmentation Fault가 발생합니다.
세그멘테이션 폴트
프로그램이 허용되지 않는 메모리 영역에 접근하거나, 허용되지 않은 방법으로 메모리에 접근할 때 발생하는 오류로, 프로그램이 비정상적으로 종료되는 현상이 발생합니다. 세그폴트(Segfault)라고 줄여서 불리기도 함
이러한 이유로 사람이 직접 메모리 관리를 하는 것은 위험하는 논의가 생긴것 입니다.
🧑💻누가 만들었는가?
존 매카시(John McCarthy)
1959년, 함수형 언어인 LISP를 만들면서, 가비지 컬렉션이라는 개념을 최초로 도입하였습니다.
댓글남기기