Early Riser
생각정리
Early Riser
전체 방문자
오늘
어제
  • 분류 전체보기 (128)
    • JS (19)
    • React (33)
    • React Native (2)
    • Library, Tool (13)
    • CSS (2)
    • Algorithm (40)
    • Computer Science (3)
    • 회고 (3)
    • AI (13)

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • 글쓰기

공지사항

인기 글

태그

  • 부스트캠프 9기
  • js
  • RNN
  • 딥러닝
  • BFS
  • 자바스크립트
  • 부스트캠프 합격 후기
  • 밑바닥
  • local minima
  • 알고리즘
  • 완전탐색
  • 프로그래머스
  • 부스트캠프 합격
  • boosting
  • useEffect
  • 백트래킹
  • 손실함수
  • 비동기
  • react
  • 파이썬
  • 구현
  • 논문리뷰
  • lightgbm
  • javascript
  • 오늘의불경
  • LGBM
  • useState
  • 백준
  • global minima
  • dfs

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Early Riser

생각정리

JS

[JS] Javascript에서의 Closures

2025. 9. 19. 11:47

1. 클로저란

 클로저는 외부 함수에서 선언된 변수를 참조하는 내부 함수에서만 발생하는 현상이다. 클로저의 정의에 대해선 매체마다 저자마다 다르다. MDN에선 클로저를 "함수와 그 함수가 선언된 Lexical Environment의 조합"라 정의한다. 내가 생각하는 클로저의 제일 정확한 정의는 'A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).'이다.

Lexical Environment는 함수가 선언될 당시의 모든 지역 변수를 포함한다. 선언된 당시의 Lexical Environment이란 실행 컨텍스트의 구성 요소 중 하나인 outerEnvironmentReference를 포함하여 변수의 유효 범위(스코프)를 결정하고 스코프 체인을 형성하는 데 영향을 끼친다. JavaScript에서 클로저는 함수 생성 시 함수가 생성될 때마다 생성된다.

2. 클로저의 범위

 클로저는 어휘적 범위 지정, 즉 함수가 선언될 때의 변수 범위에 바인딩되는 특성을 가지고 있다. 예를 들어, 외부 함수에서 선언된 변수를 내부 함수가 참조하면 외부 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 소멸되지 않는 현상이 발생한다. 예를 들어, 외부 함수 A 내에서 선언된 내부 함수 B의 실행 컨텍스트가 활성화될 때, B의 outerEnvironmentReference는 A의 Lexical Environment에 접근할 수 있다. 이는 B가 A에서 선언된 변수들에 접근할 수 있음을 의미하며, 반대로 A는 B에서 선언된 변수에는 접근할 수 없다.

JavaScript의 ES6 버전부터는 스코프의 개념이 확장되어, ES5에서 존재하던 전역 스코프, 함수 스코프 외에도 블록 스코프가 추가된다. 예를 들어, 함수 스코프는 특정 함수 내에서 선언된 변수들이 그 함수 내에서만 접근 가능함을 의미한다. 아래는 함수 스코프의 예제 코드이다.

function MyObject(name) {
  this.name = name
  this.myMethod = function() {
    return this.name
  };
}

반면에 var 키워드를 사용하여 선언된 변수는 중괄호로 구분된 블록 내에 있더라도 전역 스코프에 속하게 된다. 이 혼란을 피하기 위해 이는 let과 const 키워드로 변수들을 생성하면 블록 스코프를 생성할 수 있다.아래의 예제를 보면 var 키워드와 let, const 키워드로 생성되는 스코프의 차이에 대해 알 수 있다.

if(true){
  var x = 1; // 전역 스코프
 }
 console.log(x) // 1
 
 if(true){
  let x = 1; // 블록 스코프
 }
 console.log(x) // 1

3. Stale 클로저

클로저는 lexcial environment의 '참조'를 저장하기 때문에, 참조하는 값이 최신 값과 다른, stale한 경우가 생길 수 있다.

이는 스냅샷을 따로 저장하거나, 변수를 새 값/새 객체로 재할당하거나, 렌더마다 다른 바인딩이 생기는 환경(React)에서는 stale한 값을 보게 된다. 

3.1 Primitive / Reference 값을 직접 변경할 때

Primitive는 증가가 재할당이므로 스냅샷은 stale가 된다. 바인딩을 직접 읽으면 최신을 본다

function primitiveMutationLike() {
  let x = 0
  const snapshot = x
  const inc = () => { x++ }
  const getBinding = () => console.log('binding:', x)
  const getSnapshot = () => console.log('snapshot:', snapshot)

  getBinding()  // 0
  getSnapshot() // 0
  inc()
  getBinding()  // 1
  getSnapshot() // 0 (stale)
}

 

3.2  Primitive / Reference 값을 새 값으로 재할당할 때

Primitive는 재할당이 기본이므로 스냅샷은 stale가 된다

function primitiveReassign() {
  let x = 0
  const snapshot = x
  x = x + 1
  console.log('binding:', x)      // 1
  console.log('snapshot:', snapshot) // 0 (stale)
}

Reference를 새 객체로 교체(불변 업데이트)하면 스냅샷은 옛 참조를 보므로 stale가 된다

function referenceReassign() {
  let x = { value: 0 }
  const snapshot = x
  x = { value: x.value + 1 }
  console.log('binding:', x.value)        // 1
  console.log('snapshot:', snapshot.value) // 0 (stale)
}

 

3.3 React에서의 stale closure 문제

React에서 state는 렌더마다 다른 바인딩이 생기는, 즉  함수가 매번 새로 실행되어 state를 Return하기 때문에 콜백이 “생성된 렌더의 값”을 캡처한다.

// ❌: 의존성 없어서 stale하다. 첫 렌더의 count만 캡처.
function C() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const id = setInterval(() => console.log(count), 1000) // 0
    return () => clearInterval(id)
  }, []) // 
}

// ✅: 의존성으로 함수를 매번 재생성하여 최신값을 참조.
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000)
  return () => clearInterval(id)
}, [count])

// ✅: state의 snapshot을 ref에 매번 저장.
function C() {
  const [count, setCount] = useState(0)
  const latest = useRef(count)
  useEffect(() => { latest.current = count }) // 매 렌더 최신값 반영
  useEffect(() => {
    const id = setInterval(() => console.log(latest.current), 1000)
    return () => clearInterval(id)
  }, [])
}

클로저의 메모리 관리

 클로저는 외부 함수가 종료된 후에도 외부 함수의 지역 변수에 접근할 수 있게 하기에 외부함수의 지역변수가 메모리에 남아 있어 메모리 소모를 일으킨다. 내부 함수가 외부 함수의 변수에 대한 참조를 가지고 있기 때문에 이러한 변수들은 가비지 컬렉션의 대상이 되지 않다.

 클로저가 참조하는 변수들이 더 이상 필요하지 않게 되면 자바스크립트 엔진은 이를 감지하고 메모리를 해제한다. 그렇다 하더라도 특정 작업에 클로저가 필요하지 않는데 다른 함수 내에서 함수를 불필요하게 작성하는 것은 메모리 누수를 일으킬 수 있기에 메서드를 생성할 때 객체 생성자가 아닌 프로토타입에 추가하여 클로저를 사용하지 않을 수 있다.

// 객체 생성자로 메서드 생성: 클로저생성
function MyObject(name) {
  this.name = name
  this.myMethod = function() {
    return this.name
  };
}


// 프로토 타입에 메서드 추가: ~클로저생성 
function MyObject(name) {
  this.name = name
}

MyObject.prototype.myMethod = function() {
  return this.name
};

클로저의 활용

클로저는 다양한 상황에서 유용하게 사용될 수 있다. 예를 들어, 클로저를 활용하여 콜백 함수 내에서 외부 변수를 참조하거나 정보를 은닉하고 부분 적용 함수나 커링 함수를 구현할 수 있다.

비공개 메서드

 클로저를 활용하여 자바스크립트에서 비공개(Private) 메서드와 변수를 흉내내어 모듈 디자인 패턴을 구현할 수 있다. 모듈 디자인 패턴은 함수 내에서 데이터와 메서드를 캡슐화하여 외부에서 직접적인 접근을 제한함으로써 정보 은닉과 캡슐화의 원칙을 따른다.

 모듈 디자인 패턴은 즉시 실행 함수(IIFE: Immediately Invoked Function Expression)를 사용하여 구현된다. 이 패턴은 외부에서 접근할 수 없는 비공개 변수와 함수를 생성하고, 공개적으로 사용하는 퍼블릭 함수를 반환한다. 아래는 모듈 디자인 패턴을 사용하여 클로저를 구현한 예시 코드이다

const Module = (function() {
  // 비공개 변수
  const privateConst = "비밀";

  // 비공개 함수
  function privateMethod() {
    console.log("비공개 메서드에 접근");
    return privateConst;
  }

  return {
    // 퍼블릭 메서드
    publicMethod: function() {
      console.log("퍼블릭 메서드에 접근");
      return privateMethod();
    }
  };
})();

console.log(Module.publicMethod()); 
// 퍼블릭 메서드에 접근
// 비공개 메서드에 접근
// 비밀

 

부분 적용 함수

 클로저를 활용하여 함수형 프로그래밍인 부분 적용 함수를 만들 수 있다. 부분 적용 함수는 주어진 함수의 일부 인자만 미리 적용하고, 나머지 인자를 나중에 적용할 수 있는 새로운 함수를 생성하는 기법이다. 

 아래 코드는 두 인자를 더하는 함수에서 부분적용으로 x를 5로 미리 선언하고 나머지 하나의 인자를 받아 5와 더하는 부분 적용 함수이다. 클로저를 활용하여 내부 함수에서 외부 함수의 변수인 x를 가져옴을 볼 수 있다.

function sum(x) {
  return function (y) {
    return x + y;
  };
}

const add5 = sum(5);
console.log(add5(2)); // 7

커링 함수

 커링은 함수와 함께 사용하는 기법이다. 커링은 f(a, b, c)처럼 단일 호출로 처리하는 함수를 f(a)(b)(c)와 같이 각각의 인수가 호출 가능한 프로세스로 호출된 후 병합되도록 변환하는 것이다. 커링은 함수를 호출하지 않고 변환만 한다. 

 다음은 클로저를 활용하여 커링 변환을 하는 curry 함수를 구현한 예제 코드이다.

function curry(f) { // 커링 변환을 하는 curry 함수
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

function sum(a, b) {
  return a + b;
}

let curriedSum = curry(sum);

참고자료

Javascript.info. (n.d.). 커링.

Mozilla. (n.d.). Closures - JavaScript. MDN Web Docs.

정재남. (2019). 코어 자바스크립트. 위키북스.

'JS' 카테고리의 다른 글

TypedArray에 대하여  (0) 2025.11.24
브라우저에게 양보하기  (0) 2025.09.28
debounce와 throttle의 차이점  (0) 2025.05.29
ES2021에서 추가된 신기능 5가지  (1) 2025.02.13
JavaScript 이벤트 루프 동작방식: Microtask Queue, Macrotask Queue  (0) 2024.11.21
    'JS' 카테고리의 다른 글
    • TypedArray에 대하여
    • 브라우저에게 양보하기
    • debounce와 throttle의 차이점
    • ES2021에서 추가된 신기능 5가지
    Early Riser
    Early Riser
    2년차 프론트엔드 개발자입니다. https://github.com/EarlyRiser42

    티스토리툴바