JS에서도 상태관리가 가능하다고?(부제 : Javascript 클로저)

박민형·2023년 1월 15일
post-thumbnail

📌 클로저를 알아본 이유

  • 실행 컨텍스트에 개념 중 하나인 클로저를 알아야 클로저로 인한 에러가 발생했을 때 예방 할 수 있으므로
  • 클로저를 활용하기 위해

📌 사전 개념 인지

렉시컬 스코핑

function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  innerFunc();
}
outerFunc();
  • 어떻게 내부 함수에서 외부 함수의 변수에 접근할 수 있을까?
  • 스코프 체인이 전역 스코프의 전역 객체 및 외부 및 내부 스코프의 활성객체를 순차적으로 바인딩
  • 스코프 체인이 바인딩한 객체가 바로 렉시컬 스코프
  • JS 엔진이 스코프 체인을 검색할 수 있기 때문에 내부 함수가 외부함수의 변수에 접근할 수 있음

📌 클로저란?

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

/**
 *  함수 outerFunc를 호출하면 내부 함수 innerFunc가 반환된다.
 *  그리고 함수 outerFunc의 실행 컨텍스트는 소멸한다.
 */
var inner = outerFunc();
inner(); // 10

소스코드 분석
1. inner에 outerFunc 함수의 내부 함수인 innerFunc 함수를 return 받음
2. inner 함수를 호출 하면 innerFunc을 호출하는 것과 같으므로 10 출력

10이 출력 되는 이유

  • outerFunc이 innerFunc을 return 하게되면 자기 역할 다 했으므로 outerFunc 스코프의 변수 x도 더 이상 외부에서 사용할 수 없을 것이라 예상 했고 그 예상 대로라면 10이 출력 되지 않았어야 했다.
  • 하지만 JS의 스코프 개념을 통해 10이 출력이 가능

클로저

  • 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 `함수
  • 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수
  • 선언 되었던 곳의 환경을 밖에서도 사용할 수 있다는 뜻

알아두면 좋은 것

  • 클로저에 의해 참조 되는 외부함수의 변수를 자유변수라고 함
  • 클로저를 자유변수에 엮여있는 함수라고도 함
  • 활성 객체(외부함수의 변수, 함수 선언 등의 정보)는 내부 함수에 의해 참조되는 한 유효하여 내부함수가 스코프 체인을 통해 참조할 수 있음
  • 내부 함수는 외부함수 변수의 복사본을 참조하는 것이 아니라 실제 변수를 참조하고 있음

📌 클로저 활용

  • 클로저는 자신이 선언되었을 때의 렉시컬 스코프를 기억하고 있어야 하므로 메모리 차원에서 손해를 볼 수 있음
  • 그럼에도 클로저는 JS의 강력한 기능으로 여러 방면에서 유용하게 사용된다.

🔎 웹에서 사용

MDN-실용적인 클로저

🔎 상태 관리

  • 클로저의 필요성을 위해 클로저를 사용하지 않는 방법 2가지(전역 변수, 지역 변수)에 대해 먼저 설명하겠습니다.

전역 변수

var counter = 0;

function increase() {
  return ++counter;
}
	
console.log(increase());
console.log(increase());
console.log(increase());
  • 값은 수정할 수 있음
  • 하지만 counter는 전역 변수여서 다른 곳에서 수정하게되면 심각한 오류가 발생 될 수 있음(counter는 0부터 시작되어야 하는데 누군가가 수정해 100부터 시작하게 되면 오류 발생)
  • 다른 곳에서의 값 수정을 방지하기 위해 increase 함수에서 관리 => 지역 변수 사용

지역 변수

function increase() {
  var counter = 0;
  return ++counter;
}
	
console.log(increase());
console.log(increase());
console.log(increase());
  • 값의 수정은 막을 수 있음
  • 하지만 호출할 때 마다 counter 값을 0으로 초기화 하므로 이전 상태를 기억할 수 없음
  • 이전 상태를 기억해서 새로운 상태를 만들고 외부에서 상태를 변경할 수 없는 방법은 없을까? => 클로저

클로저

var increase = (function() {
  var counter = 0;
  return function() {
    return ++counter;
  }
}());
	
console.log(increase());
console.log(increase());
console.log(increase());
  • 최신 상태로 수정도 가능하고 외부에서 counter 변수에 접근도 못함
  • react에서 임의로 state 값을 바꾸지 말라는 이유와 어느정도 연관이 있다고 생각을 했었음

    변수의 값은 누군가에 의해 언제든지 변경될 수 있어 오류 발생의 근본적 원인이 될 수 있다. 상태 변경이나 가변(mutable) 데이터를 피하고 불변성(Immutability)을 지향하는 함수형 프로그래밍에서 부수 효과(Side effect)를
    최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.

클로저 활용

  • poiemaweb 참조
// 함수를 인자로 전달받고 함수를 반환하는 고차 함수
// 이 함수가 반환하는 함수는 클로저로서 카운트 상태를 유지하기 위한 자유 변수 counter을 기억한다.
function makeCounter(predicate) {
  // 카운트 상태를 유지하기 위한 자유 변수
  var counter = 0;
  // 클로저를 반환
  return function () {
    counter = predicate(counter);
    return counter;
  };
}

// 보조 함수
function increase(n) {
  return ++n;
}

// 보조 함수
function decrease(n) {
  return --n;
}

// 함수로 함수를 생성한다.
// makeCounter 함수는 보조 함수를 인자로 전달받아 함수를 반환한다
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동하지 않는다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2
  • 독립된 렉시컬 환경 가짐 => 함수를 호출할때 마다 고유의 렉시컬 환경이 생성

궁금한 점

  • 자유 변수 참조시 실제 변수를 참조 => 실제 변수 자체가 각각 다른 것인가?

🔎 정보 은닉

  • 클로저를 이용해 JAVA의 private 키워드를 흉내낼 수 있다.

MDN-private 흉내내기

function Counter() {
	var counter = 0
    
    this.increase = function() {
    	return ++counter;
    }
    
    this.decrease = function() {
    	return --counter;
    }
}

var controlCounter = new Counter();

console.log(controlCounter.increase()); //1
console.log(controlCounter.increase()); //2
console.log(controlCounter.decrease()); //1
console.log(controlCounter.counter); //undefined
  • 생성자 함수 Counter는 increase, decrease 메소드를 갖는 인스턴스(controlCounter) 생성
  • 이 메소드들은 객체의 프로퍼티 뿐만 아니라 렉시컬 환경의 counter 환경 변수도 참조할 수 있는 클로저
  • increase, decrease가 클로저 이므로 이 메소드들을 통해 counter 값을 참조 및 수정할 수 있지만 외부에서 직접 counter 변수를 수정 할 수는 없다. => private 키워드 처럼 사용 가능

📌 클로저 실수

  • MDN-클로저 스코프 체인
  • 바로 위의 변수만 참조하는 문제가 아닐까 예상(똑같은 변수명에 있어 바로위의 스코프 말고 더 상위 스코프의 변수를 참조하고 싶을 수도 있음)
  • poiemaweb 참조
var arr = [];

for (var i = 0; i < 5; i++) {
  arr[i] = function () {
    return i;
  };
}

for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}
  • 답이 뭐라고 생각하시나요?
  • 반복문을 돌면서 참조하고 있는 i는 for문 스코프의 i가 아니라 전역 스코프의 i

클로저를 이용한 보완

var arr = [];

for (var i = 0; i < 5; i++){
  arr[i] = (function (id) { // ②
    return function () {
      return id; // ③
    };
  }(i)); // ①
}

for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}
  • 즉시 실행 함수를 사용
  • 1번에서 즉시 실행함수의 전달인자로 i를 넘겨줌
  • 2번에서 전달 받은 i를 id를 통해 가지고 있음
  • 3번은 클로저 로써 렉시컬 환경에 의해 매개변수 id를 참조할 수 있으므로 반복문을 돌때마다 다른 id를 참조
  • for문 사용시 var 키워드를 이용한 변수를 사용할 경우 해당 변수가 전역으로 처리 되기 때문에 발생하는 문제로써 let 키워드를 사용하면 해결 된다.

고차 함수 사용

  • 변수와 반복문의 사용을 억제할 수 있기 때문에 에플리케이션의 오류를 줄이고 가독성을 좋게 만듬
const arr = new Array(5).fill();

arr.forEach((v, i, array) => array[i] = () => i);

arr.forEach(f => console.log(f()));

📌 참조

poiemaweb-클로저

MDN-클로저

0개의 댓글