3년차가 되어서야, 클린 코드의 중요성을 깨달았다. 이제부터라도 최대한 좋은 소스를 많이 보면서 작성하려고 하는데, 그것의 처음이 useToggle hook이었다.
tr;dr -
1. 인터넷에서 useToggle을 useReducer로 구현하는 방식을 봄.
2. 해당 pull request는 훅의 사용 수를 줄이는(기존: useCallback, useState -> useReducer 하나) optimization으로 제안되었고 merge되었는데 기존 방식에 비해 어떤게 나아졌는지 궁금해짐.
3. hooks가 어떻게 돌아가는지 알아야할 필요가 있음. 그것을 확인하려면 react fiber structure와 기존 structure를 좀 심도 있게 확인해야 할 필요가 있음.
import { useCallback, useState } from 'react';
// Usage
function App() {
// Call the hook which returns, current value and the toggler function
const [isTextChanged, setIsTextChanged] = useToggle();
return (
<button onClick={setIsTextChanged}>{isTextChanged ? 'Toggled' : 'Click to Toggle'}</button>
);
}
// Hook
// Parameter is the boolean, with default "false" value
const useToggle = (initialState = false) => {
// Initialize the state
const [state, setState] = useState(initialState);
// Define and memorize toggler function in case we pass down the component,
// This function change the boolean value to it's opposite value
const toggle = useCallback(() => setState(state => !state), []);
return [state, toggle]
}
일반적으로 사용했던 useToggleHook은 다음과 같다.
그냥 별다를 것 없이 useState와 useCallback을 써서 state에 boolean값을 담고, toggle 함수를 callback으로 in-memory에 담아서 메모리 손실을 줄인다... 까지 사용하고 있었는데,
https://github.com/streamich/react-use/blob/master/src/useToggle.ts
에서 다른 사용법을 확인할 수 있었다.
import { Reducer, useReducer } from 'react';
const toggleReducer = (state: boolean, nextValue?: any) =>
typeof nextValue === 'boolean' ? nextValue : !state;
const useToggle = (initialValue: boolean): [boolean, (nextValue?: any) => void] => {
return useReducer<Reducer<boolean, any>>(toggleReducer, initialValue);
};
export default useToggle;
useState가 아닌, useReducer를 사용해서 boolean 값을 리턴한다.
pull request 항목을 확인해 보니, 예전 버전의 코드는 일반적인 그것과 같았다.
그런데 useReducer optimization, use a single hook 이라는 제목은 어떻게 된 것일까?
hooks는 하나만 사용하는 것이 무조건 최적화에 좋은 것일까?
아니, 사실 모든 코드라는 것이 많아지면 퍼포먼스에 부담을 주겠지만... 그렇게 깊게 생각해본 적이 없어서 잘 모르겠다.
https://dev.to/wuz/linked-lists-in-the-wild-react-hooks-3ep8
// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
위 설명에 의하면,
react Hooks는 linked list의 형태로 fiber의 memoizedState에 저장된다.
현재 훅 리스트는 현재 fiber에 저장되고, work-in-progress 훅의 변동이 생긴다면 새로운 list를 생성해 work-in-progress fiber에 저장된다.
이걸 확인하려면 react fiber부터 다시 보아야 겠다.
원래 알고있던 내용.
1. 실제 dom rerender가 일어날 때, tree 구조 상 렌더의 숫자가 무분별하게 늘어날 수 있다는 것이다.
내가 알고 있는 가상 돔은..
a. DOM Tree를 자바스크립트 객체로 in-memory에 저장하고,(virtual DOM)
b. 변화가 일어나면 diffing algorithm을 통해 새 virtual DOM과 이전 virtual DOM을 비교하고,
c. 변화가 일어날 경우 변화가 일어난 부분만 DOM에 전달해 rendering 하는데, 이때 브라우저의 layout 단계가 생략되어 더 퍼포먼스가 좋다고 알고 있다.
그렇다면, react의 기존 diffing 메소드와 16버전에 소개된 fiber가 어떻게 퍼포먼스를 더 향상시켰는지, 어떻게 rerender를 줄였는지 확인해보면 되겠다.
<-- 여기서부터 전부 다음 포스팅 -->
예전에 정리해야 한다고 생각만 했던, react hooks는 linked list를 사용한다는 url을 다시 살펴보기로 했다.
아래 함수는
function createWorkInProgressHook(): Hook {
if (workInProgressHook === null) {
// This is the first hook in the list
if (firstWorkInProgressHook === null) {
isReRender = false;
currentHook = firstCurrentHook;
if (currentHook === null) {
// This is a newly mounted hook
workInProgressHook = createHook();
} else {
// Clone the current hook.
workInProgressHook = cloneHook(currentHook);
}
firstWorkInProgressHook = workInProgressHook;
} else {
// There's already a work-in-progress. Reuse it.
isReRender = true;
currentHook = firstCurrentHook;
workInProgressHook = firstWorkInProgressHook;
}
} else {
if (workInProgressHook.next === null) {
isReRender = false;
let hook;
if (currentHook === null) {
// This is a newly mounted hook
hook = createHook();
} else {
currentHook = currentHook.next;
if (currentHook === null) {
// This is a newly mounted hook
hook = createHook();
} else {
// Clone the current hook.
hook = cloneHook(currentHook);
}
}
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
} else {
// There's already a work-in-progress. Reuse it.
isReRender = true;
workInProgressHook = workInProgressHook.next;
currentHook = currentHook !== null ? currentHook.next : null;
}
}
return workInProgressHook;
}
if (workInProgressHook === null) {
참고할 url
https://usehooks.com/useToggle/
https://dev.to/wuz/linked-lists-in-the-wild-react-hooks-3ep8
https://github.com/streamich/react-use/pull/1079
https://github.com/reduxjs/react-redux/issues/1468
https://github.com/reduxjs/react-redux/issues/1468
https://dzone.com/articles/understanding-of-react-fiber-architecture
function createWorkInProgressHook() {
if (workInProgressHook === null) {
// This is the first hook in the list
if (firstWorkInProgressHook === null) {
isReRender = false;
firstWorkInProgressHook = workInProgressHook = createHook();
} else {
// There's already a work-in-progress. Reuse it.
isReRender = true;
workInProgressHook = firstWorkInProgressHook;
}
} else {
if (workInProgressHook.next === null) {
isReRender = false; // Append to the end of the list
workInProgressHook = workInProgressHook.next = createHook();
} else {
// There's already a work-in-progress. Reuse it.
isReRender = true;
workInProgressHook = workInProgressHook.next;
}
}
return workInProgressHook;
}