diff --git a/frontend/jest.config.json b/frontend/jest.config.json index 404f38d60..c0bd17adf 100644 --- a/frontend/jest.config.json +++ b/frontend/jest.config.json @@ -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)$": "/src/mocks/fileMock.js" }, - "setupFiles": ["./jest.polyfills.js"], + "setupFiles": ["./jest.polyfills.js", "./src/mocks/worker.js"], "setupFilesAfterEnv": ["/jest.setup.ts"], "clearMocks": true } diff --git a/frontend/src/mocks/worker.js b/frontend/src/mocks/worker.js new file mode 100644 index 000000000..e69989bf9 --- /dev/null +++ b/frontend/src/mocks/worker.js @@ -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; diff --git a/frontend/src/pages/GamePage/components/SelectContainer/Timer/Timer.test.tsx b/frontend/src/pages/GamePage/components/SelectContainer/Timer/Timer.test.tsx index 2927d59d8..a3c634662 100644 --- a/frontend/src/pages/GamePage/components/SelectContainer/Timer/Timer.test.tsx +++ b/frontend/src/pages/GamePage/components/SelectContainer/Timer/Timer.test.tsx @@ -1,6 +1,3 @@ -import { act, renderHook } from '@testing-library/react'; - -import useTimer from './hooks/useTimer'; import { convertMsecToSecond, formatLeftRoundTime } from './Timer.util'; describe('Timer 테스트', () => { @@ -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(); - }); - }); }); diff --git a/frontend/src/pages/GamePage/components/SelectContainer/Timer/hooks/timerWorker.ts b/frontend/src/pages/GamePage/components/SelectContainer/Timer/hooks/timerWorker.ts new file mode 100644 index 000000000..abdb37182 --- /dev/null +++ b/frontend/src/pages/GamePage/components/SelectContainer/Timer/hooks/timerWorker.ts @@ -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); + } +}; diff --git a/frontend/src/pages/GamePage/components/SelectContainer/Timer/hooks/useTimer.ts b/frontend/src/pages/GamePage/components/SelectContainer/Timer/hooks/useTimer.ts index 3d0a4cda1..8760b7ec8 100644 --- a/frontend/src/pages/GamePage/components/SelectContainer/Timer/hooks/useTimer.ts +++ b/frontend/src/pages/GamePage/components/SelectContainer/Timer/hooks/useTimer.ts @@ -11,11 +11,27 @@ interface UseTimerProps { const useTimer = ({ timeLimit, isSelectedOption, isVoted, vote }: UseTimerProps) => { const [leftRoundTime, setLeftRoundTime] = useState(timeLimit); + const workerRef = useRef(null); const isVoteTimeout = leftRoundTime <= 0; const isAlmostFinished = leftRoundTime <= ALMOST_FINISH_SECOND; - const timeout = useRef(); + 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) { @@ -23,20 +39,11 @@ const useTimer = ({ timeLimit, isSelectedOption, isVoted, vote }: UseTimerProps) 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 }; };