JS) 변수의 유효범위와 클로저(closure)

Cecilia·2023년 1월 3일
0

JavaScript

목록 보기
33/36
post-thumbnail

https://ko.javascript.info/closure




🔍 클로저(closure)

클로저를 이해하기 위해 변수의 유효범위를 먼저 알아보자.


코드 블록

{코드 블록}안에서 선언한 변수는 블록 안에서만 사용할 수 있다.

{
  // 지역 변수를 선언하고 몇 가지 조작을 했지만 그 결과를 밖에서 볼 수 없습니다.

  let message = "안녕하세요."; // 블록 내에서만 변숫값을 얻을 수 있습니다.

  alert(message); // 안녕하세요.
}

alert(message); // ReferenceError: message is not defined

let으로 블록 없이 이미 선언된 변수와 동일한 이름을 가진 변수를 let으로 선언하면 에러가 발생한다.
if, for, while 등에서도 마찬가지로 {코드 블록} 안에서 선언한 변수는 오직 블록 안에서만 접근 가능하다.



중첩 함수

함수 내부에서 선언한 함수는 ‘중첩(nested)’ 함수라고 부른다.
자바스크립트에서는 중첩 함수가 흔히 사용되는데, 예시는 아래와 같다.

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

//alert를 더 추가하면 숫자도 1씩 추가된다.


렉시컬 환경

1. 변수

자바스크립트에서는 실행중인 함수, {코드 블록}, 스크립트 전체는 렉시컬 환경(Lexical Environment)이라 불리는 내부 숨김 연관 객체(internal hidden associated object)를 갖는다.

이 렉시컬 환경 객체는 두 부분으로 구성된다.
1) 환경 레코드(Environment Record) – 모든 지역 변수를 프로퍼티로 저장하고 있는 객체. this 값과 같은 기타 정보도 여기에 저장된다.
2) 외부 렉시컬 환경(Outer Lexical Environment) 에 대한 참조 – 외부 코드와 연관됨

변수는 특수 내부 객체인 환경 레코드의 프로퍼티일 뿐이다.
변수를 가져오거나 변경하는 것 = 환경 레코드의 프로퍼티를 가져오거나 변경을 의미한다.

👉 중간 정리

  • 변수는 특수 내부 객체인 환경 레코드의 프로퍼티다. 환경 레코드는 현재 실행 중인 함수와 코드 블록, 스크립트와 연관되어 있다.
  • 변수를 변경하면 환경 레코드의 프로퍼티가 변경된다.


2. 함수 선언문

함수는 변수와 마찬가지로 값이다.
다만 함수 선언문(function declaration)으로 선언한 함수는
일반 변수와는 달리 바로 초기화 된다는 점에서 차이가 있다.

변수는 let을 만나 선언이 될 때까지 사용할 수 없지만
함수 선언문으로 선언한 함수는 렉시컬 환경이 만들어지는 즉시 사용할 수 있다.

선언되기 전에도 함수를 사용할 수 있는 것은 바로 이 때문이다.

물론 let say = function(name)...같이 함수를 변수에 할당한 함수 표현식(function expression)은 해당하지 않는다.



3. 내부와 외부 렉시컬 환경

코드에서 변수에 접근할 땐, 먼저 내부 렉시컬 환경을 검색 범위로 잡는다.

내부 렉시컬 환경에서 원하는 변수를 찾지 못하면
검색 범위를 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경으로 확장된다.

이 과정은 검색 범위가 전역 렉시컬 환경으로 확장될 때까지 반복된다.

let pharse = "Hello";

function say(name) {
  alert( `${pharse), ${name}` );
}
say("John"); //Hello, John

함수 say 내부의 alert에서 변수 name에 접근할 땐,
먼저 내부 렉시컬 환경을 살펴본다. 내부 렉시컬 환경에서 변수 name을 찾았다.

alert에서 변수 phrase에 접근하려는데,
phrase에 상응하는 프로퍼티가 내부 렉시컬 환경엔 없다.
따라서 검색 범위는 외부 렉시컬 환경으로 확장된다.
외부 렉시컬 환경에서 phrase를 찾았다.



4. 함수를 반환하는 함수

makeCounter 예시에서

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

makeCounter를 호출하면 호출할 때마다 새로운 렉시컬 환경 객체가 만들어지고
여기에 makeCounter를 실행하는데 필요한 변수들이 저장된다.

위에서 살펴본 say("John") 예시와 마찬가지로
makeCounter를 호출할 때도 두 개의 렉시컬 환경이 만들어진다.


그런데 say("John") 예시와 makeCounter 예시에는 차이점이 하나 있다.
makeCounter()가 실행되는 도중엔 본문(return count++)이 한줄 짜리인 중첩 함수가 만들어진다는 점이다.
현재는 중첩함수가 생성되기만 하고 실행은 되지 않은 상태다.


여기서 중요한 사실이 있는데,
모든 함수는 함수가 생성된 곳의 렉시컬 환경을 기억한다는 점이다.
함수는 [[Environment]]라 불리는 숨김 프로퍼티를 갖는데,
여기에 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.


따라서 counter.[[Environment]]{count: 0}이 있는 렉시컬 환경에 대한 참조가 저장된다.
호출 장소와 상관없이 함수가 자신이 태어난 곳을 기억할 수 있는 건 바로 이 [[Environment]] 프로퍼티 덕분이다.
[[Environment]]는 함수가 생성될 때 딱 한 번 값이 세팅되고 영원히 변하지 않는다.


counter()를 호출하면 각 호출마다 새로운 렉시컬 환경이 생성된다.
그리고 이 렉시컬 환경은 counter.[[Environment]]에 저장된 렉시컬 환경을 외부 렉시컬 환경으로서 참조한다.


실행 흐름이 중첩 함수의 본문으로 넘어오면 count 변수가 필요한데,
먼저 자체 렉시컬 환경에서 변수를 찾는다.

익명 중첩 함수엔 지역변수가 없기 때문에 이 렉시컬 환경은 비어있는 상황이다.(<empty>).
이제 counter()의 렉시컬 환경이 참조하는 외부 렉시컬 환경에서 count를 찾는다.

이제 count++가 실행되면서 count 값이 1 증가해야하는데,
변숫값 갱신은 변수가 저장된 렉시컬 환경에서 이뤄진다.

counter()를 여러 번 호출하면 count 변수가 2, 3으로 증가하는 이유가 바로 여기에 있다.




📌 정리


1. 클로저의 정의

클로저는 외부 변수를 기억하고 이 외부 변수에 접근할 수 있는 함수를 의미한다.
= 함수가 정의될 때의 렉시컬 환경을 기억하는 함수



2. JS에서 왜 모든 함수가 클로저인가?

❗ 자바스크립트에서는 new Function을 제외한 모든 함수가 자연스럽게 클로저가 된다.

자바스크립트 함수는 숨김 프로퍼티인 [[Environment]]를 이용해 자신이 어디서 만들어졌는지를 기억하기 때문이다.



3. [[Environment]] 프로퍼티와 렉시컬 환경이 어떤 방식으로 동작하는가?

함수는 [[Environment]]라 불리는 숨김 프로퍼티를 갖는데,
여기에 함수가 만들어진 곳의 렉시컬 환경에 대한 참조가 저장된다.
호출 장소와 상관없이 함수가 자신이 태어난 곳을 기억할 수 있는 건 바로 이 [[Environment]] 프로퍼티 덕분이다.

자바스크립트의 함수는 숨김 프로퍼티인 [[Environment]]를 이용해 자신이 어디서 만들어졌는지를 기억한다. 함수 본문에선 [[Environment]]를 사용해 외부 변수에 접근한다.

profile
'이게 최선일까?'라는 고찰을 통해 끝없이 성장하고, 그 과정을 즐기는 프론트엔드 개발자입니다. 사용자의 입장을 생각하며 최선의 편의성을 찾기 위해 노력합니다.

0개의 댓글