티스토리 뷰

클로저

클로저는 생명주기가 끝난 외부함수의 컨텍스트에 접근하는 함수를 뜻한다. 아래 예제를 통해 살펴보자.

function outer() {
    // 자유 변수
    let num = 1;
    // 클로저
    return function() {
        // 외부 함수의 변수 접근
        console.log(num);
    }
}

let inner = outer();
// 외부 함수의 생명주기가 끝난 상태에서 호출
inner(); // 1

위 예제를 보면 외부함수의 생명주기가 끝났음에도 정상적으로 num이 출력됨을 볼 수 있다. 이 때 외부함수의 변수를 자유 변수라고 하고, 자유 변수에 접근하는 함수를 클로저라고 한다.

이전 글에서 함수는 생성 당시의 컨텍스트를 [[scope]] 프로퍼티에 가지고 있는다고 말하였다. 아래 그림 1처럼 외부함수의 변수를 [[scope]] 프로퍼티가 참조하고 있는 상태이기 때문에 가비지 컬렉팅 대상이 되지 않고 계속 참조 가능한 것이다.

[그림 1] [[scope]] 프로퍼티의 외부 변수 참조

클로저와 캡슐화

자바스크립트의 변수와 프로퍼티는 외부에서 수정이 가능하다. 예를 들어 아래와 같이 객체를 생성하면, 언제든지 외부에서 프로퍼티를 수정할 수 있다.

function Person(name) {
    this.name = name;
}

let person = new Person('kook');
console.log(person.name); // kook

person.name = 'kim';
console.log(person.name); // kim

위와 같이 외부에서 프로퍼티 수정을 하지 못하도록 막으려면 클로저를 사용해야 한다.

function Person(name) {
    let _name = name;
    
    this.getName = function() {
        return _name;
    }

    this.setName = function(name) {
        _name = name;
    }
}

let person1 = new Person('kook');
let person2 = new Person('park');

console.log(person1._name); // undefined
console.log(person1.getName()); // kook

console.log(person2.getName()); // park

Person 생성자 함수의 생명주기가 끝나고 _name은 오직 getName, setName 메서드에서만 참조 가능하므로 은닉화가 되는 것이다. 위와 같은 방식은 아래의 모듈 패턴으로도 구성할 수 있다.

function Person(name) {
    // 비공개 프로퍼티
    let _name = name;
    
    // 필요한 부분만 반환
    return {
        getName : function() {
            return _name;
        },

        setName : function(name) {
            _name = name;
        }
    }
}

let person1 = new Person('kook');
let person2 = new Person('park');

console.log(person1._name); // undefined
console.log(person1.getName()); // kook

console.log(person2.getName()); // park

위와 같이 외부에서 접근할 수 있는 프로퍼티만 객체 리터럴로 내보내는 방식을 모듈 패턴이라고 한다. 다만 생성자 함수에서 객체 리터럴로 리턴하면 프로토타입이 Object 이므로, 후에 상속과 같은 이점을 누리기 힘들다는 것을 알아두자.

클로저와 모듈 패턴을 이용하면 아래 코드와 같은 싱글턴 방식의 모듈도 작성이 가능하다.

const emailModule = (function() {
    // 비공개 프로퍼티
    const pattern = /^[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/;
    
    // 공개 메서드
    return {
        checkPattern : function(emailAddr) {
            return pattern.test(emailAddr);
        }
    };
})();

emailModule.checkPattern('person@naver.com'); // true
emailModule.checkPattern('personnaver.com'); // false

즉시 실행 함수를 이용해 필요한 메서드만 반환 받고, 프로퍼티는 클로저를 이용해 접근하는 방식이다.

루프에서 클로저 생성 시 주의사항

아래와 같이 반복문에서 클로저를 생성하는 코드가 있을 때 생기는 문제점을 예상해보자.

function countSeconds(seconds) {
    for (var i = 1; i <= seconds; i++) {
        setTimeout(function() {
            console.log(i);
        }, i * 1000);
    }
}

countSeconds(3); // 4, 4, 4

위 코드의 실행 결과는 예상과 달리 4가 세번 출력된다. 그 이유는 변수 i가 이미 4가 된 상태로 클로저에서 참조하고 있기 때문이다.

위와 같이 반복문에서 모든 클로저가 같은 자유 변수를 참조하는 것을 막으려면, 한번 더 함수로 감싸야한다.

function countSeconds(seconds) {
    for (var i = 1; i <= seconds; i++) {
        setTimeout((function(secondI) {
            return function() {
                console.log(secondI);
            };
        })(i), i * 1000);
    }
}

countSeconds(3); // 1, 2, 3

이제 setTimeout함수에 등록된 클로저가 i 대신 secondI 라는 자유변수를 참조하므로, i가 변해도 영향이 없게된다.

ES6부터는 위와같이 복잡하게 해결하지 않아도 된다. let 키워드로 변수를 생성하면 var 변수와 달리, 블록 단위의 유효범위를 갖기 때문이다.

function countSeconds(seconds) {
    // let 변수 생성
    for (let i = 1; i <= seconds; i++) {
        setTimeout(function() {
            console.log(i);
        }, i * 1000);
    }
}

countSeconds(3); // 1, 2, 3

 


참고

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

 

클로저

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.

developer.mozilla.org

 

'프로그래밍 > Javascript' 카테고리의 다른 글

자바스크립트 클로저  (0) 2019.11.12
자바스크립트의 실행 컨텍스트  (0) 2019.11.08
프로토타입 체이닝  (0) 2019.11.07
자바스크립트 this  (0) 2019.11.07
자바스크립트 함수  (0) 2019.11.06
자바스크립트 데이터 타입  (0) 2019.11.06
댓글
댓글쓰기 폼