Skip to content

[3팀 장루빈] Chapter 1-3. 프레임워크 없이 SPA 만들기#54

Open
JangRuBin2 wants to merge 10 commits intohanghae-plus:mainfrom
JangRuBin2:main
Open

[3팀 장루빈] Chapter 1-3. 프레임워크 없이 SPA 만들기#54
JangRuBin2 wants to merge 10 commits intohanghae-plus:mainfrom
JangRuBin2:main

Conversation

@JangRuBin2
Copy link

@JangRuBin2 JangRuBin2 commented Jul 24, 2025

과제 체크포인트

배포 링크

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을 작성하면서 재학습하는 식으로 진행했는데요
    정리한 내용중에 모호하거나 본질적인 내용과 거리가 있다면 그것이 궁금합니다!

@BBAK-jun
Copy link

루빈쨩 화이팅

@zenna9
Copy link

zenna9 commented Jul 26, 2025

PR너무 잘 써주셨는데요? 고생많으셨습니다! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants