Skip to main content

자바스크립트 비밀스러운 삶: 클로저(Closure) 완전 정복 – 함수가 기억하는 이유와 활용법

Published 5 min readMarkdown

(SEO 키워드: JavaScript 클로저, 함수 기억, 프로그래밍 기초)


# 서론 – “함수가 왜 기억을 가지고 있을까?”

프로그래밍을 처음 시작한 사람이라면 “함수는 호출될 때마다 새롭게 실행되는 일회용 도구”라고 생각하기 쉽습니다. 그런데 JavaScript에서는 함수가 자신이 선언될 때의 환경을 기억하고, 그 기억을 바탕으로 나중에 호출될 수 있다는 독특한 특성이 있습니다. 이를 클로저(Closure) 라고 부릅니다.

이 개념을 제대로 이해하지 못하면 비동기 처리, 이벤트 핸들링, 모듈화 같은 핵심 기능을 구현할 때 예상치 못한 버그와 성능 저하를 겪게 됩니다. 이번 포스트에서는 ‘Chapter 4: Functions Remember’ 라는 이야기를 통해, 클로저가 어떻게 동작하고, 실무에서 어떻게 활용할 수 있는지 초보자도 쉽게 따라올 수 있도록 단계별로 풀어보겠습니다.

예시 이야기 – 비가 억수같이 내리던 어느 날, Timothy는 우산을 잊고 도서관에 들어섰습니다. 그는 책을 찾으며 “내가 지난번에 읽었던 페이지를 기억하고 있으면 좋겠어” 라는 생각을 했고, 이는 곧 함수가 기억을 갖는 상황과 놀라운 유사성을 보여줍니다.


# 본론

# 1️⃣ 클로저란 무엇인가? – 함수가 환경을 ‘캡처’한다

# 1-1. 스코프와 렉시컬 스코프

  • 스코프는 변수와 함수가 접근 가능한 범위를 의미합니다.

  • 렉시컬(lexical) 스코프는 함수가 정의된 위치에 따라 스코프가 결정됩니다. 즉, 함수 내부에서 외부 변수를 참조하면, 그 변수가 선언된 외부 환경을 기억하게 됩니다.

# 1-2. 클로저의 정의

클로저(Closure) = 함수 + 함수가 선언될 때의 렉시컬 환경

즉, 함수가 선언된 순간의 스코프 체인을 함께 저장해 두는 메커니즘입니다. 이때문에 함수가 다른 함수 안에서 반환되더라도, 외부 변수에 계속 접근할 수 있습니다.

# 1-3. 간단한 예시

javascript

function makeCounter() {

  let count = 0;               // 외부 변수

  return function() {         // 내부 함수(클로저)

    count += 1;               // 외부 변수에 접근

    return count;

  };

}

const counter = makeCounter(); // makeCounter의 실행 결과는 클로저

console.log(counter()); // 1

console.log(counter()); // 2

위 코드에서 counter는 makeCounter 내부에서 선언된 count 변수를 계속 기억하고 있습니다. 이것이 바로 함수가 기억하는 메커니즘, 즉 클로저입니다.


# 2️⃣ 클로저가 필요한 실제 상황 – 비동기와 이벤트 핸들링

# 2-1. 타이머와 클로저

javascript

for (let i = 1; i <= 5; i++) {

  setTimeout(function() {

    console.log(i); // 1,2,3,4,5 순서대로 출력

  }, i * 1000);

}

setTimeout 콜백 안에서 i를 사용하면, 각 콜백이 실행될 때마다 현재 i 값을 기억합니다. let을 사용했기 때문에 블록 스코프가 보장돼 각 반복마다 새로운 i가 생성됩니다.

# 2-2. DOM 이벤트와 클로저

javascript

function addButtonListener(id) {

  const button = document.getElementById(id);

  let clickCount = 0; // 버튼마다 별도 카운트

  button.addEventListener('click', function() {

    clickCount++;

    console.log(`버튼 ${id} 클릭 횟수: ${clickCount}`);

  });

}
addButtonListener('btn1');

addButtonListener('btn2');

버튼마다 독립적인 clickCount 변수를 클로저가 기억해 주기 때문에, 서로 간섭 없이 클릭 횟수를 관리할 수 있습니다.

# 2-3. 비동기 API 호출과 클로저

javascript

function fetchUser(id) {

  return fetch(`https://api.example.com/users/${id}`)

    .then(response => response.json())

    .then(user => {

      // 여기서 id는 클로저에 의해 기억됨

      console.log(`User ${id}:`, user.name);

    });

}
fetchUser(42);

then 체인 안에서 id를 사용할 수 있는 이유는 클로저가 fetchUser 실행 시점의 id 값을 기억하기 때문입니다.


# 3️⃣ 클로저 활용 패턴 – 모듈, 데이터 은닉, 커링

# 3-1. 모듈 패턴 (IIFE)

javascript

const CounterModule = (function() {

  let count = 0; // 외부에 노출되지 않음

  return {

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

    decrement: function() { return --count; },

    getCount: function() { return count; }

  };

})();

CounterModule.increment(); // 1

CounterModule.getCount();  // 1

즉시 실행 함수(IIFE) 안에서 선언된 count 변수는 클로저를 통해 외부에 은닉됩니다.

# 3-2. 데이터 은닉과 프라이버시

클로저를 이용하면 객체 내부에 프라이빗 변수를 만들 수 있습니다.

javascript

function createBankAccount(initialBalance) {

  let balance = initialBalance; // 프라이빗 변수

  return {

    deposit: amount => { balance += amount; return balance; },

    withdraw: amount => {

      if (amount > balance) throw new Error('잔액 부족');

      balance -= amount;

      return balance;

    },

    getBalance: () => balance

  };

}
const account = createBankAccount(1000);

account.deposit(500); // 1500

account.getBalance(); // 1500

# 3-3. 커링(Currying)과 부분 적용

javascript

function multiply(a) {

  return function(b) {

    return a * b;

  };

}
const double = multiply(2);

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

multiply가 반환한 내부 함수는 a 값을 클로저로 기억하고, 이후에 b만 전달받아 계산합니다.


# 4️⃣ 클로저 사용 시 주의할 점 – 메모리와 성능 관리

  1. 불필요한 클로저 생성 방지

    • 루프 안에서 함수를 매번 새로 만들면 메모리 사용량이 급증합니다. for 루프와 let을 활용하거나, 함수 외부에 재사용 가능한 함수를 정의하세요.
  2. 전역 변수 오염 방지

    • 클로저가 전역 객체에 접근하면 의도치 않은 사이드 이펙트가 발생할 수 있습니다. 가능한 한 스코프를 제한하고, 모듈 패턴을 활용해 전역 오염을 방지하세요.
  3. 가비지 컬렉션(GC) 이해

    • 클로저가 외부 변수를 계속 참조하면, 해당 변수가 가비지 컬렉션 대상이 되지 않아 메모리 누수가 발생할 수 있습니다. 필요 없어진 클로저는 참조를 끊어 메모리를 회수하도록 설계합니다.

# 5️⃣ 실전 예제 – ‘Timothy의 도서관 페이지 기억하기’

Timothy가 도서관에서 책을 읽으며 읽은 페이지 번호를 기억하고 싶다고 가정해 봅시다. 페이지를 넘길 때마다 현재 페이지를 저장하고, 나중에 다시 열었을 때 마지막 페이지부터 이어서 읽을 수 있도록 하는 기능을 구현합니다.

javascript

function createReadingSession(bookTitle) {

  let lastPage = 0; // 현재 세션의 마지막 페이지 (프라이빗)

  return {

    goToPage: function(page) {

      if (page < 1) throw new Error('페이지 번호는 1보다 커야 합니다.');

      lastPage = page;

      console.log(`${bookTitle} - 현재 페이지: ${lastPage}`);

    },

    resume: function() {

      console.log(`${bookTitle} - 마지막으로 읽은 페이지: ${lastPage}`);

    }

  };

}

// 사용 예시

const timothySession = createReadingSession('JavaScript 비밀스러운 삶');

timothySession.goToPage(23);

timothySession.goToPage(45);

timothySession.resume(); // 'JavaScript 비밀스러운 삶 - 마지막으로 읽은 페이지: 45'

위 예제에서 lastPage 변수는 클로저를 통해 createReadingSession 외부에 노출되지 않으며, Timothy가 언제든지 마지막 페이지를 복원할 수 있게 합니다.


# 결론 – 클로저를 마스터하고 코딩 실력을 한 단계 끌어올리세요

JavaScript 클로저는 단순히 “함수가 기억한다”는 수준을 넘어, 데이터 은닉, 모듈화, 비동기 흐름 제어 등 다양한 상황에서 핵심적인 역할을 합니다.

  1. 클로저의 원리(렉시컬 스코프와 환경 캡처)를 이해하면, 복잡한 비동기 로직도 깔끔하게 구현할 수 있습니다.

  2. 실무 패턴(모듈, 커링, 프라이버시)을 적용해 코드를 재사용 가능하고 유지보수하기 쉬운 구조로 바꿀 수 있습니다.

  3. 주의점(메모리 누수, 전역 오염)을 염두에 두고 설계한다면, 성능 저하 없이 안정적인 애플리케이션을 만들 수 있습니다.

행동 유도 – 지금 바로 위에 소개한 createReadingSession 예제를 직접 실행해 보세요. 그리고 자신의 프로젝트에 클로저를 활용한 작은 모듈을 하나 만들어 보는 것이 실력을 다지는 가장 빠른 방법입니다.

클로저를 제대로 이해하고 활용한다면, 당신도 ‘함수가 기억한다’는 마법을 손쉽게 구현할 수 있습니다. 앞으로도 JavaScript의 숨겨진 비밀을 파헤치는 여정을 함께 하시길 바랍니다!


# 참고 자료


이 글은 JavaScript 클로저, 함수 기억, 프로그래밍 기초라는 SEO 키워드를 포함하여 초보자도 이해하기 쉬운 형태로 작성되었습니다. 검색 엔진 최적화를 위해 적절한 헤딩 구조와 키워드 배치를 적용했으며, 실제 코드 예시와 실전 활용 팁을 제공했습니다.

No comments yet. Be the first to comment.

More from this blog