React Study Code Cahllenge중 Pomodoro라는걸 만드는 과정이 있었다.
사실 Requirements를 읽어봤을때 크게 어려워 보이는 부분은 없어서 "금방 하겠네~" 라고 생각하고 덤볐는데 나의 큰 착각 이었다ㅋㅋ
일단 Requirement는 간단하게 25분 타이머를 만들라는 내용이였다.
따라서 25분에서 0초가 될때가지 1초씩 시간을 감소하면서 화면에 남은 시간만 보여주면 됐다. (물론 이쁘게 꾸며야지?)
1500초(25 * 60)에서 시작해서 1초마다 1씩 감소시키는 코드를 구현했다.
그런데 아래와 같이 처음 start button을 누른 직후에만 -1이 되었고, 콘솔창에는 1초간격으로 1500만 계속 출력되고 있었다.
import { useEffect, useState } from "react";
function App() {
const [time, setTime] = useState(25 * 60 * 1);
const [isStart, setIsStart] = useState(false);
function computeTime(t: number) {
return --t;
}
function onButtonPress() {
setIsStart((prev) => !prev);
}
useEffect(() => {
if (isStart) {
setInterval(() => {
const left = computeTime(time);
setTime(left);
console.log(`left time seconds: ${time}`);
}, 1000);
}
}, [isStart]);
return (
<div>
<button onClick={onButtonPress}>START</button>
<p>
{time}
</p>
</div>
);
}
export default App;
빠르게 짱구를 굴려서 생각을 정리해봤다.
그래서 setInterval 내부에서 업데이트가 정상적으로 이루어지지 못하고 있고, state를 제대로 못가지고 오거나
아니면 state를 제대로 업데이트를 못하거나
둘 중 하나라고 생각했다.
분석할때는 computeTime
함수가 정상적으로 time을 못받아와서 계속 초기값만 받아들이나? 생각도 해봤는데, 원인은 setInterval 내부에 중첨함수인 setTime이 클로저가 되었기 때문이닷
클로저를 확인하기 이전에 현재 코드의 스코프 구조를 살펴보아야 한다.
자바스크립트의 스코프는 렉시컬 스코프인데, 이는 함수가 어디에서 호출되었는지는 중요하지 않고 어디에서 선언되었는지가 스코프를 결정한다는 내용이다.
아직 나도 자바스크립트에 경험이 많지 않아서 클로저와 스코프의 개념에 미숙하지만ㅠㅠ
가장 외부 함수인 App에서 const [time, setTime] = useState(25 * 60 * 1);
으로 선언하면서 time의 초기값은 1500이 된다.
그리고 중첩함수(setInterval 내부함수)에서 setTime
을 통해서 1500에서 -1한 값을 업데이트를 하고있다.
여기가 포인트인데, setInterval 내부 함수에서 time을 선언하지 않았지만, 외부함수 App에 있는 time이라는 값을 참조해서 사용하고 있는걸 알 수 있다.
자바스크립트의 함수는 중첨함수(내부함수)에서 선언되지 않은 데이터를 외부함수에서 참조하는데.
이때 방향을 반대로해서, 즉 내부함수 → 외부함수를 참조한다.
그리고 클로저는 이 과정에서 최초로 참조한 값을 계속 가지고 있게 된다🤦♂️
사실 생각해보면 맞는말이기는 하다? 스크립트가 처음에 실행되면서 클로저가 기억할 수 있는 상태는 초기 상태이지 중간상태를 기억할 수 있는건 아니지 않나?
따라서 렉시컬 스코프인 App이 호출이 종료되면(return을 만나면 함수가 종료되지, 즉 화면에 렌더링이 되면) 지역변수인 time도 종료가 되면서 실행 컨텍스트 스택에서 제거가 되었지만,
렉시컬 환경까지는 손을대지 않기에 내부함수(중첩함수) 즉, setInterval 내부함수에서 time을 자유변수로 참조할 수 있게된다.
따라서 time의 초기값만을 계속 참조한다고 볼 수 있겠다.
대안으로는 useState set함수에 update function을 사용하면 되는데, setTime(prev => prev - 1);
이렇게 사용하게 되면, set함수의 콜백함수가 클로저가 되어서 prev를 참조할 수 있게 된다고 한다.
사실 처음에 코드를 작성할때 minutes, secconds를 각각 따로 계산 하는 구조로 잡았다.
여차저차 update function을 못쓰는 구조가 되었는데 다시한번 구조를 잘 잡아야 한다는걸 느낌과 동시에, 이번 코드챌린지로 작성한 뽀모도로 결과물도 올려본다.