JS. closure

자몽·2021년 9월 15일
1

JavaScript

목록 보기
12/25

호이스팅과 마찬가지로 면접에서 나오는 단골 문제이고, 또 그만큼 중요한 위치에 있다.
하지만 처음 봤을때 클로저가 명확하게 이해되지 않았는데, 이해한 내용을 바탕으로 최대한 쉽게 설명해보고자 한다.

렉시컬 스코프

우선 클로저를 알아보기 전 기초 지식을 알 필요가 있다.

const x = 10; // 1 
function num(){
  const x = 1; // 2
}

위와 같은 코드에서 함수 외부에 있는(1) x와 함수 내부에 있는(2) x는 서로 다르다는 것을 우리는 알고있다.

근데 잘 생각해보자.
왜 같은 x를 썼는데 다르다는거지??
이유는 다음과 같다.
/
1번의 경우 "전역"에 x를 저장했고,
2번의 경우 "num 함수"에 x를 저장했기 때문이다.


(전역, 함수, 블록문 같은 것들을 우리는 "스코프"라고 하고,
이러한 스코프를 구분해 식별자를 등록하고 관리하는 "렉시컬 스코프"가 존재한다.)

스코프에 따른 변수 접근

자, 그럼 이런 기본 지식을 배경으로 조금 더 학습해보도록 하자.

case 1

const a = 1;

function first() {
  const b = 2;

  function second() {
    const c = 3;
    console.log("sum", a + b + c);
  }
  
  return second();
}

first(); // "sum 6"

case 2

const a = 1;

function first() {
  const b = 2;
}

function second() {
  const c = 3;
  console.log("sum", a + b + c);
}

second(); // "error"

case 1과 case 2의 가장 큰 차이는 무엇일까?

case 2에서 에러가 나오는 이유는,
변수 b가 first 함수에서 생성되었기 때문에, first 함수에서만 유효해
second 함수에서 접근이 불가능했다.

case 1에서는 마찬가지로 변수 b가 first 함수에서 생성되었지만, first 함수 내부에는 second 함수가 존재한다.
따라서 second는 자신의 상위 스코프(first)에서 b를 가져와 쓸 수 있다.

자바스크립트에서 모든 함수는 자신의 상위 스코프를 기억하고 있고, 이로 인해 상위 스코프의 변수들을 참조할 수 있다.

클로저

자, 이제 대망의 클로저이다.
앞에서 학습한 내용을 짜집기하고 약간의 내용을 추가하면 클로저가 된다.

const a = 1; // 1 

function first() {
  const a = 2; // 2

  function second() {
    console.log(a);
  }

  return second;
}

const runc = first();
runc(); // 2

변수 a를 유심히 보자.
a는 1번과 2번에서 모두 만들어졌다.
이 상태에서 second에서 a를 호출하면 second의 상위 스코프에 있는 first에서 a를 발견하고 이를 값으로 사용한다.

따라서 runc에서 나온 결과가 2라고 당연하게 생각하면 안된다.

여기서 가장 큰 문제는, 함수 내부의 변수들은 해당 함수를 모두 실행하면 사라진다는 점이다.
(실행 컨텍스트 스택에서 사라짐)
따라서 runc에서 first 함수를 모두 실행하고 해당 함수가 가지고 있던 내용은 모두 사라진다.

하지만, runc()를 실행했을 때, a의 값은 1이 아닌 2가 나온다.

이렇게 동작하는 이유는 모두 '클로저'와 관련있다.

위의 코드에서 first 함수는 const runc = first()에서 실행되고 이미 라이프사이클을 끝내고 사라진다.
그 상태에서 다음 줄에 있는 코드인 runc()를 마주치게 되는데, runc는 first의 return 값인 second 함수를 가지고 있다.

뭔가 이상하지 않은가? first 함수의 내부에 있는 second 함수가 더 오래 지속된다니..?
심지어 first 함수에 있던 변수를 사용한다고?

클로저

외부 함수보다 중첩 함수가 더 오래 유지되는 경우,
중첩 함수는 이미 생명 주기가 다한 외부 함수의 변수를 참조할 수 있으며,
이러한 중첩 함수를 "클로저(closure)"라고 한다.

위의 내용이 이해가 가지 않는다면 다시 한 가지의 예시를 들어보겠다.

const click = (function () {
  let count = 0;
  function changeNum(num) {
    count += num;
  }
  return {
    increase() {
      changeNum(1);
    },
    val() {
      return count;
    }
  };
})();

console.log("counter", click.val()); // "counter 0"
click.increase();
console.log("counter", click.val()); // "counter 1"
click.increase();
console.log("counter", click.val()); // "counter 2"

위의 코드는, 증가 버튼을 누르면 번호가 0부터 1씩 증가하는 함수를 가진 코드이다.

우선 중첩 함수는 increase()와 val()이다.

독특한 점은 분명 함수를 한 번씩 따로 실행하는데, click.val의 결과는 "counter 0"이 반복되지 않고, "counter 0"=>"counter 1" 처럼 값이 공유되며 하나씩 증가한다는 점이다.

이러한 이유는 클로저의 정의처럼 중첩 함수는 외부 함수의 변수를 참조할 수 있기 때문에, click함수에서 만들어진 중첩 함수들(increase(), val())이 모두 click 함수의 변수인 count를 참조할 수 있게된다.

(function() { ... 와 같이 ()로 둘러쌓인 함수를 즉시 실행 함수라고 하는데,
즉시 실행 함수는 정의되자마자 즉시 실행되는 함수이다.
이러한 특징으로 인해, 즉시 실행 함수가 반환한 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하고 있다.

클로저의 장점

  • 상태가 의도치 않게 변경되지 않도록 안전하게 은닉시킨다.
  • 특정 함수에만 상태 변경을 허용해 안전하게 변경하고 유지할 수 있게 한다.

클로저 특징 및 정리

  • 클로저는 상위 스코프의 값을 "기억"한다.
  • 클로저는 return을 통한 중첩 함수여야 한다.
  • 클로저의 특징을 사용해 이전 상태를 유지하는 코드를 짤 수 있다.

useState에 사용되는 클로저[in React]


const React = (function() {
  let _val;
  
  function useState(initVal) {
    const state = _val || initVal;
    const setState = newVal => {
      _val = newVal;
    };

    return [state, setState];
  }
  
  function render(Component) {
    const C = Component();
    C.render();
    return C;
  }
  
  return { useState, render };
})();

function Component() {
  const [count, setCount] = React.useState(1);
  
  return {
    render: () => console.log(count),
    click: () => setCount(count + 1),
  }
}

var App = React.render(Component);
App.click(); // 1
var App = React.render(Component);
App.click(); // 2
var App = React.render(Component);
App.click(); // 3

(추후 수정 예정)
출처: https://medium.com/humanscape-tech/자바스크립트-클로저로-hooks구현하기-3ba74e11fda7

profile
꾸준하게 공부하기

0개의 댓글