closure

장세진·2023년 6월 19일
1

JavaScript

목록 보기
10/12
post-thumbnail

실무경험을 하면서 클로저를 사용한 경험이 많지 않았다. 물론 클로저가 없어도 코드를 작성하는데 큰 무리가 있었던 경험은 없지만 최근 자바를 공부하면서 조금 더 객체지향적으로 코드에 접근하는것이 성장하는데 큰 기여를 할거 같다는 생각이 들었다. 클로저에 대한 경험을 더 정확하게 파악한다면 실무에서 더 적극적으로 사용할 수 있지 않을까 하는 생각에 클로저의 개념을 더 명확하게 알기위해서 mdn 문서를 통해 정리를 해보고자 한다.

클로저란

mdn 문서에 따르면

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

라고 쓰여 있다. 코드를 통해 이해해보면

function init() {
    var name = "Mozilla"; // name is a local variable created by init
    function displayName() { // displayName() is the inner function, a closure
        alert (name); // displayName() uses variable declared in the parent function    
    }
    displayName();    
}
init();

위에 코드에서 init() 을 실행시키면 displayName 이라는 함수가 생성이 된다. 여기서 displayName 은 일반 함수처럼 파라미터를 설정하고 함수내부의 지역변수만을 사용해서 특정 기능을 실행시키는 함수가 아닌 외부 렉시컬 환경의 변수 name에 접근할 수 있는 함수가 된다. 이를클로저라고 한다. (물론 displayName 이 별개로 name이라는 변수를 선언하면 내부에 선언된 지역변수 name을 참조할 것이다.)

클로저 활용 (1)

클로저는 어떤 데이터(어휘적 환경)와 그 데이터를 조작하는 함수를 연관시켜주기 때문에 유용하다. 이것은 객체가 어떤 데이터와(그 객체의 속성) 하나 혹은 그 이상의 메소드들을 연관시킨다는 점에서 객체지향 프로그래밍과 분명히 같은 맥락에 있다.

클로저는 객체지향의 상속의 특징을 가지고 있다. 코드를 통해 이해해보면

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

makeSizer 는 클래스, 그리고 size12, size14, size16 은 생성자를 통해 생성된 인스턴스가 처럼 보인다. makeSizer 는 함수를 생성하는 공장과 같다. 결과적으로 생성된 함수들은 클로저가 되며 서로다른 어휘적 환경을 저장한다.

위 처럼 생성된 함수를 아래 코드와 같이 사용 할 수 있다.

	document.getElementById('size-12').onclick = size12;
    document.getElementById('size-14').onclick = size14;
    document.getElementById('size-16').onclick = size16;
	<a href="#" id="size-12">12</a>
    <a href="#" id="size-14">14</a>
    <a href="#" id="size-16">16</a>

클로저 활용 (2)

이번에는 클로저를 활용해서 카운터함수를 만들어 보자.

const makeCounterClosure = function () {
    let counter = 0;
    function increaseBy(val) {
        counter = counter + val
    }
    function decreaseBy(val) {
        counter = counter - val
    }
    return {
        plus: function (val = 1) {
            increaseBy(val)
        },
        minus: function (val = 1) {
            decreaseBy(val)
        },
        value: function () {
            return counter
        }
    }
}

const counterClosure1 = makeCounterClosure();
const counterClosure2 = makeCounterClosure();

console.log(counterClosure1.value()) // 0
counterClosure1.plus()
console.log(counterClosure1.value()) // 1
counterClosure1.minus(3)
console.log(counterClosure1.value()) // -2

console.log(counterClosure2.value()) // 0

위 코드에서는 counterClosure1.plus, counterClosure1.minus, counterClosure1.value 세 함수에 의해 공유되는 하나의 어휘적 환경을 만든다. counterClosure1 함수는 makeCounterClosure 를 실행할 때 어휘적 환경을 가지는 클로저가 된다. 이 어휘적 환경은 counter 변수와 increseBy, decreaseBy 함수를 가지는데 익명 함수의 외부에서는 익명 함수의 어휘적 환경에 접근할 수 없다. 이는 마치 자바의 private method 와 비슷한데 익명 함수의 어휘적 환경에 접근하기 위해서는 makeCounterClosure 에서 반환 된 세 개의 퍼블릭 함수를 사용해야만 한다.

counterClosure1counterClosure2는 서로 다른 어휘적 환경을 가지는 클로저이므로 어휘적 환경 변수 counter 를 공유하지 않는다.

var & let & const

ES6 이전에는 스코프가 함수 스코프, 전역 스코프만 존재했었다. 하지만 ES6 부터 블록 또한 블록 스코프를 가지게 되었는데 문제는 var의 경우 블록 스코프 안에서 선언 시 전역 변수를 생성한다. 따라서 블록 스코프를 유지하기 위해서는 letconst 를 사용해야 한다.

console.log(x); // undefined
if (Math.random() > 0.5) {
    var x = 1;
} else {
    var x = 2;
}
console.log(x); // 1 or 2

if (Math.random() > 0.5) {
    const y = 1;
} else {
    const y = 2;
}
console.log(y); // ReferenceError: y is not defined

위의 문제점은 for 문 블록 스코프 안에서도 발생한다. 아래 코드를 통해 이해해보자.

function setClick() {
  for (var i = 0; i < 3; i++) {
    btns[i].onclick = function () {
      console.log(i);
    };
  }
}

setClick();

다음과 같이 코드를 작성하게 되면 var 로 선언된 변수 i 가 결론적으로 전역 변수로 생성되기 때문에 console.log(i)에서 i는 모두 전역 변수인 i를 가리키게 되고 최종적으로 i는 2가 되므로 버튼을 누를 때 모두 2를 출력하게 된다.

위를 해결하기 위해서 ES6 이전에는 클로저를 다음과 같이 해결책으로 제시했다.

function makeClosure (val) {
    return function closure() {
        console.log(val)
    }
}

function setClick() {
    for (var i = 0; i < 3; i++) {
        btns[i].onclick = makeClosure(i)
    }
}

setClick();

하지만 ES6 이후로 letconst 가 나오면서 varletconst 로 선언해주기만 해도 위와 같은 문제를 피할 수 있다.

function setClick() {
  for (const i = 0; i < 3; i++) {
    btns[i].onclick = function () {
      console.log(i);
    };
  }
}

setClick();

오늘은 closure 에 대해서 정리하는 시간을 가져봤다. 앞으로 실무에서 closure 를 응용해서 실용적인 코드를 짤 기회가 생기면 적극적으로 활용할 수 있을 것 같다.

profile
풀스택이 되고 싶은 프론트엔드 개발자

0개의 댓글