레포지토리 설명
react 프로젝트에서 사용하기 좋은 다양한 hook을 제공하는 라이브러리
ISSUE링크
PR링크
문제 상황
•
onMount 상황을 제외하고 호출되는 useEffect인 useUpdateEffect 에 대한 이슈
원인 파악
•
정확한 확인을 위해 로컬에서 CRA를 사용해 리액트 프로젝트를 재현 후 체크
◦
react, react-dom이 18버전인 경우 onMount 시 호출되지 않음
◦
react, react-dom이 19버전인 경우 onMount 시 두 번 호출 됨
•
원인을 좁히기 위해 useUpdateEffect 의 코드를 살펴보았다
// react-use/src/useUpdateEffect.ts
import { useEffect } from 'react';
import { useFirstMountState } from './useFirstMountState';
const useUpdateEffect: typeof useEffect = (effect, deps) => {
const isFirstMount = useFirstMountState(); // 첫 번째 마운트 여부 확인
useEffect(() => {
if (!isFirstMount) { / 첫 번째 마운트가 아닐 경우에만 effect 실행
return effect();
}
}, deps); // 의존성 배열 deps가 변경될 때 실행됨
};
export default useUpdateEffect;
JavaScript
복사
// react-use/src/useFirstMountState.ts
import { useRef } from 'react';
export function useFirstMountState(): boolean {
const isFirst = useRef(true); // 첫번째 랜더링 유뮤를 판별
if (isFirst.current) { // isFirst.current 값이 true라면 (첫 번째 렌더링 시)
isFirst.current = false; // 첫 번째 렌더링 후, false로 변경
return true; // 첫 번째 렌더링이면 true 반환
}
return isFirst.current; // 이후 렌더링에서는 항상 false 반환
}
JavaScript
복사
•
useRef를 통해 첫번째 랜더링 여부를 관리하고 useEffect에서 분기처리를 하고 있었다
•
직접 언급 된 변경사항은 없어 문서를 읽던 중 strict mode 내용이 눈에 들어왔다
StrictMode changes
React 19 includes several fixes and improvements to Strict Mode.
When double rendering in Strict Mode in development, useMemo and useCallback will reuse the memoized results from the first render during the second render. Components that are already Strict Mode compatible should not notice a difference in behavior.
As with all Strict Mode behaviors, these features are designed to proactively surface bugs in your components during development so you can fix them before they are shipped to production. For example, during development, Strict Mode will double-invoke ref callback functions on initial mount, to simulate what happens when a mounted component is replaced by a Suspense fallback.
•
변경사항이 있다면 두 번 호출되는 부분이 strict mode 의 동작을 의심할만하다는 생각이 들었다
•
단순히 react 19 뿐만 아니라 strict mode 사용이 이슈의 발동조건이었다!
해결방안
•
ahook에는 이슈에서 언급 된 것처럼 동일한 기능을 하는 useUpdateEffect 훅이 있었다
•
use-hook과 비슷한 기능을 제공하지만 useEffect를 사용해 구현하여 19에서도 문제가 없었다.
•
첫번째 랜더링 여부를 바꾸어주는 로직이 useEffect 안에 존재하면서 문제가 해결된 것으로 보였다
◦
의존성 배열에 [] 를 사용해 한 번만 동작하는 것을 더 확실히 보장하거나
◦
useEffect의 특성상 타이밍이 달라지면서인 것으로 생각되었다
•
더 정확히 18 ver. 와 19 ver. 에서의 동작 차이를 보기 위해 콘솔을 찍어 보았다
// src/useUpdateEffect
import { useEffect } from 'react';
import { useFirstMountState } from './useFirstMountState';
const useUpdateEffect: typeof useEffect = (effect, deps) => {
const isFirstMount = useFirstMountState();
console.log('update', isFirstMount);
useEffect(() => {
console.log('update-useEffect', isFirstMount);
if (!isFirstMount) {
return effect();
}
}, deps);
};
export default useUpdateEffect;
JavaScript
복사
import { useRef } from 'react';
export function useFirstMountState(): boolean {
const isFirst = useRef(true);
if (isFirst.current) {
isFirst.current = false;
console.log('first-if', isFirst.current);
return true;
}
return isFirst.current;
}
JavaScript
복사
import { useLayoutEffect, useState } from 'react';
import { useUpdateEffect } from 'react-use';
function App() {
console.log('App rendered');
const [count, setCount] = useState(0);
useUpdateEffect(() => {
console.warn('useUpdateEffect triggered');
}, [count]);
useLayoutEffect(() => {
console.log('useLayoutEffect: DOM updated');
});
return (
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
);
}
export default App;
JavaScript
복사
수정 전 코드에 대한 리액트 버전별 차이
수정 전 (React 18 + strict mode)
수정 전 (React 19 + strict mode)
수정 후 코드에 대한 리액트 버전별 차이
import { useRef, useEffect } from 'react';
export function useFirstMountState(): boolean {
const isFirst = useRef(true);
useEffect(() => {
isFirst.current = false;
console.log('first-useEffect', isFirst.current);
return () => {
isFirst.current = true;
};
}, []);
return isFirst.current
}
JavaScript
복사
수정 후 (React 18 + strict mode)
수정 후 (React 19 + strict mode)
•
수정을 통해 strict mode에서도 동일하게 동작하도록 수정을 완료 하였다
추가 탐구
•
ahook의 코드을 참고하여 쉽게 힌트를 얻기는 했지만, 정확한 원인은 이해가 잘 되지 않았다
•
ahook 코드의 git blame을 보면 3년전 수정 내용으로, 특별히 react 19에 맞춘 수정은 없었다
수정 전 (React 19 + strict mode)
•
또한, 리액트 19에서의 두번째 실행의 콘솔을 보면 첫번째 랜더링 시에 상황이 이상하였다
◦
첫번째 랜더링 여부를 나타내는 isFirstMount 가 이미 false 로 시작되고
◦
첫번째 랜더링처럼 useFirstMountState 내부에 있는 first-if 콘솔이 실행되지도 않았다
•
첫번째와 두번째 실행이 정확하게 동일한 콘솔을 보여주는 리액트 18과 완전히 다른 모습이다
수정 전 (React 18 + strict mode)
•
업데이트 문서에는 간략하게만 적혀있어 리액트 19.0.0의 릴리즈 노트를 살펴보았다.
Notable Changes
•
StrictMode changes: useMemo and useCallback will now reuse the memoized results from the first render, during the second render. Additionally, StrictMode will now double-invoke ref callback functions on initial mount.
All Changes
•
Changes to StrictMode
◦
◦
•
all changes에서 의심이 가는 PR은 일부 있었지만, 확신을 가질 정도의 설명이나 예시는 없었다
•
조금 더 살펴보고 싶었지만 PR의 주요 코드가 담긴 파일이 길고 방대하여 쉽지 않은 상황이다
◦
ReactFiberHooks.js (5774 lines)
•
나중에 여유가 생기면 조금 더 살펴보아야 겠다
느낀점
•
완전히 동일한 hook을 구현한 두 개의 라이브러리를 보면서 비교해 배울 수 있었다
◦
ahook의 경우 useEffect와 useLayoutEffect를 의식해 hook을 인자로 받도록 구성하였다
import { useRef } from 'react';
import type { useEffect, useLayoutEffect } from 'react';
type EffectHookType = typeof useEffect | typeof useLayoutEffect;
export const createUpdateEffect: (hook: EffectHookType) => EffectHookType =
(hook) => (effect, deps) => {
const isMounted = useRef(false);
// for react-refresh
hook(() => {
return () => {
isMounted.current = false;
};
}, []);
hook(() => {
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
export default createUpdateEffect;
JavaScript
복사
◦
hook을 인자로 전달하는 것이 새로우면서 상황에 잘 맞는 해결책이라는 생각이 들었다
◦
아무래도 오픈소스의 경우 추상화 레벨이 높다보니 코드를 살펴보는 것이 공부가 많이 된다
•
PR을 올리면서 공통 모듈을 위한 레포지토리를 관리하는 법을 배울 수 있었다
### Commit messages
This repo uses [semantic-release](https://github.com/semantic-release/semantic-release) and [conventional commit messages](https://conventionalcommits.org) so prefix your commits with `fix:` or `feat:` if you want your changes to appear in [release notes](https://github.com/streamich/react-use/blob/master/CHANGELOG.md).
Markdown
복사
◦
PR의 prefix에 따라 릴리즈노트에 보여진다는 내용이었는데
◦
공통 라이브러리의 긴 릴리즈 노트를 보면서 어떻게 관리하나 싶었는데 방법이 다 있었다
◦
기업 레벨의 레포라도 규모에 따라 유용하게 사용할 수 있을 것 같아서 좋은 내용을 배웠다
•
평소 유용하게 잘 사용하던 라이브러리이지만 기여를 하면서 조금 아쉽다는 생각이 들었다
◦
많은 PR과 이슈가 조금 방치된 것처럼 계속해서 시간이 흐르고 있다는 점
◦
PR 업로드 시에 Github Action을 통한 테스트나 린트, 타입 체크 등의 과정이 없다는 점
◦
그래서 앞으로의 적용은 신중하게 진행할 것 같다는 생각이 들었다
◦
반면에 ahook은 이번에 코드를 보면서 좋은 인상을 받았지만
▪
antd를 사용하면서 중국이 주도하는 라이브러리는 중국어의 벽이 있다는 것을 느껴
▪
특정적인 부분에 대해 중국어로 작성되어 있으면 에러핸들링이 어려울 것 같은 생각이다
▪
기술을 고를 때마다 느끼는 거지만 항상 쉽지 않다는 느낌이다..