본문 바로가기

Sparta x 이노베이션 캠프/JavaScript

TIL: JavaScript Closer 클로저

반응형

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

function outter(){
    function inner(){
        var title = 'coding everybody'; 
        alert(title);
    }
    inner();
}
outter();

결과는 alert으로 coding everybody가 출력된다.

위 예제에서 함수 outter 내부에 함수 inner가 정의 되어있음. 이를 내부 함수라고 한다. 내부함수는 외부함수의 지역변수에 접근 가능하다. 

function outter(){
    var title = 'coding everybody';  //외부함수의 지역변수
    function inner(){                
        alert(title); 
    }
    inner();
}
outter();

결과는 마찬가지로 alert으로 coding everybody가 출력된다. 이 예제는 내부함수에서 외부함수의 지역변수에 접근할 수 있음을 보여줌.

클로저는 내부함수와 밀접한 관계를 가지고 있는 주제다. 내부함수는 지역변수에 접근 할 수 있는데 외부함수의 실행이 끝나서 외부함수가 소멸된 이후에도 내부함수가 외부함수의 변수에 접근 할 수 있다. 이러한 메커니즘을 클로저라고 한다. 아래 예제는 이전의 예제를 조금 변형한 것이다. 결과는 alert으로 coding everybody를 출력할 것이다.

function outter(){
    var title = 'coding everybody';  
    return function(){        
        alert(title);
    }
}
inner = outter();
inner();

예제의 실행순서를 주의깊게 살펴보자. 7행에서 함수 outter를 호출하고 있다. 그 결과가 변수 inner에 담긴다. 그 결과는 이름이 없는 함수다. 실행이 8행으로 넘어오면 outter 함수는 실행이 끝났기 때문에 이 함수의 지역변수는 소멸되는 것이 자연스럽다. 하지만 8행에서 함수 inner를 실행했을 때 coding everybody가 출력된 것은 외부함수의 지역변수 title이 소멸되지 않았다는 것을 의미한다. 클로저란 내부함수가 외부함수의 지역변수에 접근 할 수 있고, 외부함수는 외부함수의 지역변수를 사용하는 내부함수가 소멸될 때까지 소멸되지 않는 특성을 의미한다.

function factory_movie(title){
    return {
        get_title : function (){
            return title;
        },
        set_title : function(_title){
            title = _title
        }
    }
}
ghost = factory_movie('Ghost in the shell');
matrix = factory_movie('Matrix');
 
alert(ghost.get_title());
alert(matrix.get_title());
 
ghost.set_title('공각기동대');
 
alert(ghost.get_title());
alert(matrix.get_title());

위 예제를 통해 알 수 있는 것들

 

1. 클로저는 객체의 메소드에서도 사용할 수 있다. 위 예제는 함수의 리턴값으로 객체를 반환하고있다. 이 객체는 메소드 get_title, set_title을 가지고 있다. 이 메소드들은 외부함수인 factory_movie의 인자값으로 전달된 지역변수 title을 사용하고 있다.

 

2. 동일한 외부함수 안에서 만들어진 내부함수나 메소드는 외부함수의 지역변수를 공유한다. 17행에서 실행된 set_title은 외부함수 factory_movie의 지역변수 title의 값을 '공각기동대'로 변경했다. 19행에서 ghost.get_title(); 의 값이 '공각기동대'인 것은 set_title과 get_title 함수가 title값을 공유하고 있다는 의미다.

 

3. 그런데 똑같은 외부함수 factory_movie를 공유하고 있는 ghost와 matrix의 get_title의 결과는 서로 각각 다르다. 그것은 외부함수가 실행될때마다 새로운 지역변수를 포함하는 클로저가 생성되기 때문에 ghost와 matrix는 서로 완전히 독립된 객체가 된다. 

 

4. factory_movie의 지역변수 title은 2행에서 정의된 객체의 메소드에서만 접근 할 수 있는 값이다. 이 말은 title의 값을 읽고 수정 할 수 있는 것은 factory_movie 메소드를 통해서 만들어진 객체 뿐이라는 의미다.JasvaScript는 기본적으로 Private한 속성을 지원하지 않는데, 클로저의 이러한 특성을 사용해서 Private한 속성을 사용할 수 있게된다. 

 

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

 

var arr = []
for(var i = 0; i < 5; i++){
    arr[i] = function(){
        return i;
    }
}
for(var index in arr) {
    console.log(arr[index]());
}

위 예제는 클로져 관련해 자주 언급되는 예제이다. 콘솔에 0,1,2,3,4 가 찍혀야 할 것 같지만 5만 다섯번 찍힌다. 그 이유는 i의 값이 정의한 함수의 외부 변수의 값이 아니기 때문이다.  5가 출력이 되는 이유는 i는 for문에서 정의된 전역변수이기 때문에 증가가 끝난 상태로 5의 값을 갖고 있고 배열 arr은 함수를 저장하고 있기 때문에 arr함수를 호출하면 이미 증가가 끝난 5만 출력되는 것.

 

function(){  return i; }를 외부함수로 하는 내부함수를 정의하고 외부함수의 지역변수 값을 내부함수가 참조하도록 하면 0,1,2,3,4 가 찍힘

var arr = []
for(var i = 0; i < 5; i++){
    arr[i] = function(id) {
        return function(){
            return id;
        }
    }(i);
}
for(var index in arr) {
    console.log(arr[index]());
}

마지막 code에서는 첫 번째 for문 내부 함수를 마지막(i)변수를 넣어 줌으로써 function(id)에 id 값으로 i를 넣어준다. 그러면 다시 function(id)함수의 내부함수인 function()에서 id값을 그냥 return하니깐 제일 안의 내부함수 값은 id값을 그대로 갖는 것이고 바로 상위 function(id) 함수는 내부함수가 뱉어낸 id값을 다시 return해 줌. 그러면 function(id)함수는 그냥 i 값을 id값으로 해서 값의 형태로 결과를 내어 주니깐 결국 i=0일때 arr[0] = 0; 이라는 결과가 되는 것.

 

References

https://opentutorials.org/course/743/6544

 

클로저 - 생활코딩

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

opentutorials.org

 

const outer = () => {
  const outerVariable = 'outer!'; // 1. 바깥 함수 outer의 스코프에 변수선언

  const inner = () => {
    console.log(outerVariable); // 2. 내부 함수 inner의 스코프에서 스코프체인을 타고 바깥 함수 스코프의 변수 참조
  };

  return inner; // 3. 1급 시민인 함수 inner를 바깥으로 반환
};

const fano = outer(); // 4.  fano에 inner함수의 주소값이 저장됨

fano(); // 5. outer함수 호출은 종료가 되어서 스코프가 사라져야 하지만 outerVariable은 여전히 잘 참조된다.

이 예제 코드의 5번째 주석에서 관찰할 수 있는 현상이 클로져이다. 

outer함수 바깥으로 반환된 inner함수가 outer함수의 outerVariable 변수를 참조하기에 메모리에 outer의 스코프가 여전히 남아있다.

 

클로져를 응용할 수 있는 영역

1. 함수를 여러번 호출하면 상태가 연속적으로 유지되어야 할 때

const counterCreator = () => {
  let value = 0;

  return {
    increase() {
      console.log(++value);
    },
    decrease() {
      console.log(--value);
    },
  };
};

const counter = counterCreator();

counter.increase(); // 1
counter.increase(); // 2
counter.decrease(); // 1

위와 같이 함수를 호출하면 이전 함수 호출 상태가 기억되길 바랄 때 사용할 수 있다. 실제 사례로 프론트엔트 프레임워크인 React의  hook API가 클로저를 통해서 구현되었다. hook은 함수를 여러번 호출하는 상황에서 데이터를 연속적으로 유지하는 기능. 

const Counter = () => {
  const [value, setValue] = useState(0); // 이 hook함수가 클로져를 통해 구현되었습니다.

  return (
    <div>
      <p>{value}</p>
      <button onClick={() => setValue(value + 1)}>+</button>
      <button onClick={() => setValue(value - 1)}>-</button>
    </div>
  );
};

상태 (value)가 바뀌어 렌더링이 계속 일어남에따라 Counter 함수가 여러 번 호출된다. 하지만 useState는 0이 아니라 이전상태 value의 값을 유지하고있다. 이는 useState선언 시점으이 바깥 변수에 0을 초기화한 다음, setValue로 해당 바깥 변수를 변경하는 것이다. 다음 Counter가 호출되고 그 안의 useState가 다시 호출되면 변경된 바깥변수를 value로 반환. 

 

2. 변수를 숨겨야 할 때

  let value = 0;

  function increase() {
    console.log(++value);
  };

  function decrease() {
    console.log(--value);
  }

  function unknown() {
    value = -100000;
  }

  increase(); // 1
  unknown(); // value: -100000
  decrease(); // -100001

1번 케이스는 전역변수로도 해결 가능하긴 하나 전역변수는 로직이 복잡해질 시, 어디서 변경이 되는지 추적이 어려워져 가독성과 유지보수에 어려움을 겪게 한다. 그러므로 함수 클로저가 더 적절한 선택이다.

const counterCreator = () => {
  let value = 0;

  return {
      increase() {
        console.log(++value);
      },
      decrease() {
        console.log(--value);
      },
    };
  };

const counter = counterCreator();

function unknown() {
  value = -100000;
}

counter.increase(); // 1
unknown(); // error: Uncaught ReferenceError: value is not defined
counter.decrease(); // 정상적으로 진행된다면 0

위와 같이 클로저로 함수를 만들면 외부에서 변수에 접근시 레퍼런스 에러가 발생.

 

3. 함수가 독립적으로 동작해야 할 때

카운터가 두개 필요한 상황이라고 가정할때

  let myValue = 0;
  let yourValue = 0;

  function increaseMyCounter() {
    console.log(++myValue);
  };

  function decreaseMyCounter() {
    console.log(--myValue);
  }

  function increaseYourCounter() {
    console.log(++yourValue);
  };

  function decreaseYourCounter() {
    console.log(--yourValue);
  }

카운터 두개를 만들기 위해 두배의 코드를 작성하는 것은 비효율적이다. 클로저를 활용하면 외부함수를 호출할때마다 새로운 컨텍스트를 생성-> 클로저함수 하나로 여러개 독립적인 카운터를 만들어 줄 수 있다.

const counterCreator = () => {
  let value = 0;

  return {
    increase() {
      console.log(++value);
    },
    decrease() {
      console.log(--value);
    },
  };
};

const myCounter = counterCreator();
const yourCounter = counterCreator();

myCounter.increase(); // 1
myCounter.increase(); // 2
yourCounter.increase(); // 1
myCounter.decrease(); // 1

위와같이 myCounter, yourCoutrer의 상태는 독립적이 되었다. 클로저 활용 코드 작성으로 가독성이 훨씬 좋아짐.

 

반응형

'Sparta x 이노베이션 캠프 > JavaScript' 카테고리의 다른 글

TIL : JavaScript 데이터타입  (0) 2022.10.09
TIL: [JavaScript] this  (0) 2022.09.26
TIL: 스코프 (Scope), var & let & const  (0) 2022.09.21
TIL: 호이스팅이란?  (0) 2022.09.20
TIL) 논리 연산자 || OR  (0) 2022.08.30