[데브코스/TIL] DAY15 - JavaScript(4) 클로저

Minha Ahn·2024년 10월 28일
1

데브코스

목록 보기
12/22
post-thumbnail

🔭 스코프

1. 블록 스코프

  • let, const
let a = 10;
{
  let b = 20;
}
console.log(a, b); // b로 인한 에러

2. 함수 스코프

  • var
var a = 10;
{
  var b = 20;
}

function fn() {
  var c = 30;
}

console.log(a, b, c); // c로 인한 에러



🗑️ 가비지 컬렉터와 가비지 컬렉션

실행 컨텍스트가 콜스택에 존재할 때 이는 메모리에 실행 컨텍스트가 적재되어 있다는 것을 의미한다.

🔎 가비지 컬렉터란?

메모리에서 더 이상 사용되지 않는 객체를 자동으로 감지하고, 해당 메모리를 회수하여 메모리에서 삭제해주는 시스템

  • 콜스택에서 실행 컨텍스트가 제거되었다면 메모리에서도 제거되는데, 이를 가비지 컬렉터가 해준다.
  • 모든 프로그래밍 언어들마다 가비지 컬렉터가 존재하며, 자바스크립트에서는 자동으로 구축되어 있다.
    • 이는 메모리 회수를 개발자가 고려하지 않아도 됨을 의미한다.
  • 실행 컨텍스트가 콜스택에서 제거되는 순간 가비지 컬렉터의 대상으로 지정되며, 자동으로 회수된다.
    • 이런 이유 때문이라도 상위 컨텍스트는 하위 컨텍스트를 참조하지 못한다. (상위 컨텍스트가 실행될 때는 하위 컨텍스트가 사라지고 없는 상태이기 때문)

🔎 가비지 컬렉션이란?

가비지 컬렉터에 의해 메모리가 회수되는 과정



🚪 클로저

🔎 클로저란?

함수가 자신이 생성된 스코프에 대한 참조를 기억하고 유지하는 능력

  • 함수는 호출된 위치 기반이 아닌, 생성된 위치를 기반으로 스코프를 결정함 (렉시컬 스코핑)
  • 외부 함수가 반환되어 스코프가 종료된 후에도, 반환된 함수(내부 함수)는 원래의 상위 스코프에 있는 변수를 계속해서 참조할 수 있음
  • 참조가 되고 있는 동안은 가비지 컬렉터 대상으로 삼지 않기 때문에 생겨난 현상
  • 클로저는 위험하니 유의해서 사용
    • 메모리가 자동으로 삭제되지 않기 때문에, 클로저가 많으면 회수되지 않는 메모리가 많아짐
    • 위의 상황은 자바스크립트의 가비지 콜렉터 시스템을 위협할 수 있음
  • 클로저는 개념 하나를 지칭하기 보다는 현상을 이야기하는 것

클로저 사용 패턴

🔎 은닉화란?

외부에서 직접 접근할 수 없는 변수를 만들어, 해당 변수를 내부 함수에서만 접근 및 수정할 수 있도록 하는 것

  • 접근 제한을 두는 것
  • 코드의 안정성과 일관성 유지 가능하며, 의도치 않은 오류 방지 가능
  • 리액트에서는 아래의 있는 패턴 중 대부분 구현된 기능들이 많기 때문에 은닉화 정도 사용될 것이다.
function counter() {
  let count = 0; // 자유 변수
  
  return {
    increment: function() {
      count++; // 자유 변수 참조
      return count;
    },
    decrement: function() {
      count--;
      return count;
    },
  };
}

let mycount = counter();
console.log(mycount.increment());

// 더이상 사용하지 않으므로 가비지 컬렉터 대상으로 만들기
mycount = null;

🔎 함수 팩토리란?

클로저를 활용하여 여러 개의 함수를 생성하는 패턴

  • 특정 기능을 수행하는 함수를 동적으로 생성 & 생성된 함수가 외부 변수에 접근할 수 있도록 한다.
  • 코드의 재사용성을 높이고, 함수의 생성 과정에서 필요한 상태(외부 변수 참조 유지)를 유지할 수 있다.
function makeMultiple(multiplier) {
  // 동적으로 함수 생성
  return function (x) {
    return x * multipler;
  };
}

let double = makeMultiple(2);
let triple = makeMultiple(3);

console.log(double(5)); // 5 * 2
console.log(triple(5)); // 5 * 3

// 가비지 컬렉터 대상으로 만들기
double = null;
triple = null;

🔎 비동기 프로그래밍에서 클로저란?

콜백 함수가 실행될 때 사용할 변수의 값을 유지하거나 기억하는 데 유용함

  • 비동기 프로그래밍 : 함수가 호출된 후 나중에 응답을 받을 수 있는 작업을 처리할 때 사용
  • 클로저를 통해 비동기 함수가 실행되는 동안 필요한 변수를 참조할 수 있음
  • 왜 필요할까?
    • 특정 이벤트 발생 or 요청 완료된 후에 콜백 함수가 실행되기 때문에, 그 시점에서 사용할 변수의 상태를 기억하는 것이 중요함
    • 클로저 사용 => 비동기 코드가 실행될 때 함수가 정의된 환경에 있던 변수 참조 가능
function fetchData(url) {
  let result; // 클로저 대상
  
  return function (callback) {
    setTimeout(() => {
      result = "Fetched... Success";
      callback(result);
    }, 1000);
  }
  
  let fetchFromNaver = fetchData("https://www.naver.com");
  fetchFromNaver((data) => console.log(data));
  
  // 가비지 컬렉터 대상으로 만들기
  fetchFromNaver = null;
function createTimer() {
  for (let i = 1; i <= 3; i++) {
    
    // setTimeout은 비동기 함수
    setTimeout(function() {
      console.log(`Timer ${i}초 후`);
    }, i * 1000);
  }
}

// createTimer가 종료되었음에도 i값을 참조하여 콜백함수 실행
createTimer();

실은 위에서 비동기 프로그래밍에서 클로저가 왜 필요한지 크게 와닿지 않았는데,
실행 컨텍스트와 엮어 생각해보니 이해가 잘 되었다.

1분의 시간이 소요되는 API 요청을 해야한다고 가정하자.
비동기 프로그래밍을 사용하지 않는다면, 우리는 API 요청 후 1분동안 결과가 올 때까지 기다려야 한다.
그리고 1분동안은 아무것도 할 수가 없다.

그런데 해야할 작업이 많은 와중에 1분동안 가만히 있을 수는 없다!! (비동기를 하고 싶다!)
그러기 위해서는 API 요청 내용이 담긴 실행 컨텍스트가 콜스택에서 제거되어야 한다.
그러나 콜스택에서 제거되면 메모리 역시 제거되므로 API 요청의 결과에 접근할 수 없다.

이럴 때 클로저가 필요하다.
클로저를 통해 비동기 작업이 완료될 때까지 필요한 변수(API 요청 결과를 받아낼 변수)를 참조하며 기억하는 것이다.
그리고 보통 그 참조를 기억하는 것이 콜백함수(이럴 땐 비동기 작업의 결과를 처리하는 로직일 듯)이다.
참조한 변수에 API 요청에 대한 결과를 받아오면 콜백함수의 실행 컨텍스트가 생성되며 실행된다.

오해하지 말아야 할 것은, 콜백함수가 참조한 변수에 대해 값의 변경을 인지하는 능력이 있는 것은 아니고,
자바스크립트 엔진의 이벤트 루프가 모든 동기 작업을 완료한 뒤 비동기 작업의 결과가 반환되었는지를 확인한 후
콜백 함수를 실행시켜주는 것이다.

그리고 비동기 함수에서 꼭 클로저가 발생하는 것은 아니다!
비동기 함수 내의 변수를 참조를 하냐 마냐에 달렸다.

또한, 꼭 콜백함수가 참조해서 클로저가 발생하지 않는다. 다른 내부 함수일 수도 있다.


🔎 메모이제이션 패턴이란?

함수의 결과를 저장하여 동일한 인수로 다시 호출할 때 계산을 반복하지 않고 저장된 결과를 반환하는 최적화 기법

  • 처음에는 느려도, 같은 요청이 다시 들어오면 빠르게 반환할 수 있다. (계산 비용이 큰 함수 성능 개선 가능)
  • 클로저를 활용해 상태(저장해둔 이전 계산 결과) 유지
function memoization(fn) {
  const cache = {}; // 클로저 대상
  
  return function (...args) {
    const key = JSON.stringify(args);
    
    if (cache[key]) return cache[key];
    cache[key] = fn(...args);
    return cache[key];
  };
}

function slowFunction(num) {
  for (let i = 0; i < 9999999999; i++);
  return num * 2;
}

let memoizedFn = memoization(slowFunction);
console.log(memoizedFn(5)); // 첫 실행에서 출력까지 시간이 걸림
console.log(memoizedFn(5)); // 바로 출력됨

// 가비지 컬렉터 대상으로 만들기
memoizedFn = null;



✏️ 메모

클로저 어렵다🥲

내가 생각한 클로저의 핵심

  • 외부 함수의 리턴값이 내부 함수인 점
    내부 함수는 자신이 정의된 환경(외부 함수의 스코프)을 기억함
  • 내부 함수가 외부 함수의 변수에 접근할 수 있는 점
    외부 함수의 실행 컨텍스트가 종료된 후에도 내부 함수에서 사용가능하며, 이를 통해 내부 함수는 외부 함수에서 정의된 상태 유지 가능

setTimeout 함수 추가 설명

  • 자바스크립트 엔진 자체에서 실행되는 것이 아닌, 브라우저나 Node.js 환경에서 제공하는 타이머 API의 일부

  • 동작 흐름

  1. 콜백 함수 등록
    setTimeout 호출 -> 콜백 함수를 브라우저의 타이머 API나 Node.js 타이머 API에 등록

  2. 타이머 시작
    타이머 API는 지정된 시간만큼 대기 -> 시간 지나면 콜백 함수를 태스크 큐에 추가

  3. 이벤트 루프와 콜백 함수 실행
    이벤트 루프는 메인 스레드에서 실행 중인 모든 동기 작업이 종료될 때까지 대기 (자바스크립트는 싱글 스레드라서)
    -> 이벤트 루프가 태스크 큐에서 준비된 콜백을 호출 스택에 올려 실행





📌 출처

수코딩(https://www.sucoding.kr)

profile
프론트엔드를 공부하고 있는 학생입니다🐌

0개의 댓글