2강. 함수와 클로저 — JavaScript 함수는 왜 특별한가?

by gg582 · 2026-07-09 13:30:54 · 51 views

2강. 함수와 클로저 — JavaScript 함수는 왜 특별한가?


들어가기 전에

C나 C++에서 함수는 코드를 실행하기 위한 단위입니다.

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

함수는 호출되기 위해 존재하며, 일반적으로 변수처럼 자유롭게 다루지는 않습니다.

Python은 조금 다릅니다.

def add(a, b):
    return a + b

f = add

함수를 변수에 저장하거나 다른 함수에 전달할 수 있습니다.

JavaScript는 이러한 특징을 더욱 적극적으로 활용합니다.

JavaScript에서는 함수(Function) 역시 하나의 객체(Object) 입니다.

이 사실 하나만 이해해도 JavaScript의 절반 이상을 이해했다고 해도 과언이 아닙니다.


함수도 객체이다

다음 코드를 보겠습니다.

function hello() {
    console.log("Hello");
}

많은 사람은 이것을 단순한 함수 선언이라고 생각합니다.

그러나 엔진 내부에서는 함수 객체가 생성됩니다.

개념적으로는 다음과 같습니다.

Call Stack

hello
  │
  ▼

Heap

Function Object

즉,

변수와 마찬가지로

hello

라는 식별자가

Heap에 존재하는 함수 객체를 참조하고 있습니다.

따라서 함수 역시 객체처럼 다룰 수 있습니다.


함수를 변수에 저장할 수 있다

function hello() {
    console.log("Hello");
}

const f = hello;

f();

출력은

Hello

입니다.

여기서 복사된 것은 함수가 아니라 함수 객체를 가리키는 참조입니다.

메모리 구조는 다음과 같습니다.

Call Stack

hello  ───┐
             │
f ───────────┘
             │
             ▼

Heap

Function Object

새로운 함수가 만들어진 것이 아닙니다.

동일한 함수 객체를 두 개의 변수가 공유합니다.


함수를 인자로 전달할 수 있다

함수가 객체이므로

다른 함수의 인자로 전달할 수도 있습니다.

function greet() {
    console.log("Hello");
}

function execute(fn) {
    fn();
}

execute(greet);

실행 결과는

Hello

입니다.

여기서도 전달되는 것은 함수 코드가 아니라

함수 객체를 가리키는 참조입니다.

이러한 특성 덕분에 JavaScript에서는

  • 콜백(Callback)
  • 이벤트 처리
  • Promise
  • 비동기 API

등이 자연스럽게 구현됩니다.


함수를 반환할 수도 있다

객체이므로 반환도 가능합니다.

function createAdder(x) {

    return function(y) {

        return x + y;

    };

}

const add10 = createAdder(10);

console.log(add10(5));

출력은

15

입니다.

여기서 가장 중요한 질문이 생깁니다.

createAdder()는 이미 종료되었습니다.

그런데

x

가 아직 존재할까요?

이것이 바로 클로저**(Closure)** 입니다.


렉시컬 스코프 복습

지난 강의에서

JavaScript는

함수가 호출된 위치가 아니라 정의된 위치를 기준으로 스코프를 결정한다.

고 설명했습니다.

이를 Lexical Scope라고 합니다.

다음 예제를 보겠습니다.

let message = "Global";

function hello() {

    console.log(message);

}

hello();

출력은

Global

입니다.

함수가 정의될 당시

외부 환경을 기억하기 때문입니다.


함수는 외부 환경도 함께 저장한다

함수 객체는 단순히 코드만 저장하지 않습니다.

생성 당시의 Lexical Environment도 함께 저장합니다.

개념적으로는

Function Object

├── Code
└── [[Environment]]

와 같은 구조를 가진다고 생각하면 됩니다.

[[Environment]]

함수가 만들어질 당시의

외부 Lexical Environment를 가리킵니다.

이 정보는 함수가 호출될 때

Scope Chain을 구성하는 데 사용됩니다.


클로저(Closure)

다시 예제를 보겠습니다.

function createCounter() {

    let count = 0;

    return function() {

        count++;

        return count;

    };

}

사용해 보겠습니다.

const counter = createCounter();

console.log(counter());
console.log(counter());
console.log(counter());

출력은

1
2
3

입니다.

count

가 계속 살아 있을까요?

메모리를 살펴보겠습니다.

Call Stack

counter
    │
    ▼

Heap

Function Object

[[Environment]]

↓

Lexical Environment

count = 0

createCounter()는 종료되었습니다.

그러나 반환된 함수가

여전히

Lexical Environment를 참조하고 있습니다.

따라서 가비지 컬렉터는

이 환경을 제거할 수 없습니다.

이러한 현상을

Closure

라고 합니다.


클로저는 메모리를 보존한다

클로저는

함수가 외부 변수에 접근하기 위해

외부 실행 환경을 유지하는 메커니즘입니다.

따라서

function test() {

    let value = 10;

    return function() {

        return value;

    };

}

에서는

value

함수 종료 후에도 메모리에 남아 있습니다.

반대로

function test() {

    let value = 10;

}

처럼

아무도 참조하지 않는다면

함수 종료와 함께 제거됩니다.


클로저와 가비지 컬렉션

지난 강의에서

가비지 컬렉터는

참조되지 않는 객체를 회수한다고 설명했습니다.

클로저도 동일한 원리입니다.

Function Object

↓

Lexical Environment

↓

count

이 참조가 존재하는 동안에는

Lexical Environment 역시 살아 있습니다.

마지막 함수 객체까지 제거되면

비로소 가비지 컬렉터가

Lexical Environment도 함께 회수합니다.


클로저의 활용

클로저는 매우 자주 사용됩니다.

예를 들어

function createLogger(prefix) {

    return function(message) {

        console.log(prefix, message);

    };

}

사용하면

const info = createLogger("[INFO]");

info("Server started");

출력은

[INFO] Server started

입니다.

매번

prefix

를 전달할 필요가 없습니다.

클로저가 기억하고 있기 때문입니다.


private 변수 구현

ES2022 이전에는

클로저를 이용하여

private 상태를 만드는 경우가 많았습니다.

function BankAccount() {

    let balance = 0;

    return {

        deposit(amount) {

            balance += amount;

        },

        getBalance() {

            return balance;

        }

    };

}

사용해 보면

const account = BankAccount();

account.deposit(100);

console.log(account.getBalance());

출력은

100

입니다.

그러나

console.log(account.balance);

undefined

입니다.

외부에서는

balance

에 직접 접근할 수 없습니다.

클로저만 접근할 수 있기 때문입니다.


클로저를 사용할 때 주의할 점

클로저는 편리하지만

불필요하게 큰 객체를 참조하면

메모리가 오래 유지될 수 있습니다.

예를 들어

function createHandler() {

    const hugeObject = ...

    return function() {

        console.log(hugeObject);

    };

}

반환된 함수가 살아 있는 동안

hugeObject

도 계속 메모리에 남아 있습니다.

이러한 참조는

메모리 사용량 증가의 원인이 될 수 있습니다.

따라서 클로저는

필요한 데이터만 캡처하도록 작성하는 것이 좋습니다.


이번 강의 요약

JavaScript에서 함수는 객체입니다.

따라서 변수에 저장하거나, 인자로 전달하거나, 반환값으로 사용할 수 있습니다.

함수 객체는 실행 코드뿐 아니라 자신이 생성될 당시의 Lexical Environment도 함께 기억합니다.

이 덕분에 함수가 종료된 이후에도 외부 변수에 접근할 수 있으며, 이러한 메커니즘을 클로저(Closure)라고 합니다.

클로저는 상태 유지, 정보 은닉, 콜백, 이벤트 처리 등 JavaScript의 핵심 기능을 구현하는 기반이 됩니다.


다음 강의

다음 강의에서는 JavaScript의 비동기 실행 모델을 살펴봅니다.

  • JavaScript는 왜 싱글 스레드인가
  • Call Stack과 Web API
  • Callback Queue
  • Microtask Queue
  • Event Loop
  • Promise와 async / await의 내부 동작

이 강의를 이해하면 JavaScript의 비동기 코드가 어떤 순서로 실행되는지 정확하게 예측할 수 있게 됩니다.

Back

Comments

No comments yet.