JangRuBin2 님의 상세페이지[3팀 장루빈] Chapter 1-3. 프레임워크 없이 SPA 만들기

과제 체크포인트

배포 링크

https://jangrubin2.github.io/front_6th_chapter1-3/

기본과제

equalities

  • shallowEquals 구현 완료
  • deepEquals 구현 완료

hooks

  • useRef 구현 완료
  • useMemo 구현 완료
  • useCallback 구현 완료
  • useDeepMemo 구현 완료
  • useShallowState 구현 완료
  • useAutoCallback 구현 완료

High Order Components

  • memo 구현 완료
  • deepMemo 구현 완료

심화 과제

hooks

  • createObserver를 useSyncExternalStore에 사용하기 적합한 코드로 개선
  • useShallowSelector 구현
  • useStore 구현
  • useRouter 구현
  • useStorage 구현

context

  • ToastContext, ModalContext 개선

과제 셀프회고

기술적 성장

  1. ShallowEqual 참조 무결성 유지 목적 React의 memo, useMemo, useEffect, useCallback 등에서는 참조 변경 여부로 re-render 판단함 얕은 비교로도 대부분의 상태 변경 감지 목적에는 충분

  2. DeepEquals 상태 비교 / 동기화 여부 판단 프론트엔드 상태 관리(예: Redux, Zustand)에서 이전 상태와 다음 상태가 완전히 같은지 확인할 때. 재귀적으로 호출하여 값을 비교

  3. useRef

import { useState } from "react";
export function useRef<T>(initialValue: T): { current: T } {
  const [ref] = useState<{ current: T }>({ current: initialValue });
or
 const [ref] = useState(() => ({ current: initialValue }));

  return ref;
}

useState의 lazy(지연) or eager(즉시) initialization + 불변성 유지를 통해 useRef구현 useState(() => init())는 초기 렌더링 시 한 번만 init() 함수를 호출하여 초기값을 설정하고, useState(init())는 매 렌더링마다 init()이 평가되지만 처음 값만 사용합니다.

복잡한 초기화 로직이 있다면 useState(() => init()) 방식이 더 효율적

  1. useMemo
export function useMemo<T>(factory: () => T, deps: DependencyList, equals = shallowEquals): T {
  const ref = useRef<{ deps: DependencyList; value: T } | null>(null);

  if (ref.current === null || !equals(ref.current.deps, deps)) {
    ref.current = {
      deps,
      value: factory(),
    };
  }

  return ref.current.value;
}

deps가 변경되지 않으면 factory()를 다시 호출하지 않고, 기존 값을 재사용 dependency array (의존성 배열)과 함수를 ref에 캐싱하고 캐시 무효화 조건문을 거쳐 기존 값 혹은 새로운 값을 반환하게 했습니다.

  1. useCallback
export function useCallback<T extends Function>(factory: T, _deps: DependencyList) {
  // eslint-disable-next-line react-hooks/exhaustive-deps
  return useMemo(() => factory, _deps);
}

useCallback(fn, deps)은 deps가 바뀌지 않는 한 fn의 참조를 유지해야합니다. 기존에 만든 useMemo 훅을 이용해 메모이제이션하고 참조값이 바뀔 때만 함수를 다시 선언하게합니다.

여기서 _deps에 아래 에러가 생기게 되는데

React Hook useMemo was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies.eslint[react-hooks/exhaustive-deps](https://github.com/facebook/react/issues/14920)
React Hook useMemo has a missing dependency: 'factory'. Either include it or remove the dependency array.eslint[react-hooks/exhaustive-deps](https://github.com/facebook/react/issues/14920)

이것은 _deps가 리터럴 값이 아니어서 deps 안에 무엇이 들어있는지 정적으로 분석할 수 없고 콜백 안에서 사용하는 factory가 의존성 배열에 없다는 뜻인데 의존성 배열에 factory를 넣으면 의미 없는 캐싱이 되기 때문에 주석을 추가했습니다.

  1. useShallowState
export const useShallowState = <T>(initialValue: T | (() => T)) => {
  const [state, setState] = useState<T>(initialValue);
  // setShallowState는 호출마다 새로정의 되기 때문에 메모이제이션
  const setShallowState = useCallback((nextState: T) => {
    setState((prev) => {
      return shallowEquals(prev, nextState) ? prev : nextState;
    });
  }, []);

  return [state, setShallowState] as const;
};

react에서 useState는 새로운 값이 Object.is(prev, next) 비교 결과 다르면 무조건 리렌더링을 발생시킵니다. useShallowState는 내부적으로 shallowEquals를 사용하여 객체나 배열도 값만 같다면 상태를 변경하지 않고 리렌더링을 막습니다.

얕은 비교는 객체 참조까지만 비교하므로 내부 객체가 새로 만들어지면 리렌더링이 발생합니다.

  1. useDeepMemo
export function useDeepMemo<T>(factory: () => T, deps: DependencyList): T {
  return useMemo(factory, deps, deepEquals);
}

React의 기본 useMemo는 deps 배열을 얕은 비교 (shallow equality) 로 비교합니다. 깊은 비교(deep equality)를 사용해 값이 같으면 재계산 방지

  1. useAutoCallback
export const useAutoCallback = <T extends AnyFunction>(fn: T): T => {
  const fnRef = useRef(fn);
  // 갱신필요 => stale closure
  fnRef.current = fn;
  const stableFn = useCallback((...args: Parameters<T>) => {
    return fnRef.current(...args);
    // 참조유지
  }, []);

  return stableFn as T;
};

참조는 고정되지만 내부 로직은 항상 최신 상태인 콜백 함수 fn이 최신값일 수 있으므로 매 렌더마다 갱신함. 이렇게 하지 않으면 fnRef.current는 오래된 함수(초기값) 를 참조하게 됨 → stale closure 발생. https://javascript.plainenglish.io/stale-closures-in-react-afb0dda37f0b

참조는 유지하되 내용은 항상 최신으로 갱신하는 것이 핵심 코드. useCallback의 deps가 []이므로 stableFn은 렌더링 간 절대로 바뀌지 않음

  1. memo 얕은 비교를 기반으로 리렌더링을 방지하는 고차 컴포넌트(HOC) prevProps와 prevResult를 클로저에 저장하여 비교 및 캐싱 수행
export function memo<P extends object>(Component: FunctionComponent<P>, equals = shallowEquals) {
  let prevProps: P | null = null;
  let prevResult: ReturnType<FunctionComponent<P>> | null = null;

  return function MemoizedComponent(props: P) {
    if (prevProps === null || !equals(prevProps, props)) {
      prevResult = Component(props);
      prevProps = props;
    }

    return prevResult;
  };
}
  1. deepMemo 컴포넌트 렌더링 결과를 deep equality로 캐싱하는 고차 컴포넌트(HOC) 객체 안에 중첩된 속성이 있어 React.memo의 얕은 비교로는 리렌더링 방지가 어려운 경우에 사용합니다.
export function deepMemo<P extends object>(Component: FunctionComponent<P>) {
  let prevProps: P | null = null;
  let prevResult: ReturnType<FunctionComponent<P>> | null = null;

  return function MemoizedComponent(props: P) {
    if (prevProps === null || !deepEquals(prevProps, props)) {
      prevResult = Component(props);
      prevProps = props;
    }

    return prevResult;
  };
}

deepEquals(prevProps, props)를 통해 이전 프롭과 새 프롭을 깊은 비교 동일한 경우 캐시된 렌더링 결과 prevResult를 반환하여 리렌더링 방지 최초 렌더링이거나 프롭이 달라졌을 때만 실제 컴포넌트를 실행

학습 효과 분석

직접 리액트훅을 만들어서 내부적으로 어떻게 동작하는지 알 수 있게 된 것이 가장 큰 러닝포인트였고, 제네릭 타입의 활용에대한 학습이 필요할 듯합니다. 실무에서는 커스텀 훅을 직접 만들 일이 없지만 내부적으로 어떻게 동작하는지 이해했으니 좀 더 적절한 때에 사용할 수 있지 않을까라는 생각이 듭니다.

과제 피드백

이번 과제는 AI를 적극적으로 사용했습니다. 또 다른 분들의 pr을 보면서 참고한 내용이 많았는데 남의 코드를 보는 것에 익숙해져야겠다고 느꼈습니다. 좋은 레퍼런스가 많네요

학습 갈무리

리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.

  1. 상태 변경 or props 변경 발생
  2. 해당 컴포넌트의 함수 다시 실행 (Function Component 재호출)
  3. 이전 Virtual DOM과 새로운 Virtual DOM을 비교
  4. 변경된 부분만 실제 DOM에 반영

메모이제이션에 대한 나의 생각을 적어주세요.

동일한 입력에 대해 계산 결과를 캐싱하여 불필요한 계산을 줄이고 렌더링 최적화 메모이제이션을 하지 않으면 매 렌더링마다 함수나 값이 새로 생성되어 참조가 달라짐 useEffect, React.memo 등에서 불필요한 리렌더링/재실행 발생 무거운 연산이 매번 다시 계산되어 성능 저하

컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.

컴포넌트 간 데이터 공유와 상태 동기화를 효율적으로 하기 위함 컨텍스트를 사용하지 않으면 상태를 props로 계속 전달해야 함 (prop drilling) 컴포넌트가 많아질수록 코드 복잡도가 상승할 것이고 상태의 출처와 흐름을 추적하기 어려워질 것입니다.

리뷰 받고 싶은 내용

  1. 이런 과제를 수행한다면 코치님은 AI를 어떻게 활용할 것인지 궁금합니다.
  2. 이번 과제는 완료하는 것을 목표로 했습니다. 과정을 완벽하게 이해하지 않고 넘어간 부분도 있으나 pr을 작성하면서 재학습하는 식으로 진행했는데요 정리한 내용중에 모호하거나 본질적인 내용과 거리가 있다면 그것이 궁금합니다!

과제 피드백

안녕하세요 루빈님! 3주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!!

이런 과제를 수행한다면 코치님은 AI를 어떻게 활용할 것인지 궁금합니다.

저는 딱 영서님이 진행하신게 생각나는데요, 내가 고민이 되는 부분에 대해 더 딥다이브를 할 때 사용할 것 같아요 ㅎㅎ

과제는 "학습을 위한 수단"을 수단이라고 생각하고, 학습에 대한 설계는 결국 스스로 하는거죠. 가령, 제가 1년차 때 "간단한 할 일 목록"이라는 주제로 코드를 작성했어요. 필수기능을 일단 다 구현해놓고 여기서 난이도를 최도로 높여서 내가 할 수 있는 최대 수준의 오버엔지니어링을 하는거죠

그리고 오버엔지니어링을 하면서 습득하는 지식을 정리해서 글로 작성한다거나!? 이런 방식으로 진행할 것 같아요.

이번 과제의 경우 hook을 만드는게 주제인데요, 지금은 useState를 통해 만들어가고 있는데

아예 "모든 시스템을 내가 처음부터 만들어간다면 어떻게 될까?" 라는 가정을 해보는거죠. 혹은 여태까지 만들어놓은 훅을 이용하여 app 폴더에 적용해본다거나? 새로운 기능을 만들어본다거나?

이런 시도를 할 것 같습니다 ㅎㅎ

정해진 틀을 계속 깨부수는 연습을 해보시면 좋겠어요!

이번 과제는 완료하는 것을 목표로 했습니다. 과정을 완벽하게 이해하지 않고 넘어간 부분도 있으나 pr을 작성하면서 재학습하는 식으로 진행했는데요 정리한 내용중에 모호하거나 본질적인 내용과 거리가 있다면 그것이 궁금합니다!

잘 진행해주셧다고 생각합니다! PR을 작성하면서 복습하는 과정도 잘 해주셨어요! 이렇게 해야 기억에 남는 것 같아요 ㅎㅎ 고생하셨습니다!