클로저 (Closure)

지은·2022년 9월 6일
1

JavaScript

목록 보기
12/42

클로저(Closure)

A closure is the combination of a function and the lexical environment within which that function was declared.

: 클로저는 함수와 그 함수가 선언됐을 때의 lexical 환경의 조합이다.


예제 1)

함수 outerFunc내에서 함수 innerFunc가 선언되고 호출되었다.

function outerFunc() {
	let x = 10;
  	let innerFunc = function() { console.log(x) };
	innerFunc();
}

outerFunc(); // 10
  • 이때 내부함수 innerFunc는 자신을 포함하고 있는 외부함수 outerFunc의 변수 x에 접근할 수 있다.
  • 이는 함수 innerFunc가 함수 outerFunc 내부에 선언되었기 때문이다. 이를 렉시컬 스코핑이라 한다.

렉시컬 스코핑(Lexical scoping)

lexical scope는 함수를 어디서 호출하는지가 아니라 어디에 선언했는지에 따라 결정된다.

  • 함수 innerFunc가 함수 outerFunc 내부에서 선언되었다면 ➡️ 함수 innerFunc의 상위 스코프는 outerFunc이다.
  • 함수 innerFunc가 전역에 선언되었다면 ➡️ 함수 innerFunc의 상위 스코프는 전역 스코프가 된다.

따라서, 함수 innerFunc은 함수 outerFunc의 내부에 선언되었으므로, 함수 innerFunc은 자신이 속한 lexical scope(전역, 함수 outerFunc, 자신의 스코프)를 참조할 수 있다.


스코프 체인(scope chain)

함수 innerFunc이 변수 x를 찾아가는 과정

  1. 먼저 자신의 스코프에서 변수 x를 검색한다. 찾지 못하면...
  2. 외부함수인 outerFunc의 스코프에서 변수 x를 검색한다.

함수 innerFunc이 변수 x에 접근하는 4번째 줄에 breakpoint를 걸고, 실행해보면...

➡️ 외부함수 outerFunc에서 변수 x를 찾은 것을 확인할 수 있다.

breakpoint(중단점)

: 말 그대로 자바스크립트의 실행이 중단되는 코드 내 지점을 의미한다.
중단점을 이용하면 실행이 중지된 시점에 변수가 어떤 값을 담고 있는지 알 수 있다.


예제 2)

이번에는 함수 innerFunc을 함수 outerFunc 내에서 호출하는 것이 아니라 반환하도록 변경했다.

function outerFunc() {
	let x = 10;
    let innerFunc = function() { console.log(x) };
	return innerFunc; // 함수 outerFunc을 호출하면,
}                     // 내부함수 innerFunc이 반환되고 함수 outerFunc은 종료된다.

let inner = outerFunc();
inner(); // 10 // 외부함수 밖에서 내부함수를 호출했다.
  • 함수 outerFunc이 종료되었으므로 변수 x 또한 더 이상 유효하지 않게 되어 변수 x에 접근할 수 있는 방법이 없어보이지만,
  • 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있다.

➡️ 이러한 함수를 클로저(Closure)라고 부른다.


A closure is the combination of a function and the lexical environment within which that function was declared.

: 클로저는 함수와 그 함수가 선언됐을 때의 lexical 환경의 조합이다.

위에서 말하는 "함수"란 반환된 내부함수를 의미하고, "그 함수가 선언됐을 때의 lexical 환경"이란 내부함수가 선언됐을 때의 스코프를 의미한다.

즉, 클로저는 반환된 내부함수가 자신이 선언됐을 때의 lexical 환경인 스코프를 기억하여, 자신이 선언됐을 때의 lexical 환경(스코프) 밖에서 호출되어도, 그 lexical 환경(스코프)에 접근할 수 있는 환경을 말한다.


클로저 함수의 특징

1. 리턴값이 함수이다.

  • 클로저 함수는 함수를 리턴하는 함수이다.
  • 함수를 리턴하는 함수가 클로저 형태를 만든다.
typeof adder(5) // 'function'
adder(5) // function(y) { 5 + y }

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

  • 클로저 함수는 외부함수의 변수에 접근할 수 있는 내부함수이다.
  • 리턴하는 함수에 의해 스코프가 구분된다.
  1. 외부함수는, 내부함수의 변수y에 접근 가능한가? 🙅‍♀️
    바깥 스코프에서는 안쪽 스코프로의 접근은 불가능 하다.
  2. 내부함수는, 외부함수의 변수x에 접근 가능한가? 🙆‍♂️
    안쪽 스코프는 바깥 스코프에서 선언된 변수에 접근이 가능하다.

클로저의 활용

1. 데이터를 보존하는 함수

: 외부함수(adder)의 실행이 끝나더라도, 외부함수 내 변수 x를 사용할 수 있다.

  • 일반적인 함수는, 함수의 실행이 종료되면, garbage collection의 대상이 되어 함수 내부의 변수를 사용할 수 없게 된다.
  • 하지만 클로저는 lexical 환경을 메모리에 저장하기 때문에 외부함수의 실행이 끝나더라도, 외부함수 내 변수가 메모리 상에 저장된다.
  • 이는 외부함수가 내부함수에 의해 언제든지 참조될 수 있기 때문이다.

예제)

위의 예시에서, 변수 add5에는 클로저를 통해 리턴한 함수가 담겨 있다.

add5 = function(y) {
	return 5 + y;
}

add5(7); // 12
add5(10); // 15
  • add5는 외부함수(adder)의 실행이 끝났음에도, 외부함수에서 인자로 넘긴 5라는 값을 변수 x에 계속 담은 채로 남아있다.
  • 이 때 변수 x를 사용할 수 있다.

HTML 문자열 생성기

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

const divMaker = tagMaker('div');
divMaker('hello') // <div>hello</div>

const anchorMaker = tagMaker('a');
anchorMaker('learn more') // <a>learn more</a>

위의 화살표 함수 tagMaker를 함수 표현식으로 바꾸어 쓰면...

const tagMaker = function(tag) {
	return function(content) {
		return `<tag>${content}</tag>`
    }
} // 외부함수에서 tag, 내부함수에서 content를 입력받아 HTML 태그를 만들어주는 함수
  • 함수 divMaker는 'div'라는 문자열을 tag라는 변수에 담아두고 있고,
  • 함수 anchorMaker는 'a'라는 문자열을 tag라는 변수에 담아두고 있다.

➡️ 클로저는 이처럼 특정 데이터를 스코프 안에 가두어 둔 채로 계속 사용할 수 있게 해준다.


2. 정보의 접근 제한

클로저 모듈 패턴

const makeCounter = () => {
  let value = 0;

  return { // 객체를 리턴, 객체 안에는 함수 increase, decrease, getValue가 있다.
    increase: () => {
      value = value + 1
    },
    decrease: () => {
      value = value - 1
    },
    getValue: () => value // value를 리턴한다.
  }
}

const counter1 = makeCounter();
console.log(counter1); // {increase: ƒ, decrease: ƒ, getValue: ƒ} // 객체가 반환된다.
  • 클로저를 이용해 내부함수를 하나만 리턴하는 것에 그치지 않고, 객체에 담아 여러 개의 내부함수를 리턴하도록 만들 수 있다.
  • makeCounter 함수는 객체를 리턴하고, 이 객체는 increase, decrease, getValue 메소드를 포함하고 있다.
  • 이 객체를 변수 counter1에 할당했다.

makeCounter 함수를 바꾸지 않고, 변수 value에 새롭게 값을 할당할 수 있을까?

"외부 스코프에서는 내부 스코프의 변수에 접근할 수 없다"라는 규칙에 의해, 변수 value는 직접 수정하는 것이 불가능하고, 리턴하는 객체가 제공하는 메소드를 통해서만 value의 값을 조작할 수 있다.

➡️ 이를 캡슐화(정보의 접근 제한)라고 부른다.


캡슐화

왜 캡슐화를 하는걸까?

  • 만약, 스코프로 value를 감싸지 않았다면, value는 전역 변수여야 했을 것이다.
  • 하지만, makeCounter 함수가 value 값을 보존하고 있기 때문에, 전역 변수로 따로 만들 필요가 없다.

전역 변수가 좋지 않은 이유

전역 변수는 다른 함수나 로직 등에 의해 의도하지 않은 변경(side effect)을 초래하기 때문이다.

➡️ 따라서, 클로저를 통해 불필요한 전역 변수를 줄이고, 스코프를 이용해 값을 보다 안전하게 다룰 수 있다!


3. 모듈화

재활용 가능한 makeCounter 함수

: 여러 개의 counter를 만드는 것이 가능하다.

const counter1 = makeCounter();
const counter2 = makeCounter();

counter1.increase();
counter1.increase();
counter1.decrease();
counter1.getValue(); // 1 
// getValue 메소드는 외부 함수에 선언된 `value`값을 리턴하는 함수이다.

counter2.decrease();
counter2.decrease();
counter2.decrease();
counter2.getValue(); // -3
  • makeCounter 함수에 의해 리턴된 객체는, makeCounter 함수를 실행할 때 선언되는 변수 value의 값을 각자 독립적으로 가지게 된다.
  • 따라서, counter1에서의 value와 counter2에서의 value는 서로 영향을 끼치지 않고 각각의 값을 보존한다.

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

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

❔ 학습 후 궁금한 점

  • 실행 컨택스트(execution context)는 무엇인지?
  • 스코프 체인에 대해서 좀 더 자세히 알아보자.

이 글은 아래 링크를 참고하여 작성한 글입니다.
https://poiemaweb.com/js-closure

profile
개발 공부 기록 블로그

0개의 댓글