지난 글에서는 JavaScript 프로그램이
디스크 → 메모리 → CPU 순서로 실행된다는 흐름을 정리했다.
이번 글에서는 그중에서도
메모리(RAM)가 실제로 어떻게 사용되는지를 조금 더 깊게 살펴보겠다.
JavaScript 개발을 하다 보면 이런 상황을 자주 마주친다 :(
이 문제들의 중심에는 항상 메모리 구조가 있다.
JavaScript 실행 시 모든 데이터와 함수 호출은 정해진 메모리 영역에 저장되며,
그 구조를 이해하지 못하면 실행 흐름을 따라가기 어렵다.
📌 핵심: 메모리 구조를 이해하면 "왜 내 코드가 이렇게 동작하는가?" 에 대한 답을 찾을 수 있다.
JavaScript 실행 환경에서 메모리는 크게 두 영역으로 나뉜다.
🧠 메모리(RAM)
├── Call Stack (콜 스택)
│ └── 함수 호출 순서와 실행 정보 관리
│
└── Heap (힙)
└── 객체, 배열 등 실제 데이터 저장
이 두 영역은 역할도 다르고, 사용되는 방식도 완전히 다르다.
| 영역 | 역할 | 특징 | 비유 |
|---|---|---|---|
| Call Stack | 함수 호출 관리 | 후입선출(LIFO) 구조 | 책을 쌓는 더미 |
| Heap | 데이터 저장 | 자유로운 할당/해제 | 창고 공간 |
💬 Call Stack(콜 스택) 이란?
함수 호출 순서를 관리하는 메모리 공간이다.
마치 책을 차곡차곡 쌓는 것처럼, 함수가 호출될 때마다 위로 쌓이고, 실행이 끝나면 위에서부터 제거된다.
Call Stack은 함수 호출 순서를 관리하는 공간이다.
JavaScript에서 함수가 호출되면 그 함수의 실행 정보가 Call Stack에 쌓인다.
그리고 함수 실행이 끝나면 가장 마지막에 들어온 함수부터 스택에서 제거된다.
💬 LIFO(후입선출) 란?
Last In, First Out의 약자로, "마지막에 들어간 것이 먼저 나온다"는 의미다.
접시를 쌓아두고 사용할 때, 가장 위에 있는 접시를 먼저 꺼내는 것과 같은 원리다.
이 구조를 후입선출(LIFO) 이라고 한다.
즉,
📚 Call Stack 구조 (LIFO)
[validate] ← 3번째 호출, 맨 위 (가장 먼저 제거됨)
[getUser] ← 2번째 호출
[main] ← 1번째 호출, 맨 아래 (가장 나중에 제거됨)
이 때문에 "지금 어떤 함수가 실행 중인지"는 Call Stack의 맨 위를 보면 알 수 있다.
다음과 같은 코드를 실행한다고 가정해보자:
function validate(name) {
if (!name) throw new Error("이름이 필요합니다");
return true;
}
function getUser(name) {
validate(name);
return { name, age: 30 };
}
function main() {
const user = getUser("Kim");
console.log(user);
}
main();
이때 Call Stack에는 다음 순서로 정보가 쌓인다:
1️⃣ 프로그램 시작
Call Stack: [전역]
2️⃣ main() 호출
Call Stack:
[main] ← 현재 실행 중
[전역]
3️⃣ main 안에서 getUser("Kim") 호출
Call Stack:
[getUser] ← 현재 실행 중
[main]
[전역]
4️⃣ getUser 안에서 validate(name) 호출
Call Stack:
[validate] ← 현재 실행 중 ⭐
[getUser]
[main]
[전역]
5️⃣ validate 실행 완료 → Stack에서 제거
Call Stack:
[getUser] ← 다시 실행 재개
[main]
[전역]
6️⃣ getUser 실행 완료 → Stack에서 제거
Call Stack:
[main] ← 다시 실행 재개
[전역]
7️⃣ main 실행 완료 → Stack에서 제거
Call Stack:
[전역]
📌 핵심 원리: validate 함수의 실행이 끝나면 Call Stack의 맨 위에서 제거되고, 실행 흐름은 다시 getUser 함수로 돌아간다.
함수가 실행될 때 다음과 같은 정보들이 Call Stack에 저장된다:
| 저장 정보 | 설명 | 예시 |
|---|---|---|
| 함수 실행 위치 | 코드의 어느 줄을 실행 중인지 | getUser 함수의 2번째 줄 |
| 매개변수 값 | 함수에 전달된 인자 | name: "Kim" |
| 지역 변수 | 함수 내부에서 선언된 변수 | const user = {...} |
| 반환 주소 | 함수 실행 후 돌아갈 위치 | main 함수의 3번째 줄 |
💬 Stack Overflow 란?
Call Stack의 크기에는 제한이 있다. 함수 호출이 너무 깊게 중첩되거나 재귀 호출이 끝나지 않으면, Stack이 허용된 크기를 초과하여 발생하는 오류다.
그래서 함수가 깊게 중첩되거나 재귀 호출이 과도해지면 Call Stack이 넘칠 수 있다.
// ❌ 잘못된 재귀 함수 (종료 조건 없음)
function infiniteRecursion() {
infiniteRecursion(); // 자기 자신을 계속 호출
}
infiniteRecursion();
// ⚠️ RangeError: Maximum call stack size exceeded
Call Stack 상태 (Stack Overflow 발생 직전)
[infiniteRecursion] ← 10000번째 호출
[infiniteRecursion]
[infiniteRecursion]
[...] ← 계속 쌓임
[infiniteRecursion] ← 1번째 호출
[전역]
💥 Stack Overflow!
이때 발생하는 대표적인 오류가 Stack Overflow이다.

💬 Heap(힙) 이란?
객체, 배열처럼 크기가 가변적이고 구조가 복잡한 데이터를 저장하는 메모리 공간이다.
Call Stack처럼 정해진 순서 없이, 필요할 때마다 빈 공간에 자유롭게 할당된다.
Heap은 객체와 데이터가 저장되는 공간이다.
JavaScript에서 생성되는:
{ name: "Kim" })[1, 2, 3])function() {})들은 모두 Heap 영역에 저장된다.
Heap은 Call Stack처럼 정해진 순서로 쌓이지 않고,
필요할 때마다 빈 공간에 데이터를 할당하는 방식으로 사용된다.
📦 Heap 메모리 구조
┌─────────────────────────────────┐
│ 빈 공간 │
├─────────────────────────────────┤
│ { name: "Kim" } ← 0x1234 │
├─────────────────────────────────┤
│ 빈 공간 │
├─────────────────────────────────┤
│ [1, 2, 3] ← 0x5678 │
├─────────────────────────────────┤
│ 빈 공간 │
└─────────────────────────────────┘

실제 개발 상황에서 Heap에 저장되는 데이터를 살펴보자:
// 1️⃣ 사용자 정보 객체
const user = {
name: "Kim",
age: 30,
email: "kim@example.com"
};
// 2️⃣ 게시글 목록 배열
const posts = [
{ id: 1, title: "첫 번째 글" },
{ id: 2, title: "두 번째 글" },
{ id: 3, title: "세 번째 글" }
];
// 3️⃣ 서버에서 받아온 JSON 응답 데이터
const response = {
status: "success",
data: {
users: [...],
pagination: { page: 1, total: 100 }
}
};
이런 데이터들은 실행 중에 크기나 내용이 계속 바뀔 수 있다.
예를 들어:
user 객체의 내용이 변경됨posts 배열의 크기가 늘어남response 객체가 새로 생성됨이런 가변적인 데이터들은 Call Stack이 아닌 Heap에 저장된다.
그렇다면 Call Stack과 Heap은 어떻게 연결되어 있을까?
💬 참조(Reference) 란?
실제 데이터의 위치를 가리키는 주소 정보다.
마치 "3층 302호"라는 주소가 실제 방을 가리키는 것처럼, 참조는 Heap에 있는 데이터의 위치를 가리킨다.
Call Stack에는 실제 객체 데이터가 저장되지 않는다.
대신:
에 저장된다.
function getUser() {
const user = { name: "Kim", age: 30 };
return user;
}
const result = getUser();
이 코드가 실행될 때 메모리 상태:
📚 Call Stack 📦 Heap
┌─────────────────┐ ┌──────────────────────┐
│ getUser() │ │ │
│ user → 0x1234 │────────────→│ 0x1234: { │
└─────────────────┘ │ name: "Kim", │
│ age: 30 │
┌─────────────────┐ │ } │
│ 전역 │ │ │
│ result → 0x1234 │────────────→│ (같은 객체 참조) │
└─────────────────┘ └──────────────────────┘
📌 핵심: Call Stack의
user변수와result변수는 둘 다 같은 Heap 메모리 주소(0x1234)를 참조한다.
즉:
이 구조 덕분에 JavaScript는 여러 함수가 같은 객체를 공유하면서도 유연하게 동작할 수 있다.
💬 Garbage Collection(가비지 컬렉션) 이란?
더 이상 사용되지 않는 메모리를 자동으로 해제하는 JavaScript 엔진의 기능이다.
마치 청소부가 쓰레기를 치우듯, 참조되지 않는 객체를 Heap에서 제거한다.
Heap에 저장된 데이터는 더 이상 참조되지 않으면 Garbage Collection의 대상이 된다.
function processUser() {
const temp = { name: "Kim" }; // Heap에 객체 생성
console.log(temp.name);
// 함수 종료 → temp 참조 사라짐
} // ✅ Garbage Collection 대상이 됨
processUser();
실행 전:
📦 Heap: { name: "Kim" } ← temp가 참조 중
실행 후:
📦 Heap: (비어있음) ← 🗑️ GC가 자동 제거
💬 메모리 누수(Memory Leak) 란?
더 이상 사용하지 않는 데이터가 메모리에 계속 남아있어, 사용 가능한 메모리가 점점 줄어드는 현상이다.
// ❌ 전역 변수에 저장 → 계속 참조됨
const userCache = {};
function loadUser(id) {
const user = fetchUserFromDB(id);
userCache[id] = user; // 전역 객체에 계속 추가
// ⚠️ userCache는 절대 비워지지 않음!
}
// 사용자가 100명, 1000명, 10000명...
// Heap 메모리가 계속 증가 📈
예를 들어:
이 객체는 Heap에 계속 남아 있게 된다.
Node.js 서버처럼 프로세스가 오래 실행되는 환경에서는 이런 객체들이 쌓이면 메모리 사용량이 점점 증가하게 된다.
🧠 서버 메모리 사용량 추이
시작: [████░░░░░░] 40%
1시간: [██████░░░░] 60%
2시간: [████████░░] 80%
3시간: [██████████] 100% 💥 서버 다운!
이것이 흔히 말하는 메모리 누수(memory leak) 이다.
Call Stack과 Heap 구조를 이해하면 다음 개념들이 자연스럽게 연결된다:
| 개념 | 메모리와의 관계 | 이유 |
|---|---|---|
| 이벤트 루프 | Call Stack과 Callback Queue의 관계 | Stack이 비어야 다음 작업 실행 |
| 비동기 처리 | 왜 setTimeout이 Stack에서 빠져나가는가 | 비동기 작업은 별도 공간에서 대기 |
| 서버 성능 저하 | Heap 메모리가 부족하면 발생 | GC가 자주 실행되어 CPU 점유 |
| 메모리 누수 | 사용하지 않는 객체가 Heap에 남아있는 경우 | 참조가 끊기지 않아 GC 불가 |
특히 Node.js 환경에서는 프로세스가 계속 살아 있기 때문에 Heap 관리가 서버 성능에 직접적인 영향을 준다.
이 글을 읽고 나서 다음 질문에 답할 수 있다면 메모리 구조는 제대로 이해한 것이다:
Q1. 함수 호출 순서는 누가 관리할까?A. Call Stack이 LIFO 방식으로 관리한다.
함수가 호출되면 Stack에 쌓이고, 실행이 끝나면 맨 위에서부터 제거된다.
A. Heap 영역에 저장된다.
Call Stack에는 객체의 참조(메모리 주소)만 저장되고, 실제 데이터는 Heap에 있다.
A. 재귀 함수가 종료되지 않거나 함수 호출이 너무 깊게 중첩되면
Call Stack의 크기 제한을 초과하여 발생한다.
A. 더 이상 어떤 변수도 그 데이터를 참조하지 않을 때
Garbage Collection의 대상이 되어 자동으로 제거된다.
🧠 JavaScript 메모리 구조
┌─────────────────────────────────────────────────┐
│ JavaScript 실행 환경 │
├──────────────────────┬──────────────────────────┤
│ 📚 Call Stack │ 📦 Heap │
│ (함수 호출 관리) │ (데이터 저장) │
├──────────────────────┼──────────────────────────┤
│ [함수3] ← 실행 중 │ 0x1234: { name: "Kim" } │
│ [함수2] │ 0x5678: [1, 2, 3] │
│ [함수1] │ 0x9abc: { age: 30 } │
│ [전역] │ ... │
├──────────────────────┴──────────────────────────┤
│ 🗑️ Garbage Collector │
│ (참조 없는 데이터 자동 제거) │
└─────────────────────────────────────────────────┘
↓ ↓
실행 순서 데이터 저장
👉 다음 글 예고
다음 글에서는 이 메모리 구조를 가진 JavaScript 코드가 어떤 단위로 실행되고 관리되는지, 즉 프로세스에 대해 이야기해볼 예정이다.