ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바스크립트 클로저(Closure) _속 깊은 Javascript 2장
    프론트엔드 2019. 12. 15. 00:45

    ※ 책 <속 깊은 Javascript> 2장. 자바스크립트의 스코프와 클로저 단원에서 '클로저'부분을 보고 작성한 글입니다. 글에 사용된 예제는 책에 있는 예제를 살짝 변형해서 만들었습니다. 공부하려고 쓴 글이라 부정확한 부분이 있을 수 있습니다.

     


    클로저에 대해 설명하기 전에, 반드시 알고 넘어가야할 '스코프 체인'에 대해 먼저 알아보자.

    스코프 체인

    자바스크립트는 중괄호( {} )를 기준으로 스코프가 생성되는 다른 언어들과 달리,

    '함수 단위'로 스코프가 생성된다. 스코프는 함수가 호출될 때마다 새로 생성된다.

    이 코드는 다음과 같은 스코프 체인을 생성한다.

     

     

     

    스코프 체인에서 변수를 탐색할 때는 다음과 같은 과정을 거친다.

     

    inner함수에서 어떤 변수를 사용하려고 하면, inner 함수의 스코프에서 먼저 그 변수를 찾아보고 ->

    없으면 상위 스코프 체인인 outer 함수의 스코프에서 그 변수를 찾아보고 ->

    그래도 없으면 글로벌 스코프에서 그 변수를 찾아서 사용한다.

     

    스코프 체인은 이렇게 하위 스코프가 상위 스코프에 포함되는 관계를 이루고 있기 때문에,

    하위 스코프에서는 체인으로 연결된 상위 스코프의 변수들에 접근할 수 있다.

     


    클로저란?

     

    클로저는 기본적으로 function 안에 function이 있을 때(=스코프 안에 스코프가 있을 때) 생성된다.

    클로저가 발생하는 가장 대표적인 두 가지 케이스는,

    1. 내부함수가 반환되어 호출될 때

    2. 이벤트 핸들러의 콜백함수로 활용될 때

    이다.

     


    클로저 이해하기

     

    먼저 내부함수가 반환되어 호출되는 경우의 간단한 예시를 통해 클로저가 무엇인지 알아보자.

    var func = outer(); 에서 outer()함수를 호출했다.

    그러면 outer함수가 실행되고 inner함수가 리턴되어 func변수에 담긴다.

    그리고 다음 줄에서 func()을 호출했다. -> inner 함수가 실행된다.

     

    이 때, inner 함수는 outer함수의 변수인 'a'를 참조하고 있는데,

    outer()함수는 이미 실행이 끝난 후이므로 (= 즉, outer함수와 outer함수의 변수(=outer함수의 스코프)가 사라진 상태이기 때문에)

    원래라면 inner함수가 정상적으로 실행되지 않아야 한다.(참조하는 변수가 존재하지 않으므로)

     

    그러나 결과를 보면 inner 함수는 정상적으로 실행됨을 알 수 있다.

    inner함수가 참조하는 변수 a를 포함하는 외부함수 outer()와, 외부함수 outer()의 스코프가 사라지지 않고 유지되고 있기 때문이다.

     

     

    이처럼 외부함수가 내부함수를 리턴하고, 리턴한 내부함수를 글로벌 변수에 담아 실행했을 경우, 내부함수가 참조하고 있는 외부함수와 외부함수의 스코프가 사라지지 않고 유지된다.

    이때 (외부함수 + 외부함수의 스코프)를 묶은 덩어리를 '클로저'라고 한다.

    클로저를 통해 각 함수는 자기만의 고유한 값을 보유하고 스코프 체인을 유지하면서 그 체인 안에 있는 모든 변수들의 값을 유지한다.

     

    다시 한 번 설명하면,

    outer()함수의 실행은 앞에서 이미 끝났지만, 실행이 끝난 후에도 outer()함수의 변수인 'a'가 담겨있는 스코프가 사라지지 않고 '클로저'를 생성했다.

    따라서 inner()함수에서 outer()함수의 스코프에 있는 변수 a를 참조할 수 있는 것이다.

     

     

    또한 위 예시에서 outer()함수의 변수인 a 는 outer()함수의 로컬 변수이므로,

    원래는 외부에서 접근할 수 없다. 함수 내부에서만 접근할 수 있다.

    그런데 inner 함수는 outer 함수의 내부에 있으므로 outer함수의 변수인 a에 접근할 수 있는데,

    outer함수의 로컬 변수인 a에 접근할 수 있는 inner 함수를 글로벌 변수인 'func'에 할당함으로써

    outer함수 외부에서도 변수 a에 접근할 수 있게 됐다.

    var는 전역적인 특성을 지니고 있지만, 클로저의 특성을 이용해서 private 변수처럼 사용할 수 있다.

    let가 const가 등장하기 전에는 특히 클로저가 변수의 제어에 많이 활용되어왔다고 한다. (*var은 함수 단위의 스코프 생성, let은 중괄호 단위로 스코프 생성)

     

     

    두 번째 예제를 보자.

     

    앞의 예제에 코드 한 줄이 추가되었다.

     

    스코프는 함수가 호출될 때마다 새롭게 생성되므로,

    func2 변수를 선언하면서 outer()함수를 호출 할 때, 새로운 outer 함수 스코프가 추가로 생성된다.

    그래서 다음과 같은 스코프 체인이 만들어진다.

    func에 담긴 inner함수와

    func2에 담긴 inner함수가 각각 다른 outer함수의 스코프를 참조하고 있음을 알 수 있다.

     

    그런데 변수들이 이렇게 따로따로 저장되고 활용되는 대신에,

    모든 func, func2, func3 ... 이 동일한 a변수를 공유하도록 할 수는 없을까?

     

    여러 함수 간에 같은 변수를 공유할 수 있도록 하려면, 변수 a를 static 변수처럼 만들면 된다.

    static변수를 만들기 가장 쉬운 방법은 '글로벌 변수'를 사용하는 것이다.

    그러나 많은 개발자들이 글로벌 변수 사용을 지양하므로, 글로벌 변수를 쓰는 대신

    클로저를 이용해 변수 a를 static 변수처럼 공유할 수 있게 만들어보자.

     

     

    IIFE로 outer함수 스코프 상위에 스코프 하나를 더 추가했다.

     

    IIFE 스코프에는 staticA 변수를, outer 함수의 스코프에는 localA 변수를 선언했다.

    이렇게 하면 outer함수를 여러번 호출해서 outer함수의 스코프를 여러 개 생성해도, 모든 outer함수의 스코프가 같은 IIFE 상위 스코프를 공유하게 된다.

    따라서 staticA변수를 static 변수처럼 사용할 수 있다.

    반면에 localA 변수는 outer함수의 스코프에 있으므로, local 변수와 같은 기능을 하게 된다.

     

    프로토타입 기반 객체지향 개념을 지원하는 자바스크립트에서는 이렇게 클로저를 통해서

    클래스 기반 객체지향 언어처럼 캡슐화, 모듈화 작업을 수행할 수 있다.

     


    클로저 활용하기

     

    다음으로 이벤트 핸들러의 콜백함수로 활용되는 예시를 보고

    클로저를 어떻게 활용해서 문제를 해결할 수 있는지 알아보자.

     

    문제가 있는 소스이다.

    Div 0, 1, 2를 눌렀을 때 각각 You clicked div #0, 1, 2가 알림창으로 떠야 하는데

    어떤 div를 눌러도 "You clicked div #3"이라는 알림창이 뜬다.

     

    이런 문제가 발생하는 이유는, 자바스크립트의 스코프가 함수 단위로 생성되기 때문이다.

     

    for문의 중괄호 {} 에서 따로 스코프가 생성되지 않아

    for문 안의 변수 i가 for문 밖에서 선언된 전역변수 i를 참조하게 된다.

    코드의 흐름을 따라가 보면

    5번째 줄에서 변수 i를 선언하면서 값을 3으로 초기화 했다. ( i = 3 )

    6번째 줄 for문의 선언부에서 i에 0이 대입된다. ( i = 0 )

    divScope0에 이벤트 핸들러(addEventListener)를 연결해준 다음,

    다시 for문의 선언부로 들어와 i의 값이 1 증가한다. ( i = 1 )

     

    이렇게 for문이 진행되면서 각각의 divScope에 이벤트 핸들러를 연결해주고, 다시 선언부로 돌아오는 과정을 거치면서

    i값은 1씩 증가해서 최종적으로 다시 3이 된다. ( i = 3 )

     

    그 후에 사용자가 div를 클릭하는 이벤트가 발생했을 때 콜백함수가 호출되는데, 이때 i의 값은 3으로 저장되어있으므로 i = 3으로 출력이 된다.

     

    스코프 체인을 보면, 각 div에 연결된 세 개의 콜백함수가 모두 글로벌 스코프에 있는 변수 i 를 참조하기 때문에 (세 개의 콜백함수가 똑같은 변수 i를 공유하는 형태) 똑같이 div# 3이 출력되는 것이다.

     

     

    이 소스의 문제를 클로저를 통해 해결해보자.

     

    콜백 함수 상위에 스코프 하나를 추가하는 방법으로 간단하게 해결할 수 있다.

    코드의 실행 과정은 이렇다.

    즉시 호출 함수 function (index) { /**/ }(i) 안에서 새로운 함수를 선언해서 반환한다.

    → 반환된 함수는 index 변수를 상위 스코프 체인에 추가한 뒤,

    → addEventListener() 함수의 두번째 인자로 들어간다(=콜백함수가 된다)

     

    IIFE를 이용해 상위 스코프를 하나 추가함으로써 다음과 같은 스코프 체인이 만들어진다.

     

    각 div의 콜백함수들이 글로벌 스코프의 i 변수가 아닌,

    자신의 상위 스코프 체인에 추가된 IIFE의 스코프에 있는 index 변수를 참조하기 때문에

    어떤 div를 클릭하는지에 따라 각각 다른 값이 출력된다.

     

    클로저를 활용해 소스코드의 문제를 해결해 의도한 결과를 얻을 수 있게 됐다.

     

     


    클로저의 단점

     

    클로저의 단점과, 클로저를 활용할 때 유의해야할 점에 대해서도 알아보자.

     

    • 메모리를 소모한다.

    루프를 돌면서 클로저를 계속 생성하는 설계는 피해야 한다.

    클로저를 생성할 때는 하나의 커다란 클로저를 생성하기보다는 각 변수나 함수들의 생명주기를 분석한 다음 효율적으로 나누면 좋다.

     

    • 스코프 생성과 이후 변수 조회에 따른 퍼포먼스 손해가 있다.

    하위 스코프에서 상위 스코프의 변수에 접근하고자 한다면,

    스코프 체인을 따라 상위 스코프로 올라가면서 탐색을 해나가야하기 때문에,

    퍼포먼스에 영향을 미칠 수 있다.

    따라서 클로저를 활용할 때는, 과하게 사용하면 퍼포먼스에 영향을 미칠 수 있다는 점을 염두에 둬야한다.

     

    • 익숙하지 않으면 이해하기 어렵다

    협업할 때는 명확한 주석과 문서화를 할 필요가 있다

    댓글

Designed by Tistory.