Front-end/JavaScript

JS 클로저 6편 - 클로저 사용 이유(메모리 절약 & 캡슐화)와 호이스팅

파리외 개발자 2023. 1. 21. 19:55

Memory Efficient

//memory efficient
function heavy(idx) {
  const bigArr = new Array(10000).fill("大");
  console.log("created");
  return bigArr[idx];
}

heavy(411); //참조 때마다 배열1만개가 생성되고 제거됨
heavy(411);
heavy(411);
heavy(411);

크기 10000의 배열을 만들고 인자로 받은 인덱스의 배열값을 리턴하는 함수가 있을 때

해당 함수는 호출할 때마다 배열을 생성하고 삭제하길 반복한다.

그 증거로 콘솔창에 created문구가 호출 횟수만큼 출력되는 것을 확인할 수 있다.

function heavy2() {
  const bigArr = new Array(10000).fill("大");
  console.log("created again");
  return function (idx) {
    return bigArr[idx];
  };
}

const getHeavy = heavy2();

getHeavy(600); //첫 참조때 한번만 배열이 생성되고 참조가 끝나면 제거됨
getHeavy(700);
getHeavy(800);
getHeavy(900);

클로져를 사용하면 getHeavy에 저장된 heavy2의 리턴함수가 

bigArr에 참조하고 있는 한 해당 배열은 한번만 생성되고 

getHeavy가 모두 호출되어 참조가 끝나면 배열이 제거되어

클로저를 사용해 중복행동을 방지하여 메모리를 효율적으로 사용할 수 있다.

예제 코드

let view;

function init() {
  view = "view";
  console.log("view is setting");
}

init();
init();
init();
console.log(view);

init은 view변수 초기화 함수이며 함수호출시 마다 매번 변수할당을 하게 된다.

let view;

function init() {
  let called = 0;
  return function () {
    if (called > 0) {
      return;
    } else {
      view = "view";
      called++;
      console.log("view is setting");
    }
  };
}

const startOnce = init();
startOnce();
startOnce();
startOnce();
console.log(view);

변수에 초기화함수를 저장해서 called로 조건분기를 해서 추가호출 시 리턴해주는 방식이다.

called변수는 startOnce가 참조되고 있는 한 클로저현상으로 메모리에서 삭제되지 않는다.

Encapsulation

클로저를 사용해서 민감한 정보에 접근을 막고 필요한 것들에만 접근하게 만들 수 있다.

아래에 핵버튼을 조작하는 함수가 있다고 했을 때,

  • 접근 불가능 : countDown, passTime
  • 접근 가능 : totalPeaceTime, launch (리턴하는 객체의 속성에 할당)

으로 카운트다운이 얼마나 되었는지 확인하는 행동인 totalPeaceTime과 핵폭발버튼 launch는 사용자가 접근해야 하므로 

객체의 내장 메서드로 리턴한다.

countDown과 passTime은 외부에서 접근 시 핵 폭발 카운트다운 로직에 영향을 줄 수 있으므로 

리턴하지 않는다.

//Encapsulation
const makeNuclearButton = () => {
  let countDown = 0;
  const passTime = () => countDown++;
  const totalPeaceTime = () => countDown;
  const launch = () => {
    countDown = -1;
    return "boom";
  };

  setInterval(passTime, 1000);
  return {
    launch: launch,
    totalPeaceTime: totalPeaceTime,
  };
};

const Fatman = makeNuclearButton();
Fatman.totalPeaceTime();
Fatman.launch(); //count초기화

변수 Fatman에는 리턴된 launch와 totalPeaceTime메서드가 저장되고

사용자는 객체의 내장메서드형식으로 해당 메서드를 계속해서 사용할 수 있지만,

리턴되지 않는 항목들은 내장메서드에서 참조하고 있기에 메모리에서 사라지진 않지만

Fatman에도 저장되지 않았고 함수가 호출되지도 않았기 때문에

어떤 방법으로도 접근하여 조작할 수 없게 된다.

예제 코드

const arr = [1, 2, 3];
for (var i = 0; i < arr.length; i++) {
  setTimeout(function () {
    console.log("idx : " + i);
  }, 1000);
}

배열의 인덱스를 1초 뒤 출력해 주려고 헀으나

결과로는 반복문이 끝날 때 i는 2에서 3으로 증가되고

콜 스택이 모두 종료되고 setTimeout이 실행되면서 i는 3으로 세 번 호출된다.

const arr = [1, 2, 3];
for (var i = 0; i < arr.length; i++) {
  (function (closureI) {
    setTimeout(function () {
      console.log("idx : " + closureI);
    }, 1000);
  })(i);
}

반복문 안에 IIFE를 사용해 인덱스를 매번 전달한다면 setTimeout이 참조하는 변수는

IIFE에서 받아온 인덱스 값이 되므로 인덱스가 정상적으로 전달되게 된다.

이렇게 api에서 참조하는 변수에 맞는 값을 전달하기 위해선 var대신 let을 사용해도 되지만

IIFE와 클로저를 사용한 위 방식은 api 내에서는 인덱스 i에 접근하지 못하는

캡슐화 현상을 추가로 가질 수가 있다.

Bonus : 클로저에서의 호이스팅

//Clusour Exercise
function callMaybe() {
  const callMe = "Hi! now here!";
  setTimeout(function () {
    console.log(callMe);
  }, 4000);
  // const callMe = "Hi! now here!";
}

callMaybe();

클로저현상에서 호이스팅은 상관없이 동작한다.

callMe변수가 선언된 위치와 상관없이 setTimeout이 참조하고 있다면 

callMe변수에서 클로저현상은 정상적으로 일어난다.