티스토리 뷰

클로저

클로저는 함수와 그 함수의 주변환경과의 조합이다.
함수와 그 함수의 주변환경과의 조합. 이 말에 따르면 모든 함수를 그 주변환경과 조합하면 되기 때문에 모든 함수에 대해 클로저라고 생각할 수 있겠다. 하지만 모든 함수에 대해 클로저라고 칭하지 않는다. 또한 렉시컬 스코프를 따르기 때문에 함수는 어디에서 호출했든 자신이 정의된 환경인 상위 스코프의 변수를 참조한다.

❓ 스코프에 대하여 다음 포스팅을 참고할 수 있다.
https://hahagarden.tistory.com/94

 

JS 스코프, 그 규칙과 종류

스코프 스코프란 모든 식별자(변수 이름, 함수 이름, 클래스 이름 등)는 자신이 선언된 위치에 의해 다른 코드가 자신을 참조할 수 있는(접근할 수 있는) 유효범위가 결정된다. 스코프는 식별자

hahagarden.tistory.com

 

아무튼, 아무 함수나 클로저가 되는 것이 아니라 이야기했던 원리들을 이용해서 함수와 그 주변환경을 조합시켜서 어떤 특정 목적(데이터 보존, 정보 은닉 등)으로 사용할 때를, 그 현상을 클로저라고 한다. (명백히 표준화된 클로저의 정의가 없다. 제각각 다른 문장으로 설명하지만 의미는 비슷하다.)

 

function addBy(x) { // 본문 외부함수
  return function(y) { // 본문 내부함수
    return x + y;
  }
}

const addBy3 = addBy(3);
console.log(addBy3(5)); // 8

const addBy10 = addBy(10);
console.log(addBy10(5)); // 15
console.log(addBy3(1)); // 4

위 코드에서 클로저가 사용됐다. 내부함수와 내부함수의 주변환경.

addBy(3)이 호출되어 변수 addBy3에 반환값인 내부함수가 할당되고 외부함수를 끝까지 완료했으니 실행컨텍스트에서 제거되어 소멸한다. 실행컨텍스트가 종료되면 더이상 메모리를 잡아먹지 않도록 하기 위해 가비지컬렉팅 대상이 되어 정보가 사라진다. 더이상 외부함수의 변수 x를 참조할 수 없다는 뜻이다.
그런데 다음 줄의 addBy3(5)을 호출하니 내부함수의 매개변수 y에 5를 전달하는 것은 자연스러운데 소멸된 외부함수의 x인 3이 마치 저장되어있는 것처럼 참조되어 3+5=8의 결과를 반환하고 있다.

어떤 현상을 이용했기 때문이다. 실행컨텍스트가 소멸되어 가비지 컬렉팅이 되어야 하는데 지역변수(x)를 참조하는 내부함수가 외부로 전달(addBy3, addBy10)된 경우 예외가 되어 x가 사라지지 않는다. 클로저를 통해서 이미 생명주기가 끝난 외부함수의 변수를 내부함수가 참조하는 것이 가능하다.

이처럼 내부 함수가 상위 스코프의 식별자를 참조하고 있고 내부함수가 외부 함수보다 더 오래 유지되는 경우에 클로저라고 한다. 이 때, 내부함수로부터 참조되고 있는 변수 x를 `자유변수(free variable)`이라고 한다. 클로저(closure)란 `함수가 자유변수에 대해 닫혀있다(closed)`라는 의미이다. 자유변수로부터 감싸져있는 함수라고 이해하면 될 것 같다.

이제 클로저가 눈에 보인다.

  1. 함수 addBy10과 주변환경 x=10의 조합.
  2. 함수 addBy3과 주변환경 x=3의 조합.

 


 

클로저 활용 사례

다음은 클로저의 활용 사례이다. 실제로 어떤 상황에 클로저가 등장할까?

데이터 보존

function getFoodRecipe (foodName) {
  let ingredient1, ingredient2;
  return `${ingredient1} + ${ingredient2} = ${foodName}!`;
}

console.log(getFoodRecipe("하이볼")); // undefined + undefined = 하이볼!

ingredient1과 ingredient2에 어떻게 값을 할당할 수 있는가? 스코프 밖에서 안쪽을 접근(참조)할 수 없다. 제 기능을 못하는 함수이므로 아직은 데이터를 보존할 필요성도 못느낀다.

function createFoodRecipe (foodName) {
  const getFoodRecipe = function (ingredient1, ingredient2) {
    return `${ingredient1} + ${ingredient2} = ${foodName}!`;
  }
  return getFoodRecipe;
}

const highballRecipe = createFoodRecipe('하이볼');
highballRecipe('콜라', '위스키'); // '콜라 + 위스키 = 하이볼!'
highballRecipe('탄산수', '위스키'); // '탄산수 + 위스키 = 하이볼!'
highballRecipe('토닉워터', '연태고량주'); // '토닉워터 + 연태고량주 = 하이볼!'

클로저를 사용해서 외부에서 매개변수에 접근하여 각각 다른 값도 할당하고 출력했다. 또한 '하이볼'은 데이터가 저장되어있는 것처럼 보존되어 전달인자로 주지 않아도 계속해서 '하이볼'이 출력이 된다. 이처럼 데이터를 보존할 수 있다. '하이볼'을 한 번 전달인자로 전달하고 다양한 레시피를 제작했다.

 

정보 은닉

function makeCalculator() {
  let displayValue = 0;

  return {
    add: function(num) {
      displayValue = displayValue + num;
    },
    subtract: function(num) {
      displayValue = displayValue - num;
    },
    multiply: function(num) {
      displayValue = displayValue * num;
    },
    divide: function(num) {
      displayValue = displayValue / num;
    },
    reset: function() {
      displayValue = 0;
    },
    display: function() {
      return displayValue
    }
  }
}

const cal = makeCalculator();
cal.display(); // 0
cal.add(1);
cal.display(); // 1
console.log(displayValue) // ReferenceError: displayValue is not defined

마지막 줄과 같이 displayValue를 출력하려고 하면 참조에러가 뜬다. 값을 재할당하여 변경하는 것도 당연히 불가능하다. 이처럼 어떤 변수를 외부에서 접근(참조)할 수도 없고 수정할 수도 없게 숨기는 용도로 사용된다. 다른 언어의 private처럼 말이다.

 


 

회고

스코프나 클로저나 자바스크립트의 개념을 잘 몰랐을 때에는 예제코드들을 보고 return을 따라가며 예측이 가능한 실행결과였고 '어떤 값이 반환되겠네' 생각하며 별 다른 의문을 품지 않았다. 그냥 수용했다. 그런데 개념적으로 짚어보니 어디선가(실행컨텍스트, 스코프..) 모순이 생겨나는 코드였다.

클로저라는 개념을 이해하는데 정말 어려웠다. 클로저덕분에 메타인지를 했다. 술술 읽으면서 이해했는데 말로 설명하려니 머릿속은 뒤죽박죽 아무 말도 안나오는...😇

그러나 이제 클로저가 어떤 대상이 아닌 어떤 현상이라는 것을 알았다. 가비지컬렉팅의 예외를 활용한 방법, 특정 목적을 구현하고 싶을 때 사용할 수 있는 일종의 패턴이라는 것을 알았다. 3일이나 걸렸지만 열심히 찾아보고 고민한 보람이 있다!

 

반응형
댓글