Skip to content

[9팀 박상수] Chapter 1-3. React, Beyond the Basics #59

Open
parksangsoo wants to merge 7 commits intohanghae-plus:mainfrom
parksangsoo:main
Open

[9팀 박상수] Chapter 1-3. React, Beyond the Basics #59
parksangsoo wants to merge 7 commits intohanghae-plus:mainfrom
parksangsoo:main

Conversation

@parksangsoo
Copy link

@parksangsoo parksangsoo commented Jul 25, 2025

과제 체크포인트

배포 링크

https://hanghae-plus.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 개선

과제 셀프회고

리액트에서 쓰이는 hooks에 대해 어떤 원리로 쓰이고 어떻게 만드는지 좀 더 자세히 알게된 거 같았다.
아직은 부족하지만 반복해서 공부하다보면 코드 리팩토링과 성능최적화를 좀 더 능숙하게 할 수 있을 거 같다

기술적 성장

어떤 hook을 어떤 상황에 필요하고 어떻게 써야하는 지에 대한 이해도를 바탕으로 좀 더 정교하고 세밀한 코딩이 가능해진 거 같다!
나만 그렇게 느껴지는 건진 몰라도;

자랑하고 싶은 코드

캡처

내가 자랑하고 싶은 코드는 깊은 비교 함수에서 배열 비교와 객체 비교 부분인데
왜 자랑하고 싶냐면 재귀함수라는 개념은 알고 있었고 알고리즘 공부할 때 깊은탐색 할 때만 써본 게 전부였었다
그래서 알고리즘용 함수라는 인식이 박혔었는데 이번 과제를 통해서 그 인식이 깨져버린 거 같다
이럴 때도 쓸 수 있는 거였다니

개선이 필요하다고 생각하는 코드

전체적으로 개선이 필요할 거 같긴 한데
현재 코드 흐름이 어떻게 되는 지 파악하는데 집중하고 있다

학습 효과 분석

아직은 잘 모르겠다 좀 더 공부를 해야 할 거 같다

과제 피드백

코드파악하는데 어려움이 있어 어느 부분을 피드백을 받아야 할지 감이 안오는 거 같다;

학습 갈무리

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

state나 props가 변경되면
현재의 돔을 가상돔으로 만든 후 이전에 만든 가상돔과 비교하여(diffing)
바뀐 부분만 실제 DOM에 반영하여 렌더링이 이루어진다

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

메모이제이션은 어차피 계산 결과는 똑같은데 렌더링 과정에서 불필요하게 계산이 이뤄지는 로직으로 인해
성능 저하를 유발하여 사용자 체감이 나빠지는 걸 방지하기 위해 꼭 필요한 과정이라고 생각한다
하지만 너무 남발해서 쓰게되면 코드도 복잡해지고 메모리 낭비도 심해질 거 같다

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

리뷰 받고 싶은 내용

@parksangsoo parksangsoo changed the title 박상수 [9팀 박상수] Chapter 1-3. React, Beyond the Basics Jul 25, 2025
@parksangsoo
Copy link
Author

캡처1 캡처2 캡처3

Copy link

@susmisc14 susmisc14 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상수님, 3주차도 정말 고생 많으셨습니다. 과제뿐만 아니라 보이지 않는 곳에서도 항상 노력하시는 모습이 정말 멋지다고 생각해요. 남은 기간도 지금처럼 힘내서, 꼭 함께 무사히 수료했으면 좋겠습니다!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 depth라는 인자를 추가해서 비교의 깊이를 조절할 수 있게 만들었어요. 이 depth 값을 재귀적으로 1씩 줄여나가다가 0이 되면 === 비교로 마무리하는 방식입니다. 이렇게 하면 deepEquals와 shallowEquals를 하나의 함수로 통합할 수 있는 장점이 있습니다. 예를 들어, shallowEquals(a, b)는 내부적으로 deepEquals(a, b, 1)을 호출하는 거죠. 이런 방식을 고려한다면 중복되는 구조를 깔끔하게 해결할 수 있을 것 같아요!

Comment on lines +8 to +11
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((val, i) => deepEquals(val, b[i])); // 재귀 (DFS)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 처음에 배열도 '인덱스를 키로 갖는 객체'와 유사하다고 생각해서, 두 타입을 구분하지 않고 객체 비교 로직으로 한 번에 처리했었는데요. 다른 분들 코드를 보고 있다보니 ['a']와 { '0': 'a', length: 1 }이 같게 처리될 수 있지만, 실제로는 타입도 다르고 .map 같은 배열 메서드도 사용할 수 없다는 점에서 예상치 못한 부작용이 생길 수 있겠다는 걸 깨달았습니다.

Comment on lines +21 to +23
return aKeys.every(
(key) => Object.prototype.hasOwnProperty.call(bObj, key) && deepEquals(aObj[key], bObj[key]), // 재귀 (DFS)
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

객체의 속성 존재 여부를 확인할 때 Object.prototype.hasOwnProperty.call() 대신 최신 자바스크립트(ES2022)에 추가된 Object.hasOwn()을 사용하면 어떨까요? Object.hasOwn(b, key) 형태로 사용하면 call 없이도 안전하게 속성을 확인할 수 있어서 코드가 조금 더 간결하고 직관적으로 변하는 효과가 있을 것 같아요!

Comment on lines +6 to +9
const ref = useRef(fn);

// 매 렌더마다 최신 함수로 갱신
ref.current = fn;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 if 문을 추가해서 이전 callback과 현재 callback을 비교하는 과정을 추가했었는데 지금와서 생각해보면 과한 로직인 것 같아요.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타입 안정성을 더 높일 수 있는 방법이 있어서 공유드려요! 실제 React의 useRefuseRef()처럼 초기값 없이 호출하는 경우와 useRef(0)처럼 초기값을 주는 경우의 타입이 다르게 추론돼요.

TypeScript의 **함수 오버로딩(Function Overloading)**을 사용하면 이런 케이스들을 더 정교하게 처리할 수 있습니다. 예를 들어, 아래처럼 여러 시그니처를 선언하는 거죠.

import { useState } from "react";

interface MutableRefObject<T> {
  current: T;
}

export function useRef<T = undefined>(): MutableRefObject<T | undefined>;
export function useRef<T>(initialValue: T): MutableRefObject<T>;
export function useRef<T>(initialValue?: T): MutableRefObject<T | undefined> | MutableRefObject<T> {
  return useState(() => ({ current: initialValue }))[0];
}

이렇게 하면 useRef를 초기값 없이 호출하는 케이스도 지원할 수 있고, 타입스크립트가 ref.current의 타입을 더 정확하게 추론해주는 장점이 있습니다!

Comment on lines +9 to +14
setState((prev) => {
if (shallowEquals(prev, next)) {
return prev; // 동일하므로 상태 변경 없이 리렌더 방지
}
return next; // shallow하지 않으면 상태 변경
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 setState 바깥에서 새로운 값을 계산했는데, setState 콜백 안에서 prevValue를 사용해야 여러 번의 상태 업데이트가 연달아 일어날 때도 stale state 문제 없이 항상 최신 상태를 기반으로 값을 계산할 수 있겠네요. 제가 놓쳤던 부분인데 다른 분들은 다 꼼꼼히 챙기셔서 약간 슬퍼졌어요...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

과제의 요구사항에 맞게 정확히 State와 Command 컨텍스트를 분리하신 것 같아요! 추가로 제가 평소에 즐겨 사용하는 createSafeContext라는 패턴을 소개해 드리고 싶어요.
createContext를 사용할 때마다 초기값을 반드시 설정해야 하거나 Provider 외부에서 컨텍스트를 사용하는 실수를 방지하기 위해 null 체크를 하고 값을 useMemo로 감싸는 등 반복적인 작업을 하나로 묶어줄 수 있어요.

import { createContext, createElement, useContext, useMemo } from "react";

export function createSafeContext<ContextValue extends object | null>(
  rootComponentName: string,
  defaultValue?: ContextValue,
) {
  const Context = createContext<ContextValue | undefined>(defaultValue);

  const Provider = ({ children, ...rest }: ContextValue & { children?: React.ReactNode }) => {
    // eslint-disable-next-line react-hooks/exhaustive-deps
    const value = useMemo(() => rest, Object.values(rest)) as ContextValue;

    return createElement(Context, { value }, children);
  };

  const useSafeContext = (consumerName: string) => {
    const ctx = useContext(Context);
    if (ctx) return ctx;
    if (defaultValue) return defaultValue;

    throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
  };

  Provider.displayName = rootComponentName + "Provider";

  return [Provider, useSafeContext] as const;
}

이 헬퍼를 적용하면, 현재 컨텍스트를 생성하는 부분을 아래처럼 바꿀 수 있습니다.

export const [ToastStateProvider, useToastState] =
  createSafeContext<ContextState>("ToastStateProvider");
export const [ToastCommandProvider, useToastCommand] =
  createSafeContext<ContextCommand>("ToastCommandProvider");

Copy link

@ldhldh07 ldhldh07 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다! 젭에서 중간중간 이해하신부분을 공유해주셔서 저도 도움 많이 받았습니다 감사합니다!

});

// Toast는 외부 상태 변화와 무관하게 고정되도록 메모이제이션
const MemoizedToast = memo(Toast);
Copy link

@ldhldh07 ldhldh07 Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토스트 자체를 메모이제이션하는 건 생각못했는데 리렌더링 문제를 해결하기 위한 새로운 관점을 배웠습니다!

다만 이 프로젝트의 토스트를 봤을 때 토스트의 state는 전부 provider로 제공되는 값이여서 토스트 컴포넌트에 대한 메모이제이션은 현재로서는 불필요하지 않나 생각이 듭니다.


return (state: T) => {
const next = selector(state);
return shallowEquals(prev.current, next) ? prev.current : (prev.current = next);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상수님 코드 보니까 왜 전 삼항자를 쓰지 않았는지 바로 제 코드가 생각났습니다ㅎㅎ 감사합니다.

@q1Lim
Copy link

q1Lim commented Jul 25, 2025

상수님 고생많으셨습니다!! 다음주도 같이 화이팅입니다~

Copy link

@tomatopickles404 tomatopickles404 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인간고양이 상수님~~
이번주도 고생 많으셨습니당!!!!!!!!!!!
이제 페어팀도 생겼으니 젭 더 자주 들어오세요 ㅎㅎ
남은 주도 같이 재밌게 해봐요~~!

Comment on lines +2 to +6
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;

return a.every((val, index) => val === b[index]);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deepEqual이랑 비슷한 처리를 하다보니 중복 로직이 생기는 것 같은데, 유틸로 분리해보면 좋을 것 같아요~

Comment on lines +11 to +18
return useSyncExternalStore(router.subscribe, () => {
const selected = shallowSelector(router);
// shallowSelector가 null을 반환할 수 있으므로, fallback 처리
if (selected === null) {
return selector(router);
}
return selected;
}) as S;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useSyncExternalStore 인터페이스만 봤을 때는 어떤 훅인지 직관적이지 않은 것 같아요!
선언적으로 표현해보는건 어떨까요?!

const subscribeToRouter = router.subscribe;

const getRouterState = () => {
  const selected = shallowSelector(router);
  // shallowSelector가 null을 반환할 수 있으므로, fallback 처리
  if (selected === null) {
    return selector(router);
  }
  return selected;
};

return useSyncExternalStore(
  subscribeToRouter,
  getRouterState,
) as S;

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.

5 participants