Skip to content

Commit

Permalink
merge: 탭 전환 시 발생하는 타이머 오차를 Web Worker를 활용하여 사용자 경험 개선 #403
Browse files Browse the repository at this point in the history
[REFACTOR] 탭 전환 시 발생하는 타이머 오차를 Web Worker를 활용하여 사용자 경험 개선
  • Loading branch information
rbgksqkr authored Nov 22, 2024
2 parents dbded16 + afd961d commit f41d55e
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 68 deletions.
2 changes: 1 addition & 1 deletion frontend/jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"\\.[jt]sx?$": "@swc/jest",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/src/mocks/fileMock.js"
},
"setupFiles": ["./jest.polyfills.js"],
"setupFiles": ["./jest.polyfills.js", "./src/mocks/worker.js"],
"setupFilesAfterEnv": ["<rootDir>/jest.setup.ts"],
"clearMocks": true
}
15 changes: 15 additions & 0 deletions frontend/src/mocks/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default class TimerWorker {
constructor(stringUrl) {
this.url = stringUrl;
this.onmessage = () => {};
}

postMessage(message) {
this.onmessage(message);
}

terminate() {}
}

// Jest 환경에서 Web Worker를 Mock으로 대체
window.Worker = TimerWorker;
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { act, renderHook } from '@testing-library/react';

import useTimer from './hooks/useTimer';
import { convertMsecToSecond, formatLeftRoundTime } from './Timer.util';

describe('Timer 테스트', () => {
Expand All @@ -17,56 +14,4 @@ describe('Timer 테스트', () => {
expect(convertedTime).toBe(10);
});
});
describe('Timer 훅 테스트', () => {
jest.useFakeTimers();
const voteMock = jest.fn();
const timeLimit = 10;
const timeLimitMs = timeLimit * 1000;

it('타이머가 종료되었을 때 선택 완료를 누르지 않아도 선택된 옵션이 있으면 투표한다.', () => {
const isSelectedOption = true;
const isVoted = false;

const { result } = renderHook(() =>
useTimer({ timeLimit, isSelectedOption, isVoted, vote: voteMock }),
);

act(() => {
jest.advanceTimersByTime(timeLimitMs);
});

expect(result.current.leftRoundTime).toBe(0);
expect(voteMock).toHaveBeenCalled();
});
it('타이머가 종료되었을 때 이미 투표를 했다면 또 투표를 하지 않는다.', () => {
const isSelectedOption = true;
const isVoted = true;

const { result } = renderHook(() =>
useTimer({ timeLimit, isSelectedOption, isVoted, vote: voteMock }),
);

act(() => {
jest.advanceTimersByTime(timeLimitMs);
});

expect(result.current.leftRoundTime).toBe(0);
expect(voteMock).not.toHaveBeenCalled();
});
it('타이머가 종료되었을 때 선택된 옵션이 없다면 투표되지 않고 기권한다.', () => {
const isSelectedOption = false;
const isVoted = false;

const { result } = renderHook(() =>
useTimer({ timeLimit, isSelectedOption, isVoted, vote: voteMock }),
);

act(() => {
jest.advanceTimersByTime(timeLimitMs);
});

expect(result.current.leftRoundTime).toBe(0);
expect(voteMock).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
let intervalId: NodeJS.Timeout;

self.onmessage = function (e) {
const { type, delay } = e.data;

if (type === 'start') {
const startTime = Date.now();

intervalId = setInterval(() => {
const elapsedTime = Date.now() - startTime;

self.postMessage({ elapsedTime });
}, delay);
} else if (type === 'stop') {
clearInterval(intervalId);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,39 @@ interface UseTimerProps {

const useTimer = ({ timeLimit, isSelectedOption, isVoted, vote }: UseTimerProps) => {
const [leftRoundTime, setLeftRoundTime] = useState(timeLimit);
const workerRef = useRef<Worker | null>(null);

const isVoteTimeout = leftRoundTime <= 0;
const isAlmostFinished = leftRoundTime <= ALMOST_FINISH_SECOND;

const timeout = useRef<NodeJS.Timeout>();
useEffect(() => {
const timerWorker = new Worker(new URL('./timerWorker.ts', import.meta.url));
workerRef.current = timerWorker;

timerWorker.postMessage({ type: 'start', delay: POLLING_DELAY });

timerWorker.onmessage = () => {
setLeftRoundTime((prev) => prev - 1);
};

// 타이머가 끝나기 전에 투표가 완료될 경우 clean-up
return () => {
timerWorker.postMessage({ type: 'stop' });
timerWorker.terminate();
};
}, []);

useEffect(() => {
if (isVoteTimeout) {
if (isSelectedOption && !isVoted) {
vote();
}

clearInterval(timeout.current);
workerRef.current?.postMessage({ type: 'stop' });
workerRef.current?.terminate();
}
}, [isVoteTimeout, isSelectedOption, isVoted, vote]);

useEffect(() => {
timeout.current = setInterval(() => {
setLeftRoundTime((prev) => prev - 1);
}, POLLING_DELAY);

return () => {
clearInterval(timeout.current);
};
}, [timeLimit]);

return { leftRoundTime, isAlmostFinished };
};

Expand Down

0 comments on commit f41d55e

Please sign in to comment.