본문 바로가기
공부 정리/You Don't Know Js

[YDKJ] Scope & Closures - Using Closures

by 경적필패. 2023. 5. 29.
반응형

요약

이번 장에서는 클로저에 대해 다룰 것임.

이전 챕터에서 말했던대로, 클로저는 POLE원칙에 기반한 것임.

POLE => 변수접근을 최대한 어렵게!!(블록스코핑) => 코드이해 쉬움, 유지보수 쉬움, 스코프 충돌 피하기 쉬움

 

See the Closure

  • 클로저는 함수인 경우에만 작동
  • 내부 함수가 외부 범위의 변수에 대한 참조를 만드는 것이 클로저

 

Pointed Closure

  • 화살표 함수도 스코프 버블을 생성함 => 화살표 함수도 클로저에  참여!!

 

Adding Up Closures

클로저에 인용되는 전형적인 예시

function adder(num1) {
    return function addTo(num2){
        return num1 + num2;
    };
}

var add10To = adder(10);
var add42To = adder(42);

add10To(15);    // 25
add42To(9);     // 51

 

adder이 실행될 때마다 새로운 inner addTo함수 인스턴스가 생성됨.

 

Live Link, Not a Snapshot

  • 클로저는 어떤 특정한 순간의 스냅샷이 아님, 실시간으로 읽기, 쓰기가 가능

시각화

그림처럼 adder를 호출할 때마다 addTo함수 스코프가 생성됨.

 

 

 

var keeps = [];

for (var i = 0; i < 3; i++) {
    keeps[i] = function keepI(){
        // closure over `i`
        return i;
    };
}

keeps[0]();   // 3 -- WHY!?
keeps[1]();   // 3
keeps[2]();   // 3

var이므로 값을 공유한다 따라서 3,3,3이 출력됨

var keeps = [];

for (let i = 0; i < 3; i++) {
    keeps[i] = function keepI(){
        // closure over `i`
        return i;
    };
}

keeps[0]();   // 1
keeps[1]();   // 2
keeps[2]();   // 3

그러나 let으로 하면 1,2,3이 정상 호출됨 => 반복문을 돌 때마다 스코프가 형성되기 때문!

 

 

Common Closures: Ajax and Events

  • ajax 비동기통신, event에서도 클로저를 활용할 수 있다.

 

What If I Can't See It?

function say(myName) {
    var greeting = "Hello";
    output();

    function output() {
        console.log(
            `${ greeting }, ${ myName }!`
        );
    }
}

say("Kyle");
// Hello, Kyle!

위 greeting 변수는 글로벌 범위 변수로 어디에서나 접근 가능하다. 따라서 클로저로 간주되지 않음.

이는 단지 렉시컬 스코프임

=>어디에서나 접근할 수 있다면 굳이 클로저로 처리할 필요 없음!

 

Observable Definition

이제 클로저를 정의 해보겠음

클로저는 함수가 외부 스코프의 변수를 사용하는 것.

 

핵심요소

  • 함수가 포함되어야 함.
  • 최소한 하나의 외부 스코프 변수를 참조해야함
  • 변수와 다른 분기(다른 스코프)에서 호출되야함.

 

The Closure Lifecycle and Garbage Collection (GC)

더이상 필요하지 않은 참조(클로저)는 폐기하는 것이 중요함

만약 10개의 함수가 동일한 변수를 클로저로 가지고 있고, 함수 참조 중 9개가 폐지된다면 여전히 변수를 보존할 것이고, 마지막 참조가 사라져야 클로저도 사라질 것.

function manageBtnClickEvents(btn) {
    var clickHandlers = [];

    return function listener(cb){
        if (cb) {
            let clickHandler =
                function onClick(evt){
                    console.log("clicked!");
                    cb(evt);
                };
            clickHandlers.push(clickHandler);
            btn.addEventListener(
                "click",
                clickHandler
            );
        }
        else {
            // passing no callback unsubscribes
            // all click handlers
            for (let handler of clickHandlers) {
                btn.removeEventListener(
                    "click",
                    handler
                );
            }

            clickHandlers = [];
        }
    };
}

// var mySubmitBtn = ..
var onSubmit = manageBtnClickEvents(mySubmitBtn);

onSubmit(function checkout(evt){
    // handle checkout
});

onSubmit(function trackAction(evt){
    // log action to analytics
});

// later, unsubscribe all handlers:
onSubmit();

이 프로그램에서 onClick 함수는 전달된 cb를 클로저로 유지함.

마지막 인자가 빈 함수를 호출 할때, clickHandler 배열이 비워지면서, 모든 클릭 핸들러 함수 참조가 폐지됨.

따라서 cb 참조 클로저도 폐기됨. => 이렇게 더이상 필요하지 않은 이벤트 핸들러를 구독 해제하는게 중요

 

Per Variable or Per Scope?

클로저는 변수단위로 이루어 질까, 스코프 단위로 이루어질가?

function manageStudentGrades(studentRecords) {
    var grades = studentRecords.map(getGrade);

    return addGrade;

    // ************************

    function getGrade(record){
        return record.grade;
    }

    function sortAndTrimGradesList() {
        // sort by grades, descending
        grades.sort(function desc(g1,g2){
            return g2 - g1;
        });

        // only keep the top 10 grades
        grades = grades.slice(0,10);
    }

    function addGrade(newGrade) {
        grades.push(newGrade);
        sortAndTrimGradesList();
        return grades;
    }
}

var addNextGrade = manageStudentGrades([
    { id: 14, name: "Kyle", grade: 86 },
    { id: 73, name: "Suzy", grade: 87 },
    { id: 112, name: "Frank", grade: 75 },
    // ..many more records..
    { id: 6, name: "Sarah", grade: 91 }
]);

// later

addNextGrade(81);
addNextGrade(68);
// [ .., .., ... ]

이 코드는 grades, sortAndTrimGradesList를 클로저로 활용하는 코드 스니펫임.

grades를 통해 학생 전체리스트 참조 하는 것을 방지할 수 있음.(클로저 하지 않음으로써)

 

 

그러나

function storeStudentInfo(id,name,grade) {
    return function getInfo(whichValue){
        // warning:
        //   using `eval(..)` is a bad idea!
        var val = eval(whichValue);
        return val;
    };
}

var info = storeStudentInfo(73,"Suzy",87);

info("name");
// Suzy

info("grade");
// 87

이코드를 보면 내부함수에서 id, name, grade어떤것도 사용하고 있지만 접근할 수 있음.

변수들은 명시적으로 내부 함수에서 참조되지 않더라도 클로저를 통해 보존된 것.

그럼 클로저는 스코프 단위로 작동하는게 맞는걸까?

=>상황에 따라 다름

현대의 js엔진은 참조되지 않는 클로저 스코프 내의 변수를 제거해서 최적화함 => 그러나 eval같은 상황에서는 최적화 불가능.

 

Why Closure?

페이지에 있는 버튼을 클릭했을 때, ajax를 이용하여 데이터를 보내는 상황을 가정해보자

var APIendpoints = {
    studentIDs:
        "https://some.api/register-students",
    // ..
};

var data = {
    studentIDs: [ 14, 73, 112, 6 ],
    // ..
};

function makeRequest(evt) {
    var btn = evt.target;
    var recordKind = btn.dataset.kind;
    ajax(
        APIendpoints[recordKind],
        data[recordKind]
    );
}

// <button data-kind="studentIDs">
//    Register Students
// </button>
btn.addEventListener("click",makeRequest);

이 코드는 잘 작동하지만, 매번 이벤트핸들러가 돔 속성을 읽어야 된다는 단점(비효율적)이 있음

 

function setupButtonHandler(btn) {
    var recordKind = btn.dataset.kind;
    var requestURL = APIendpoints[recordKind];
    var requestData = data[recordKind];

    btn.addEventListener(
        "click",
        function makeRequest(evt){
            ajax(requestURL,requestData);
        }
    );
}

이 코드를 사용한다면 data-kind속성은 초기 설정시 한번만 검색되어 recordKind에 할당됨.

스후에 클로저로 유지되며 필요할 때 마다 접근할 수 있음.

 

function defineHandler(requestURL,requestData) {
    return function makeRequest(evt){
        ajax(requestURL,requestData);
    };
}

function setupButtonHandler(btn) {
    var recordKind = btn.dataset.kind;
    var handler = defineHandler(
        APIendpoints[recordKind],
        data[recordKind]
    );
    btn.addEventListener("click",handler);
}

완성된 코드.

 

Closer to Closure

클로저가 프로그램에 가져다주는 이점

  • 함수 인스턴스가 매번 계산을 다시 수행하는 대신 이전에 결정된 데이터를 사용해 효율적인 프로그램이 됨
  • 클로저는 변수들을 함수 인스턴스 내부에 캡슐화해서 스코프 노출을 제한하고, 그 변수에 있는 정보를 향후 사용 가능하도록 보장함.

 

 

 

느낀 점

책을 읽기전에 관심있는 챕터 중 하나였다.

어떤 기업 면접을 볼 때, 면접관이 클로저가 무엇인지 물어보았었다.

어떻게 공부한대로 대답을 하긴 했지만, 정확히 대답 했는지 의문이 있었고 항상 내가 클로저를 안다고 하기엔 찝찝함이 있었다.

 

이 챕터를 통해 클로저의 정의, 클로저의 핵심요소를 파악할 수 있었고

js엔진이 클로저를 어떻게 최적화 하는지,

클로저를 사용하면 계속 다시 계산 안해도 된다는 장점을 알게되고, 메모리로 인한 단점도 알게 되었다.

 

이 책을 모두 읽었을 때 다시한번 와서 읽어봐야겠다.

 

 

 

 

 

 

 

 

 

반응형

댓글