TIL : JavaScript Closure

박지후·2022년 8월 1일
0

TIL

목록 보기
3/3
post-thumbnail

활용해 본 경험은 없지만, 면접 단골 질문으로 등장하는 렉시컬 환경과 클로저에 대해 정확하게 알아야겠다고 생각했다.

렉시컬 환경 (Lexical environment)

출처: 모던 자바스크립트 튜토리얼 - 렉시컬 환경

위 글을 정말 차근차근 읽어보니 확실히 정리가 되었다.

정리하자면,

변수

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

  • '렉시컬 환경’은 language specification에서 자바스크립트가 어떻게 동작하는지 설명하는 데 쓰이는 ‘이론상의’ 객체이다. 따라서 코드를 사용해 직접 렉시컬 환경을 얻거나 조작하는 것은 불가능하다.

  • 렉시컬 환경 객체는 모든 지역 변수를 프로퍼티로 저장하고 있는 환경 레코드(Environment Record)외부 렉시컬 환경(Outer Lexical Environment)에 대한 참조 로 구성된다.

  • 스크립트가 시작되면 스크립트 내에서 선언한 변수 전체가 렉시컬 환경에 올라간다.

함수 선언문

함수 또한 변수와 마찬가지로 값이지만, 함수 선언문으로 선언한 함수는 일반 변수와 달리 바로 초기화 된다는 점에서 차이가 있다. 따라서, 선언되기 전에도 함수를 사용할 수 있다.

내부와 외부 렉시컬 환경

함수를 호출해 실행하면 새로운 렉시컬 환경이 자동으로 만들어지고, 이 렉시컬 환경엔 함수 호출 시 넘겨받은 매개변수와 함수의 지역 변수가 저장된다.

함수가 호출 중인 동안엔,

  1. 호출 중인 함수를 위한 내부 렉시컬 환경과
  2. 내부 렉시컬 환경이 가리키는(참조하는) 외부 렉시컬 환경을 갖게 된다.

코드에서 변수에 접근할 땐, 먼저 내부 렉시컬 환경을 검색 범위로 잡고, 내부 렉시컬 환경에서 원하는 변수를 찾지 못하면 검색 범위를 내부 렉시컬 환경이 참조하는 외부 렉시컬 환경으로 확장한다. 이 과정은 검색 범위가 전역 렉시컬 환경으로 확장될 때까지 반복된다.

전역 렉시컬 환경에 도달할 때까지 변수를 찾지 못하면 strict mode 에선 에러가 발생한다. (Reference Error)

클로저 Closure

함수를 반환하는 함수

아래와 같은 특수한 경우의 예시를 살펴보면..!

function makeCounter() {
    let num = 0; 
    
    return function () {
        return num++;
    }
}

let counter = makeCounter(); 

makeCounter()를 호출하면 호출할 때마다 새로운 두 개의 렉시컬 환경이 만들어진다. (화살표는 실행 흐름)

그리고, makeCounter() 가 실행되는 도중엔, return count++ 한 줄짜리 본문을 가지는 중첩 함수가 만들어지며, 이 함수는 생성되기만 하고 실행은 되지 않은 상태이다.

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

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

counter() 를 호출하면, 각 호출마다 새로운 렉시컬 환경이 생성된다.

실행 흐름이 counter() 함수의 본문으로 넘어오면 count 변수가 필요한데, 먼저 자체적인 렉시컬 환경에서 변수를 찾는다. 익명 중첩 함수엔 지역변수가 없기 때문에, 렉시컬 환경은 <empty> 상태이므로, count() 의 렉시컬 환경이 참조하는 외부 렉시컬 환경에서 count 를 찾게 된다.

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

따라서 counter() 를 호출하면, 참조하고 있는 외부 렉시컬 환경의 count 에 접근하여 갱신하기 때문에,counter() 를 호출하면 변수가 2, 3 으로 계속해서 증가한다.

이처럼 외부 변수를 기억하고, 이 외부 변수에 접근할 수 있는 함수를 클로저(closure) 라고 한다!!!

끝으로..

자바스크립트에선 모든 함수가 자연스럽게 클로저가 된다. (new Function 문법은 예외!)

프런트엔드 개발자 채용 인터뷰에서 "클로저가 무엇입니까?"라는 질문을 받으면, 클로저의 정의를 말하고 자바스크립트에서 왜 모든 함수가 클로저인지에 관해 설명하면 될 것 같습니다. 이때 [[Environment]] 프로퍼티와 렉시컬 환경이 어떤 방식으로 동작하는지에 대한 설명을 덧붙이면 좋습니다!

useState()

함수형 컴포넌트는 렌더가 필요할 때마다 다시 호출된다. 이 때, 매번 다시 호출되는 함수 컴포넌트 내부의 상태값은 어떻게 유지가 되는 것일까?

Deep dive: How do React hooks really work?

번역

useState 는 함수 컴포넌트 외부에 선언된 상태값에 접근하여 이전 상태를 가져오거나, 변경된 상태값을 적용한다. 즉, useState 는 클로저를 이용하여 동작한다.

예시 etc

렉시컬 환경 예시

// 코드가 실행되면, 선언한 변수들이 lexical 환경 위로 올라간다.(전역 lexical 환경)
// one : 초기화 X (사용 불가)
// addOne : function 

let one; // one : undefined (사용 가능)
one = 1; // one : 1 (할당)

function addOne(num) {
    console.log(one + num);
}

addOne(5); 
// 함수 실행 시 새로운 lexical 환경이 생성된다. (매개변수와 지역변수들이 저장) (함수 내부 lexical 환경)
// num : 5
  • 스크립트 실행 시, 전역 렉시컬 환경이 생성되어 변수와 함수의 선언이 먼저 이루어진다. (호이스팅)
  • 함수 실행 시, 함수 내부의 렉시컬 환경이 생성되며, 이 렉시컬 환경은 전역(외부) 렉시컬 환경을 참조한다.

클로저 예시

// 코드 실행 시  `makeAdder` 와 `add3` 가 전역 Lexical 환경에 올라간다. 
// makeAdder: function, add3: 초기화 X
function makeAdder(x) {
    return funtion(y) {
        return x + y;
    }
}

const add3 = makeAdder(3); 
// makeAdder Lexical 환경이 생성된다. 
// x : 3
// 전역 Lexical 환경의 add3 이 function 으로 초기화됨 
console.log(add3(2));
// 위 코드 실행 시, add3에 대한 익명함수 Lexical 환경이 생성
// y : 2

const add10 = makeAdder(10);
console.log(add10(5)); // 15
console.log(add3(1)); // 4

클로저를 이용한 은닉화 예시

function makeCounter() {
    let num = 0; // 은닉화 => makeCounter 외부에선 num 에 접근할 수 없다. 
    
    return function () {
        return num++;
    }
}

let counter = makeCounter();

console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2

0개의 댓글