이름 지우는게 너무 힘들었다.
항해99 과정에서 선의의 경쟁? 을 위해 출석체크를 하는 페이지가 있는데,
이 페이지의 시계는 5시간정도 뒤에 보면 1시간이상 느려져있다.
우리는 아마 항해99의 서버시간을 get요청으로 가져와서,
현재시간을 기반으로 setTimeout으로 타이머를 작동시키는 것으로 보인다.
setTimeout은 세팅한 시간이 되면 브라우저가 큐로 던져주고, 이벤트루프가 콜스택이 비어있는걸 확인하게 되면 settimeout를 스택으로 던져주고, 내부의 콜백함수가 실행되게 된다.
즉, setTimeout에 세팅한 시간은 해당 시간이 되면 콜백함수를 실행해줘! 가 아니라,
해당 시간이 지나면 콜백함수를 실행해줘! 의 의미인 것이다.
이런 오차는 계속 쌓이기때문에, 아무리 콜스택이 비어있더라도 이벤트루프의 검사시간등이 누적되게 된다.
setInterval 은 이런 오차를 다음 실행되는 시간에서 가감하여 보정하기 때문에 오차는 있지만 누적은 되지 않는다.
아마 항해99에서는 setTimeout을 이용해서 오차가 누적되고 있는것으로 보임.
너무 궁금해서 소스코드를 파봄
아 리엑트도 이렇게 다 노출이 되는구나.
timer는 checkInTimer를 import해오고 있다. 상위4개폴더 위에 state/check에 있다고 한다.
check.js로 와보니
import { atom } from "jotai";
import { secondsToString } from "../businessLogics/utils/_helper";
import { ClassChecks, UserCheckIn } from "../queries/api/checks";
interface ClickedMemberType {
name: string;
firstCheckIn: null | string;
}
export const isChecksMainPageAtom = atom(true);
export const classChecksInfoAtom = atom<ClassChecks>(
new ClassChecks(0, 0, "", [], [], "", "")
);
export const classChecksInRoundTypeAtom = atom((get) => {
const classChecksInfo = get(classChecksInfoAtom);
return classChecksInfo.roundType;
});
export const selectedWeekNumAtom = atom<string>("");
export const checksRankingAtom = atom((get) => {
const classChecksInfo = get(classChecksInfoAtom);
const result = classChecksInfo.ranks.sort(
(a, b) => b.weekOrder - a.weekOrder
);
return result;
});
export const stringWeeklyTimeAtom = atom((get) => {
const ranksInfo = get(checksRankingAtom);
const week = get(selectedWeekNumAtom);
const selectedRankInfo = ranksInfo.filter(
(rankInfo) => rankInfo.weekOrder === parseInt(week)
);
const result = selectedRankInfo[0].rankedUsers.map((rankInfo) => {
return {
enrolledId: rankInfo.enrolledId,
name: rankInfo.name,
totalTime: secondsToString(rankInfo.totalTime),
1: secondsToString(rankInfo.secondsPerDay[1]),
2: secondsToString(rankInfo.secondsPerDay[2]),
3: secondsToString(rankInfo.secondsPerDay[3]),
4: secondsToString(rankInfo.secondsPerDay[4]),
5: secondsToString(rankInfo.secondsPerDay[5]),
6: secondsToString(rankInfo.secondsPerDay[6]),
0: secondsToString(rankInfo.secondsPerDay[0]),
};
});
return result;
});
export const teamsChecksInfoAtom = atom((get) => {
const classChecksInfo = get(classChecksInfoAtom);
return classChecksInfo.teamChecks;
});
export const checkInInfoModalAtom = atom(false);
export const clickedMemberAtom = atom<ClickedMemberType>({
name: "",
firstCheckIn: null,
});
export const checkInStartStopModalAtom = atom(false);
export const checkInTypeAtom = atom<string>("");
export const checkInUserInfoAtom = atom<UserCheckIn>({} as UserCheckIn);
export const formattedcheckUserInfoAtom = atom((get) => {
const userCheckInfo = get(checkInUserInfoAtom);
const formattedUserChecksInfo = {
...userCheckInfo,
firstCheckinTime:
userCheckInfo.logs &&
userCheckInfo.logs[userCheckInfo.logs.length - 1]?.type === "in"
? userCheckInfo.logs[userCheckInfo.logs.length - 1].time
.split("T")[1]
.slice(0, -5)
: "-",
};
return formattedUserChecksInfo;
});
export const checkInTimerAtom = atom<number>(0);
export const checkInTimerStartAtom = atom<boolean>(false);
맨아래 checkInTimerAtom = atom<number>(0); 이것같은데,
import { atom } from "jotai"; ?
https://velog.io/@deli-ght/jotai-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90-%EC%9D%B5%ED%9E%88%EA%B8%B0
Jotai 기본 개념 익히기
추가 리렌더링 없음, 리액트에 속한 상태, 그리고 서스펜스와 병렬 기능들의 장점들을 모두 취할 수 있고, 심플한 react.useState 대체재부터 복잡한 요구사항을 가진 큰 스케일의 애플리케이션까지
velog.io
아 이런게 있구나...
여기에서 jotai라는 전역상태 라이브러리에 시간을 저장하고 있다.
3) Atomic 패턴 - Recoil, Jotai
React의 state와 비슷하게, 컴포넌트 트리 안에 상태들이 존재하며 이들이 상향식(bottom-up)으로 수집 및 공유되는 패턴이다.
상태들은 atom이라고 불리는 객체에서 설정하며, 값의 참조와 조작은 React.useState와 유사하게 [state, setState] 튜플로 수행한다.
Store에서 하향식(top-down)으로 관리되던 기존 패턴과 매우 다르기에, 다른 라이브러리보단 React의 Hooks 및 Context API와 많이 비교된다.
atom이라는 객체에 상태를 설정해주는구나.
그럼 다시 돌아와서 여길 보면
timer.tsx
이런 전역상태 라이브러리는 props처럼 직접 전달을 하지 않으므로 검색기능을 이용해서
checkInTimerAtom을 useAtom훅을 이용해 업데이트하는 곳을 찾아야 한다 .
오 몇개 안나온다
여기서 확인 못했던 곳이 timerbutton.tsx인데,
버튼에는 버튼만 있을것 같아서 살펴보지 않았었다.
아래는 timerButton.tsx
import { useEffect } from "react";
import * as S from "./TimerButton.style";
import { usePostChecks } from "../../../../queries/checks";
import { useAtom } from "jotai";
import {
checkInStartStopModalAtom,
checkInTimerAtom,
checkInTimerStartAtom,
checkInTypeAtom,
checkInUserInfoAtom,
checksRankingAtom,
classChecksInRoundTypeAtom,
} from "../../../../states/checks";
export default function TimerButton() {
const buttonText = ["Start", "Stop", "Logs"];
const [, setType] = useAtom(checkInTypeAtom);
const [, setTime] = useAtom(checkInTimerAtom);
const [timer, setTimer] = useAtom(checkInTimerStartAtom);
const [checkInUserInfo] = useAtom(checkInUserInfoAtom);
const [modal, setModal] = useAtom(checkInStartStopModalAtom);
const [rankingList] = useAtom(checksRankingAtom);
const [course] = useAtom(classChecksInRoundTypeAtom);
const { mutate } = usePostChecks();
const clickButton = (text) => {
if (
(text === "Start" && checkInUserInfo.isRunning) ||
(text === "Stop" && !checkInUserInfo.isRunning)
)
return;
text === "Start"
? setTimer(true)
: text === "Stop"
? setTimer(false)
: null;
setType(text);
setModal(true);
if (text === "Logs") return;
const checkInType = text === "Start" ? "in" : "out";
mutate({ userId: checkInUserInfo.userId, type: checkInType });
};
useEffect(() => {
let interval = null;
if (timer) {
interval = setInterval(() => {
setTime((prevTime) => prevTime + 1);
}, 1000);
} else {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timer]);
useEffect(() => {
modal
? (document.body.style.overflow = "hidden")
: (document.body.style.overflow = "unset");
}, [modal]);
return (
<>
{buttonText.map((text, idx) => (
<S.StyledButton
course={course}
buttonText={text}
key={idx}
onClick={() => {
clickButton(text);
}}
disabled={!rankingList.length}
>
{text}
</S.StyledButton>
))}
</>
);
}
오호
useEffect(() => {
let interval = null;
if (timer) {
interval = setInterval(() => {
setTime((prevTime) => prevTime + 1);
}, 1000);
} else {
clearInterval(interval);
}
여기서 timer이 true(버튼을 클릭함)일때 setInterval을 이용해서 타이머가 구현되고,
const [, setTime] = useAtom(checkInTimerAtom);
1초마다 setTime으로 useAtom을 실행해서 상태를 업데이트해준다 .
그리고 timer가 false일때(stop버튼) clearInterval로 타이머가 정지된다.
아 이게 setInterval로 해도 오차가 쌓이는걸 완전히 막을순 없구나.
사실 결국 이 타이머도 최초에 서버에서 시간을 받아오는 그 하나의 정보를 제외하곤
클라이언트 브라우저 내부에서 모든 연산이 실행되기 때문에,
크롬 브라우저가 백그라운드에서 cpu사용량을 줄이기 위해 setInterval, setTimeout 실행빈도를 줄여버리면, 이 정확성은 떨어질수 밖에 없다. 또한 전역 상태 라이브러리등으로 1초마다 데이터가 오가면서도 오차가 쌓일 수 있고, 여러가지 작업 처리로 인해 콜백이 지연되었지만 setInterval 자체이 보정기능으로는 보정이 충분치 않을수 있다.
결국 여기서 정확한 시간을 보여주려면, setInterval의 콜백함수로 매초 현재시간을 새로 불러와서 보여줘야 한다 .
여기서는 Date.now() API를 사용해서 수정해봄.
let startTime;
const startTimer = () => {
startTime = Date.now();
setInterval(() => {
let elapsedTime = Date.now() - startTime; // 밀리초
// 초로 변환하여 상태 업데이트
setTime(elapsedTime / 1000);
}, 1000);
};
이건 결국 사용자의 시스템시계(컴퓨터 내장 시계)를 기반으로 동작하므로 스크립트의 자체 문제로 인한 지연은 없지만, 결국 서버시간을 정확하게 보여주는건 아니고 사용자의 시계를 기반으로 보여줄 뿐이다.
그러나 매초 서버 시계를 불러오는건 서버의 부하를 과도하게 줄수 있으므로, 이정도로 보정을 해주는게 바람직해 보인다.
뜯어보니 흥미로움.