깊이 우선 탐색(DFS)을 활용한 일정 프로세스 성능 개선

Kim Young Jae·2024년 6월 21일
0

안녕하세요 앞선 게시글로 일정 컴포넌트를 만들었었는데 발생한 문제와 해결 과정을 공유해드립니다.

문제

만약 일정을 삭제할 때 단순히 그 데이터만 삭제를 하는게 아니라 일정이 겹쳐져 있는 Day 컴포넌트들도 리렌더링을 해주어야합니다.

쉽게 말해 무슨 말이냐면

예를 들어 아래 이미지처럼 일정들이 있을때 "일정1"을 삭제한다면

아래 이미지 처럼 "일정1"과 같은 단계에 있는 데이터들을 삭제해줘야한다

그렇지 않으면 아래처럼 됩니다

아래 이미지처럼 계단식으로 쭉 연결되어있다면 연결된 모든 일정들을 렌더링 해주어야한다.

해결 1

계단식으로 쭉 연결되어있는게 어디까지인지를 몰랐기때문에 처음부터 끝까지 모든 일정을 다 부르는 방식으로 했었습니다. 물론 원하는 방식으로 동작을 하긴했지만 시간, 비용적인 측면에서 너무 낭비가 있다는 것을 느꼈고 개선의 여지가 필요했습니다. 아래는 모니터링으로 확인한 결과입니다.

모니터링을 확인하면 일정 한개가 삭제가 되었을때 4.3초가 걸렸습니다. 일정 한개가 삭제 되었을 뿐인데 모든 데이터를 호출하고 모든 데이터를 가공하는 형태입니다. 지금은 일정이 몇개 없어서 이정도 걸렸지만 관련이 있든 없든 일정이 많아질수록 시간은 더 증가할것입니다.

해결 2

꼭 필요한 데이터만 렌더링하자라는 생각이 들었습니다. 그래서 해당 날짜가 삭제가 되면 DFS 알고리즘을 통해 타고 타고 들어가서 시작날짜와 끝날짜를 찾자라는 계획입니다.

삭제한 일정의 시작, 종료 날짜로부터 시작해서 해당 날짜를 순회해서 계속 해서 탐색해 나갑니다. startingDay, endingDay를 통해 마지막 날짜를 찾을 수 있습니다. 해당 함수를 사용하면 연결되어있는 일정중 시작날짜와 끝날짜를 알 수 있어 모든 일정을 부르지 않고 필요한 데이터만 요청 할 수 있습니다.

const firstDate = getScheduleStartDate(삭제한 일정의 시작 날짜);
const lastDate = getScheduleLastDate(삭제한 일정의 종료 날짜);

const getScheduleStartDate = (startDate: string) => {
  let firstDate = format(new Date(startDate), 'yyyy-MM-dd');
  
  const DFS = (date: string) => {
    schedule.scheduleByDate[date]?.forEach((event) => {
      if (event) {
        const _fDate = fDate(event?.startDate);
		if (!event.startingDay) {
          DFS(_fDate);
        } else {
          if (isBefore(_fDate, firstDate)) {
            firstDate = _fDate;
          }
        }
      }
    });
  };
  
  DFS(firstDate);
  return firstDate;
};

const getScheduleLastDate = (endDate: string) => {
  let lastDate = format(new Date(endDate), 'yyyy-MM-dd');

  const DFS = (date: string) => {
    schedule.scheduleByDate[date]?.forEach((event) => {
      if (event) {
        const _fDate = fDate(event?.endDate);

        if (!event.endingDay) {
          DFS(_fDate);
        } else {
          if (isAfter(_fDate, lastDate)) {
            lastDate = _fDate;
            }
        }
      }
    });
  };
  
  DFS(lastDate);
  return lastDate;
};

DFS알고리즘을 통해 연결된 일정의 시작과 종료날짜를 찾고 그 부분만 호출해 렌더링하기때문에 시간이 4.3초에서 2.7초로 단축되었다. 하지만 탐색 경로를 보면 한번 들렸던 부분도 또 들리는 부분을 캐치했다. check 방식을 통해 들렸던 부분은 다시 들리지않는 방식을 하면 더 단축할 수 있을것같다.

해결3

해결2 방법으로도 많은 시간을 단축했지만 더 단축 할 수 있는 여지가 있습니다. check 방식을 통해 들렸던 곳은 다시 들리지 않게 해보겠습니다.

아래 코드를 추가하고 조건문에 방문여부를 확인하는 로직을 추가하겠습니다.

const check: Record<string, boolean> = {};

if(!check[_fDate])
const getScheduleStartDate = (startDate: string) => {
  const check: Record<string, boolean> = {}; // 추가
  let firstDate = format(new Date(startDate), 'yyyy-MM-dd');
  
  const DFS = (date: string) => {
    schedule.scheduleByDate[date]?.forEach((event) => {
      if (event) {
        const _fDate = fDate(event?.startDate);
		if (!event.startingDay && !check[_fDate]) { // 방문여부 체크
          DFS(_fDate);
        } else {
        // 훨씬 단축됨
          check[_fDate] = true; // 방문함
          if (isBefore(_fDate, firstDate)) {
            firstDate = _fDate;
          }
        }
      }
    });
  };
  
  DFS(firstDate);
  return firstDate;
};

const getScheduleLastDate = (endDate: string) => {
  const check: Record<string, boolean> = {}; //추가
  let lastDate = format(new Date(endDate), 'yyyy-MM-dd');

  const DFS = (date: string) => {
    schedule.scheduleByDate[date]?.forEach((event) => {
      if (event) {
        const _fDate = fDate(event?.endDate);

        if (!event.endingDay && !check[_fDate]) { // 방문여부 체크
          DFS(_fDate);
        } else {
          // 훨씬 단축됨
          check[_fDate] = true; // 방문함
          
          if (isAfter(_fDate, lastDate)) {
            lastDate = _fDate;
            }
        }
      }
    });
  };
  
  DFS(lastDate);
  return lastDate;
};

check 방식을 통해 탐색의 횟수를 줄였고 당연히 렌더링 걸리는 시간도 단축했다.

마무리

코딩 테스트를 준비하면서 공부한 알고리즘을 이렇게 사용해 볼 줄은 몰랐다. 솔직히 프론트엔드가 알고리즘을 그렇게까지 알아야하나라는 생각이 있었는데 이번 계기로 생각이 완전히 바뀌었다. 알고리즘이 중요하다고 많이들 들어왔지만 직접 체감을 해보니 느낌이 달랐다. 알고리즘을 적용했을뿐인데 4.3초에서 2.4초 약 44%의 성능 개선을 얻었다. 알고리즘 공부를 더 열심히 해야겠다는 욕구가 생긴 계기가 되었다.

profile
프론트엔드 뭐시기 주로 하는 사람

0개의 댓글