이번 주차는 raect의 다양한 hooks들을 만들고 사용해보며 동작원리를 체득해보는 시간이었다.
useRef, useMemo, useCallback, memo를 직접 구현해보면서 메모이제이션에 대해 더 깊게 생각해볼 수 있었고,
useContext로 랜더링을 최적화 하는 과정을 통해 보다 응집도 있는 코드 작성하는 법에 대해 배울수 있었다.
1. useRef
렌더링 사이에 값을 유지하는 ref 객체를 생성
export function useRef<T>(initialValue: T): { current: T } {
const [ref] = useState({ current: initialValue });
return ref;
}
처음엔 아래처럼 object에 값을 넣어도 동일하게 동작할 줄 알았다
const ref = { currnet: initialValue };
그런데 이렇게하면 렌더링이 될때마다 값이 초기화돼버렸다.
새로고침 하기 전까지는 값을 유지하는 useState 내부 저장소에 값을 저장했다
useState에 저장한 값을 변경하는 방법으로 리랜더링하지 않고 랜더링 사이에도 값을 유지하는 useRef를 구현했다.
ref.current = "값 변경";
그런데 useRef를 구현하고보니 useState에 궁금증이 생겼다.
useState는 어디에 값을 저장하길래 object처럼 초기화되지 않는거지?
React는 특정 공간에 state 값을 저장하고있다.
공식문서에서도 일반 변수는 랜더링하면 값 초기화되니까 렌더링에 사용할 변수는 useState를 사용하라고 강조하고 있다 (https://react.dev/learn/state-a-components-memory#)
클로저
React는 state의 값을 저장하기위해 클로저를 사용하고있다.
클로저를 통해 useState가 호출될 때마다 초기화되지 않고 기존 상태를 유지할 수 있게된다.
// 클로저를 통해 useState 구현
const createUseState = (initialValue = 0) => {
let value = initialValue; // 클로저로 상태를 저장
const state = () => value; // 상태를 반환하는 함수
const setState = (newValue) => {
value = newValue; // 상태를 업데이트
};
return [state, setState]; // 반환
};
const customUseState = createUseState(0); // 초기 상태를 0으로 설정
export default customUseState;
// 컴포넌트에서 사용
const [state, setState] = customUseState;
const onClick = () => {
setState(state() + 1);
console.log(state());
};
useState를 비슷하게 만들어보니 호출할 때마다 useState를 구분해줘야되는 문제점이 있었다.
리액트는 어떻게 해결했나 공식문서를 보니 역시 해답이 나와있었다.
useState는 각각의 useState 인덱스를 key 값으로 구분하고있다.
let componentHooks = [];
let currentHookIndex = 0;
function useState(initialState) {
let pair = componentHooks[currentHookIndex];
if (pair) {
// 이미 호출된 useState 쌍이면 return 해서 호출 대기
currentHookIndex++;
return pair;
}
// 처음 호출된 useState 는 우리가 흔히 사용하는 [state, setState] 쌍을 만든다
pair = [initialState, setState];
function setState(nextState) {
// setState가 변경되면 state 값을 변경하고 DOM을 update한다.
pair[0] = nextState;
updateDOM();
}
// 이후 렌더링을 위해 쌍을 저장하고 다른 useState 호출 대기를 위해 인덱스 +1
componentHooks[currentHookIndex] = pair;
currentHookIndex++;
return pair;
}
2. useMemo
react는 useCallback, useMemo, memo 등의 의존성을 판단할 때 얕은 비교를 한다.
만약 깊은 비교를 통해 위 hook들을 사용하고 싶다면 별도의 커스텀 훅을 만들어 진행해야한다.
export function useMemo<T>(
factory: () => T,
_deps: DependencyList,
_equals = shallowEquals,
): T {
const prevDeps = useRef<null | DependencyList>(null);
const prevFactory = useRef<null | T>(null);
const isEqual = shallowEquals(prevDeps.current, _deps); // 얕은 비교
if (!isEqual) {
prevDeps.current = _deps;
// 참조값 _deps 가 변경되면 factory함수를 다시 생성한다.
prevFactory.current = factory();
}
return prevFactory.current as T;
}
3. useCallback
usememo는 값을 useCallback은 함수를 메모이제이션하므로 아래와 같이 간단하게 구현 가능하다.
export function useCallback<T extends Function>(
factory: T,
_deps: DependencyList,
) {
const _factory = useMemo(() => factory, _deps);
return _factory as T;
}
4. memo
memo도 useMemo, useCallback과 동일하게 얕은비교를 하는데 react element를 다시 생성해줘야한다.
이때 넘겨준 props와 children을 return 하는 createElement라는 react 메서드를 사용한다.
export function memo<P extends object>(
Component: ComponentType<P>,
_equals = shallowEquals,
) {
return function MemoizedComponent(props: P) {
const prevProps = useRef<P | null>(null);
const prevComponent = useRef<ReactElement | null>(null);
const isEqual = shallowEquals(prevProps.current, props);
if (!isEqual) {
prevProps.current = props;
prevComponent.current = createElement(Component, props); element 재생성
}
return prevComponent.current;
};
}
'항해99 ➕플러스' 카테고리의 다른 글
프레임워크 없이 SPA 만들기 (항해 2주차 회고) (0) | 2025.04.07 |
---|---|
프레임워크 없이 SPA 만들기 (항해 1주차 회고) (0) | 2025.04.04 |