본문 바로가기

Javasciprt/js

Closure를 공부해보았다 (1) - 맛보기

이 글은 클로저를 할짝해보는 글입니다. '틀린 것 없이 잘 썻는지 볼까?'라는 느낌으로 읽어주시고, 가르침을 주시면 좋겠습니다. 하하!

따라서 이 글의 대상 독자는 '알고싶은데 하나도 모르는 사람'입니다. 하하!

동기

인턴으로 들어간 회사에서 React로 개발을 하는 도중에 굉장히 골치 아팠던 상황이 있었다.

 

그 상황은 대략적으로 아래와 같은 상황이다.

const App = () => {
const [products, setProducts] = useState([]);
  const handleProducts = () =>; {
      setProducts(
            products.map(
                //뭔가합니다.
          )
      );
  }
 
  const Products = () =>; {
      return (
         <Button onClick={handleProducts}/>
      );
  }
  ...
 
};
  cs

 

위에서 App아래 Products라는 컴포넌트가 선언된다. 이 Products는 products 배열의 변화에 따라서 하나씩 추가, 삭제가 된다. 위의 handleProducts에서 products를 접근해 데이터를 생성하고, 그것을 setProducts를 통해 products의 값을 재생성하려고 시도했다.

 

위와 같은 상황에서 delete를 시켜보니 이상한 현상이 발생했다. 간단히 예를 들어보자면 다음과 같다.

 

  •  4개의 아이템을 만들었다.
  •  2번째 아이템을 변경/삭제 했다.
  •  1개의 아이템만 남게 되었다.

이런 상황을 처음 겪다보니 당황을 했다. '도대체 뭘까...' 고민이 깊어지기 시작했다. 하루동안 내 주변에 갓갓이라고 생각하면서 ''는 그래도 친하다고 생각하는 친구에게 물어도 보고, 혼자서 이거 저거 찾아도 보고, 시도도 해봤지만 도저히 알 수가  없었다. 설명하는 실력과 검색하는 실력의 부족함을 느끼면서 밤늦도록 잠을 못잤다.

 

그렇게 거의 하루동안 '참조되는 값이 제한된다 '라는 생각만을 가진채 끙끙앓다가 뇌리를 스쳐지나간 개념이 있었다.

바로 'Closure'다.

 

 


사전 지식

Closure라는 것의 느낌을 알기 위해서는 2가지 선수지식이 필요하다.

 

바로 'First class citizen'과 'Lexical Environment'라는 개념이다.

오류가 왕왕 있을 수 있습니다. 저를 너무 믿지 마세요. 하하!


First class citizen

먼저 First class citizen부터 알아보자. First class citizen의 유래는 아래 참조의 First class citizen을 참고해 주세요.

First class citizen 조건은 다음 3가지 이다.

 

  • 변수에 담을 수 있다.
  • 매개변수(parameter)로 전달 가능하다.
  • 반환 값(return value)으로 사용 가능하다.

First class citizen의 예시로는 대부분의 언어에서 위의 3가지 조건을 만족하는 숫자가 될 수 있다.

이 First class citizen 하위에 아래 2가지 개념이 있다.

 

  • First class object
  • First class function

흔히 '1급 객체'라 부르는 'First class object'는 위에서 제시한 3가지 조건을 모두 만족한 객체들이다.

또한 흔히 '1급 함수'라고 부르는 'First class function'은 위의 3가지 조건과 더불어 아래 2가지 조건을 충족하는 함수들을 지칭한다.

 

  • runtime에 생성 가능하다.
  • 익명 생성이 가능하다.

Javascript의 경우 객체를 First class citizen으로 취급하며, 함수는 '1급 함수'의 조건을 만족한다.

 

1급 객체의 장점은 고차함수(high order function)를 생성할 수 있다는데 있다.

순수 C언어로 작성한 array의 find와 javascript array의 find를 생각해 보면 그 장점을 쉽게 깨우칠 수 있다.


Lexical Environment

다음으로 Lexical Environment다.

Lexical Environment는 Lexical Scope에 따라 정해지는 Context를 의미한다.

Lexical Scope

Lexical Scope의 빠른 이해를 위해 아래 코드를 예시로 하겠다.

var name = 'heozeop';
 
const printName = () => {
 
  console.log(name);
}
 
const printNewName = () => {
 
  var name = 'supistar';
 
  print();
}
printNewName(); // heozeop
cs

 

이것의 실행 결과로 console에는 heozeop이 찍힐 것이다. Lexical scope에따라 printName 은 함수가 선언될 당시, 선언된 함수와 가장 가까운 변수 name을 참조했다. 따라서 아래 새로 선언된 supistar가 아닌 전역에 선언되었던 heozeop을 출력하는 것이다.

 

이와 같은 개념이 Lexical scope이다.

 

Context

Context의 정의는 wikipedia의 context글에서 가져왔다.

In computer science, a task context is the minimal set of data used by a task (which may be a process, thread, or fiber) that must be saved to allow a task to be interrupted, and later continued from the same point.

위의 인용구에서 잘 드러나다 싶이, Context는 간단히 말해 '재실행을 위한 최소한의 data set'를 의미한다.

좀 더 자세한 이야기는 '클로저를 공부해보았다 (2) - 자세히 알아보기'에서 다루겠습니다 .

 

 

따라서 Lexical Environment란,

        Lexical scope라는 scope방식에 따라서 정해지는 재실행을 위한 최소한의 data set을 의미한다.


Closure

드디어 여기다.

Closure는 '함수와 그 함수가 선언됐을 때의 Lexical environment와의 조합' 이라고 한다.

 

백문이 불여일견이라 했다. 코드를 보자.

const say = () => {
 
    const word = 'hi';
    
    return (() => {
        alert(word);
    });
};
 
const sayHi = say();
 
sayHi(); // hi
cs

 

위 코드에서 say의 실행 결과를 sayHi에 할당했다. 그리고 나서 sayHi를 실행시켰다. 그 결과 hi가 alert 되었다.

 

우리가 흔히 생각하는 것과 달리, say라는 함수가 끝났음에도 리턴되는 함수에서 word를 사용했다. 함수의 실행이 종료됬음에도 메모리 해제가 안된 것이다. 혼란스러우니 예시를 하나 더 보도록 하자.

 

const autoIncrease = (() => {
 
    let id = 0;
    
    return () => id++;
    
})();
cs

 

결과가 어떨지 예상이 갈 것이다. autoIncrease를 할당해 함수를 부르면 id가 리턴되고, 1씩 커진다.

 

왜 이렇게 될까?

 

바로 함수 선언시에 생성된 Lexical Environment 덕분이다.

함수 생성시에 결정된 context가 리턴된 함수에 의해 참조되는 환경이기 때문에 메모리에서 사라지지 않았다.

그 결과, autoIncrease에 의해 반환된 함수가 id를 계속 참조하고 사용할 수 있게된 것이다.

 

이제 Closure를 다시 정의해 보겠다.

Closure함수 선언시에 정해지는 Lexical Environment로, 해당 closure를 참조하는 함수(객체)가 사라질 때까지 메모리에 남아 함수가 접근해 이용가능한 데이터다.


이제 의문은 어느정도 풀렸다.

handleProducts가 선언되는 시점의 데이터가 메모리에 남아서 참조되고 있다보니 이러한 문제가 발생하는 것이다.

 

의문은 해소했는데, 내가 알고 싶은 일부분만 공부하다보니 앎의 깊이가 부족함을 느낀다.

Javascript의 scope, scope chain, hoisting, IIFE, closure 등등 내가 제대로 알지 못하고 있는 것들이 많다. 계속해서 공부하면서 글을 써야 겠다.


참조

First class citizen

1. https://bestalign.github.io/2015/10/18/first-class-object/

2. https://medium.com/@soeunlee/javascript%EC%97%90%EC%84%9C-%EC%99%9C-%ED%95%A8%EC%88%98%EA%B0%80-1%EA%B8%89-%EA%B0%9D%EC%B2%B4%EC%9D%BC%EA%B9%8C%EC%9A%94-cc6bd2a9ecac

 

Lexical Environment

1.https://www.zerocho.com/category/JavaScript/post/5740531574288ebc5f2ba97e

2. https://medium.com/@js_tut/javascript-tutorial-lexical-environment-3ee161bb2295

3. context - https://en.wikipedia.org/wiki/Context_(computing)

 

Closure

1. https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Closures

2.https://poiemaweb.com/js-closure

3.https://www.zerocho.com/category/JavaScript/post/5741d96d094da4986bc950a0

'Javasciprt > js' 카테고리의 다른 글

javascript의 안티패턴 - async & map  (0) 2020.08.16