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의 비동기 코드가 어떤 순서로 실행되는지 정확하게 예측할 수 있게 됩니다.