기술 면접에서 '스택과 힙의 차이점을 설명해 보세요'라는 질문을 받는다면, 어떻게 대답하실 건가요?
많은 분들이 '스택은 LIFO 구조이고, 빠릅니다. 힙은...' 처럼 교과서에서 외운 답을 읊기 시작합니다. 하지만 면접관이 정말 듣고 싶은 대답은 '왜'입니다. 스택은 왜 LIFO 구조로 설계되었을까요? C언어와 달리 자바스크립트 개발자가 배열을 힙/스택으로 구분하는 게 의미가 있을까요?
이 글에서는 '스택은 빠르다' 같은 단순 암기에서 벗어나, 쉬운 비유를 통해 메모리 구조의 근본적인 원리를 파헤쳐 봅니다. 이 글을 끝까지 읽으신다면, CS 기본기가 탄탄한 개발자라는 인상을 확실히 남길 수 있을 겁니다.
우선 기본적인 컴퓨터 메모리의 구조에 대해 알아봅시다.

컴퓨터 메모리는 프로그램이 실행될 때 크게 네 가지 영역으로 구성됩니다.
Text 영역에는 실행 가능한 기계어 코드 자체가 저장되고,
Data 영역과 BSS 영역에는 각각 초기값이 있거나 없는 전역 변수와 정적 변수가 자리 잡습니다.
마지막으로, 프로그램이 실행되는 동안 함수 호출과 지역 변수를 관리하기 위한 스택(Stack) 영역과, 객체나 배열처럼 동적으로 생성되는 데이터를 위한 힙(Heap) 영역이 있습니다.
스택은 왜 LIFO 구조로 설계되었을까요?
프로그램이 실행되는 것은 사실 '함수 호출의 연속'입니다. 그리고 함수는 종종 다른 함수를 호출합니다.
A라는 함수가 실행되다가 B라는 함수를 호출하고, B는 또 C라는 함수를 호출하는 상황을 상상해 봅시다.
function A() {
console.log("A 작업 시작");
B(); // B를 호출
console.log("A 작업 다시 시작");
}
function B() {
console.log("B 작업 시작");
C(); // C를 호출
console.log("B 작업 다시 시작");
}
function C() {
console.log("C 작업 시작 (가장 안쪽)");
}
A();
이 코드가 어떻게 실행될까요? A가 B를 호출하는 순간, A의 작업은 일시 정지됩니다. 그리고 B가 C를 호출하는 순간, B의 작업도 일시 정지됩니다.
이 과정을 '스테이크 요리' 레시피에 비유해 보겠습니다.
A (메인 요리): "스테이크 굽기"를 시작합니다.
B (소스 만들기): 레시피에 "소스를 만드세요"라고 나옵니다. "스테이크 굽기"를 일시 정지하고, "소스 만들기" 작업을 시작합니다.
C (양파 다지기): 소스 레시피를 보니 "양파를 다지세요"라고 나옵니다. "소스 만들기"를 일시 정지하고, "양파 다지기" 작업을 시작합니다.
이제 모든 작업이 끝나는 순서를 생각해 보세요.
가장 마지막에 시작한 "양파 다지기"(C)가 끝나야, 멈춰 놨던 "소스 만들기"(B)를 다시 시작할 수 있습니다.
"소스 만들기"(B)가 끝나야, 멈춰 놨던 "스테이크 굽기"(A)를 마저 끝낼 수 있습니다.
"양파 다지기"(C)가 가장 마지막에 시작했지만, 가장 먼저 끝나야 합니다(First-Out). 이것이 바로 LIFO(Last-In, First-Out) 구조입니다.
스택은 이처럼 작업을 중단하고, 더 작은 단위의 새 작업을 처리한 뒤, 다시 원래 중단했던 작업으로 정확히 돌아오게 만드는 가장 논리적이고 자연스러운 방식입니다.
개발자들은 이 구조를 콜 스택(Call Stack)이라고 부릅니다. 함수가 호출(Call)될 때마다, 그 함수의 작업 공간(변수, 돌아갈 주소 등)이 스택이라는 메모리 공간에 차곡차곡 쌓이기(Stack) 때문입니다.
위에서 '스택(Stack)'이 함수를 위한 '임시 작업대'처럼 작동한다고 알아봤습니다. 작업이 시작되면 작업대 위에 도구가 쌓이고, 작업이 끝나면 작업대가 즉시 정리되는, 빠르고 자동화된 공간이었죠.
하지만 이런 의문이 생깁니다.
데이터가 얼마나 필요할지 미리 알 수 없으면 어떡하지? (예: 사용자 입력에 따른 배열 크기)
함수가 끝나도 데이터가 계속 살아남아야 한다면 어떡하지? (예: 함수에서 객체를 반환할 때)
스택은 이 두 가지 문제를 해결할 수 없습니다. 그래서 우리에게는 거대하고 유연한 '공용 창고', 바로 힙(Heap) 메모리가 필요합니다.
힙(Heap)은 프로그램이 실행되는 도중에(Runtime) 원하는 크기의 메모리를 동적으로 할당할 수 있는 거대한 메모리 공간입니다.
'스택'이 컴파일러에 의해 미리 계획된 공간이라면, '힙'은 개발자의 요청에 따라 실시간으로 공간을 빌려주는 유연한 공간입니다.
힙은 어떨 때 사용할까요?
스택은 크기가 작고 고정되어 있습니다. 하지만 힙은 거대합니다.
사용자로부터 몇 개의 데이터를 입력받을지 모르거나, 파일 크기가 얼마나 될지 모를 때, 우리는 힙 관리자(메모리 할당자)에게 "이만큼의 공간을 지금 당장 빌려줘!"라고 요청(new, {})할 수 있습니다.
자바스크립트의 배열이나 객체도 보통은 힙 영역을 사용합니다. 이런 참조 타입 데이터들은 처음 만들 때부터 최종 크기가 얼마가 될지 정해져 있지 않습니다. 배열에 push를 해서 요소를 계속 추가할 수도 있고, 객체에 새로운 속성을 덧붙일 수도 있죠.
이렇게 크기가 유동적이고 예측 불가능한 데이터는 빠르고 작은 스택에 보관할 수 없습니다. 그래서 힙이라는 거대한 공용 창고에 저장하는 것입니다.
// 사용자가 몇 개의 아이템을 담을지 미리 알 수 없습니다.
function createListFromUserInput(count) {
const arr = []; // 👈 힙(Heap)에 동적으로 공간이 할당됩니다.
for (let i = 0; i < count; i++) {
arr.push(i); // 힙에 할당된 공간이 필요에 따라 늘어납니다.
}
return arr;
}
이것이 힙을 사용하는 가장 본질적인 이유입니다.
스택의 데이터는 함수가 끝나면 즉시 사라집니다. 하지만 함수 안에서 만든 객체나 배열을 함수 밖으로 반환(return)해서 계속 사용하고 싶다면, 그 데이터는 '임시 작업대'가 아닌 힙에 보관되어야만 합니다.
function createUser(name) {
// 1. user 객체 본체는 '힙(창고)'에 생성됩니다.
const user = { id: 1, name<: name };
// 2. 이 객체를 가리키는 '참조(리모컨)'를 반환합니다.
return user;
} // 3. 함수가 종료되어 스택(작업대)은 정리되지만,
// 힙(창고)에 있는 user 객체는 그대로 살아남습니다.
// 4. 힙에 살아있는 데이터를 계속 사용할 수 있습니다.
const newUser = createUser("minjae");
console.log(newUser); // { id: 1, name: "minjae" }
스택이 LIFO(Last-In, First-Out)라는 엄격한 구조를 가진 것처럼, 힙도 그런 구조가 있을까요?
정답은 '아니오'입니다.
가장 많이 하는 오해는, 메모리 '힙(Heap)'이 자료구조 '힙(Max/Min Heap)'처럼 정렬된 트리 구조일 것이라 생각하는 것입니다. 전혀 다릅니다.
여기서 '힙'은 그저 "아무렇게나 쌓여있는 거대한 더미(a heap of memory)"라는 사전적 의미로 쓰였습니다.
힙의 동작 방식은 '창고 보관함'과 같습니다.
할당 (Allocation): 내가 "10칸짜리 짐(객체)을 맡아줘!"라고 요청합니다.
탐색: 창고 관리자(할당자)는 비어있는 보관함 중 10칸을 넣기에 적절한 아무 곳(순서 X)이나 찾습니다.
반환 (Reference): 관리자는 "당신의 짐은 A-105번 보관함에 있습니다"라는 '티켓(메모리 주소/참조)'을 줍니다.
개발자는 이 '티켓'을 변수에 저장해두고, 짐이 필요할 때마다 티켓을 보여주며 창고를 이용합니다.
스택은 함수가 끝나면 작업대가 자동으로 정리되니 청소부가 필요 없습니다. 하지만 힙(창고)에 맡긴 짐은 내가 직접 계약을 해지(delete, free in C/C++)하지 않으면 영원히 그 자리를 차지합니다.
자바스크립트나 파이썬 같은 언어에는 가비지 컬렉터(Garbage Collector, GC)라는 자동 '청소부'가 있습니다.
GC의 역할: 창고를 주기적으로 돌며, 아무도 티켓(참조)을 들고 있지 않은 '주인 없는 짐'을 찾아 치워줍니다.
메모리 누수 (Memory Leak): 만약 우리가 티켓(참조)을 잃어버리지는 않았는데, 짐(객체)의 존재 자체를 잊어버리고 창고 구석에 계속 방치한다면 어떨까요? 청소부(GC)는 "어? 저 짐은 아직 티켓이 있네?"라고 판단하고 치우지 않습니다.
이렇게 더 이상 쓰지 않는데도 참조가 남아있어 해제되지 않는 데이터가 계속 쌓이는 현상을 메모리 누수라고 부릅니다.
// 메모리 누수의 간단한 예시 (의도치 않은 전역 참조)
function addClickListener() {
const largeData = new Array(1000000).fill('data'); // 힙에 큰 데이터 생성
// element가 DOM에서 제거되어도,
// listener가 해제되지 않으면 largeData에 대한 참조가 계속 살아남아
// GC가 수거하지 못하고 메모리 누수가 발생할 수 있습니다.
element.addEventListener('click', function() {
console.log(largeData[0]);
});
}
자바스크립트의 '클로저'는 힙이 있기에 가능한 마법입니다.
outerFunction이 실행되며 변수 myVar를 만듭니다.
엔진은 innerFunction이 myVar를 참조하는 것을 보고, "이 변수는 함수가 끝나도 살아남아야겠다!"라고 판단합니다.
엔진은 myVar를 '임시 작업대(스택)'가 아닌 '공용 창고(힙)'에 저장합니다.
outerFunction이 끝나 작업대가 치워져도, myVar는 힙에 계속 살아있습니다.
나중에 innerFunction이 호출되면, 힙에 저장된 myVar를 꺼내와 사용합니다.
function outerFunction() {
let myVar = "I am outside!"; // 1. myVar가 힙에 생성됨
function innerFunction() { // 2. innerFunction이 myVar를 참조함 (클로저)
console.log(myVar);
}
return innerFunction; // 3. innerFunction(리모컨) 반환
} // 4. outerFunction은 종료되지만 myVar는 힙에 살아남음
const myFunc = outerFunction(); // 5. myFunc는 힙의 myVar를 참조
myFunc(); // "I am outside!" 출력
이처럼 메모리 구조는 단순한 암기 대상이 아닙니다.
'작업대(스택)'와 '공용 창고(힙)'의 비유처럼, 각 영역이 '왜' 그렇게 작동하는지 그 이유를 이해하면, 코드가 보이지 않는 곳에서 어떻게 작동하는지 상상할 수 있게 됩니다.
이 글이 컴퓨터의 메모리 구조를 이해하는 데 작은 도움이 되었기를 바랍니다.
읽어주셔서 감사합니다.