hee.hee 2022. 5. 13. 09:41

 

클로저란?


"외부 함수의 변수에 접근할 수 있는 내부 함수"

클로저는 함수와 함수가 선언된 어휘적 환경의 조합을 말한다. 외부 함수의 변수에 접근할 수 있는 내부 함수를 클로저 함수라고 하며, 함수를 리턴하는 함수가 클로저의 형태를 만든다. 함수와 함수가 선언된 어휘적(lexical) 환경의 조합을 말한다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.

주의해야할 것, 클로저로 간주되는 함수는 '내부 함수'이다!

더보기

JavaScript에서 클로저 정의

클로저는 "함수와 함수가 선언된 어휘적(lexical) 환경의 조합을 말합니다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다." 라고 합니다.

여기서의 키워드는 "함수가 선언"된 "어휘적(lexical) 환경"입니다. 특이하게도 자바스크립트는 함수가 호출되는 환경과 별개로, 기존에 선언되어 있던 환경 - 어휘적 환경 - 을 기준으로 변수를 조회하려고 합니다. 유어클래스 영상에서 언급되는 "외부 함수의 변수에 접근할 수 있는 내부 함수"를 클로저 함수로 부르는 이유도 그렇습니다.

클로저 함수: 클로저는 외부 함수의 컨텍스트에 접근할 수 있는 내부 함수를 뜻합니다. 외부 함수의 실행이 종료된 후에도, 클로저 함수는 외부 함수의 스코프, 즉, 함수가 선언된 어휘적 환경에 접근할 수 있습니다.

클로저 사용 예시: 클로저를 통해 커링(currying, 함수 하나가 n개의 인자를 받는 대신 n개의 함수를 만들어 각각 인자를 받게 하는 방법), 클로저 모듈(변수를 외부 함수 스코프 안쪽에 감추어, 변수가 함수 밖에서 노출되는 것을 막는 방법) 등의 패턴을 구현할 수 있습니다.

클로저의 단점: 일반 함수였다면 함수 실행 종료 후 가비지 컬렉션(참고 자료: MDN '자바스크립트의 메모리 관리') 대상이 되었을 객체가, 클로저 패턴에서는 메모리 상에 남아 있게 됩니다. 외부 함수 스코프가 내부 함수에 의해 언제든지 참조될 수 있기 때문입니다. 따라서 클로저를 남발할 경우 퍼포먼스 저하가 발생할 수도 있습니다.

자바스크립트는 가비지 컬렉션을 통해 메모리 관리를 합니다. 객체가 참조 대상이 아닐 때, 가비지 컬렉션에 의해 자동으로 메모리 할당이 해제됩니다.

 

 

 

 

그냥 함수와 클로저 함수의 비교

그냥 함수

const add = (x, y) => x + y;
add(5, 7); //12

클로저 함수

const adder = x => y => x + y;
adder(5)(7); //12

위의 화살표 함수를 함수 표현식으로 다시 보자.

const adder = function (x) {
	return function (y) {
		return x + y;
        }
        }

함수 안에서 함수를 리턴하고 있다.

클로저는 함수를 리턴하는 함수이다.

클로저의 핵심은 스코프를 이용해서, 변수의 접근 범위를 닫는(closure; 폐쇄) 것

내부 함수는 외부 함수에 선언된 변수에 접근 가능하다.

 

 

y를 매개변수로 갖는 function을 내부함수, x를 매개변수로 갖는 function을 외부함수라고 하자.
- Q. 외부 함수는 y에 접근이 가능한가요? → no 바깥 스코프에서는 안쪽 스코프로의 접근이 불가능합니다.
- Q. 내부 함수는 x에 접근이 가능한가요? → yes, 안쪽 스코프는 바깥 스코프에서 선언된 변수에 접근이 가능합니다.
여기서 '접근'은 '사용할 수 있는지' 라고 생각하면 이해가 간다. 남들에게 말할 때에 대비해 저 표현에 익숙해지자.

 

 

 

 

 

 

클로저 함수의 구분

let multiplyByX = function(x) {
  return function(y) {
    return x * y;
  }
}

let multiplyBy5;
multiplyBy5 = multiplyByX(5);

multiplyBy5(4);
let multiplyByFive = function() {
  return function(y) {
    return 5 * y;
  }
}

let multiplyBy5 
multiplyBy5 = multiplyByFive();
multiplyBy5(4);

리턴 함수가 x에 접근할 수 있기 때문에 multiplyByX가 클로저를 사용하고 있다고 볼 수 있다.

반면 multiplyByFive는 함수를 리턴하고 있으나 리턴 함수가 외부 함수의 변수를 사용하지(=변수에 접근하지) 않고 있다.

더보기

클로저 함수는 "외부 함수의 컨텍스트에 접근할 수 있는 내부 함수" 라고 배웠습니다. 정답인 A를 볼까요? 리턴되는 함수가 x에 접근할 수 있죠? 이 x가 위치한 곳은 외부 함수의 context, x에 접근 가능한 스코프 안입니다. 이렇게 x를 포함하여 쓰여있는 코드 자체를 어휘적 환경(lexical environment)라고 부를 수 있습니다. 사실은 더 심오한 개념을 포괄하고 있습니다만, 우선은 이렇게 알아두고 넘어가도 좋습니다.

그렇기 때문에, 클로저의 정의 "함수와 함수가 선언된 어휘적(lexical) 환경의 조합을 말한다."로

부터 우리는 "아, x에 접근할 수 있는 이유는, 리턴되는 함수가 선언된 주변의 어휘적 환경 중 x가 포함되어 있기 때문이구나"라고 이해할 수 있고, 이런 함수를 통칭하여 클로저 함수("외부 함수의 컨텍스트에 접근할 수 있는 내부 함수")라고 부릅니다.

클로저에 대한 심도 있는 학습을 하고 싶으시다면 찾아봐야 하는 자료가 방대하기 때문에, 아직 이해되는 내용이 많지 않다면 "함수 내에서 다른 함수(내부 함수)가 리턴이 되면, 이 함수를 클로저 함수라고 부르고, 외부 함수에 있는 변수에 접근 가능하구나" 정도로 요약해서 기억하시면 됩니다.

심도있는 학습을 원하시면 Execution context에 대해서 공부하시고, lexical environment에 대해서 공부하시면 더 확실히 아실 수 있습니다.

 

 

 

 

 

 

 

 

 

캡슐화와 모듈패턴


정보의 접근 제한(캡슐화)

클로저를 사용하면 스코프 규칙에 따라 함수 내부의 변수를 외부에서 접근하지 못한다. 이것이 캡슐화이다. 

전역 변수가 좋지 않은 이유는, 전역 변수는 다른 함수 혹은 로직 등에 의해 의도되지 않은 변경을 초래하기 때문입니다. 이를 side effect라고 합니다. side effect를 최소화하면, 의도되지 않은 변경을 줄일 수 있습니다. 따라서 이에 따른 오류로부터 보다 안전하게 값을 보호할 수 있습니다. 클로저를 통해 불필요한 전역 변수 사용을 줄이고, 스코프를 이용해 값을 보다 안전하게 다룰 수 있습니다.

 

 

 

 

클로저 모듈패턴

클로저를 변수에 할당해서 메서드를 사용할 때, 같은 클로저를 다른 각각 다른 변수들에 할당할 수 있다.

여기서 같은 메서드를 가지는 각기 다른 변수들은, 어떻게 사용해도 원본인 클로저에 영향을 끼치지 않는다.

원본 안의 변수에는 접근할 수 없기 때문이다. 서로에게도 영향을 끼치지 않는다.

 

 

 

 

 

캡슐화 예시

const makeCounter = () => {
	let value = 0;
    
    return {
    	increase: () => {
         value = value + 1
        },
        decrease: () => {
         value = value - 1
        },
        getValue: () => value
     }
}

const counter1 = makeCounter();
counter1 // {increase: f, decrease: f, getValue: f}

객체가 담겨있는 makeCount 함수를 변수 counter1에 담아줬다.

counter1은 여러 메서드를 가진 객체가 되었다.

이때, 스코프 규칙에 의해 외부에서는 내부 스코프의 value라는 변수 값에 새로 값을 할당할 수 없다.

이것이 정보의 접근 제한(캡슐화)이다.

 

 

 

 

 

모듈화 예시

함수 재사용성을 극대화하여, 함수 하나를 완전히 독립적인 부품 형태로 분리하는 것을 모듈화라고 한다.

클로저를 통해 데이터와 메서드를 같이 묶어서 다룰 수 있습니다. 즉, 클로저는 모듈화에 유리함.

const counter1 = makeCounter();
counter1.increase();
counter1.increase();
counter1.increase();
counter1.decrease();
counter1.getValue(); // 2

const counter2 = makeCounter();
counter2.increase();
counter2.decrease();
counter2.decrease();
counter2.decrease();
counter2.getValue(); // -2

const counter3 = makeCounter();
counter3.getValue(); // 0

makeCounter 함수를 이용해서 메서드를 가지는 여러 객체(counter)를 만들 수 있다.

makeCounter에 의해 리턴된 객체는, makeCounter를 실행할 때에 선언되는 value 값을 각자 독립적으로 가진다.

따라서 counter1에서의 value와 counter2에서의 value는 서로에게 영향을 끼치지 않고, 각각의 값을 보존할 수 있다.

 

 

 

 

 

 

예제


1. A, B, C, D 각각에 출력 될 내용은? 

var a = 0;
function foo() {
    var b = 0;
    return function() {
        console.log(++a, ++b);
    };
}

var f1 = foo();
var f2 = foo();

f1(); // --> A
f1(); // --> B
f2(); // --> C
f2(); // --> D
더보기

1 1 , 2 2, 3 1, 4 2

 

 

 

 

 

 

 

 

 

 

 

 

reference


클로저 활용에 대한 영상

클로저가 왜 필요한지 쉽게 설명.

https://www.youtube.com/watch?v=LL0DGc5pg7A&t=370s