Closure 익히기

kyle kwon·2022년 10월 19일
0

JavaScript

목록 보기
2/5
post-thumbnail

Prologue

해당 내용은 블로그를 이전하면서 기존에 작성된 글을 옮겼습니다.

자바스크립트를 공부하면서 처음에 클로저에 대해서 익힐 때, 단어 자체도 생소하고 어떻게 사용해야 하는지 막막하여, 나중에 다시 공부해야겠다고 클로저에 대한 공부를 멈췄었습니다.

이제 왠만한 배열의 메서드, 고차함수, 객체의 기본조작, 함수 호출, 선언 및 구현하는 방법들에 대해 익숙해져, 클로저에 대해 다시 정리하기 시작했습니다. 




Concept


https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

클로저는 MDN 공식문서를 확인해보면, 함수와 함수가 선언된 어휘적 환경의 조합이라고 기술되어 있습니다.
또, 클로저는 자바스크립트의 고유 개념이 아니라 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어들에서 사용되는 중요한 특성이라고 합니다.


💡 알다시피, 자바스크립트는 prototype 기반의 언어입니다.

MDN 공식 문서에서 클로저를 다음과 같이 정의하고 있습니다.

클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다


01

예시를 들어서, 렉시컬 스코핑이 무엇인지 보겠습니다

function returnOuter(str){
    let outerStr = str;
    return function returnInner(){
        return outerStr + ' world'
    }
}

const returnStr = returnOuter('hello');
console.log(returnStr()); // hello world

구조를 보면,
함수 returnOuter 내에 returnInner 함수가 선언되었습니다.


작동과정을 보면

returnOuter라는 함수가 먼저 선언되고, returnOuter 함수에 ‘hello’라는 문자열을 인자로 주어 호출한 것을 returnStr에 할당하였습니다. 호출한 것은 returnInner라는 함수를 반환하였습니다.
그리고, returnStr(), 즉 반환한 returnInner라는 함수를 호출하여, hello world라는 값을 출력하였습니다.

여기서 중요한 것은 스코프가 함수를 호출할 때가 아니라, 함수를 어디에 선언했는지에 따라 결정되는 것인데요. 이를 렉시컬 스코핑이라고 합니다.

함수 returnInner가 함수 returnOuter 내부에서 선언되었기 때문에, returnInner 함수의 상위 스코프는 함수 returnOuter라는 것을 알 수 있습니다.

이 때, 내부 함수 returnInner라는 함수는 자신이 속한 렉시컬 스코프(전역공간, 함수 returnOuter, 자신의 스코프)를 참조할 수 있습니다. 이 말은 내부 함수 returnInner가 outerStr라는 변수에 접근 및 참조할 수 있다는 것을 말합니다!

💡 클로저는 반환된 내부 함수가 자신이 선언되었을 때의 환경인 스코프를 기억해서, 외부(전역공간 등)에서 호출되어도 선언되었을 때의 환경에 접근할 수 있는 함수를 의미합니다.



Closure 활용 예시

01

이번에는 숫자를 반환하는 예시를 들어보겠습니다.

function multiply(num1){
    let tempNum = num1;
    return function(num2){
        return tempNum * num2;
    }
}

const result = multiply(10);
console.log(result(50));

구조를 보면,
multiply라는 외부 함수가 있고, 그 안에 곱한 값을 반환하는 익명함수가 있습니다.


작동 과정을 보면,

multiply라는 함수가 선언되고, multiply라는 함수에 10이라는 인자를 담아 호출하여, 이 호출한 것을 result라는 변수에 할당하였습니다. 호출한 것은 익명함수를 반환합니다.

그리고 result(50), 즉 익명 함수에 50이라는 인자를 넘겨 호출하여, 이 익명함수는 렉시컬 스코핑에 의해 자신이 속한 렉시컬 스코프를 기억하기 때문에, tempNum이라는 변수에 접근하고, 값을 참조해서 num2(50)와 곱한 값을 반환했습니다.


결과는 다음과 같습니다.



02

추가적으로 간단한 UI 조작에 클로저를 활용해보겠습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="box" style="margin-bottom: 20px; width: 200px; height: 200px; background-color: #1f86c1"></div>
    <button class="toggle" style="padding: 5px 15px;">change</button>

    <script>
        const box = document.querySelector('.box');
        const toggleBtn = document.querySelector('.toggle');

        const showAndHide = function (){
            let state = false;

            return function (){
                box.style.display = state ? 'block' : 'none';
                state = !state;
            };
        };

        toggleBtn.onclick = showAndHide();

    </script>
</body>
</html>

showAndHide라는 함수 내부에는 state라는 변수를 이용해서 삼항논리 연산자에 의해 box라는 클래스의 요소가 block이 될 지 none이 될지에 대해서 컨트롤해주는 익명함수가 있습니다. (마치, state 변경값을 비교하는 react의 state hook과 같습니다)

결과적으로 버튼을 클릭하는 이벤트를 발생시키면, 박스를 숨겼다가 나타났다가 하는 동작을 할 수 있습니다.


💡 이처럼 클로저를 활용했는데, 위의 예시와 같이 현재 상태(state)를 기억하고, 변경된 최신 상태를 유지할 때 클로저를 유용하게 사용할 수 있습니다.

state라는 변수는 전역공간에 있지 않고, 외부 함수 안에 있는데, 이처럼 은닉화를 할 수 있습니다. 밖에서 보이지 않게 숨기고 싶을 때 사용합니다.
진짜 바깥에서 호출 할 수 없는지 확인해 볼까요?

const showAndHide = function (){
            let state = false;

            return function (){
                box.style.display = state ? 'block' : 'none';
                state = !state;
            };
};

console.log(state);

위와 같이 state가 정의되어 있지 않다고 출력됩니다.

이렇게 은닉화한 사례를 보여주고 있는데, 유의할 것은 클로저가 꼭 은닉화를 위해서 사용하는 것은 아닙니다.



03

특정 데이터를 은닉화 하는 예시를 한 번 더 보겠습니다.

function CounterApp(initValue){
    let countValue = initValue ?? 0;
    
    return {
        value() {
            return countValue;
        },
        increment() {
            return countValue += 2;
        },
        decrement() {
            return countValue -= 2;
        },
        returnRandom() {
            return Math.random() * countValue;
        }
    }
}

const counter1 = CounterApp(2);
console.log(counter1.value()); // 2
console.log(counter1.increment()); // 4
console.log(counter1.decrement()); // 4
console.log(counter1.returnRandom()); // 0.22029289755520276

CounterApp이라는 생성자 함수를 만들고, initValue를 넣었는데, 내부에 반환하는 메서드를 호출했더니, 각기 다른 결과를 나타냅니다.

countValue라는 변수는 외부에서 접근할 수 없고, 인스턴스화 한 후, 인스턴스에서 반환하는 객체 내부의 각각의 메서드에 접근하여 호출하면 countValue라는 변수에 의존해서, 각기 다른 값을 반환하는 것입니다.




Conclusion

위의 예시들 이외에도 클로저는 특정 이벤트가 실행될 때, 과하게 실행되는 것을 지연해주는 debounce와 같은 기능, 그리고 무한 스크롤 또는 클릭을 여러 번 누를 때 지연시켜주는 기능 등에도 사용되곤 합니다.

이후에 debounce와 throttle과 같은 클로저 활용 사례에 대해서 한 번 공부해보고, 포스팅 해보겠습니다.

profile
FrontEnd Developer - 현재 블로그를 kyledot.netlify.app으로 이전하였습니다.

0개의 댓글