타이머가 포함된 페이지 리팩토링

seonja kim·2022년 9월 12일
0

현재 유저가 사용하고 있는 서비스에 추가적인 기능을 넣기위해 살펴보다가 문제점을 발견했고, 그걸 어떻게 개선하면 좋을지 고민한 흔적입니다.

페이지의 특성을 설명한 이후, 어떤 문제점이 있었고, 그 문제점을 어떻게 해결했는지 풀어가도록 하겠습니다.


페이지의 특성

15초 또는 25초라는 제한 시간 내에 선택지를 선택하면 다음 문제로 넘어가면서 타이머가 리셋되는 형태입니다.

페이지는 크게 하얀색 부분에 해당하는 문제 부분과 하늘색 부분에 해당하는 타이머 기능, 빨간색 부분에 해당하는 progress bar, 그리고 초록색 부분에 해당하는 사용자가 현재 어떤 파트에 있는지 진행상황을 알려주는 부분으로 나눠볼 수 있습니다.


페이지가 가지고 있던 문제점과 개선 방법

페이지 자체가 가지고 있는 UI 컴포넌트가 적다보니 대부분 하나의 컴포넌트에서 다뤄지고 있었고, 그에 따라 대부분의 로직이 얽혀있었습니다.

1초 마다 clearInterval하고 setInterval 생성

사용자가 선택지를 선택해서 문제가 바꼈는지, 선택하지 않고 시간이 지나가 다음 문제로 넘어가고 타이머를 리셋해야 하는지를 매 초마다 확인하고 있었습니다. 그에 따라 매초마다 clearInterval이 되고 setInterval이 새로 생성되는 현상을 겪고 있었습니다.


  useEffect(() => {
    let interval = null;
    if (timerActive && seconds > 0) {
      interval = setInterval(() => {
        setSeconds((seconds) => seconds - 1);
      }, 1000);
    } else if (!timerActive) {
      clearInterval(interval);
    } else if (
      seconds === 0 &&
      timerActive &&
      !isRequesting.current &&
    ) {
      clearInterval(interval);
      handlePostAnswer('c', 0, questionNo);
    }
    return () => clearInterval(interval);
  }, [
   // some dependencies
  ])

이 문제는 타이머만 따로 컴포넌트로 분리해 해당 타이머 컴포넌트 내부에서 useEffect를 이용해 타이머를 작동시키고 타이머 리셋이 필요할 경우 unmount해서 리셋하는 방식으로 변경이 필요하다고 판단했습니다.
export const Timer = ({ seconds, setSeconds }: TimerProps) => {
  useEffect(() => {
    if (!setSeconds) {
      // 문제 제출 완료를 위해 서버와 통신하는 동안 
      // 해당 컴포넌트를 placeholder처럼 사용
      return; 
    }
    let interval = null;
    interval = setInterval(() => {
      setSeconds((prevSecond) => prevSecond - 1);
    }, 1000);
    return () => clearInterval(interval);
  }, [setSeconds]);

  return (
    // 타이머 UI
  );
};
// 타이머가 작동해야 하는지 판단할 수 있는 요건
{timerActive ? (
  <Timer seconds={seconds} setSeconds={setSeconds} />
  ) : (
  <Timer seconds={seconds} />
)}

이렇게 변경할 경우 언제 clearInterval이 되어야 하는지에 대한 추가적인 고민없이 하나의 컴포넌트에서 setInterval 관련된 기능은 정리가 가능합니다.


타이머 기능이 다른 로직들과 섞이면서 seconds를 update하는 로직 단일화

이 타이머와 관련되어 정말 다양한 side effect들이 발생하고 있었는데요. 그 중 하나가 선택지가 선택되고 다음 문제로 넘어갈 때, seconds를 업데이트하고 타이머가 작동해야 하는지 판단할 수 있는 요건이 문제의 진행도를 알려주는 Part indicator과 연관성 없이 작동하고 있는 부분이었습니다.


  const handleResetTimer = useCallback(() => {
    setSeconds(TIME_LIMIT); 
    setTimerActive(true);
  }, []);

  useEffect(() => {
    if (!questionNo) {
      return;
    }
    handleResetTimer(); // 문제가 변경될 때마다 작동 (로직적으로 문제는 없지만...)
    if (questionNo < 7) {
      setPartNo(1);
    } else if (questionNo < 21) {
      setPartNo(2);
    } else if (questionNo < 45) {
      setPartNo(3);
    } else if (questionNo < 69) {
      setPartNo(4);
    } else if (questionNo < 105) {
      setPartNo(5);
    } 
    return;
  }, [
   // some dependencies
  ]);

handleResetTimer를 제거하고 그 내부 함수들을 답안 제출이 완료되고 서버로 부터 200 또는 201 응답을 받았을 때, seconds를 업데이트하고 timer가 시작될 수 있도록 로직 단일화했습니다.


  const testAnswer = useMutation(
    // server request
    },
    {
      onSuccess: async () => {
        // 문제가 서버에 잘 제출되었을 때 필요한 로직들
        setSeconds(TIME_LIMIT); 
        setTimerActive(true);
      },
      onError: async (err) => {
        // 에러일 때 필요한 로직
      },
    },
  );

Part 표시를 위한 불필요한 state 관리

위의 handleResetTimer의 예시에서 발견할 수 있는 또 다른 문제는 Part 표시는 단순한 함수로 구현할 수 있음에도 state로 관리하고 useEffect를 이용해서 체크하고 있는 부분입니다.


  const setPartNumber = (questionNumber) => {
    const endPoint = [7, 21, 45, 69, 105];
		for (let i = 0; i < endPoint.length; i++) {
      if (questionNumber < endPoint[i]) return i + 1
    }
  };

이와 같이 함수만 하나 생성해서 값을 return할 경우 추가적인 state 관리와 useEffect를 이용해서 매번 확인해야할 필요가 사라집니다.

결론

  • 한 페이지에서 setInterval을 여러번 이용할 경우 되도록이면 분리해서 복잡하게 언제 clearInterval해야하는지 추가적인 고민을 줄이도록 합니다.
  • 불필요한 state 관리나 useEffect 사용이 없는지 항상 고민하고 줄일 수 있는 방안을 생각해봅니다.

개인적으로 이번 기회를 통해 코드를 어떻게 개선할 수 있을지 고민하는 걸 즐기는 편이라는 걸 깨달았습니다. 개선하기 위해서는 더 많이 알아야 하고, 그만큼 좋은 코드를 많이 보고 배워야겠다는 생각이 들었습니다. 선배님들께, 더 나은 방안이 있으시다면 조언 부탁드립니다!:)

profile
Adventurer

0개의 댓글