diff --git a/blog/staleclosureissue/esc.jpeg b/blog/staleclosureissue/esc.jpeg new file mode 100644 index 0000000..36d2546 Binary files /dev/null and b/blog/staleclosureissue/esc.jpeg differ diff --git a/blog/staleclosureissue/index.mdx b/blog/staleclosureissue/index.mdx new file mode 100644 index 0000000..078a182 --- /dev/null +++ b/blog/staleclosureissue/index.mdx @@ -0,0 +1,107 @@ +--- +title: "React 개발을 하면서 마주한 javascript 이슈들." +date: "2024-07-07" +slug: "react-javascript-issue" +tags: ["React", "javascript"] +hero_image: "./esc.jpeg" +hero_image_alt: "esc" +hero_image_credit_link: "" +hero_image_credit_text: "" +--- + +기초를 중요히하자! javascript 기본의 중요성에 대해 더더욱 깨달은 최근 1~2주였다. + +### 이슈 1 : stale closure issue + +개발을 하던 중 무슨 짓을 해도 React에서 state 값을 읽으려고 할 때마다 +state의 초기값을 읽는 이슈가 생겼다. + +```javscript + const [play, setPlay] = useState(true); + + const onPlayVideo = () => { + if(!play){ // stale closure 이슈 발생 + setPlay(true); + ... + } + } + + const onPauseVideo = () => { + if(play){ + setPlay(false); + ... + } + } + + + useEffect(() => { + const onVisibilityChange = () => { + if (document.visibilityState === 'hidden') { + onPauseVideo(); + ... + } else if (document.visibilityState === 'visible') { + onPlayVideo(); // stale closure 이슈 발생 + ... + } + }; + + // 다른 페이지, 앱, 탭으로 변경 시 동작 + document.addEventListener('visibilitychange', onVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', onVisibilityChange); + }, []); + +``` + +화면이 다른 화면으로 바뀔 때, play를 false로 업데이트하고, 다시 들어올 때 play값이 당연히 false라고 생각했는데, +계속해서 true값으로 읽어 다시 재생이 되지 않았다. + +onVisibilityChange 핸들러가 초기의 play 상태값(오래된 값)을 바라보고 있기 때문이었다. +stale closure issue라고 아주 흔하게 발생하는 문제였는데, +내가 그동안 React안에서만 전체 코드를 바라보고 있다는 것을 깨달았다. + +해당 이슈를 해결하기 위해서는 적절한 useEffect 의존값 설정(play를 useEffect의 의존성으로 설정), +useState 사용시 업데이터 함수 이용하기, useRef 사용하기 등이 best practice로 제시되는 듯했다. +우선 해당 컴포넌트는 이벤트 핸들러를 많이 달아야하는데, +자주 변경되는 play 값을 기준으로 계속해서 추가/해제 하는 액션이 부담스러웠기 때문에 의존성을 추가하는 방향은 피했다. +따라서 play를 useRef로 관리하기로 했다. 렌더링과 상관없이 video의 재생여부 혹은 렌더링 이전에 사용자가 재생버튼을 눌렀는지, +등을 알아야하기 때문에 useRef를 사용하여 제어하는 것이 가장 깔끔해보였다. + +### 이슈 2 : useEffect의 cleanup 함수 내의 removeEventListener 미동작 + +앞서 얘기했듯이 해당 컴포넌트는 다양한 이벤트 핸들러를 달아둔 상태였다. +특히 사용자가 페이지 진입 시, 페이지를 떠날 때마다 관련한 이벤트에 대한 핸들러를 많이 달아두었는데 +컴포넌트가 사라질 때 cleanup 함수 내에서 해당 이벤트 핸들러를 적절히 해제하는 작업이 필요했다. + +그러나 사용자가 해당 페이지를 떠날 때 발생하는 핸들러 액션과 cleanup 펑션 내에서 이벤트 핸들러를 해제하는 액션이 충돌하는 듯하였다. +cleanup 함수 내까지는 진입하는데 이벤트 동작이 해제되지 않았기 때문이다. + +사실 removeEventListener가 제대로 동작하지 않는 줄 모르고 있었다. +최초 페이지 진입 이후 나갔다가 다른 페이지에서 다시 들어올 때마다 영상이 멈췄다가 재생되는 것이 +찰나에 일어나기 때문에 눈으로는 확인하기 어려운 일이기 때문이었다. +하지만 영상재생/중지 시마다 나가야하는 api 호출이 있어 해당 api 호출을 하는 것을 보며 알게되었다. +정말 식겁할 뻔했다!! 모든 페이지에 내가 만든 핸들러가 달릴 뻔ㅜㅜ +dom 조작하는 것이 두려운데 visibilitychange 같은 건 불가피하게 써줘야하기 때문에 +주의해서 잘 써줘야하는듯하다! + +> If an event listener is removed from an EventTarget while another listener of the target is processing an event, it will not be triggered by the event. + +[EventTarget.removeEventListener](https://developer.mozilla.org/ko/docs/Web/API/EventTarget/removeEventListener) + +결국 이렇게 충돌하는 문제에 대해서는 cleanup 함수 내에서 removeEventListener 호출부를 setTimeout으로 감싸서 해결하였다. +javascript의 비동기 큐(macro queue)에 넣어 실행 순서를 강제로 늦춘 것이다. 다른 방법을 알면 더 좋은 방법을 썼을 것 같지만 +일단은 여기까지가 한계였다..! + +```javascript + return () => { + setTimeout(() => { + document.removeEventListener('visibilitychange', onVisibilityChange); + }, 0) + }, []); +``` + +블로그를 쓰다보니 더 부족한 것도 보였다. +[페이지 수명주기](https://developer.chrome.com/docs/web-platform/page-lifecycle-api?hl=ko#how_to_observe_state_changes)에 대해 안일하게 생각한 부분도 있는 것 같고, +여전히 useState 뿐만 아니라 useEffect 그리고 더 다양한 react 훅에 대한 이해가 완벽하지 않는 것 같다. +앞으로 이런 부분을 좀 더 공부해보아야겠다