본문 바로가기

javascript/Basic

TIL no.47 - 클로저란 무엇일까

클로저란 무엇인가

클로저는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 가리킵니다. 클로저는 자바스크립트를 이용한 고난이도 테크닉을 구사하는데 필수적인 개념으로 활용됩니다.

Mozilla에서 개념적인 정리 부분을 보면 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.

Closure =  function() + lexical scope

클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다고 합니다.

lexical 이란, 어휘적 범위 지정(lexical scoping) 과정에서 변수가 어디에서 사용 가능한지 알기 위해 그 변수가 소스코드 내 어디에서 선언되었는지 고려한다는 것을 의미합니다.

 

한마디로 클로저는 필요한 변수를 저장해놓고 내부함수에서 외부함수의 변수를 사용할 때 저장했던 외부변수를 불러올 수 있는 자바스크립트의 특성 중 하나입니다. 즉, 함수 내부에서 외부변수를 참조할 수 있도록 자신이 생성된 환경을 기억하는 함수입니다.

(참조할 곳이 있는 애들을 없애지 않는다 .저장해놓는다)

 

 

클로저를 이해하기 위한 개념

스코프, 스코프체인, LexicalEnvironment, 실행 컨텍스트

렉시컬 환경은 자신이 생성된 환경을 기억하는 것을 말합니다. 즉, 자신이 선언된 곳을 기억합니다.

 

기본적인 개념 부분 이해

function outer() {
	var title = 'Merry Christmas';
    return function() {
    	alert(title);
      }
    }
  inner = outer();
  inner();

이 코드의 결과가 무엇이라고 생각하시나요.

바로 title 인 Merry Christmas 가 경고창에 출력될 것입니다.

그런데 이게 왜 이상하다고 생각할까요?

더보기

그 이유는 함수는 return 을 하면 죽어버립니다.

그런데 outer는 return 을 했음에도 죽지 않고 outer함수에서 정의해준 변수를 익명함수에 전달해주고 있습니다.

이 점을 이상하다고 여기는 것이며 이렇게 외부함수에 있는것을 내부함수에 전달해서 변수를 참조하게 하는 것을 클로저의 개념이라고 할 수 있습니다.

클로저의 특징으로 죽어버린 외부함수에서 정의되었던 지역변수에 내부함수가 접근할 수 있게 합니다.

이러한 특징을 어디서 사용하게 되는지 궁금하지 않나요?

 

클로저의 예시

 

useState

const useState = (init = undefined) => {
  let value = init
  
  const getter = () => value //클로저
  const setter = next => (value = next) //클로저
  
  return [getter, setter]
}


const [state, setState] = useState('클로저')

이 안에서 콜백함수를 가지고 있는 getter라는 변수가 value 라는 지역변수를 참조하고 있습니다.

또, setter는 그 value 를 가져와 새로운 인자를 받으면 갈아끼우기 위해 참조하고 있습니다.

이 두가지가 다 클로저가 사용되고 있는 부분입니다.

 

 

가장 많이 봤을 예시

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

 

위 코드를 동작시키면 어떠한 결과가 나올 것이라고 생각하시나요?

저도 처음에 보고 당연히 1 - 100까지가 콘솔창에 출력되겠구나 라고 생각했습니다.

하지만 예상과 다르게 이 코드를 동작시키면 100이 100번 출력됩니다.

이 코드를 우리가 원하는 결과로 바꾸기 위해서는 아래와 같이 바꾸면 됩니다.

 

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

이렇게 바꾸게 되면 우리가 원하는 대로 1, 2, 3, 4, ... 100 까지 차례로 출력됩니다.

왜 이러한 결과가 나오는걸까요?

 

 

 

클로저의 실용적인 예제

소프트웨어가 커지는 과정에 있을 때 내부의 정보를 아무나 수정할 수 없게 만드는 것을 방지하기 위해 사용하기도 합니다.

참고 Private 속성은 객체의 외부에서는 접근 할 수 없는 외부에 감춰진 속성이나 메소드를 의미한다. 이를 통해서 객체의 내부에서만 사용해야 하는 값이 노출됨으로서 생길 수 있는 오류를 줄일 수 있다. 자바와 같은 언어에서는 이러한 특성을 언어 문법 차원에서 지원하고 있다.

 

- 정보은닉

var outer = function() {
	var a = 1;
    var inner = function() {
    	return ++a;
      };
      return inner;
  };
  
  var outer2 = outer();
  console.log(outer2());

 

위 코드에서 console을 찍어보면 inner 값만 찍힐 것이라는 것을 예상할 수 있습니다.

만약에 outer을 바로 찍는다면 그 함수 자체가 찍히기 때문에 외부에서 사용자가 a 값을 임의로 바꿀 수도 있을 것입니다.

이렇게 보여주고 싶지 않은 값을 보여주지 않기 위해서 클로저를 사용합니다.

inner가 a 값을 참조하게 하고 outer를 그대로 보여주지 않고 inner를 return 하여 새로운 변수에 담기 때문에 사용자는 inner만 볼 수 있습니다.

 

커링함수

커링 은 함수 하나가 n개의 인자를 받는 과정을 n개의 단일 인자 함수열로 만드는 것 을 말하며, 특히 함수를 재사용 할 때 유용하게 쓰이는 JavaScript의 함수형 프로그래밍 기법 중 하나다.

// 커링 함수
function multiplyThree(x) {
    // 클로저로 생성된 공간
    return function(y) {
        return function(z) {
            console.log(x * y * z);
        };
    };
}

multiplyThree(4)(5)(2); // 40

 

위 코드는 ES6의 화살표 함수 (arrow function)를 이용하면, 좀 더 간결하게 표현할 수 있다.

 

let multiplyThree = x => y => z => console.log(x * y * z); // 커링 함수

multiplyThree(4)(5)(2); // 40

 

더 쉽게 예시를 들어보자면,

 

//일반함수

var fullName = function(last, first) {
    console.log(last + first);
};

fullName('조', '다솜'); // 조다솜
fullName('조', '다영', ); // 조다영
fullName('조', '성준'); // 조성준

이것을 

 

let fullName = last => first => console.log(last + first); // 커링 함수

let family = fullName('조'); // 클로저로 저장
family('다솜'); // 조다솜
family('다영'); // 조다영
family('성준'); // 조성준

 

커링함수로 표현하면 위와 같습니다. 생각보다 쉽죠?

 

 

 

 

 

 

클로저는 단순히 함수 외부의 변수에 접근 가능한 내부함수가 아니라 함수가 선언되는 순간에 함수가 실행될때 실제 외부변수에 접근하기 위한 객체이다. 클로저도 남발하면 위험하다. 가비지컬렉션 대상이 되어야할 객체들이 메모리상에 남아 있게 되므로, 클로저를 남발하면 오버플로우가 발생할수도 있다. 이는 클로저에 대해 정확히 알아야 하는 이유이기도 하다.