[javascript] 렉시컬 환경과 클로저 이해

insung·2025년 4월 14일

클로저란 무엇인가?

클로저는 자신의 스코프(Scope)와 함께 상위 스코프에 있는 변수에도 접근할 수 있는 함수를 말함.

자바스크립트의 중요한 특징 중 하나이며, 함수가 선언될 당시의 주변 환경(렉시컬 환경)을 기억하기 때문에 가능.

핵심: 함수가 함수 내부 변수뿐만 아니라 자신이 속한 외부 함수의 변수에도 접근할 수 있는 현상

렉시컬 환경 (Lexical Environment)

렉시컬 환경은 실행 컨텍스트의 구성 요소 중 하나로, 식별자(변수, 함수 선언 등)와 그 식별자에 대한 바인딩 정보를 저장하는 환경.

쉽게 말해, 특정 스코프 내에서 어떤 변수와 함수에 접근할 수 있는지, 그리고 그 값은 무엇인지 등을 담고 있는 논리적인 구조

렉시컬 환경은 두 가지 주요 컴포넌트로 구성

  1. Environment Record (환경 레코드)
    • 현재 스코프에서 선언된 지역 변수와 함수에 대한 정보를 저장하는 객체
  2. Outer Environment Reference (외부 환경 참조): 상위 스코프의 렉시컬 환경에 대한 참조.
    • 이를 통해 스코프 체인이 형성

클로저와 렉시컬 환경:

클로저가 중요한 이유는 함수가 생성될 때의 렉시컬 환경을 기억하기 때문.

내부 함수가 외부 함수의 변수를 참조하면, 자바스크립트 엔진은 내부 함수의 렉시컬 환경의 외부 환경 참조를 통해 외부 함수의 렉시컬 환경까지 연결

외부 함수가 종료되더라도 내부 함수의 렉시컬 환경은 유지되며, 이 렉시컬 환경을 통해 외부 함수의 변수에 접근할 수 있게 되는 것

요약하면, 클로저는 함수가 자신의 렉시컬 환경(자신이 선언될 때의 주변 환경)을 기억하고, 이 환경 내의 변수들에 접근할 수 있는 능력.

렉시컬 환경은 스코프 내의 식별자와 그 바인딩 정보를 담는 구조이며, 클로저가 상위 스코프의 변수에 접근하는 메커니즘의 핵심적인 개념

클로저 동작 원리

자바스크립트는 렉시컬 스코프(Lexical Scope)라는 규칙을 따름. 이는 함수의 스코프가 함수가 선언될 때 결정된다는 의미.

함수가 호출되는 위치와는 상관없이, 함수가 어디에서 정의되었느냐에 따라 접근 수 있는 변수의 범위가 결정

함수가 실행되면 자신만의 실행 컨텍스트(Execution Context)를 생성.

이 실행 컨텍스트는 변수 객체(Variable Object), 스코프 체인(Scope Chain), this 등을 포함.

여기서 스코프 체인은 현재 실행 컨텍스트의 변수 객체와 상위 스코프의 변수 객체들을 연결한 리스트

클로저가 발생하는 상황은 다음과 같음

  1. 외부 함수가 실행.
  2. 외부 함수 내부에 내부 함수가 정의.
  3. 내부 함수는 외부 함수의 스코프에 있는 변수를 참조.
  4. 외부 함수가 실행을 완료하고, 내부 함수가 외부 함수의 스코프 밖으로 반환.

이때, 반환된 내부 함수는 여전히 자신이 선언될 당시의 렉시컬 환경(외부 함수의 스코프)을 기억하고 있으며, 그 스코프 내의 변수들에 접근할 수 있음.

외부 함수는 이미 실행이 끝났지만, 내부 함수(클로저)가 그 변수들을 참조하고 있기 때문에 가비지 컬렉터는 해당 변수들을 메모리에서 해제하지 않음.

코드 예시 분석

1. 클로저 미사용 함수:

JavaScript

function countWithoutClosure() {
    let count = 0;
    return count;
}

console.log(countWithoutClosure()); // 0
console.log(count); // ReferenceError: count is not defined
  • countWithoutClosure 함수 내부에 선언된 count 변수는 함수 내부에서만 접근 가능 (렉시컬 스코프).
  • 함수가 실행되고 return 문을 만나면, 함수 내부의 변수는 메모리에서 해제됨.
    • 따라서 함수 외부에서는 count 변수에 접근할 수 없음.

2. 클로저 활용 함수:

function countWithClosure() {
    let count = 0;

    return {
        increase: function() {
            count++;
            return count;
        },
        decrease: function() {
            count--;
            return count;
        },
        getCount: function() {
            return count;
        },
    };
}

const counter = countWithClosure();

console.log(counter.getCount()); // 0
counter.increase();
console.log(counter.getCount()); // 1
counter.decrease();
console.log(counter.getCount()); // 0
  • countWithClosure 함수는 객체를 반환.
    • 이 객체는 내부 함수 increase, decrease, getCount를 프로퍼티로 가지고 있음.
    • 이 내부 함수들은 외부 함수 countWithClosure의 스코프에 있는 count 변수를 참조.
  • countWithClosure 함수가 실행 완료된 후에도, 반환된 객체의 메서드(increase, decrease, getCount)는 여전히 count 변수에 접근할 수 있음.
    • 이것이 바로 클로저의 힘.
    • 내부 함수들이 count 변수에 대한 클로저를 형성했기 때문.

왜 함수 안에서 또 함수를 return 하는가?

외부 함수의 내부 상태(count 변수)를 외부 스코프에서 직접 접근하는 것은 렉시컬 스코프 규칙상 불가능

내부 함수를 반환함으로써, 이 내부 함수를 통해 외부 함수의 변수에 간접적으로 접근하고 조작할 수 있게 되는 것.

클로저 함수를 호출해서 꼭 변수에 할당해야 하는가?

반드시 변수에 할당해야 클로저가 동작하는 것은 아님.

중요한 것은 반환된 내부 함수가 외부 함수의 변수를 참조하고 있다는 점.

변수에 할당하는 이유는 일반적으로 반환된 내부 함수를 여러 번 호출하여 외부 함수의 상태를 유지하고 조작하기 위함.

함수 바깥에서 스코프에 접근한다는 게 뭔가?

일반적으로 함수 내부에서 선언된 변수는 함수 외부에서 접근할 수 없음.

하지만 클로저를 사용하면, 외부 함수가 반환한 내부 함수를 통해 마치 외부 함수의 스코프에 접근하는 것처럼 변수를 사용하고 변경할 수 있게 됨.

이는 직접적인 접근은 아니지만, 내부 함수라는 매개체를 통해 외부 스코프의 변수를 활용하는 것.

3. useState 훅의 클로저 활용:

JavaScript

function useState(initialValue) {
    let state = initialValue;

    return [
        function getState() {
            return state;
        },
        function setState(newValue) {
            state = newValue;
        },
    ];
}

const [count, setCount] = useState(0);
console.log(count()); // 0
setCount(1);
console.log(count()); // 1
  • useState 함수는 state 변수를 선언하고, getState 함수(현재 상태 반환)와 setState 함수(상태 업데이트)를 담은 배열을 반환
  • getStatesetState 함수는 useState 함수의 스코프에 있는 state 변수를 클로저를 통해 접근하고 수정
  • useState 함수가 종료된 후에도 count 함수(getState를 참조)와 setCount 함수(setState를 참조)는 여전히 state 변수에 접근할 수 있음

클로저 활용의 장점

  • 함수 선언문 바깥에서도 함수 스코프 참조: 클로저를 통해 외부 함수의 변수를 은닉하고, 내부 함수를 통해 안전하게 접근 및 수정할 수 있음
  • 모듈 패턴 구현 용이: 관련 있는 변수와 함수를 묶어 모듈처럼 구성하고, 외부로 필요한 기능만 노출시킬 수 있음.
  • 정보 은닉 (캡슐화): 외부에서 직접 접근하면 안 되는 변수를 클로저 내부에 숨기고, 접근 권한을 가진 함수를 통해서만 조작하도록 제어할 수 있음.
  • 의도치 않은 값 변경 방지: setter 함수 등을 통해 값 변경 로직을 제어하여 데이터의 무결성을 유지할 수 있음.
  • 상태 유지: 외부 함수의 실행이 끝나도 클로저 내부의 변수 상태가 유지되므로, 상태 관리 등에 유용하게 사용될 수 있음.

클로저

function countWithoutClosure() {
		let count = 0;
		return count;
}

console.log(countWithoutClosure()) // 0
console.log(count) 
// 레퍼런스 에러, 자바스크립트가 렉시컬 스코프의 규칙을 따르기 때문
// 즉 함수 안에 선언된 변수는 함수 안에서만 접근 가능 
  • 함수 실행 시 함수 내부에 선언된 변수는 메모리 상에 올라가게 됨
    • 리턴문을 만나 함수가 실행이되면 가비지 컬렉터는 해당 변수를 메모리상에서 지움
    • 변수에 접근을 하려면 변수가 메모리상에 존재해야 하는데 함수 밖에서는 이미 해당 변수가 제거된 상태이므로 참조할 수 없는 것
    • 만약 함수밖에서 함수 내부의 상태를 실시간으로 확인하고 싶다면?
      1. count를 전역변수로 만들기

        • 메모리 누수를 초래할 수 있고 가독성 및 유지보수성이 크게 떨어지게 됨
      2. 클로저를 활용하는 방법

        • 함수의 return문에서 함수 내부의 변수를 사용하는 함수를 다시 return
        function countWithClosure() {
        		let count = 0;
        		
        		return {
        				increase: function(){
        						count++;
        						return count
        				},
        				decrease: function(){
        						count--++;
        						return count
        				},						
        				getCount: function(){
        						return count
        				},
        		}
        }
        
        const counter = createCounter();
        
        console.log(count.getCount()):
        counter.increase();
        console.log(count.getCount()):
        
        counter.decrease();
        console.log(count.getCount()):
        
  • 왜 함수 안에서 또 함수를 return하는것인가?
    • 클로저 함수를 호출해서 꼭 변수에 할당해야 하는가?
    • 도대체 함수 바깥에서 스코프에 접근한다는게 뭔가?
  • 일단 위와 아래 코드의 차이는 값을 리턴하는가 함수를 리턴하는가의 차이였음
    • 즉 클로저 함수를 호출한 결과는 function으로 저장돼있을것임

    • 그리고 각 function은 함수 내부의 변수를 접근함

    • 즉 함수 내부의 변수가 메모리상에 없으면 사용할 수 없는 함수임
      - 그래서 클로저에 의해 return시 함수 실행에 필요한 상위 스코프도 함께 메모리로 가져오는 것
      - 클로저에 의해 메모리에 캡쳐돼있기 때문
      - counter에 다른 값이 할당되지 않는 이상태는 계속 유지됨

      react에서 대표적으로 useState와 같은 훅을 아래랑 비슷하게 클로저로 구현함

      function useState(initialValue){
      		let state = initialValue;
      		
      		return [
      				function getState(){
      						return state;
      				},
      				function setState(newValue) {
      						state = newValue;
      				}
      		]
      }
      
      const [count, setCount] = useState(0);
      console.log(count()); // 0 
      setCount(1);
      console.log(count()) // 1

클로저를 활용할때의 장점

  • 함수 선언문 바깥에서도 함수 스코프를 참조할 수 있음
  • 모듈패턴 구현이 용이
    • 외부로 노출시킬 변수와
    • 캡슐화 하고 싶은 변수를 구분 가능
    • setter함수를 통해 의도치 않은 값 변경을 방지 가능

자바스크립트는 렉시컬 스코프 규칙을 따른다

  • 따라서 일반적인 방식으로는 함수 선언문 밖에서 함수 스코프에 접근할 수 없다
  • 이를 보완하기 위한 방법이 클로저 함수이다
  • 클로저 함수는 스코프가 상위 함수의 스코프를 참조하는 함수를 return하는 방식이다
  • React의 useState, moment 라이브러리 등에서 사용하는 방식이다
profile
안녕하세요 프론트엔드 관련 포스팅을 주로 하고 있습니다

0개의 댓글