스택 자료 구조를 공부할 때 가장 헷갈리는 부분은 push와 pop의 문법보다 “왜 최근 것이 먼저 나가야 하는가”입니다.
스택은 가장 나중에 들어온 데이터를 가장 먼저 꺼내는 LIFO 구조이고, 괄호 검사, undo, DFS, 호출 스택처럼 최근 상태가 중요한 문제에서 자주 쓰입니다.
이 글에서는 아래를 빠르게 정리합니다.
- 스택의 핵심 개념과 연산
- 언제 스택이 자연스럽게 떠올라야 하는지
- 괄호 검사, undo, DFS, 재귀와의 연결
- 초보자가 자주 헷갈리는 포인트
짧게 말하면 스택은 가장 최근 상태를 먼저 꺼내거나 되돌려야 하는 문제에 가장 잘 맞는 자료 구조입니다.
스택 자료 구조란 무엇인가?
스택은 LIFO(Last In, First Out) 구조입니다.
가장 나중에 들어온 데이터가 가장 먼저 나갑니다. 접시를 위로 쌓아 올린 뒤, 위에서부터 꺼내는 장면을 떠올리면 이해하기 쉽습니다.
스택을 고를 때 가장 중요한 질문은 이것입니다.
- 가장 최근 상태를 추적해야 하는가?
- 방금 들어온 정보가 다음 판단에 가장 중요한가?
- 이전 단계로 되돌리는 기능이 필요한가?
그렇다면 스택이 잘 맞을 가능성이 큽니다.
스택은 언제 사용하는가?
1. undo와 뒤로 가기
문서를 수정했다가 되돌리거나, 브라우저에서 방금 본 페이지를 다시 이전 상태로 돌아가는 흐름은 스택과 잘 맞습니다.
2. 괄호 짝 맞추기
닫는 괄호를 만났을 때 가장 최근에 열린 괄호와 비교해야 하므로 스택이 자연스럽습니다.
3. DFS와 백트래킹
깊게 들어갔다가 막히면 최근 경로부터 되돌아오는 흐름은 스택의 성질과 잘 맞습니다.
4. 함수 호출 흐름
재귀 호출을 배울 때 나오는 호출 스택(call stack)은 스택 개념을 실감 나게 보여 주는 대표 사례입니다.
스택의 핵심 연산은 무엇인가?
push: 맨 위에 넣기pop: 맨 위에서 꺼내기peek: 맨 위를 보기isEmpty: 비어 있는지 확인
시간 복잡도 감각
일반적으로 스택은 맨 끝 요소만 다루면 되기 때문에 구현이 단순하고 빠른 편입니다.
예를 들어 JavaScript 배열의 push()와 pop()은 보통 스택 모델과 잘 맞습니다.
즉, 큐처럼 앞 요소를 당겨야 하는 문제가 없어서, 학습 단계에서는 배열로 구현해도 개념과 성능 감각이 비교적 잘 맞아떨어집니다.
스택은 어떻게 구현할까?
class Stack {
constructor() {
this.items = [];
}
push(item) {
this.items.push(item);
}
pop() {
if (this.items.length === 0) return undefined;
return this.items.pop();
}
peek() {
return this.items[this.items.length - 1];
}
isEmpty() {
return this.items.length === 0;
}
}
사용 예제는 아주 직관적입니다.
const stack = new Stack();
stack.push('first');
stack.push('second');
stack.push('third');
console.log(stack.pop()); // third
console.log(stack.peek()); // second
가장 나중에 넣은 third가 가장 먼저 나옵니다. 이 한 줄만 이해해도 스택 감각의 절반은 잡힌 셈입니다.
스택 예제 1: 괄호 검사에 왜 스택을 사용할까?
스택을 설명할 때 가장 자주 등장하는 예제는 괄호 검사입니다.
function isBalanced(text) {
const stack = [];
const pairs = {
')': '(',
']': '[',
'}': '{'
};
for (const ch of text) {
if (ch === '(' || ch === '[' || ch === '{') {
stack.push(ch);
continue;
}
if (ch === ')' || ch === ']' || ch === '}') {
const top = stack.pop();
if (top !== pairs[ch]) {
return false;
}
}
}
return stack.length === 0;
}
왜 스택이 필요할까요?
닫는 괄호 )를 만났을 때 비교해야 하는 대상은 “처음 열린 괄호”가 아니라 “가장 최근에 열린 괄호”입니다.
예를 들어 { [ ( ) ] }에서는:
{push[push(push)를 만나면 가장 최근의(와 비교
이 흐름은 스택이 아니면 설명이 어색해집니다.
스택 예제 2: undo 기능은 어떻게 구현할까?
에디터에서 undo를 구현한다고 생각해 보겠습니다.
작업을 쌓아 두고 오래된 순서대로 처리하는 문제와는 다르다는 점이 중요합니다. 그런 흐름은 큐 자료 구조 가이드와 비교해 보면 차이가 더 선명해집니다.
const undoStack = [];
let currentText = '';
function applyChange(newText) {
undoStack.push(currentText);
currentText = newText;
}
function undo() {
if (undoStack.length === 0) return currentText;
currentText = undoStack.pop();
return currentText;
}
applyChange('hello');
applyChange('hello world');
applyChange('hello world!!!');
console.log(undo()); // hello world
console.log(undo()); // hello
가장 최근 상태부터 복원되므로 스택과 정확히 맞아떨어집니다.
redo까지 만들고 싶다면 보통:
- undo용 스택
- redo용 스택
두 개를 조합합니다.
스택 예제 3: DFS와 백트래킹에 왜 스택이 필요할까?
DFS는 재귀로 많이 배우지만, 내부적으로는 스택 흐름을 가지고 있습니다.
같은 그래프 탐색이어도 BFS처럼 가까운 노드부터 넓게 가려면 큐가 필요하므로, 이 부분은 큐 자료 구조 가이드와 같이 보면 특히 좋습니다.
명시적으로 스택을 사용하면 아래처럼 쓸 수 있습니다.
function dfs(graph, start) {
const visited = new Set();
const stack = [start];
while (stack.length > 0) {
const node = stack.pop();
if (visited.has(node)) continue;
visited.add(node);
console.log('visit:', node);
for (const next of graph[node].reverse()) {
if (!visited.has(next)) {
stack.push(next);
}
}
}
}
최근에 넣은 노드가 먼저 나오기 때문에, 한 방향으로 더 깊이 들어가는 DFS 특성이 자연스럽게 만들어집니다.
큐를 쓰면 BFS가 되고, 스택을 쓰면 DFS가 된다는 연결은 꼭 같이 기억하는 편이 좋습니다.
재귀와 호출 스택은 어떻게 연결될까?
스택은 함수 호출을 이해할 때도 매우 중요합니다.
재귀 함수가 실행될 때는 호출 정보가 스택처럼 쌓입니다.
function countdown(n) {
if (n === 0) return;
console.log(n);
countdown(n - 1);
}
이 코드가 실행되면:
countdown(3)호출countdown(2)호출countdown(1)호출countdown(0)호출
처럼 호출이 쌓이고, 종료될 때는 역순으로 빠져나옵니다.
그래서 재귀를 이해하려면 “호출 스택 위에 프레임이 쌓인다”는 감각이 꼭 필요합니다.
스택은 다른 자료 구조와 무엇이 다를까?
스택 vs 큐
- 스택: 최근 것이 먼저 나감
- 큐: 먼저 들어온 것이 먼저 나감
undo라면 스택, 작업 대기열이라면 큐가 더 자연스럽습니다.
순위나 정렬 기준 자체가 중요한 문제라면 소티드 셋 가이드처럼 아예 다른 구조를 떠올려야 할 때도 있습니다.
스택 vs 재귀
재귀는 기법이고, 스택은 구조입니다.
재귀 호출은 내부적으로 호출 스택을 사용하므로 둘은 연결되어 있지만 같은 개념은 아닙니다.
스택 vs 배열
배열은 저장 방식이고, 스택은 사용 규칙입니다.
배열을 스택처럼 사용할 수는 있지만, 배열 전체를 자유롭게 다루기 시작하면 더 이상 스택 규칙을 지키는 것이 아닙니다.
스택을 공부할 때 자주 하는 실수는?
1. 최근 상태 추적 문제를 변수 하나로 덮어쓰는 경우
이전 상태가 사라지기 때문에 undo 같은 기능을 만들기 어려워집니다.
2. DFS와 스택의 연결을 놓치는 경우
DFS는 깊게 들어가고, 그 흐름을 만드는 핵심이 스택이라는 점을 같이 기억해야 합니다.
3. 재귀를 “마법”처럼 외우는 경우
호출 스택이 쌓이고 풀린다는 관점으로 보면 훨씬 이해가 쉬워집니다.
4. 큐와 섞어서 기억하는 경우
둘 다 넣고 빼는 구조이지만, 꺼내는 순서가 완전히 다릅니다.
어떤 문제에서 스택을 먼저 떠올리면 좋을까?
아래 질문에 “예”가 많다면 스택을 먼저 떠올리면 좋습니다.
- 가장 최근 상태가 다음 판단에 중요한가?
- 되돌리기나 백트래킹이 필요한가?
- 닫는 토큰이 최근에 열린 토큰과 짝을 맞춰야 하는가?
- 깊게 들어갔다가 역순으로 복귀하는 흐름인가?
FAQ
Q. 스택은 왜 배열로 구현하기 쉬운가요?
맨 끝에 넣고 맨 끝에서 빼면 되기 때문입니다. 많은 언어에서 이 패턴은 배열과 잘 맞습니다.
Q. 재귀와 스택 중 무엇을 먼저 이해해야 하나요?
스택 감각을 먼저 잡으면 재귀가 훨씬 덜 추상적으로 느껴집니다.
Q. 실무에서 스택을 자주 보나요?
undo/redo, 파서, 탐색 알고리즘, 호출 흐름 이해, 브라우저 히스토리 같은 맥락에서 자주 나옵니다.
Read Next
- 순차 처리와 작업 대기열 관점은 큐 자료 구조 가이드와 같이 보면 대비가 잘 됩니다.
- 점수와 순위, 예약 작업처럼 정렬 기준이 필요한 문제는 소티드 셋 가이드로 이어서 보면 좋습니다.
Related Posts
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis vs RabbitMQ vs Kafka 개발자를 위한 미들웨어 트러블슈팅 허브 글입니다. Redis, RabbitMQ, Kafka 중 어떤 증상부터 먼저 봐야 하는지와 어떤 문제 패턴이 각 시스템에 가까운지 정리합니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Kafka consumer lag가 계속 늘 때: 트러블슈팅 가이드 Kafka consumer lag가 계속 늘어날 때 무엇부터 봐야 하는지 정리합니다. poll 주기, 처리 속도, rebalance, consumer 설정까지 실전 기준으로 다룹니다.
- Kafka Rebalancing Too Often 가이드 Kafka consumer group에서 rebalance가 너무 자주 일어날 때 membership flapping, poll timing, protocol, assignment churn을 어떤 순서로 봐야 하는지 설명하는 실전 가이드입니다.
- Docker container가 계속 재시작될 때: 먼저 확인할 것들 exit code, command failure, environment mistake, health check 관점에서 Docker restart loop를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.