프론트엔드/JavaScript

(JavaScript) 클로저(Closure)란 무엇인가?

그린티_ 2026. 2. 1. 14:33
반응형

서론

JavaScript로 개발하다 보면 함수 안에 함수를 쓰는 경우가 정말 많음
React에서 useState, 이벤트 핸들러, useEffect 안에서 외부 변수를 참조하는 것도 사실 다 클로저와 관련이 있었음

처음엔 "함수가 끝났는데 왜 변수가 살아있지?" 싶었고, 면접 준비하면서 클로저가 단골 질문이라는 걸 알게 됨
"클로저가 뭔가요?"라는 질문에 제대로 대답하려면 렉시컬 스코프부터 알아야 해서, 처음부터 정리해봄


본론

1. 먼저 렉시컬 스코프(Lexical Scope)를 알아야 함

클로저를 이해하려면 렉시컬 스코프부터 알아야 함

function outer() {
  const name = '그린티';

  function inner() {
    console.log(name); // '그린티'
  }

  inner();
}

outer();
  • inner 함수는 자신이 선언된 위치 기준으로 상위 스코프(outer)의 변수 name에 접근할 수 있음
  • 함수가 어디서 호출되었는지가 아니라, 어디서 선언되었는지에 따라 스코프가 결정되는 것 → 이게 렉시컬 스코프

여기까진 당연하게 느껴질 수 있음. 근데 함수를 밖으로 꺼내면 이야기가 달라짐


2. 클로저란?

innerreturn으로 밖으로 꺼내보자

function outer() {
  const name = '그린티';

  function inner() {
    console.log(name);
  }

  return inner; // 함수 자체를 반환
}

const closureFn = outer(); // outer 실행 완료
closureFn(); // '그린티' — outer는 끝났는데 name에 접근 가능!

outer()는 이미 실행이 끝났음. 일반적으로 함수 실행이 끝나면 내부 변수는 메모리에서 사라져야 함
그런데 closureFn()을 호출하면 여전히 name에 접근할 수 있음

"함수가 끝났는데 왜 변수가 살아있지?" → 이게 바로 클로저

클로저 = 함수 + 그 함수가 선언된 렉시컬 환경(Lexical Environment)

반환된 inner 함수가 자신이 생성될 때의 스코프(변수 name이 있는 환경)를 기억하고 있기 때문에 가능한 것


3. 클로저가 없으면 생기는 문제

문제: 데이터를 보호할 수 없음

// 클로저 없이 카운터 만들기
let count = 0;

function increment() {
  count++;
  return count;
}

console.log(increment()); // 1
console.log(increment()); // 2

// 근데 누구나 직접 수정 가능...
count = 999;
console.log(increment()); // 1000 💀

문제점:

  • count가 전역에 노출되어 있어서 어디서든 수정 가능
  • 다른 코드가 실수로 count를 바꿔버리면 버그 발생
  • 여러 개의 독립적인 카운터를 만들 수 없음

해결: 클로저로 데이터 은닉

function createCounter() {
  let count = 0; // 외부에서 직접 접근 불가!

  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    },
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1

// counter.count → undefined (직접 접근 불가!)
// count = 999 같은 실수가 불가능!

// 독립적인 카운터도 만들 수 있음
const counter2 = createCounter();
console.log(counter2.increment()); // 1 (counter와 별개)

count 변수는 createCounter 안에 갇혀 있고, 오직 increment, decrement, getCount를 통해서만 접근 가능함. 이걸 모듈 패턴이라고도 부름


4. 클로저 실전 활용

4-1. 함수 팩토리

function multiply(x) {
  return function (y) {
    return x * y; // x를 클로저로 기억
  };
}

const double = multiply(2);
const triple = multiply(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

multiply(2)를 호출하면 x = 2를 기억하는 클로저가 반환됨. 매번 새로운 함수를 만들 수 있어서 유용함

4-2. 디바운스 (실무에서 자주 씀)

검색 입력할 때마다 API를 호출하면 낭비니까, 타이핑을 멈춘 후 일정 시간 뒤에만 호출하는 패턴

function debounce(fn, delay) {
  let timer = null; // 클로저로 timer 유지

  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn(...args);
    }, delay);
  };
}

// 사용 예시
const searchAPI = debounce((query) => {
  console.log(`API 호출: ${query}`);
}, 300);

searchAPI('리'); // 취소됨
searchAPI('리액'); // 취소됨
searchAPI('리액트'); // 300ms 후 "API 호출: 리액트" 실행

timer 변수가 클로저 덕분에 함수 호출 사이에서도 유지됨. 이게 없으면 매번 timer가 초기화돼서 디바운스가 안 됨

4-3. React에서의 클로저

사실 React 개발하면서 이미 클로저를 쓰고 있었음

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // handleClick은 count를 클로저로 기억함
    setCount(count + 1);
  };

  useEffect(() => {
    // 이 콜백도 count를 클로저로 기억함
    console.log(`현재 카운트: ${count}`);
  }, [count]);

  return <button onClick={handleClick}>{count}</button>;
}

handleClickuseEffect의 콜백 함수가 count를 클로저로 기억하고 있음. React의 상태 관리가 클로저 위에서 동작하는 것!


5. 클로저 주의점

주의 1: var와 반복문의 함정 (면접 단골!)

// ❌ var 사용 — 의도대로 안 됨
for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}
// 출력: 3, 3, 3 (모두 3!)

var는 함수 스코프라서 i가 하나만 존재함. 루프가 끝난 시점의 i = 3을 세 함수가 공유하게 됨

// ✅ let 사용 — 정상 동작
for (let i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}
// 출력: 0, 1, 2

let은 블록 스코프라서 각 반복마다 새로운 i가 생성되고, 각 클로저가 고유한 값을 기억함

주의 2: 메모리 누수

function heavyClosure() {
  const bigData = new Array(1000000).fill('데이터');

  return function () {
    console.log(bigData.length);
  };
}

const fn = heavyClosure();
// fn이 존재하는 한 bigData(약 4MB)는 메모리에서 해제되지 않음!

// 더 이상 필요없으면 참조를 끊어야 함
// fn = null;

클로저는 외부 변수에 대한 참조를 유지하기 때문에, 큰 데이터를 잡고 있으면 메모리 낭비가 됨. 사용이 끝나면 null로 참조를 끊어주는 게 좋음


6. 면접 예상 질문 & 답변

Q1. 클로저란 무엇인가요?

클로저는 함수가 선언될 당시의 렉시컬 환경을 기억하여, 함수가 해당 스코프 밖에서 실행되더라도 외부 변수에 접근할 수 있는 현상입니다.

Q2. 클로저는 어디에 쓰이나요?

데이터 은닉(private 변수), 함수 팩토리, 커링, 디바운스/쓰로틀, React의 useState/useEffect 등에 활용됩니다.

Q3. 클로저의 단점은?

외부 변수에 대한 참조를 계속 유지하므로, 불필요한 클로저가 남아있으면 메모리 누수가 발생할 수 있습니다. 사용이 끝난 클로저는 참조를 null로 끊어주는 것이 좋습니다.

Q4. for문에서 var와 let이 클로저에서 다르게 동작하는 이유는?

var는 함수 스코프이므로 반복문 전체에서 하나의 변수를 공유하지만, let은 블록 스코프이므로 각 반복마다 새로운 변수가 생성되어 각 클로저가 고유한 값을 기억합니다.


결론

클로저는 JavaScript에서 가장 중요한 개념 중 하나

  • 렉시컬 스코프: 함수가 선언된 위치 기준으로 스코프 결정
  • 클로저: 함수 + 선언 당시의 렉시컬 환경을 기억하는 현상
  • 활용: 데이터 은닉, 함수 팩토리, 디바운스, React 상태 관리
  • 주의: var 반복문 함정, 메모리 누수
개념 설명
렉시컬 스코프 함수가 선언된 위치 기준으로 스코프 결정
클로저 함수 + 선언 당시의 렉시컬 환경
활용 데이터 은닉, 함수 팩토리, 디바운스, React
주의점 var 반복문 함정, 메모리 누수

React 프로젝트에서 useState, useEffect, 이벤트 핸들러를 쓸 때마다 이미 클로저를 쓰고 있었다는 걸 알게 됨
앞으로 디바운스나 데이터 은닉이 필요할 때 클로저 패턴을 적극적으로 활용해봐야겠음!

반응형