React 에서 Ctrl+Z 기능 구현하기

우디·2024년 2월 21일
0
post-thumbnail

안녕하세요:) 개발자 우디입니다! 아래 내용 관련하여 작업 중이신 분들께 도움이되길 바라며 글을 공유하니 참고 부탁드립니다😊
(이번에 벨로그로 이사오면서 예전 글을 옮겨적었습니다. 이 점 양해 부탁드립니다!)

작업 시점: 2021년 4월

기능 개요

  • 방금 전 작업을 취소하여 이전 버전으로 돌아가기(undo)
  • 취소했던 작업을 다시 실행하기 (redo)

필요 사항

  • 스택 자료구조 개념
  • 어떤 데이터에 대해 실행 취소 및 되돌리기 작업을 할 것인지

스택 자료구조 (핵심적인 것만)

  • '쌓다'라는 의미로 데이터를 차곡차곡 쌓아올린 형태의 자료구조
  • 한 쪽에서만 삽입과 삭제가 이루어지며, 가장 늦게 삽입된 데이터가 가장 먼저 삭제되는 구조 (Last In First Out)
  • 활용 사례
    • 방문 기록
    • 실행 취소
    • ...

실행 취소 및 되돌리기 대상 데이터

  • 현재 개발 중인 프로그램은 사용자가 '마커'를 조작해서 결과물을 얻어가는 구조.
  • 결국 사용자가 조작한 부분에 대해 실행 취소가 이루어지는 것이기 때문에, 프로그램에서 사용자가 조작 가능한 부분인 '마커'가 대상 데이터가 될 것.

구현 과정

  • 마커 변경사항을 관리하는 History 생성

    constructor(props, context) {
      this.markerHistory = [];
      this.redoHistory = [];
    }
    • 히스토리의 변경이 렌더링과 직접적인 관계는 없기 때문에 constructor에 생성
  • History에 작업 내역을 추가하는 함수

    addToMarkerHistory = (reset = true) => {
      let prevMarkerInfo = this.getMarkerInfo();
      let currentMarkerHistory = this.getMarkerHistory();
    
      currentMarkerHistory.push(prevMarkerInfo);
    
      if (currentMarkerHistory.length > 50) {
        currentMarkerHistory.shift();
      }
    
      if (reset) {
        this.resetRedoHistory();
      }
    };
    • 설명

      • 마커 정보 받아와서 히스토리에 push
      • 내부적으로 정한 히스토리 개수 기준인 50이 넘어갈 경우, 제일 앞에 있는 것을 제거하기 위해 shift
    • 추가적인 관련 함수들

      getMarkerHistory = () => {
        return this.markerHistory;
      };
      
      resetRedoHistory = () => {
        this.redoHistory = [];
      };
      
      getRedoHistory = () => {
        return this.redoHistory;
      };
  • 마커가 조작되는 모든 경우에 addToMarkerHistory 함수 추가

    addMarker = (targetTime, duration, addOption) => {
        ...생략...
        this.addToMarkerHistory(); //to save prevMarkerInfo
        ...생략...
    };
  • 사용자가 '실행 취소' 및 '되돌리기' 버튼을 눌렀을 때 실행되는 함수

    undoHandler = cb => {
      let currentMarkerHistory = this.getMarkerHistory();
      let currentRedoHistory = this.getRedoHistory();
      let currentMarkerInfo = this.getMarkerInfo();
    
      if (currentMarkerHistory.length > 0) {
        currentRedoHistory.push(currentMarkerInfo);
    
        let undoResult = currentMarkerHistory[currentMarkerHistory.length - 1];
    
        this.setState(
          {
            markerInfo: undoResult,
          },
          () => {
            cb();
            this.callUpdateTitleAfterChange(this.state.savedProjectFilePath);
          },
        );
    
        currentMarkerHistory.pop();
      }
    };
    
    redoHandler = cb => {
      let currentMarkerHistory = this.getMarkerHistory();
      let currentRedoHistory = this.getRedoHistory();
      let currentMarkerInfo = this.getMarkerInfo();
    
      if (currentRedoHistory.length > 0) {
        currentMarkerHistory.push(currentMarkerInfo);
    
        let redoResult = currentRedoHistory[currentRedoHistory.length - 1];
    
        this.setState(
          {
            markerInfo: redoResult,
          },
          () => {
            cb();
            this.callUpdateTitleAfterChange(this.state.savedProjectFilePath);
          },
        );
    
        currentRedoHistory.pop();
      }
    };
    • undoHandler 설명
      • 현재 작업을 undo 하지만 나중에 redo할 것을 대비하여 currentRedoHistory에 push
      • 기존 currentMarkerHistory에서 가장 최근에 저장된 마지막 항목을 불러와서 현재 작업으로 setState
      • currentMarkerHistory의 마지막 항목 제거를 위해 pop
    • redoHandler 설명
      • 현재 작업을 다시 currentMarkerHistory에 push
      • redo history에 가장 최근에 저장했던 항목을 불러와서 현재 작업으로 setState
      • redo history의 마지막 항목 제거를 위해 pop
    • 현재 작업으로 인한 영향 검토
      • 작업 내역 수정 여부에 따라 달라지는 프로그램 상단 타이틀을 고려하기

배우고 느낀 점

  • CS 지식의 중요성
    • 맨 처음 이 기능에 대해 고민했을 때, 자료구조에 대한 사전 지식이 있었다면 더욱 수월하게 구현할 수 있었을 것. 앞으로 CS 지식도 열심히 공부해야겠다.
profile
넓고 깊은 지식을 보유한 개발자를 꿈꾸고 있습니다:) 기억 혹은 공유하고 싶은 내용들을 기록하는 공간입니다

0개의 댓글