♻️ 개발자로 재활용/🟢 JavaScript

함수가 선언된 어휘적 환경, 클로저 Closure

BuleRatel 2022. 11. 8. 09:34

MDN에서 설명하는 클로저(Closure)의 정의는 다음과 같다.

A closure is the combination of a function and the lexical environment within which that function was declared.
클로저는 함수와 함수가 선언된 어휘적 환경의 조합을 말한다.
This environment consists of any local variables that were in-scope at the time the closure was created. 
이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.

 

여기서 키워드는 '함수가 선언'된 '어휘적 환경(lexical environment)'이다. JS는 함수가 호출되는 환경과 별개로, 기존에 선언된 어휘적 환경을 기준으로 변수를 조회하려고 한다. 스코프에 따라 내부함수(inner)범위에서는 외부함수(outer)범위에 있는 변수에 접근할 수 있지만 그 반대는 실현이 불가능하다. 외부함수는 외부함수의 변수를 사용하는 내부함수가 소멸될 때까지 소멸하지 않는다. 이걸 이용해 특정 데이터를 스코프에 가두어 둔 채 계속 사용할 수 있다.

 

 


클로저 함수의 특징

  • 클로저 함수는 함수를 리턴하는 함수이다.
  • 클로저 함수에서 내부 함수는 외부 함수에 선언된 변수에 접근할 수 있다.
  • 단, 스코프 규칙에 따라 외부 함수 내부 함수에 선언된 변수에 접근할 수 없다.
const adder = function (x) {
	return function (y) { // 리턴값이 함수의 형태
		return x + y; // 내부함수는 x값을 외부에서 가져온다
	}
}

클로저는 리턴하는 함수에 의해 스코프(변수의 접근 범위)가 구분된다. 핵심은 스코프를 이용해 변수의 접근 범위를 닫는(closure)데 있다. 위의 예시에서 x와 y가 선언된 곳과 각 함수가 접근할 수 있는 범위가 어떻게 되는지 스코프 규칙을 통해 확인해 보자.

 

 


클로저의 활용

 

🗃 데이터를 보존하는 함수

일반적으로 함수는 함수 실행이 끝나고 나면 함수 내부의 변수를 사용할 수 없다. 하지만 클로저는 외부 함수 실행이 끝나더라도, 외부 함수 내 변수가 메모리상에 저장된다. 

const adder = function (x) { // x는 외부 함수의 변수
	return function (y) { // y는 내부 함수의 변수
		return x + y;
	}
}

const add5 = adder(5); 
// 함수 실행이 끝나도, x변수에 들어간 5라는 인자 사용 가능
add5(7) // adder(5)(7)로도 표현 가능
add(10) // adder(5)(10)으로도 표현 가능

다음은 클로저를 이용해 HTML 문자열을 만드는 방법이다. 변수 divMaker에 'div'라는 변수를 할당한 'tagMaker 함수'를 할당해 줬다. tagMaker의 작동은 이미 종료되었지만 'div'변수는 계속 남아 사용할 수 있다. 클로저는 이처럼 특정 데이터를 스코프 안에 가두어 둔 채로 계속 사용할 수 있게 해준다.

const tagMaker = tag => const => `<${tag}>${content}</${tag}>`

const divMaker = tagMaker('div'); // 'div'는 tagMaker의 tag의 변수가 된다.
dicMaker('hello'); // '<div>hello</div>'
dicMaker('world'); // '<div>world</div>'

const anchorMaker = tagMaker('a'); // 'a'는 tagMaker의 tag의 변수가 된다.
anchorMaker('go'); // '<a>go</a>'
anchorMaker('google'); // '<a>google</a>'

 

 

🗃 정보의 접근 제한

다음은 '클로저 모듈 패턴'이라고 불리는 아주 유용한 패턴이다. 이 코드를 꼼꼼하게 살펴보자.

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

const counter1 = makeCounter();

클로저를 이용하면 내부 함수를 객체에 담아 여러 개의 내부 함수(위에서는 increase, decrease, getValue)를 리턴하도록 만들 수 있다. 이 다음으로 makeCounter를 실행해 변수에 담아보자. makeCounter 함수는 increase, decrease, getValue 메서드를 포함한 객체 하나를 리턴한다. 따라서, counter1은 객체이다. 

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

makeCounter 함수를 변경하지 않은 채, value라는 변수에 새로운 값을 할당할 수 있을까? '외부 스코프에서는 내부 스코프의 변수에 접근할 수 없다라는 규칙에 의해, 어떤 경우라도 value를 직접 수정할 수 없다. 다만, 리턴하는 객체가 제공하는 메서드를 통해 value값을 간접적으로 조작할 수 있다. 이것을 정보의 접근 제한(캡슐화)라고 한다.

  • 클로저를 이용하면 불필요한 전역 변수 사용을 줄일 수 있다.
  • 스코프를 이용해 값을 보다 안전하게 다룰 수 있다.
  • 즉, 다른 함수나 로직 등으로 의도되지 않은 변경(side effect)를 최소화 할 수 있다.

 

 

🗃 모듈화

위의 makeCounter에 의해 리턴된 객체는, makeCounter를 실행할 때에 선언되는 value 값을 각자 독립적으로 가지게 된다. 따라서 counter1에서의 value와 counter2에서의 value는 서로에게 영향을 끼치지 않고, 각각의 값을 보존할 수 있다.

const counter1 = makeCounter(); // value = 0
counter1.increase(); // value + 1 = 1
counter1.increase(); // value + 1 = 2
counter1.decrease(); // value - 1 = 1
counter1.getValue(); // 1

const counter2 = makeCounter(); // value = 0
counter2.decrease(); // value - 1 = -1
counter2.decrease(); // value - 1 = -2
counter2.decrease(); // value - 1 = -3
counter2.getValue(); // -3

이와 같이 함수 재사용성을 극대화하여, 함수 하나를 완전히 독립적인 부품 형태로 분리하는 것을 모듈화라고 한다. 클로저를 통해 데이터와 메서드를 같이 묶어서 다룰 수 있다. 즉, 클로저는 모듈화에 유리하다.