WeniVooks

검색

JavaScript 에센셜

스코프와 클로저

1. 호이스팅

1.1. 호이스팅이란?
이렇게 무언가를 끌어올리는 장치를 영어로 호이스트(hoist)라고 합니다.

호이스팅은 변수나 함수 선언문이 해당 스코프의 최상단으로 끌어올려지는 현상 을 말합니다. 즉, 코드에서 변수나 함수를 선언하기 전에 해당 변수나 함수를 사용할 수 있는 것처럼 보이는 현상을 의미합니다.

console.log(a); // undefined var a = 10; sayHi(); // "Hello, weniv!" function sayHi() { console.log('Hello, weniv!'); }

이러한 현상이 발생하는 이유는 자바스크립트 엔진이 코드를 실행하기 전에 코드를 평가하는 과정을 거치기 때문입니다. 이 과정에서 변수나 함수 선언문을 찾아 먼저 실행합니다. 따라서 변수나 함수의 선언문이 해당 스코프의 최상단으로 끌어올려지는 것처럼 동작하게 됩니다.

var로 선언한 변수는 호이스팅과 함께 undefined로 초기화됩니다. 따라서 변수를 선언하기 전에 변수를 사용하면 undefined가 출력됩니다. 함수 선언문은 전체를 끌어올리기 때문에 함수를 선언하기 전에 함수를 호출할 수 있습니다.

1.2. 일시적 사각지대(Temporal Dead Zone)

비교적 최근에 등장한 let, const, class도 호이스팅이 발생하지만, undefined로 초기화되지 않아 변수를 사용할 수 없는 구간이 생깁니다. 이를 일시적 사각지대(Temporal Dead Zone) 라고 합니다. undefined로 암묵적 초기화가 일어나지 않기 때문에, 사용자가 직접 작성한 선언문 또는 할당문이 실행되기 전에 값에 접근하면 오류가 발생합니다.

console.log(y); let y = 10; sayHello(); // Error const sayHello = function() { console.log('Hello!'); };

코드 실행 과정에서 원래 선언문이 위치한 곳까지 도달했을 때 변수가 초기화되고 TDZ에서 벗어나 값을 사용할 수 있습니다.

1.3. 함수 표현식의 호이스팅

함수 표현식은 함수 선언문과 다르게 호이스팅이 발생하지 않습니다. 함수 표현식이 할당된 변수는 호이스팅되지만, 함수 표현식 자체는 호이스팅되지 않습니다. 따라서 함수 표현식을 선언하기 전에 함수를 호출하면 에러가 발생합니다.

sayHello(); const sayHello = function() { console.log('Hello!'); };

var로 선언한 변수는 함수 표현식 이전에는 undefined를 암묵적으로 가지고 있기 때문에 호출이 불가하며, let, const로 선언한 변수는 TDZ에 빠져 호출이 불가합니다. 이러한 에러를 피하기 위해서 변수나 함수를 사용하기 전에 미리 선언하는 것을 권장합니다.

2. 스코프 바인딩과 체이닝

2.1 실행 컨텍스트

코드를 실행하기 전에 코드를 평가하는 과정에서, 실행 컨텍스트(Execution Context)가 생성됩니다. 실행 컨텍스트는 코드가 실행되는 환경을 의미하며, 코드가 실행되는 동안 변수나 함수, 객체 등을 저장하고 관리합니다. 코드의 평가 과정에서 변수 선언문, 함수 선언문은 실행 컨텍스트의 렉시컬 환경에 등록됩니다.

다음 코드를 실행하면 평가 과정에서 전역 실행 컨텍스트가 생성됩니다. 이 실행 컨텍스트의 렉시컬 환경에 변수 a와 함수 func가 등록됩니다. 이후 함수 func가 실행되면 새로운 실행 컨텍스트가 생성되고, 변수 b가 함수의 렉시컬 환경에 저장됩니다. 이를 통해 변수의 스코프가 결정됩니다.

const a = 10; function func() { const b = 20; console.log(a + b); } func();

함수의 호출이 종료되면 실행 컨텍스트가 제거되고, 함수의 렉시컬 환경에 저장된 변수 b도 함께 제거됩니다. 이처럼 실행 컨텍스트는 코드의 실행 환경을 관리하고, 변수의 생명주기를 관리합니다.

2.2 스코프 체이닝

렉시컬 환경에는 식별자에 대한 정보뿐만 아니라 상위 스코프에 대한 참조도 저장합니다. 이를 통해 스코프 체이닝(Scope Chaining)을 구현하고, 변수를 찾는 과정에서 상위 스코프로 이동할 수 있습니다.

다음 코드를 실행하면 총 3개의 렉시컬 환경이 생성됩니다. 전역 변수의 렉시컬 환경에는 x가, outer 함수가 호출된 시점에서 outer 함수의 렉시컬 환경에는 y가, inner 함수가 호출된 시점에서 inner 함수의 렉시컬 환경에는 z가 저장됩니다. 함수가 호출된 위치가 아닌 선언된 위치를 기준으로 스코프가 결정됩니다.

let x = 'global'; function outer() { let y = 'outer'; function inner() { let z = 'inner'; console.log(x, y, z); } inner(); } outer();

렉시컬 환경들은 상위 스코프에 대한 참조를 저장합니다. inner -> outer -> global 순서로 참조가 저장되어 있습니다. 따라서 inner 함수에서 변수를 찾지 못한 경우 스코프 체인을 따라 상위 스코프로 이동하여 변수를 찾습니다. 가장 가까운 스코프부터 변수를 찾으며, 상위 스코프로 이동하며 변수를 탐색합니다. 같은 이름의 변수가 여러 스코프에 존재할 경우, 가장 가까운 스코프의 변수를 사용합니다.

let x = 'global'; function outer() { let x = 'outer x' let y = 'outer'; function inner() { let z = 'inner'; console.log(x, y, z); } inner(); } outer();

3. 클로저

3.1. 클로저란?

클로저(Closure)는 함수와 그 함수가 선언된 렉시컬 환경의 조합 입니다. 이는 함수가 자신이 생성될 당시의 환경을 기억하고 접근할 수 있게 해주는 메커니즘입니다. 클로저를 이용하면 내부 함수가 외부 함수의 변수에 접근할 수 있게 되어, 일종의 '폐쇄된 공간' 안의 데이터를 안전하게 다룰 수 있습니다.

코드와 함께 살펴보겠습니다. 다음 코드에서 makeAdder(5)를 호출하면 함수의 렉시컬 환경이 생성이 되고, 이곳에 x(5)와 y(1)의 값이 저장됩니다. add5는 이 렉시컬 환경을 참조합니다. 이를 통해 add5 함수는 외부 함수인 makeAdder의 변수에 접근할 수 있게 됩니다. makeAdder(10)이 호출되었을 때도 마찬가지로 별도의 함수 렉시컬 환경이 생성됩니다. add10은 이 렉시컬 환경을 참조하며, makeAdder(10)의 변수에 접근할 수 있습니다. 이렇게 참조되는 변수들은 가비지 컬렉터에 의해 제거되지 않고 계속 유지됩니다.

function makeAdder(x) { // 외부함수 let y = 1; // 폐쇄된 공간의 데이터 return function (z) { // 내부함수 return x + y + z; }; } // 클로저에 x와 y의 환경이 각각 저장됨 let add5 = makeAdder(5); let add10 = makeAdder(10); // 함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산 console.log(add5(2)); // 8 (x:8 + y:1 + z:2) console.log(add10(2)); // 13 (x:10 + y:1 + z:2)

클로저를 이용하면 함수 내부의 변수를 외부에서 접근할 수 없도록 숨기고, 함수 외부에서 함수 내부의 변수에 접근할 수 있도록 할 수 있습니다. 이를 통해 변수와 메서드를 private하게 은닉하고, 캡슐화할 수 있습니다.

Closure | MDN
function exponent(x) { function multiplier(y) { return y ** x; } return multiplier; } const square2 = exponent(2); const square3 = exponent(3); square2(10); square3(10);

자바스크립트 변수의 스코프는 함수가 선언된 위치를 기준으로 결정되므로, b()는 호출된 위치와 관계없이 상위 스코프의 값을 참조합니다. 스코프 체인을 통해 x는 전역 스코프에 있는 변수를 참조하게 됩니다.

const x = 100; function a() { const x = 1; b(); } function b() { console.log(x); } a(); // 100 b(); // 100

반면 클로저를 사용하면 다른 결과를 얻을 수 있습니다. b() 함수를 a() 함수 내에서 선언하면, a()의 스코프를 참조할 수 있습니다. 이것이 바로 클로저의 핵심 개념입니다. 함수가 선언된 위치를 기준으로 스코프가 결정되는 자바스크립트의 특성을 이용하여 외부 함수 a에서 선언된 변수를 내부 함수인 b 함수에서 참조할 수 있습니다. 외부 함수 변수의 권한을 내부 함수에게 부여하는 것이 클로저입니다.

const x = 100; function a() { const x = 1; function b() { console.log(x); } b(); } a(); // 1

다음 코드에서 createCounter는 내부 함수(클로저)를 반환하며, 반환된 함수는 createCounter의 지역 변수인 count에 접근할 수 있습니다. 클로저를 통해 count 변수를 외부에서 접근할 수 없도록 숨기고, 클로저를 통해서만 접근할 수 있도록 할 수 있습니다.

createCounter은 호출될 때마다 서로 다른 렉시컬 환경을 가지기 때문에 counter와 counter2는 별도의 렉시컬 환경을 가지며, count 변수가 서로 다른 값을 가집니다.

function createCounter() { let count = 0; return function() { count++; console.log(count); }; } const counter = createCounter(); counter(); // 1 counter(); // 2 counter(); // 3 const counter2 = createCounter(); counter2(); // 1 counter2(); // 2
4.2. 쓰로틀링(Throttling)

클로저로 자주 활용되는 패턴 중 하나로 쓰로틀링(Throttling) 이 있습니다. 쓰로틀링은 함수 호출의 빈도를 제어하는 기술입니다.

Throttling in JavaScript Easiest Explanation

일정 시간 동안 한 번만 함수를 실행하도록 하는 쓰로틀링을 구현해보겠습니다. 다음과 같이 throttle 함수를 작성할 수 있습니다. 외부에서 접근 불가한 timerFlag를 이용하여 일정 시간 동안 함수가 실행되지 않도록 합니다.

function throttle(mainFunction, delay) { let timerFlag = null; return (...args) => { if (timerFlag === null) { mainFunction(...args); timerFlag = setTimeout(() => { timerFlag = null; }, delay); } }; }

다음과 같이 스크롤 이벤트에 쓰로틀링을 적용하여, 스크롤 동작이 발생하더라도 5초에 한 번만 fetchData 함수가 실행되도록 할 수 있습니다. 스크롤 이벤트가 발생할 때마다 fetchData 함수가 실행되지 않고, 5초에 한 번씩만 실행됩니다. timerFlag는 함수가 호출되고 setTimeout이 실행되어 timerFlag가 null이 될 때까지 다음 함수 호출을 막습니다. 이를 통해 일정 시간 동안 한 번만 함수가 실행되도록 합니다. timerFlag는 외부에서 접근할 수 없기 때문에 안전하게 함수를 제어할 수 있습니다.

function fetchData() { console.log('데이터를 가져오는 중...'); setTimeout(() => { console.log('데이터 가져오기 완료!'); }, Math.random() * 1000); } const throttledFetchData = throttle(fetchData, 5000); window.addEventListener('scroll', throttledFetchData);
9.2 다양한 함수10장 DOM