본문 바로가기
Programming

[Javascript] 클로저(Closure) 사용하는 이유(feat 스코프 개념)

by 개발자 염상진 2022. 5. 19.

 

클로저(Closure)란?

 

Javascript 의 클로저는 함수(Function)을 일급객체로 취급하는 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다. Javascript 뿐만 아니라 리스프, 얼랭, 스칼라, 스켈 등 함수형 프로그래밍 언어에서 사용된다.

 

MDN에서 정의하는 클로저의 개념은 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.

풀어서 이해하면 어떤 외부 함수 내부에 선언된 내부 함수가 외부(outer) 함수의 지역변수를 참조하는 상태가 함수가 종료된 후에도 유지되는 현상을 말한다. 코드로 다시 한번 이해해보자.

 

function outer(){   // 외부 함수
  let outerVariable = 'This is OUTER'; // 외부 함수의 지역변수

  const innerFunction = function(){ // 내부 함수 선언
    console.log(outerVariable);
  }

  return innerFunction // 내부 함수 반환
}

const temp = outer(); // 외부함수의 주소값 복사

temp(); 외부함수 실행

 

위 코드 같은 현상이 발생하는 이유는 내부 함수 innerFunction()이 선언된 렉시컬 환경 때문이다. Javascript는 렉시컬 스코프(정적 스코프)를 따르기 때문이다.

 

실제 업무에서 많이 사용하지는 않지만 클로져(Closure)의 작동방식은 Javascript 프로그래밍 언어가 작동하는 방식 중 스코프(Scope)의 개념을 담고 있어 중요하게 다뤄진다.

 

 

 

 

스코프(Scope)란?

 

Javascript에서는 프로그램을 평가한 후 콜 스택에 함수를 올리면서 실행하게 된다. 소스코드를 평가하는 단계 이전에 프로그램 소스내의 변수 선언과 함수 선언정보를 미리 수집한다. 

 

소스코드를 평가할 때는 한줄씩 수집하다가 함수선언이 일어나면 함수의 처음 부분부터 코드를 수집하게 된다. 이 과정에서 함수 호출 전 담긴 정보로 해석이 불가능한 경우 새로 수집된 정보로 평가를 진행한다. 이 때문에 Javascript 프로그램에서는 고유한 스코프(영역)이 생기게 된다.

 

만약 아래 소스코드 처럼 foo()와 bar() 모두 전역에서 정의된 전역 함수라면 두 함수의 상위 스코프는 전역이 된다. 렉시컬 환경은 자신의 외부 렉시컬 환경에 대한 참조(Outer Lexical Environment Reference)를 통해 상위 렉시컬 환경과 연결된다. 이를 스코프 체인(Scope Chain)이라고 한다.

 

const x = 1;

function foo(){
  const x = 10;
  bar();
}

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


foo(); // 1
bar(); // 1

 

함수 뿐만 아니라 반복문, 조건문 등 블록문도 고유한 스코프를 가지게 된다. 

 

조금 어려운 이야기지만 

함수의 상위 스코프를 결정한다 = 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저정할 참조값을 결정한다

 

이를 토대로 렉시컬 환경의 개념을 다시 정의해보면 

 

 

렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다. 이것이 바로 렉시컬 스코프다.

 

 

 

스코프체인(Scrope Chain)

함수나 블록문에서 소스코드 해석이 불가능한 경우 바로 이전의 스코프로 이동해서 정보를 찾게 된다. 만약 정보가 존재하지 않으면 그 위의 스코프를 찾게 된다.

 

이렇게 소스코드의 평가를 위해 소스코드가 현재 위치에서 상위로 올라가면서 연결되는 구조를 스코프체인이라고 한다. 만약 전역 스코프에도 정보가 존재하지 않으면 Reference Error(참조에러)를 발생시킨다.

 

상위 스코프라는 것은 함수를 실행하는 시점의 외부 스코프가 아닌 함수를 선언하는 시점의 외부 스코프를 의미한다. 이를 렉시컬 스코프 라고 한다. Javascript 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정하게 된다. 렉시컬 스코프(정적 스코프)는 선언시 결정된다. 

 

Javascript의 스코프 개념 때문에 특정 함수를 평가하는 시점에 참조할 수 없는 변수 혹은 함수는 바깥 스코프에서 찾게 되면서 클로져가 가능해지는 것이다.

 

 

Javascript 함수 객체 [[Environment]] 내부슬롯

 

함수가 정의되는 시점과 호출되는 시점은 다를 수 있다. 위에서 살펴본 것 처럼 Javascript에서 함수 선언시 상위 스코프를 결정하게 되고 자신의 상위 스코프를 기억하고 있어야 한다. 이 정보는 어디에 저장되는 걸까?

 

함수 객체는 자신의 상위 스코프를 기억하기 위해 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경인 상위 스코프의 참조값을 저장하게 된다. 자신의 내부 슬롯 [[Environment]]에 저장된 상위 스코프의 참조는 현재 실행 중인 실행 컨텍스트의 렉시컬 환경을 말한다. 

 

 

 

 

 

클로져와 스코프

 

이제 처음 봤던 소스코드를 다시 한번 살펴보자. 

① innerFunction()이 선언되는 시점은 스코프가 생성되는 시점이다.

② innerFunction()은 outerVariable 변수를 참조하기 때문에 바깥 스코프는 메모리 영역에서 지워지지 않는다. 만약 이 스코프가 메모리에서 지워지면 참조에러를 발생시킬 것이다.

③ outer() 함수를 다시 실행하더라도 outerVariable(바깥 스코프에 존재하는 변수)는 그대로 값을 유지한다.(메모리에서 지워지지 않는다.)

function outer(){   // 외부 함수
  let outerVariable = 'This is OUTER'; // 외부 함수의 지역변수

  const innerFunction = function(){ // 내부 함수 선언
    
    // 스코프는 선언 시점을 기준으로 생성된다.
    // 내부 함수에서는 참조할 수 없는 변수
    // 바깥 스코프를 참조해야 한다.
    // 참조해야할 바깥 스코프는 메모리에서 지워지지 않는다.
    console.log(outerVariable); 
  }

  return innerFunction // 내부 함수 반환
}

const temp = outer(); // 외부함수의 주소값 복사

temp(); 외부함수 실행

 

외부 함수가 종료되는 시점에서 외부 함수 실행 컨텍스트는 콜 스택에서 제거되지만, 외부 함수의 렉시컬 환경은 제거되지 않는다. 내부 함수의 [[Environment]] 에서 외부 함수 렉시컬 스코프에 대한 참조를 하고 있기 때문이다. Javascript의 가비지 콜렉터는 뭔가를 참조하고 있는 메모리 공간은 절대 해제하지 않는다. 

 

클로저 함수는 가장 상위 스코프로 Closure 표시가 된다. 즉 렉시컬 환경이 상위 스코프인 함수의 경우 외부 변수를 참조하고 있기 때문에 가비지 콜렉터도 메모리 해제를 하지 않고 스코프 체인이 유지되고 있는 상황이다.

 

 

 

클로저(Closure) 실제 사례

 

클로저(Closure)를 사용하면 상태를 안전하게 변경하고 유지할 수 있다. 특정 함수(렉시컬 환경의 참조값을 가지고 있는)에게만 상태를 변경할 수 있는 권한을 준다. 이 로직이 적용된 대표적인 예시가 React의 hook API다. 

 

클로저 함수를 호출할 때 마다 num이 증가하는 예시다. 

function counter(){
  let num = 0;

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

const increase = counter();

console.log(increase()) // 1
console.log(increase()) // 2
console.log(increase()) // 3

 

클로저의 가장 대표적인 사례가 React hook API다. 단방향 데이터 흐름을 지향하는 React는 props와 state를 이용해서 컴포넌트의 데이터 흐름을 지원한다.

 

useState는 컴포넌트내에서 수시로 변경되어야 할 데이터를 담기 위한 장치다. 정보를 변경하지만 변경된 정보는 그대로 유지되어야 하는데 이 때 클로저가 사용된다.

 

 

 

[React] props vs state

React props vs state 기본 개념 React Functional Component props, state state hooks? what is props, state, state hook in react and How can I use them? 리액트 컴포넌트 사이에 데이터 교환을 하기 위한..

about-tech.tistory.com

 

어떻게 state가 계속 유지 될 수 있을까? React를 사용하면서 state가 변경되면 컴포넌트가 리로딩 되는 걸로 알고 있었지만 이게 어떻게 작동하는지는 이번에 처음 생각해봤다. 변경된 값을 state로 업데이트하고 그 값을 유지하기 위해서 상위 스코프에 state를 저장한 후 메모리 영역에 박재 시켜버리는 것이다.

 

function App(){ // 바깥 스코프

	const [inputValue, setInputValue] = useState(''); 
    // 함수가 선언될 때 스코프가 생성됨
    // setInputValue는 바깥 스코프를 계속 참조하기 때문에,
    // App() 컴포넌트 스코프는 메모리에서 지워지지 않음
    
    
    function changeHandler(e){
    	e.preventDefault();
        
        setInputValue(e.target.value);
	}
    
    return (
    	<>
        	<div>
            	// 함수 실행 영역
            	<input  value={inputValue} onClick={changeHandler}/>
                <h1>{inputValue}</h1>
            </div>
        </>
    )

}

 

 

사실 Javascript에서 스코프(Scope)와 클로저(Closure) 개념은 면접 질문에 단골로 등장하기 때문에 개념은 알아두는 것이 좋다.

 

 

 

 

React란 무엇인가?

What is React? React는 프론트엔드 개발을 위한 Javascript 기반 오픈 소스 라이브러리 입니다. React는 선언형, 컴포넌트 기반, 범용성 특징을 가지고 있습니다. 기존에는 HTML, CSS, Javascript로 웹 프론트..

about-tech.tistory.com

 

 

[Javascript] fetch 사용법 비동기식 프로그래밍 이해하기

Javascript 비동기 프로그래밍 Javascript의 대표적인 비동기 요청은 네트워크(서버)에 자원을 요청하는 작업이다. 네트워크에 자원을 요청하는 방식은 여러가지가 있지만 대표적인 방법이 URL를 사

about-tech.tistory.com

 

 

[Javascript] 명령형 vs 선언형 프로그래밍

Imperative Programming VS Declarative Programming 명령형 프로그래밍 VS 선언형 프로그래밍 리액트와 Javascript를 접하다보면 명령형 프로그래밍과 선언형 프로그래밍을 만나게 된다. 프로그래밍 하는 방식에

about-tech.tistory.com

 

댓글