스택 자료 구조란? LIFO 개념, 예제, 활용처 정리
Dev
마지막 업데이트

스택 자료 구조란? LIFO 개념, 예제, 활용처 정리


스택 자료 구조를 공부할 때 가장 헷갈리는 부분은 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;
}

왜 스택이 필요할까요?

닫는 괄호 )를 만났을 때 비교해야 하는 대상은 “처음 열린 괄호”가 아니라 “가장 최근에 열린 괄호”입니다.

예를 들어 { [ ( ) ] }에서는:

  1. { push
  2. [ push
  3. ( push
  4. )를 만나면 가장 최근의 (와 비교

이 흐름은 스택이 아니면 설명이 어색해집니다.


스택 예제 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);
}

이 코드가 실행되면:

  1. countdown(3) 호출
  2. countdown(2) 호출
  3. countdown(1) 호출
  4. countdown(0) 호출

처럼 호출이 쌓이고, 종료될 때는 역순으로 빠져나옵니다.

그래서 재귀를 이해하려면 “호출 스택 위에 프레임이 쌓인다”는 감각이 꼭 필요합니다.


스택은 다른 자료 구조와 무엇이 다를까?

스택 vs 큐

  • 스택: 최근 것이 먼저 나감
  • 큐: 먼저 들어온 것이 먼저 나감

undo라면 스택, 작업 대기열이라면 큐가 더 자연스럽습니다.

순위나 정렬 기준 자체가 중요한 문제라면 소티드 셋 가이드처럼 아예 다른 구조를 떠올려야 할 때도 있습니다.

스택 vs 재귀

재귀는 기법이고, 스택은 구조입니다.

재귀 호출은 내부적으로 호출 스택을 사용하므로 둘은 연결되어 있지만 같은 개념은 아닙니다.

스택 vs 배열

배열은 저장 방식이고, 스택은 사용 규칙입니다.

배열을 스택처럼 사용할 수는 있지만, 배열 전체를 자유롭게 다루기 시작하면 더 이상 스택 규칙을 지키는 것이 아닙니다.


스택을 공부할 때 자주 하는 실수는?

1. 최근 상태 추적 문제를 변수 하나로 덮어쓰는 경우

이전 상태가 사라지기 때문에 undo 같은 기능을 만들기 어려워집니다.

2. DFS와 스택의 연결을 놓치는 경우

DFS는 깊게 들어가고, 그 흐름을 만드는 핵심이 스택이라는 점을 같이 기억해야 합니다.

3. 재귀를 “마법”처럼 외우는 경우

호출 스택이 쌓이고 풀린다는 관점으로 보면 훨씬 이해가 쉬워집니다.

4. 큐와 섞어서 기억하는 경우

둘 다 넣고 빼는 구조이지만, 꺼내는 순서가 완전히 다릅니다.


어떤 문제에서 스택을 먼저 떠올리면 좋을까?

아래 질문에 “예”가 많다면 스택을 먼저 떠올리면 좋습니다.

  1. 가장 최근 상태가 다음 판단에 중요한가?
  2. 되돌리기나 백트래킹이 필요한가?
  3. 닫는 토큰이 최근에 열린 토큰과 짝을 맞춰야 하는가?
  4. 깊게 들어갔다가 역순으로 복귀하는 흐름인가?

FAQ

Q. 스택은 왜 배열로 구현하기 쉬운가요?

맨 끝에 넣고 맨 끝에서 빼면 되기 때문입니다. 많은 언어에서 이 패턴은 배열과 잘 맞습니다.

Q. 재귀와 스택 중 무엇을 먼저 이해해야 하나요?

스택 감각을 먼저 잡으면 재귀가 훨씬 덜 추상적으로 느껴집니다.

Q. 실무에서 스택을 자주 보나요?

undo/redo, 파서, 탐색 알고리즘, 호출 흐름 이해, 브라우저 히스토리 같은 맥락에서 자주 나옵니다.


먼저 읽어볼 가이드

검색 유입이 많은 핵심 글부터 이어서 보세요.