현재 유저가 사용하고 있는 서비스에 추가적인 기능을 넣기위해 살펴보다가 문제점을 발견했고, 그걸 어떻게 개선하면 좋을지 고민한 흔적입니다.
페이지의 특성을 설명한 이후, 어떤 문제점이 있었고, 그 문제점을 어떻게 해결했는지 풀어가도록 하겠습니다.
15초 또는 25초라는 제한 시간 내에 선택지를 선택하면 다음 문제로 넘어가면서 타이머가 리셋되는 형태입니다.
페이지는 크게 하얀색 부분에 해당하는 문제 부분과 하늘색 부분에 해당하는 타이머 기능, 빨간색 부분에 해당하는 progress bar, 그리고 초록색 부분에 해당하는 사용자가 현재 어떤 파트에 있는지 진행상황을 알려주는 부분으로 나눠볼 수 있습니다.
페이지 자체가 가지고 있는 UI 컴포넌트가 적다보니 대부분 하나의 컴포넌트에서 다뤄지고 있었고, 그에 따라 대부분의 로직이 얽혀있었습니다.
사용자가 선택지를 선택해서 문제가 바꼈는지, 선택하지 않고 시간이 지나가 다음 문제로 넘어가고 타이머를 리셋해야 하는지를 매 초마다 확인하고 있었습니다. 그에 따라 매초마다 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
])
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 관련된 기능은 정리가 가능합니다.
이 타이머와 관련되어 정말 다양한 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) => {
// 에러일 때 필요한 로직
},
},
);
위의 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를 이용해서 매번 확인해야할 필요가 사라집니다.
개인적으로 이번 기회를 통해 코드를 어떻게 개선할 수 있을지 고민하는 걸 즐기는 편이라는 걸 깨달았습니다. 개선하기 위해서는 더 많이 알아야 하고, 그만큼 좋은 코드를 많이 보고 배워야겠다는 생각이 들었습니다. 선배님들께, 더 나은 방안이 있으시다면 조언 부탁드립니다!:)