ivory's Log
그게 무엇이라도 항상 쉬운 일이다.

ivory's DevLog

[JavaScript] - Closure

ivorycode 2021. 12. 1. 19:49
반응형

이미지 출처: www.google.com

일전에도 언급했지만 내가 JavaScript를 처음 공부할 때, 가장 어려워했고 이해하기 힘들어했던 개념 중 하나가 바로 이 실행 컨텍스트(Execution Context)였다. 실행 컨텍스트는 scope, hoisting, this, function, closure 등의 동작원리를 담고 있는 자바스크립트의 핵심원리이다. 오늘은 내가 미루고 미뤘던 Closure(이하 클로저)에 대해 정리해보려고 한다.(꽤 오랫동안 미뤄왔다... 그만큼 바빴다!!)

 

Closure의 개념

외부 함수의 변수에 접근 가능한 내부 함수

클로저는 한 마디로 쉽게 정의하면 외부 함수의 변수에 접근 가능한 내부 함수이다. 예시 코드와 함께 좀 더 구체적으로 정리해보겠다.

 

example code 1

// example 1
const outerFunction = () => {
  let x = 10;
  const innerFunction = () => {
    console.log(x);
  };
  return innerFunction();
};

outerFunction();

첫 번째 코드를 살펴보자. 외부 함수 outerFunction안에 변수 x가 선언되고, innerFunction함수 호출을 반환하고 있다. 내부 함수 innerFunction은 외부 함수의 변수 x에 접근하여 console을 호출하고, 최종적으로 외부 함수 outerFunction을 전역에서 호출하고 있다. 이 상태에서 호출하면 결과값은 10이 나올 것이다.

 

처음 자바스크립트를 공부했을 때, 위의 코드를 이해하기 어려웠다. 그 이유는 외부 함수가 내부 함수를 반환하고 제 기능을 다했으므로, 내부 함수가 x에 접근할 방법이 없어 보였다고 생각했기 때문이다. 하지만 결과값은 10을 출력하고 있다. 왜냐하면 자바스크립트는 함수가 호출되는 환경과 별개로, 기존에 선언되어 있던 환경을 기준으로 변수를 조회한다. 쉽게 말하면 자신을 감싸고 있는 바깥 환경을 기준으로 변수를 조회하는 것이다. 그리고 이러한 기존의 환경이 바로 렉시컬 스코프(Lexical Scope)이다.(여기서 렉시컬 스코프는 전역, 외부 함수 outerFunction scope, 함수 자신의 scope가 되겠다.) 이런 특성에 따라 클로저는 내부 함수 innerFunction이 외부 함수 outerFunction의 지역 변수 x에 접근할 수 있게 된다.

 

다시 정리하면 클로저는 자신을 포함하고 있는 외부 함수보다 내부 함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부 함수가 호출되더라도 외부 함수의 지역 변수에 접근할 수 있는 함수다.

 

example code 2

다른 추가 예제도 작성해봤다!! 동작원리는 위의 내용과 동일하니 더 이상의 자세한 설명은 생략한다!

// example 2
const outerFunc = () => {
  let outerValue = "안녕 나는 외부 함수 참조, 외부 함수 변수";
  console.log(outerValue);

  const innerFunc = () => {
    let innerValue = "안녕 나는 내부 함수 참조, 지역 변수";
    console.log(innerValue);
    console.log(globalValue);
  };
  
  return innerFunc();
};

let globalValue = "안녕 나는 글로벌 참조, 전역 변수";
outerFunc();

클로저 함수 안에서는 지역 변수(innerValue), 외부 함수의 변수(outerValue), 전역 변수(globalValue)의 접근이 전부 가능하다.

 

대표적인 Closure 예제

커링(Curring)

커링은 여러 개의 인자를 한 번의 호출로 처리하던 함수를 분리하여 인자를 하나씩만 받는 여러 개의 함수로 만드는 기술이다. 다시 말해 함수 하나가 n개의 인자를 받는 대신, n개의 함수를 만들어서 각각의 함수가 인자를 받도록 하는 것이다. 이번에도 예시를 통해 익숙해지자.

 

example code

const increment = (x) => {
  return (y) => {
    return x + y;
  }
}

increment(2)(3); // 5

let firstCase = increment(1000);
firstCase(2); // 1002
firstCase(10); // 1010

위의 코드가 커링의 대표적인 예시 코드이다. 위의 코드를 보면 x값을 1000으로 고정시켜서 firstCase 변수에 미리 할당해 두고, firstCase(y)와 같은 형태로 사용하고 있다. 커링은 이렇게 한 인자는 값을 고정시키고, 다른 인자는 변하는 값으로 두고 싶을 때 쓰이는 형태다. 다시 말해, 커링은 외부 함수의 변수가 저장되어서 보존되고, 내부 함수의 인자를 변경하여 재사용할 수 있는 템플릿 함수처럼 사용할 수 있다.

 

클로저 모듈 패턴(Closure Module Pattern)

클로저 모듈 패턴은 변수를 클로저 함수의 스코프에 가두어 함수 밖으로 노출시키지 않는 방법이다. 외부에서 접근이 불가하므로 공개하고 싶지 않은 변수가 있을 때 유용하게 사용되는 패턴이라고 한다.

 

example code

const counter = () => {
  let privateCounter = 0;

  return {
    increment: () => {
      privateCounter++;
      console.log(privateCounter);
    },
    decrement: () => {
      privateCounter--;
      console.log(privateCounter);
    },
    getValue: () => {
      console.log(privateCounter);
      return privateCounter;
    },
  };
};

let useCounter = counter();
useCounter.increment(); // 1
useCounter.increment(); // 2
useCounter.increment(); // 3
useCounter.decrement(); // 2
useCounter.getValue(); // 2

let newCounter = counter();
newCounter.increment(); // 1
newCounter.decrement(); // 0
newCounter.getValue(); // 0

위의 코드에 대해 간략히 설명하면, counter라는 외부 함수를 선언하고 그 안에 변수 privateCounter를 할당해 주었다. 그리고 각 함수들을 객체로 묶어서 반환해 주고, 아래의 useCounternewCounter 변수에 외부 함수 호출을 할당했다. 이렇게 하면 counter함수에서만 사용할 수 있는 변수 데이터 privateCounter를 캡슐화할 수 있다. 또한 클로저(increment, decrement, getValue)들은 자신이 생성됐을 때의 렉시컬 환경을 기억하기 때문에 counter함수(외부 함수)의 생명주기가 다해도(return이 되는 시점) 바깥 스코프(클로저 함수 기준)에 해당하는 privateCounter변수를 사용할 수 있게 된다. 다시 정리하면, 클로저는 데이터를 숨기고 정해진 방법을 통해서만 데이터를 접근할 수 있도록 제한을 두는 데 활용하며, 'private 변수/함수'와 'public 변수/함수'를 구분할 수 있게 되고, 기능을 하나의 모듈로써 캡슐화할 수 있다.

 

위에서 클로저의 개념과 사용 예시를 통해 특징을 알아봤다. 조금 복잡하고 이해하기 어려운 요소들이 많지만, 익숙해지면 꽤나 유용하게 활용할 수 있다는 생각이 들었지만 클로저의 단점이자 주의할 요소가 있었다. 아래에 간단하게 정리해 봤다.

 

Closure에도 단점이 있다?

가비지 컬렉션(Garbage Collection, GC)부터 알아보자.

가비지 컬렉션은 메모리 기법 중의 하나로, 프로그램이 동적으로 할당했던 메모리 영역 중에서 필요 없게 된 영역을 해제하는 기능이다. 자바스크립트에선 눈에 보이지 않는 곳에서 메모리 관리를 수행한다. 우리가 코딩으로 작성한 원시값, 객체, 함수 등 만드는 모든 것에는 메모리가 있고 그에 맞는 용량을 차지하고, 개념에서 살짝 언급했듯이 필요 없거나 실행 종료된 함수 등은 가비지 컬렉션 대상이 되는데, 클로저일 땐 얘기가 다르다고 한다.

 

포스팅 위의 내용에서 클로저는 외부 함수의 생명주기가 다해도 변수를 참조할 수 있다고 했다. 즉, 클로저가 활용된 패턴에서는 가비지 컬렉션 대상이 되지 않고 메모리상에 존재하게 된다. 앞서 말했다시피 외부 함수 스코프가 내부 함수에 의해 언제든지 참조될 수 있기 때문이다. 따라서 클로저를 과도하게 남발하게 되면 심한 성능 저하(메모리 누수 현상)가 발생할 수 있으니, 내부 함수의 사용이 더 이상 필요 없는 상황에서 외부 변수를 초기화하여 메모리 할당을 해제시켜주는 코드를 포함하는 게 좋다고 한다.

 

example code

⛔️ 위의 클로저 모듈 패턴에서 사용한 예제 코드에 추가했습니다!! 참고 부탁드립니다!!

const counter = () => {
  let privateCounter = 0;

  // 주목!!
  return {    
    ...blabla,
    quit: () => {
      privateCounter = null;
      // 외부 변수 값을 null로 초기화 시킨다.
    }
  };
};

let useCounter = counter();
useCounter.increment(); // 1
useCounter.increment(); // 2
useCounter.increment(); // 3
useCounter.decrement(); // 2
useCounter.getValue(); // 2

let newCounter = counter();
newCounter.increment(); // 1
newCounter.decrement(); // 0
newCounter.getValue(); // 0

// 여기도 주목!!
// 클로저 사용이 종료되는 시점에
// null로 초기화 시켰던 함수를 호출한다.
counter().quit();
반응형

'ivory's DevLog' 카테고리의 다른 글

useMemo, useCallback 파헤치기 - useCallback편  (0) 2021.11.23
useMemo, useCallback 파헤치기 - useMemo편  (0) 2021.11.10
왕초보와 ESLint 설정해보기  (0) 2021.11.03
Redux의 흐름과 예제  (1) 2021.09.28
Redux  (0) 2021.09.27