HooneyLog
© 2026 Seunghoon Shin. All rights reserved.
모든 게시글
react
2022. 8. 29.•
1

React 커스텀 훅: useStateWithHistory로 상태 이력 관리하기 (Undo/Redo 구현)

Seunghoon Shin
작성자 Seunghoon Shin풀스택 개발자

React 커스텀 훅: useStateWithHistory로 상태 이력 관리하기 (Undo/Redo 구현)

1. 문제의 배경

리액트의 useState는 현재의 상태를 관리하는 데 최적화되어 있습니다. 하지만 사용자가 입력한 내용을 취소(Undo)하거나, 취소한 내용을 다시 실행(Redo)해야 하는 기능을 구현하려면 useState만으로는 부족합니다.

이전 상태값들을 별도의 배열에 저장하고 현재 가리키고 있는 위치(Index)를 관리하는 로직이 추가로 필요하기 때문입니다. 이러한 로직을 매번 컴포넌트 내부에서 작성하면 코드가 복잡해지고 재사용하기 어렵습니다.

2. 해결 방안 탐색

상태의 이력을 관리하기 위해 두 가지 핵심 전략을 사용합니다:

  1. useRef를 통한 이력 저장: 히스토리 배열과 현재 인덱스는 렌더링에 직접적인 영향을 주지 않아야 하므로 useRef를 사용하여 메모리상에 유지합니다.
  2. 커스텀 훅으로 캡슐화: 상태 업데이트, 뒤로 가기, 앞으로 가기 로직을 하나의 훅으로 묶어 어떤 컴포넌트에서도 쉽게 Undo/Redo 기능을 사용할 수 있게 만듭니다.

3. 핵심 개념 및 아키텍처

useStateWithHistory 훅은 내부적으로 현재 상태를 위한 state와, 전체 이력을 담는 history 배열, 그리고 현재 위치를 가리키는 historyIndex를 가집니다.

상태가 업데이트될 때마다 history 배열에 새로운 값이 추가되고, goBack이나 goForward를 호출하면 인덱스만 조절하여 해당 시점의 상태를 setStateInternal로 반영하는 구조입니다.

4. 구현 및 트러블슈팅

TypeScript를 적용하여 타입 안정성을 확보한 커스텀 훅의 모습입니다.

import { useState, useRef, useCallback } from 'react'; function useStateWithHistory<T>(initialState: T) { const [state, setStateInternal] = useState<T>(initialState); // 렌더링 사이에도 유지되어야 하지만, 변경 시 리렌더링을 유발하지 않아야 함 const history = useRef<T[]>([initialState]); const historyIndex = useRef(0); // 새로운 상태 설정 const setState = useCallback((newState: T) => { // 현재 인덱스 이후의 히스토리는 제거 (새로운 분기 시작) history.current = history.current.slice(0, historyIndex.current + 1); history.current.push(newState); historyIndex.current = history.current.length - 1; setStateInternal(newState); }, []); // 뒤로 가기 (Undo) const goBack = useCallback(() => { if (historyIndex.current === 0) return; historyIndex.current--; setStateInternal(history.current[historyIndex.current]); }, []); // 앞으로 가기 (Redo) const goForward = useCallback(() => { if (historyIndex.current >= history.current.length - 1) return; historyIndex.current++; setStateInternal(history.current[historyIndex.current]); }, []); return [state, setState, goBack, goForward, history.current] as const; }

트러블슈팅: 새로운 분기 처리

기존 코드에서는 단순히 push만 했지만, 실제 서비스에서는 '뒤로 가기'를 한 상태에서 새로운 값을 set 할 경우 그 이후의 미래 히스토리는 삭제되어야 합니다. 이를 위해 slice를 사용하여 현재 인덱스까지만 남기고 새로운 값을 추가하도록 개선했습니다.

5. 결과 및 Trade-off

결과

  • 사용자 경험 향상: 실수로 지운 데이터를 복구하거나 이전 설정을 빠르게 확인할 수 있는 기능을 쉽게 구현할 수 있습니다.
  • 성능 최적화: 과거 상태를 다시 계산할 필요 없이 캐싱된 값을 즉시 사용하여 부드러운 전환이 가능합니다.

Trade-off

  • 메모리 사용량: 히스토리가 무한정 쌓이면 브라우저 메모리에 부담을 줄 수 있습니다. 대규모 데이터를 다룰 때는 최대 히스토리 개수(Capacity)를 제한하는 로직이 필요합니다.
  • 참조 무결성: 객체나 배열을 상태로 관리할 때 깊은 복사(Deep Copy)를 하지 않으면 히스토리 내의 값들이 동시에 변할 수 있으므로 주의해야 합니다.

6. 마치며

useStateWithHistory는 리액트의 기본 Hook들을 조합하여 얼마나 강력한 기능을 만들 수 있는지 보여주는 좋은 예시입니다. 복잡한 폼 입력이나 그림판 같은 에디터 기능을 구현할 때 이 훅을 활용하여 사용자에게 더 높은 자유도를 제공해 보세요.

참고 자료:

  • React Hooks: useRef
  • Managing State: Undo/Redo Pattern
← 이전 글useFetch 커스텀 훅 만들기 (feat : reducer )
다음 글 →Event Target Class 만들어보기