클로저에 대해서 알아보자

imloopy·2022년 9월 24일
0

Today I Learned

목록 보기
49/56

틀린 내용이 있을 수 있습니다. 의견은 언제나 환영입니다.

이번에 debounce를 구현하면서 애매하게 알고 있었던 클로저에 대하여 다시 공부하는 시간을 가졌다. 이전에 정리한 것은 정말 인터넷에서 보고, 그냥 요약 정리만 한 느낌이었다면, 이제는 그때보다는 훨씬 그래도 내 문장 같다라는 생각이 들었다. 이것이 바로 성장...?

조만간 hook에 대하여 정리할 예정입니다.

클로저

클로저란?

클로저는, 이미 생명주기가 끝난 외부함수의 식별자에 접근할 수 있는 현상(내부 함수)을 말한다.

클로저가 생기는 이유

클로저가 생기는 이유에 대해서 알기 전에, 스코프라는 개념에 대해 알아야 할 필요가 있다.

스코프란?

유효 범위(식별자가 유효한 범위)를 뜻하며, 참조 대상 식별자를 찾아내기 위한 규칙이다. 자바스크립트(뿐만 아니라 대부분의 언어)에서는, 상위 스코프에서 선언된 식별자를 하위스코프에서 참조할 수 있다. 그러나 그 반대로는 불가능하다.

즉 어떤 함수 내부에 블록이나, 다른 선언된 함수가 있을 때, 스코프가 중첩 되어있다고 생각할 수 있으며, 하위 스코프는 상위 스코프에 교집합의 형태를 띄게 된다. 마치 타입스크립트와 자바스크립트의 관계와 동일하다.

어떤 함수에서 선언되지 않은 식별자를 참조할 때, 해당 식별자가 선언된 스코프를 찾게 된다. 선언된 위치를 발견했다면, 해당 값을 참조할 수 있다.

렉시컬 스코프란?

이미 많은 블로그 글들에서 렉시컬 스코프라는 단어를 많이 보았을 것이다. 렉시컬 스코프는 무슨 스코프지? 라고 생각을 했었는데, 정확히는 스코프는 함수가 선언되었을 때의 환경을 따라서 결정된다는 뜻이다. 일종의 규칙? 이라고 생각하면 될 듯하다.

그래서 클로저가 왜 생기는데요??

클로저는, 이미 생명주기가 끝난 외부 함수의 식별자에 접근할 수 있는 현상이다. 이 현상이 어떻게 발생할 수 있는지 생각해야 할 필요가 있다.

함수의 생명주기는 함수가 호출되고, 값을 반환할 때 까지 유지된다. 쉬운 설명을 위해 다음의 함수가 있다고 생각하자.

function someFunc() {
  let a = 1
  return function () {
    a += 1
    return a
  }
}

const add = someFunc()
console.log(add())
console.log(add())
  1. someFunc 함수의 반환 값은 익명 함수이고, 익명 함수에서 상위 스코프에 선언된 식별자에 접근하여 값을 변경하는 형태이다.

  2. add 식별자에는 someFunc이 실행되고 반환된 함수가 할당된다. 그리고, someFunc은 실행이 종료되었으므로 콜스택에서 제거된다.

  3. add 식별자를 호출하여 함수를 실행한다. 이 때, add 식별자에 할당된 함수의 선언 위치는 someFunc 내부이다. 자바스크립트는 렉시컬 스코프를 따르기 때문에, a가 가리키는 식별자는 1이 된다.

  4. 1에 1을 더한 값을 반환하기 때문에, add()의 값은 2가 된다.

  5. 다시 add 식별자를 호출하여 함수를 실행한다. 이 때, add 식별자에 할당된 함수의 선언 위치는 someFunc 내부이다.

  6. 이미 이전에 add 식별자를 호출하여 a는 2가 된 상태이다.

  7. a가 2인 상태에서 a에 1을 더한 값을 반환하므로 3이 출력되고 함수가 종료된다.

  8. 더 이상 a에 접근할 수 있는 방법이 없기 때문에 가비지 컬렉터에 의해 a에 할당된 메모리가 수거된다.

    즉, 선언된 환경에서 상위 스코프의 식별자를 함수가 존재하는 동안 계속 참조할 수 있기 때문에 그렇다.

클로저는 어디에서 사용될까?

debounce

연이어 호출되는 함수들 중, 가장 마지막의 함수만 실행되게 하는 것

일반적으로 검색 결과를 보여주는 부분에서 디바운스를 많이 사용한다. 그 외에도 다른 부분에서도 사용하겠지만, 현재 생각나는 것은 api 요청을 할 때, input에 글자를 입력할 때마다 api를 요청하는 것은 낭비이므로, 디바운스를 적용하여 사용자가 입력을 마친 뒤 api 요청을 할 수 있도록 처리한다.

디바운스의 개념은 다음과 같다.

  1. 함수를 호출한다. 호출된 함수는 setTimeout이 적용되어 있기 때문에, n 초 뒤에 실행된다.
  2. 다시 함수를 호출한다. 호출 시 clearTimeout을 통해 기존 Timeout을 초기화한다. 그리고 다시 setTimeout을 적용한다.
  3. 설정한 시간 동안 다시 함수를 호출하지 않으면, 원하는 콜백 함수가 실행된다.

즉 함수가 호출되면서 timer가 초기화되고, 다시 설정되는 과정을 반복한다. 타이머에 대한 정보를 계속 기억하고 있으려면, 타이머에 대한 정보는 실제 디바운스가 실행될 함수와 다른 스코프에 있어야 한다. 같은 스코프에 있다면, 해당 함수가 종료된 뒤 타이머에 대한 정보 역시 사라질 것이기 때문이다. 그렇기 때문에 상위 스코프에 타이머 정보를 저장하여, 반환 받은 함수를 통해 타이머에 접근할 수 있는 동안은 타이머 정보가 유지되도록 클로저 형태로 구현되어야 한다.

function debounce(callback, delay) {
  let timer = null // timer에 대한 정보는 상위 스코프에 존재한다.
  return (...args) => {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      callback(...args)
    }, delay)
  }
}

// debounced는 debounce가 적용된 콜백 함수이다.
const debounced = debounce((text) => console.log(text), 500)
const input = document.querySelector('.input')
input.addEventListener('input', (e) => {
  debounced(e.target.value) // 500ms 이후 값 출력
})

출처

클로저의 메모리 구조 + 캡처현상 / 캡처리스트

Scope | PoiemaWeb

[javascript] 콜스택/메모리힙 구조, 데이터 저장/참조 원리

[JavaScript] 스코프, 렉시컬환경

클로저(Closure)는 무엇이며, 어떻게/왜 사용하나요 ? | SEUNGWOO's TECH LOG

[React] Hook의 동작 원리 이해하기

0개의 댓글