치춘짱베리굿나이스

클로저 본문

Javascript + Typescript/이론과 문법

클로저

치춘 2023. 7. 18. 17:43

클로저

스코프, 렉시컬 스코프

먼저 렉시컬 스코프와 스코프에 관해 읽고 오자

 

내가 아는 클로저는 이 캐릭터 뿐이다

자바스크립트에서의 클로저란 무엇일까…

설명

MDN에서의 설명

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).

클로저는 함수와 함수가 선언된 어휘적 환경 (Lexical Scope) 의 조합이다.

MDN에 따르면 그렇단다

이게 무슨 말일까?

예시

function foo() {
    const str = "Hello World!";

    function bar() { // 함수 bar의 선언부
        console.log(str);
    }

    return bar;
}

const closureFunc = foo();
closureFunc(); // bar 실행

이 예시를 보자

  • barfoo 안에 선언되어 있고,
  • foostr이라는 변수를 가지고 있으며,
  • bar는 그 str을 출력하는 단순한 함수이다
  • 그리고 foobar를 반환한다

여기서 우리가 bar 함수를 반환받아 실행시킨다면, strfoo의 스코프 안에 있으니 foo가 종료된 (값을 반환한) 시점에선 존재하지 않는 변수라고 생각할 수 있다

 

하지만 그 생각은 매우 틀렸다

우리가 사용하는 언어는 자바스크립트이기 때문이다

자바스크립트는 함수를 반환했을 때, 그 반환한 함수가 클로저를 형성하기 때문에, str 변수는 클로저 안에서 살아 숨쉬게 되는 것이다

 

클로저가 형성되었다 = 함수와, 함수가 선언된 시점에서의 어휘적 환경 (= 렉시컬 스코프) 이 하나의 환경을 구성하였으므로, 렉시컬 스코프 안에 있는 변수와 상수 모두 참조가 가능하다

다르게 말하면, 함수가 자신이 선언되었을 적의 환경을 기억하고 그 환경에 그대로 접근할 수 있는 것이다!

따라서 클로저란

스코프를 이용하여 함수의 범위를 닫아 (close) , 내부 함수에서 외부 함수의 정보를 참조할 수 있도록 하는 개념이다

즉, 자신이 생성될 때의 환경 (렉시컬 스코프) 을 기억하고 이에 접근할 수 있는 함수가 클로저 함수이다

function foo() {
    const str = "Hello World!";

    function bar() { // 함수 bar의 선언부
        console.log(str);
    }

    return bar;
}

const closureFunc = foo();
closureFunc(); // bar 실행

다시 예시를 가져왔다

자바스크립트에서 모든 함수는 클로저가 될 수 있으나, 명확하게는 위의 예시에서 반환받은 bar()가 실질적으로 렉시컬 스코프를 참조하고 있으므로 bar() 를 좁은 의미의 클로저 함수라고 부른다

외부 함수 (foo) 가 종료되어 실행 컨텍스트가 반환되어도, 실행 컨텍스트의 활성 객체 (변수, 함수 선언 등의 정보가 들어있는 객체) 는 내부 함수 bar가 참조하는 한 그 생명력이 유지되어 내부 함수가 활성 객체 내의 정보들을 참조할 수 있게 된다고 한다

클로저 용례

정보 은닉

function countNumber() {
    let n = 0;

    function add() {
        n++;
        console.log(n);
    }
    return add;
}

const add = countNumber();
add(); // 1
add(); // 2
add(); // 3
add(); // 4
add(); // 5

add()countNumber() 내부에 선언되어 있고, 마찬가지로 countNumber() 내부에 선언된 n을 사용한다

countNumberadd 함수를 반환한 뒤에 컨텍스트가 반환되어 종료되어야 하지만, 활성 객체는 add에 의해 참조되어 살아있으므로 n 또한 접근 가능한 변수가 된다

  • 첫 번째 add의 시점에서 n은 0이고 이에 1을 더했으므로 n은 1으로 출력된다
  • 두 번째 add의 시점에서 n은 1이고 이에 2을 더했으므로 n은 2로 출력된다
  • 세 번째 add의 시점에서 n은 2고 이에 1을 더했으므로 n은 3으로 출력된다
  • 네 번째 add의 시점에서 n은 3이고 이에 1을 더했으므로 n은 4로 출력된다
  • 다섯 번째 add의 시점에서 n은 4고 이에 1을 더했으므로 n은 5로 출력된다

add 함수는 자신이 선언된 렉시컬 환경 countNumber을 기억하므로 n 변수에 접근 가능하고, 그 외의 외부 변수나 함수들은 자신과 관련된 환경이 아닌 countNumber 스코프를 인식할 수 없다

이처럼 무언가 변수 등을 숨기되 상태값으로 계속 사용하고 싶을 경우 클로저를 활용하며, 이를 이용하여 클래스의 private 또한 흉내낼 수 있다

useState

function useState(initValue) {
  let _val = initValue;
  const state = () => _val;
  const setState = (newVal) => {
    _val = newVal;
  };
  return [state, setState];
}

const [state, setState] = useState("hello");

console.log(state());
setState("world");
console.log(state());

리액트에서 엄청 많이 사용되는 useState가 클로저로 구현된 대표적인 사례이다

실제로는 훨씬 복잡하게 구현되어 있지만, 간략하게는 statesetState가 위와 같은 클로저 형태로 구성되어 있다

  • 위의 예시에서 state도 함수 형태로 구현되어 있는 이유는 함수와 달리 변수로는 클로저의 특성을 살리지 못하기 때문에 setState를 통해 상태값을 바꿔주어도 이전 값이 그대로 남아있기 때문이다
  • 따라서 getter 함수 형태로 구현하였다

state, setState가 클로저로 구현되어 있음으로써 _val 이라는 렉시컬 스코프의 값을 참조할 수 있게 되고, 따라서 상태를 유지했다가 호출해서 사용하는 것이 가능하다

커링

function multiply(x, y, z) {
    return x * y * z;
}

function multiplyX(x) {
    return function multiplyY(y) {
        return function multiplyZ(z) {
            return x * y * z;
        }
    }
}
multiplyX(3)(6)(9);

function multiply = x => y => z => x * y * z; // 위와 같으면서 더욱 간결함
multiply(1)(2)(3);

커링은 클로저를 이용한 예시 중 하나로, 이전 컨텍스트를 기억하는 함수" 여러개를 반환함으로서 여러 인자를 받는 함수를 단일 인자를 받는 함수 여러 개로 쪼개는 함수형 프로그래밍 기법이다

커링을 이용하면 위의 multiply 함수를 multiplyX, multiplyY, multiplyZ 함수로 쪼갤 수 있으며,

  • multiplyYmultiplyX의 실행 컨텍스트를 기억하므로, 매개변수 x를 가져다 사용할 수 있다
  • multiplyZmultiplyY의 실행 컨텍스트를 기억하므로, 매개변수 x, y를 가져다 사용할 수 있다
function multiplyX(x) {
    return function multiplyY(y) {
        return x * y;
    }
}
const multiplyBy3 = multiplyX(3);
multiplyBy3(6); // 18
multiplyBy3(9); // 27

커링을 사용하면 동일한 입력에 대하여 동일한 출력이 나오게끔 할 수 있고, 재사용성이 높아지므로 생산성과 유지보수 면에서 탁월하다고 한다

위의 예시에서 우리는 스코프에 3을 저장해둔 채로 multiplyBy3 함수를 재활용할 수 있는 것을 볼 수 있다


참고 자료

https://poiemaweb.com/js-closure

https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures

Comments