조아마시

쓸모 있는 상세페이지 만들기

웹개발/javascript

[자바스크립트] 클로저(Closure) 이해하기

joamashi 2024. 7. 30. 00:35

클로저는 마치 함수를 특별한 상자에 담아 놓은 것 같다고 생각하면 됩니다. 상자 안에는 함수뿐만 아니라, 함수가 생성되었을 때의 변수값까지 함께 담겨 있습니다. 덕분에 상자 안에 담긴 함수는 언제 어디서 호출되더라도 생성 당시의 변수값을 사용할 수 있는 특별한 능력을 가지게 됩니다.

좀 더 자세히 설명하자면, 클로저는 다음과 같은 특징을 가지고 있습니다.

  • 함수와 렉시컬 환경의 조합: 클로저는 단순한 함수가 아닌, 함수가 선언되었을 때의 변수값까지 포함하고 있습니다. 이 변수값들을 렉시컬 환경이라고 합니다.
  • 외부 변수에 대한 접근: 클로저 안의 함수는 외부 함수의 렉시컬 환경에 있는 변수에 접근할 수 있습니다. 쉽게 말해, 상자 안에 담긴 함수는 상자 밖에 있는 변수들을 사용할 수 있다는 뜻입니다.
  • 지속적인 유지: 클로저 안의 함수는 호출이 끝나더라도 사라지지 않고 계속 유지됩니다. 렉시컬 환경에 담긴 변수값들이 사라지지 않는 한, 클로저 안의 함수는 언제든지 다시 호출될 수 있습니다.

예제:

function counter(start) {
  let count = start;
  return function() {
    count++;
    return count;
  }
}

const counter1 = counter(0);
const counter2 = counter(10);

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 11
console.log(counter2()); // 12

위 예시에서 counter 함수는 start라는 변수를 매개변수로 받고, start 변수값을 기억하는 count 변수를 선언합니다. 그리고 count 변수를 증가시키고 반환하는 내부 함수를 생성하여 반환합니다.

이렇게 생성된 내부 함수는 클로저라고 불립니다. counter1counter2는 각각 counter 함수를 호출하여 생성된 클로저입니다. counter1start 변수값으로 0을 받았고, counter2는 10을 받았습니다.

따라서 counter1을 호출하면 count 변수값을 1씩 증가시키고 반환하며, counter2를 호출하면 count 변수값을 10부터 1씩 증가시키고 반환합니다.

클로저의 활용

클로저는 웹 개발에서 다양한 기능을 구현하는 데 활용됩니다. 예를 들어, 다음과 같은 기능들을 구현할 수 있습니다.

  • 프라이빗 변수: 클로저를 사용하면 함수 내부에 프라이빗 변수를 만들 수 있습니다. 외부에서 변수값을 변경하거나 참조하지 못하도록 보호할 수 있습니다.
  • 카운터: 클로저를 사용하면 반복적으로 증가하는 카운터를 만들 수 있습니다. 예를 들어, 버튼 클릭마다 숫자가 1씩 증가하는 카운터 버튼을 만들 수 있습니다.
  • 상태 유지: 클로저를 사용하면 함수가 호출될 때마다 이전 호출의 상태를 유지할 수 있습니다. 예를 들어, 게임 점수를 관리하는 함수를 만들 수 있습니다.
  • 콜백 함수: 클로저를 사용하면 비동기 작업 후에 처리할 콜백 함수를 만들 수 있습니다. 예를 들어, Ajax 요청 후 결과를 처리하는 콜백 함수를 만들 수 있습니다.

1. 프라이빗 변수 만들기

클로저를 사용하면 함수 내부에 프라이빗 변수를 만들 수 있습니다. 외부에서 변수값을 변경하거나 참조하지 못하도록 보호할 수 있는 장점이 있습니다.

function makeCounter(initialValue) {
  let count = initialValue;
  return function() {
    count++;
    return count;
  }
}

const counter1 = makeCounter(0);
const counter2 = makeCounter(10);

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 11
console.log(counter2()); // 12

위 예제에서 makeCounter 함수는 initialValue 매개변수를 받아 count라는 프라이빗 변수를 선언하고, count 변수를 증가시키고 반환하는 내부 함수를 생성하여 반환합니다.

counter1counter2는 각각 makeCounter 함수를 호출하여 생성된 클로저입니다. counter1initialValue로 0을 받았고, counter2는 10을 받았습니다.

따라서 counter1을 호출하면 count 변수값을 1씩 증가시키고 반환하며, counter2를 호출하면 count 변수값을 10부터 1씩 증가시키고 반환합니다.

2. 반복 카운터 만들기

클로저를 사용하면 반복적으로 증가하는 카운터를 만들 수 있습니다. 예를 들어, 버튼 클릭마다 숫자가 1씩 증가하는 카운터 버튼을 만들 수 있습니다.

function makeCounter() {
  let count = 0;
  const button = document.createElement('button');
  button.textContent = '클릭';
  button.addEventListener('click', () => {
    count++;
    console.log(count);
  });
  return button;
}

const counterButton = makeCounter();
document.body.appendChild(counterButton);

위 예제에서 makeCounter 함수는 count 변수를 초기화하고, 버튼을 생성하고, 버튼 클릭 이벤트에 리스너를 등록합니다. 이 리스너는 클릭될 때마다 count 변수값을 1씩 증가시키고 콘솔에 출력합니다.

counterButtonmakeCounter 함수를 호출하여 생성된 클로저입니다. 이 클로저는 버튼 요소와 count 변수를 포함하고 있습니다.

따라서 counterButton을 클릭하면 count 변수값이 1씩 증가하고 콘솔에 출력됩니다.

3. 상태 유지

클로저를 사용하면 함수가 호출될 때마다 이전 호출의 상태를 유지할 수 있습니다. 예를 들어, 게임 점수를 관리하는 함수를 만들 수 있습니다.

JavaScript
function makeGame(initialScore) {
  let score = initialScore;
  return {
    getScore: () => score,
    addPoint: () => score++,
    resetScore: () => score = initialScore
  }
}

const game = makeGame(0);

console.log(game.getScore()); // 0
game.addPoint();
console.log(game.getScore()); // 1
game.resetScore();
console.log(game.getScore()); // 0

4. 콜백 함수 처리

클로저를 사용하면 비동기 작업 후에 처리할 콜백 함수를 쉽게 만들 수 있습니다. 예를 들어, Ajax 요청 후 결과를 처리하는 콜백 함수를 만들 수 있습니다.

function loadContent(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.onload = function() {
    if (xhr.status === 200) {
      callback(xhr.responseText);
    } else {
      console.error('콘텐츠 로드 실패');
    }
  };
  xhr.send();
}

loadContent('https://example.com/data.json', function(data) {
  console.log(JSON.parse(data));
});

위 예제에서 loadContent 함수는 URL을 매개변수로 받아 Ajax 요청을 수행하고, 요청이 성공하면 콜백 함수를 호출합니다. 콜백 함수는 응답 데이터를 JSON 형식으로 파싱하여 콘솔에 출력합니다.

5. 데코레이터 패턴 구현

클로저를 사용하면 데코레이터 패턴을 쉽게 구현할 수 있습니다. 데코레이터 패턴은 기존 객체에 새로운 기능을 추가하거나 변경하는 데 사용되는 패턴입니다.

function decorate(target, decorator) {
  const decorated = {};
  for (const property in target) {
    if (typeof target[property] === 'function') {
      decorated[property] = function (...args) {
        return decorator(target[property].bind(target), ...args);
      };
    } else {
      decorated[property] = target[property];
    }
  }
  return decorated;
}

function log(fn) {
  return function (...args) {
    console.log(`함수 ${fn.name} 호출`);
    const result = fn(...args);
    console.log(`함수 ${fn.name} 결과: ${result}`);
    return result;
  };
}

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

const decoratedAdd = decorate(add, log);
console.log(decoratedAdd(1, 2)); // 함수 add 호출, 함수 add 결과: 3

위 예제에서 decorate 함수는 객체를 첫 번째 인수로, 데코레이터 함수를 두 번째 인수로 받아 데코레이션된 객체를 반환합니다. 데코레이터 함수는 원본 함수를 감싸서 새로운 기능을 추가하거나 변경합니다.

log 함수는 호출된 함수의 이름과 결과를 콘솔에 출력하는 데코레이터 함수입니다. add 함수는 두 수를 더하는 함수입니다.

decoratedAdd는 decorate 함수를 사용하여 add 함수를 log 함수로 데코레이션한 결과입니다. 따라서 decoratedAdd 함수를 호출하면 add 함수의 실행 전후에 로그 메시지가 출력됩니다.

더 깊은 이해하기

1. 클로저와 렉시컬 스코프

클로저의 핵심은 렉시컬 스코프(Lexical Scope)와 밀접하게 연결되어 있습니다. 렉시컬 스코프는 함수가 선언된 위치에서 참조할 수 있는 변수의 범위를 정의하는 개념입니다.

쉽게 말해, 클로저는 함수가 생성되었을 때의 변수값을 캡처하여, 함수가 호출되는 위치와 상관없이 그 변수값을 사용할 수 있도록 합니다. 이는 렉시컬 스코프 덕분에 가능합니다.

예를 들어, 다음 코드를 살펴보겠습니다.

function counter(start) {
  let count = start;
  return function() {
    count++;
    return count;
  }
}

const counter1 = counter(0);
const counter2 = counter(10);

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 11
console.log(counter2()); // 12

위 코드에서 counter 함수는 start 매개변수를 받아 count 변수를 선언하고, count 변수를 증가시키고 반환하는 내부 함수를 생성하여 반환합니다.

counter1counter2는 각각 counter 함수를 호출하여 생성된 클로저입니다. counter1start 매개변수로 0을 받았고, counter2는 10을 받았습니다.

따라서 counter1을 호출하면 count 변수값을 1씩 증가시키고 반환하며, counter2를 호출하면 count 변수값을 10부터 1씩 증가시키고 반환합니다.

이처럼 클로저는 렉시컬 스코프 덕분에 함수가 생성되었을 때의 변수값을 유지하고, 언제 어디서 호출되더라도 그 변수값을 사용할 수 있는 능력을 가지게 됩니다.

2. 클로저와 재귀 함수

클로저는 재귀 함수와 함께 사용될 때 더욱 강력한 기능을 발휘합니다. 재귀 함수는 자기 자신을 호출하는 함수를 의미하며, 클로저와 결합하면 자신을 호출할 때마다 이전 호출의 정보를 유지할 수 있습니다.

예를 들어, 다음 코드는 재귀 함수와 클로저를 사용하여 팩토리얼 계산 함수를 구현한 것입니다.

function factorial(n) {
  if (n === 0) {
    return 1;
  } else {
    return function(accumulator) {
      return factorial(n - 1)(accumulator * n);
    };
  }
}

const fact = factorial(5);
console.log(fact(1)); // 120

위 코드에서 factorial 함수는 재귀적으로 자신을 호출하며, 팩토리얼 값을 계산합니다. n이 0일 때는 1을 반환하고, 그렇지 않을 때는 n - 1의 팩토리얼 값을 계산하여 n을 곱한 결과를 반환합니다.

클로저를 사용하면 재귀 함수를 호출할 때마다 이전 호출의 정보를 유지할 수 있습니다. 위 코드에서 fact 변수는 factorial(5)의 결과를 저장합니다.

따라서 fact(1)을 호출하면 factorial(4)를 호출하고, factorial(4)factorial(3)를 호출하며, 이 과정을 거쳐 팩토리얼 값을 계산합니다.

3. 클로저와 콜백 함수

클로저는 비동기 프로그래밍에서 콜백 함수를 처리하는 데 유용하게 활용됩니다. 콜백 함수는 비동기 작업이 완료된 후 호출되는 함수를 의미합니다.

예를 들어, 다음 코드는 Ajax 요청을 사용하여 데이터를 로드하고, 로드가 완료된 후 콜백 함수를 호출하는 방법을 보여줍니다.

function loadContent(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.onload = function() {
    if (xhr.status === 200) {
      callback(xhr.responseText);
    } else {
      console.error('콘텐츠 로드 실패');
    }
  };
  xhr.send();
}

loadContent('https://example.com/data.json', function(data) {
  console.log(JSON.parse(data));
});

위 코드에서 loadContent 함수는 URL을 매개변수로 받아 Ajax 요청을 수행하고, 요청이 성공하면 콜백 함수를 호출합니다. 콜백 함수는 응답 데이터를 JSON 형식으로 파싱하여 콘솔에 출력합니다.

클로저를 사용하면 콜백 함수를 더욱 유연하게 처리할 수 있습니다. 예를 들어, 다음 코드는 클로저를 사용하여 콜백 함수를 여러 번 호출하는 방법을 보여줍니다.

function createCounter(initialValue) {
  let count = initialValue;
  return {
    increment: function() {
      count++;
      return count;
    },
    get: function() {
      return count;
    }
  };
}

const counter = createCounter(0);

const logger = function() {
  console.log(counter.get());
};

counter.increment();
counter.increment();
logger(); // 2를 출력
counter.increment();
logger(); // 3을 출력

위 코드에서 createCounter 함수는 initialValue 매개변수를 받아 count 변수를 초기화하고, increment 함수와 get 함수를 포함하는 객체를 반환합니다.

counter 변수는 createCounter(0)의 결과를 저장합니다. increment 함수는 count 변수값을 1씩 증가시키고 반환하며, get 함수는 count 변수값을 반환합니다.

logger 함수는 counter.get()을 호출하여 count 변수값을 출력합니다.

이처럼 클로저를 사용하면 콜백 함수를 여러 번 호출하거나, 콜백 함수 내에서 클로저 변수에 접근하는 등 다양한 기능을 구현할 수 있습니다.

4. 클로저와 모듈 시스템

클로저는 모듈 시스템에서 모듈 간의 상호 작용을 관리하는 데에도 활용될 수 있습니다. 모듈 시스템은 코드를 여러 모듈로 나누어 관리하고, 모듈 간에 필요한 기능을 제공하는 시스템입니다.

예를 들어, 다음 코드는 간단한 모듈 시스템을 구현하는 방법을 보여줍니다.

// 모듈 1: math.js
function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

// 모듈 2: app.js
import { add, multiply } from './math.js';

const result1 = add(1, 2);
console.log(result1); // 3

const result2 = multiply(3, 4);
console.log(result2); // 12

위 코드에서 math.js 모듈은 add 함수와 multiply 함수를 제공합니다. app.js 모듈은 math.js 모듈에서 add 함수와 multiply 함수를 가져와 사용합니다.

클로저를 사용하면 모듈 내부의 변수와 함수를 캡슐화하여 모듈 간의 상호 작용을 안전하게 관리할 수 있습니다. 또한, 모듈 간의 의존성을 명확하게 드러낼 수 있어 코드 유지 관리를 용이하게 합니다.

728x90