본문 바로가기
JS

Scope & Closure (Function Scope, Lexical Scope)

by Hyeon_E 2024. 6. 7.

스코프(Scope)

자바스크립트 스코프

 

스코프란 단어의 뜻 그래도 '범위'라는 의미를 가지고 있음

코드에서의 스코프는 '변수에 접근할 수 있는 범위'라고 보면 됨

  • 글로벌 스코프(Global Scope): 전역 변수로 선언. 코드 어디에서든 접근 가능
  • 함수 스코프(Function Scope): 함수 내부에서 선언. 해당 함수 내부에서만 접근 가능
  • 블록 레벨 스코프(Block Level Scope): 블록(중괄호 { }내)에서만 접근 가능(let 또는 const로 선언된 변수만 해당)

▶ Function Scope

함수 내에서 선언된 변수는 해당 함수 내에서만 접근 가능(외부에서는 접근할 수 없음)

function foo() {
  var localVar = "I am local";
  console.log(localVar); // "I am local"
}

console.log(localVar); // ReferenceError: localVar is not defined

 

함수스코프와 블록레벨 스코프 차이

function Hi(hey) {
  if (hey) {
    var hi = "Hi"
    return hi
  }
  console.log(hi)	// 여기서는 hi 변수는 undefined로 존재
}
Hi()

 

hey값을 설정하지 않아서 if 조건문을 통과하지 않아 console.log가 실행됨

실행결과를 보면 Error를 발생시키지 않고 undefined를 출력함

 

function Hi(hey) {
  if (hey) {
    let hi = "Hi"
    return hi
  }
  console.log(hi)	////ReferenceError: hi is not defined 에러가 발생
}
Hi()

 

var키워드가 아닌 let 키워드로 변경해보면 undefined가 아닌 Error를 발생함

var는 함수 스코프이기 때문에 hi 변수가 지역변수에 등록되지만 let은 블록 스코프이기 때문에 등록되지 않은것

▶ Lexical Scope

함수를 어디서 호출하는지가 아니라 어디에 선언하였는지에 따라 결정되는 것을 말함

즉, 함수를 어디에 선언하였는지에 따라 상위 스코프를 결정한다는 뜻

스코프가 컴파일 타임에 결정되고 변하지 않기 때문에 다른 말로, 정적 스코프(Static Scope)라고 부르기도 함

var x = 1; // global

function first() {
  var x = 10;
  second();
}

function second() {
  console.log(x);
}

first();
second();

 

first와 second이 10,1이 아닌 모두 1이 출력됨

 

자바스크립트에서 위의 예제와 같은 코드를 작성할 때, 이미 실행 단계에서 코드들의 스코프를 결정함

  • global 범위에 있는 변수 x
  • second() 함수 안에 있는 변수 x
  • first() 함수 안에 있는 변수 x

따라서 위 예제의 실행 결과는 함수 second()의 상위 스코프가 무엇인지에 따라 결정됨

자바스크립트는 렉시컬 스코프(Lexical Scope)를 따르므로 함수를 선언한 시점에 상위 스코프가 결정됨

그래서 second() 함수가 first() 함수 안에서 호출된 것과 상관없이 second() 함수는 global 범위에 선언되어 있으므로, global 범위에 있는 변수 x의 값인 1이 출력된것

 

클로저(Closure)

클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합(MDN 정의)

쉽게 설명하면 클로저는 내부함수가 외부 함수의 맥락(context)에 접근할 수 있는것을 말함

 

즉, 클로저란 내부함수가 외부함수의 지역변수에 접근할 수 있고, 외부함수는 외부함수의 지역변수를 사용하는 내부함수가 소멸될 때까지 소멸되지 않는 특성을 의미함

 

let outer = function () {
    let num = 99;
    let inner = function () {
    	console.log(num++);
    };
    return inner;
};
let outerCopy = outer();

outerCopy(); // 99
outerCopy(); // 100
outerCopy(); // 101

 

createCounter 함수는 내부에 count 변수를 가지고 있으며, 이 변수를 증가시키는 익명 함수를 반환

반환된 함수는 클로저를 형성하여 count 변수를 기억하고, 이를 참조할 수 있음

즉, 클로저는 단 한번만 포착하고 끝나는 것이 아닌 값이 변하더라도 지속적으로 그 변화를 추적(Live Reference)

 

 

이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수클로저라고 함

위에 예시에서는 outer에서 선언된 num을 참조하는 inner가 클로저가 됨

그리고 클로저로 참조되는 외부 변수 즉, outer의 num 같은 변수를 자유변수(Free variable)라고 함

 

클로저의 이러한 특징을 바탕으로 자바스크립트를 이용한 함수형 프로그래밍 가능(데이터 은닉과 캡슐화를 가능하게 함)

대부분의 클로저에서는 스코프 체인의 뒤쪽에 있는 객체에 자주 접근하므로, 성능을 저하하고 메모리 부담이 많아지기도 하므로 무차별적으로 사용해서는 안됨

 

클로저 예

함수를 반환(return)하는 경우

let makeAdder = function (num1) {
  return function(num2) {
    return num1 + num2;
  };
};

let add5 = makeAdder(5);
let add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

 

setInterval/setTimeout

setInterval과 setTimeout 또한 Closure를 발생

setInterval/setTimeout 내부에 전달할 콜백 함수 내부에서, 지역 변수를 참조하게 되는 것

for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i)
  }, i * 1000)
}
// 3
// 3
// 3

 

분명 setTimeout에서 i는 0, 1, 2, 3 순으로 전달되며 i는 3이 되어 반복문이 멈춤

 

자바스크립트에서는, 함수가 선언될 때의 값을 지속적으로 참조하게 됨

그래서 setTimeout이 실행될 때는, i가 5인 상태이기 때문에 5만 주구장창 실행되는 것

 

자신이 원하는대로 동작하게 하기 위해서 코드를 변경하면

첫번째로 즉시실행함수를 이용하는 방법이 있음

for (let i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, j * 1000);
  })(i);
}

 

즉시실행함수는 클로저로 이용될 수 있음

클로저로 만들 부분을 function으로 감싸고(익명으로 가능) 그 함수를 괄호로 감싼 후에 끝에 함수를 실행한다는 괄호를 한번 더 붙여주면 됨

이렇게 하면 setTimeout 내부에 있는 익명함수가 클로저가 됨(변수 i가 값을 전달할 때마다 각각의 클로저가 생김)

더 이상 외부의 i값을 지속적으로 참조하는 것이 아닌, 전달받은 i값을 참조하는 것

 

두번째 방법은 반복문과 setTimeout을 분리하는 것

let timer = function(j) {
  setTimeout(function() {
    console.log(j);
  }, j * 1000);
};

for (let i = 0; i < 3; i++) {
  timer(i);
}

 

 

마찬가지로 각각의 클로저가 생겨 0 ~ 2까지 출력됨

 

▶ 가비지 컬렉션

가비지 컬렉션

가비지 컬렉션이란 더 이상 사용되지 않는 메모리를 자동으로 해제하는 프로세스

자바스크립트 가비지 컬렉션을 통해 메모리 누수를 방지하고, 프로그램의 안정성을 높임

 

자바스크립트 엔진주기적으로 가비지 컬렉션을 수행하여, 더 이상 참조되지 않는 개체를 찾아 메모리에서 해제

이는 메모리 누수를 방지하고, 프로그램의 성능을 최적화하는데 기여함

 

let obj = { name: 'John' };
obj = null;
// obj는 더 이상 참조되지 않으므로, 가비지 컬렉션에 의해 메모리에서 해제

 

위 코드에서 obj 변수는 객체를 참조하고 있음 그러나 obj 변수를 null로 설정하면, 더 이상 객체를 참조하지 않게 됨

이 경우, 가비지 컬렉션이 객체를 메모리에서 해제함

 

가비지 컬렉션의 동작원리

  1. 루트 객체를 기준으로 참조 그래프를 생성
  2. 참조 그래프를 탐색하여, 더 이상 참조되지 않는 객체를 찾음
  3. 더 이상 참조되지 않는 객체를 메모리에서 해제

가비지 컬렉션은 자바스크립트 엔진이 자동으로 수행하므로, 개발자는 메모리 관리를 신경 쓸 필요가 없음

그러나 메모리 누수를 방지하기 위해, 불필요한 참조를 제거하는 것이 중요

가비지 컬렉션은 자바스크립트의 메모리 관리와 성능 최적화에 중요한 역할을 함

 

클로저와 가비지 컬렉션

가비지 컬렉션은 더 이상 참조되지 않는 개체를 메모리에서 해제하지만,클로저는 함수가 선언된 시점의 변수를 계속해서 참조하기 때문에, 가비지 컬렉션이 클로저를 해제하지 못할 수 있음(이는 메모리 누수를 초래할 수 있음)

 

따라서 클로저를 사용할 때는 불필요한 참조를 제거하는 것이 중요(ex. 클로저 내부에서 더 이상 사용되지 않는 변수를 null로 설정하여, 가비지 컬렉션이 이를 해체)

 

let outer = function () {
    let num = 99;
    let inner = function () {
    	console.log(num++);
    };
    return inner;
};
let outerCopy = outer();

outerCopy(); // 99
outerCopy(); // 100
outerCopy(); // 101
outerCopy(); // 102

outer = null;

 

참조

자바스크립트 - 렉시컬 스코프(Lexical Scope)

스코프란? 간단하고 쉽게 이해하기 (예제코드, 글로벌 스코프,블록 레벨 스코프, 함수 스코프, 스코프 체인)

What Is JavaScript Scope?

클로저

클로저(Closuer)

자바스크립트 클로저와 가비지 컬렉션 이해하기

Closure - Closure의 의미, 발생하는 과정, 가비지 컬렉션(GC)

'JS' 카테고리의 다른 글

Call Stack  (0) 2024.06.10
실행 컨텍스트(Execution Context)  (1) 2024.06.10
Asynchronous (Web API - setTimeout, setInterval, XMLHttpRequest, Callback Queue)  (0) 2024.06.07
this  (1) 2024.06.05
Variables (Primitive type, Reference Type, Casting)  (0) 2024.06.05

댓글