Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8팀 김도운] [Chapter 1-3] React, Beyond the Basics #14

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

devJayve
Copy link

@devJayve devJayve commented Dec 30, 2024

과제 체크포인트

기본과제

  • shallowEquals 구현 완료
  • deepEquals 구현 완료
  • memo 구현 완료
  • deepMemo 구현 완료
  • useRef 구현 완료
  • useMemo 구현 완료
  • useDeepMemo 구현 완료
  • useCallback 구현 완료

심화 과제

  • 기본과제에서 작성한 hook을 이용하여 렌더링 최적화를 진행하였다.
  • Context 코드를 개선하여 렌더링을 최소화하였다.

과제 셀프회고

React에서 Hook을 개발함으로써 Vue, Angular와 같은 기존 SPA 프레임과는 전혀 다른 발전 궤도를 그리게 되었다고 합니다.(From 발제)
그만큼 Hook이 React에서 굉장히 중요한 개념이고 이를 얼마나 능숙하게 활용하느냐가 곧 React 개발자의 역량을 가늠하는 중요한 지표가 될 수 있겠다는 생각이 들었습니다.

직접 개인 프로젝트에서 유명한 커스텀 훅 라이브러리인 React-hook-form를 사용해본 경험이 있는데 훅으로 인한 개발 생산성 향상을 직접 체감했습니다. 다만 메모이제이션을 활용한 최적화를 개념적으로만 이해하고 있을 뿐 실제로 얼마나 성능이 개선되는지 수치적으로 평가하거나 눈으로 확인해본 경험이 아직 부족했습니다. 이번 과제를 계기로 메모이제이션과 관련된 다양한 훅을 좀 더 깊이 학습하고 직접 구현해보며 이와 같은 성능 향상에 대한 부분을 더 구체적으로 이해해보겠다는 목표를 세웠습니다. 따라서 아래와 같이 기본적인 목표를 설정했습니다.

  1. 각 훅의 동작 원리와 사용 방식을 명확하게 이해하고 구현
  2. useMemo, useCallback 등 메모이제이션 관련 훅을 이용한 렌더링 최적화
  3. 커스텀 훅을 이용한 유지보수성과 가독성 개선

React의 메모화(Memoization)

메모이제이션을 본격적으로 공부하기 전 useMemo 혹은 useCallback 훅을 사용하면 값을 캐싱해서 변하지 않았을 때 값의 불변을 유지한다는 개념으로 이해하고있었습니다.내부 동작을 제대로 알지 못하니 메모이제이션이 마치 최적화 마법처럼 느꼈습니다. 그래서 메모이제이션을 본격적으로 과제에 사용하기 앞서 리액트에서는 어떻게 구현했는지 살펴보고자 합니다.

먼저 리액트에는 훅을 실행하는 Dispatcher가 존재합니다. 이때 훅을 어떤 시점에서 실행하느냐에 따라 다른 Dispacher를 사용하는데요. 마운트 시점에서는 하단의 HookDispatcherOnMount를 사용합니다. 이때 useMemo의 경우 mountMemo 함수가 매칭됩니다.

const HooksDispatcherOnMount: Dispatcher = {
    useCallback: mountCallback,
    useContext: readContext,
    useEffect: mountEffect,
    useMemo: mountMemo,
    useReducer: mountReducer,
    useRef: mountRef,
    useState: mountState,
    useMemoCache,
    // ... 생략 
};

다음은 mountMemo에서 핵심적인 부분에 대한 로직을 포함한 코드입니다. 마운트 시점인 만큼 새로 hook을 생성하여 Fiber에 연결합니다. (이때 연결하는 과정은 뒤이어 살펴보겠습니다.) 이후 정규화 및 상태 저장을 통해 이후 updateMemo에서 사용할 수 있도록 합니다.

function mountMemo<T>(
    nextCreate: () => T, // 메모이제이션할 값을 생성하는 함수
    deps: Array<mixed> | void | null // 의존성 배열
): T {
    // 1. 새로운 hook을 생성하고 현재 Fiber에 연결
    const hook = mountWorkInProgressHook();

    // 2. deps 정규화 - undefined일 경우 null로 변경
    const nextDeps = deps === undefined ? null : deps;

    // 3. 값 생성
    const nextValue = nextCreate();

    // 4. 훅의 상태 저장
    hook.memoizedState = [nextValue, nextDeps];

    // 5. 계산된 값 반환
    return nextValue;
}

mountWorkInProgressHook 에서 어떻게 훅을 연결 생성하고 연결하는지 살펴보겠습니다. 먼저 몇 가지 값들로 구성된 hook 객체를 생성합니다. 이때 hook은 React 내부적으로 연결 리스트로 관리되고 있는데요. 그래서 아래와 같이 .next를 이용하여 연결리스트에 추가하는 방식을 확인할 수 있습니다.

function mountWorkInProgressHook() {
    const hook: Hook = {
        memoizedState: null,
        baseState: null,
        baseQueue: null,
        queue: null,
        next: null,
    };

    if (workInProgressHook === null) {
        // 첫 번째 hook
        currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
    } else {
        // hook을 연결 리스트에 추가
        workInProgressHook = workInProgressHook.next = hook;
    }

    return workInProgressHook;
}

위에서는 의존성을 비교하는 것 없이 무조건 값을 생성하게 됩니다. 그럼 최적화는 어떻게 진행하는 걸까요? 예상한 바와 같이 mountMemo가 아닌 updateMemo에서 이루어지게 됩니다. 아래는 updateMemo의 핵심적인 부분입니다.
아마 이 코드가 과제와 유사해 익숙한 방식일 것 같습니다. 우리가 mount 시점에서 생성한 훅의 상태를 가져와 현재 의존성 배열과 비교하여 값의 재사용 여부를 결정하고 있습니다.

function updateMemo<T>(
    nextCreate: () => T,
    deps: Array<mixed> | void | null
): T {
    // 1. 현재 작업 중인 훅을 가져옴
    const hook = updateWorkInProgressHook();

    // 2. 의존성 배열 정규화
    const nextDeps = deps === undefined ? null : deps;

    // 3. 이전 메모이제이션 상태 가져오기
    const prevState = hook.memoizedState;

    // 4. 의존성 배열 비교 후 같다면 이전 값 재사용 
    if (nextDeps !== null) {
        const prevDeps: Array<mixed> | null = prevState[1];
        if (areHookInputsEqual(nextDeps, prevDeps)) {
            return prevState[0];
        }
    }

    // 5. 의존성이 다르거나 없을 경우 새로운 값 계산
    const nextValue = nextCreate();

    hook.memoizedState = [nextValue, nextDeps];
    return nextValue;
}

정리하자면 React의 메모이제이션은 마법 같은 최적화가 아니라 이전 의존성 배열과 현재 의존성 배열이 동일한지 비교한 뒤 달라졌다면 새로 계산하고 아니면 이전 값을 재사용하는 방식입니다. 이제 이러한 메모이제이션 훅을 언제 활용하면 좋을지를 구체적으로 살펴보겠습니다.

Memo는 언제 사용해야할까

메모이제이션 개념과 동작 방식, 그리고 복잡도가 높은 연산을 효율적으로 최적화할 수 있다는 점을 배웠다면 이제 실무에서 “언제” 메모이제이션을 적용해야 할지 고민이 생길 수 있습니다. 자료를 찾아보면 “필요하지 않은 경우에는 쓰지 않는 게 좋다”라는 의견과 “보이는 곳마다 써라”라는 의견이 엇갈리는 것 같습니다. 저 역시 전자에 가까웠지만 여기서는 제 기준으로 메모이제이션을 사용하면 좋은 상황과 그렇지 않은 상황을 정리해보려고 합니다.

Case 1) 복잡한 연산을 메모화해야하는 경우

아래 코드는 이번 과제에서 활용되었던 filteredItems 입니다. 해당 값은 items에서 검색 결과와 일치하는 item만을
팔터링하고 있습니다. filter의 경우 최악의 경우에 O(n)의 시간 복잡도를 가집니다. 여기서 가격을 오름차순으로 정렬까지 한다고 가정해보겠습니다.

const filteredItems = () => {
    return items.filter(
        (item) =>
            item.name.toLowerCase().includes(filter.toLowerCase()) ||
            item.category.toLowerCase().includes(filter.toLowerCase()),
    );
};

정렬의 경우 최악의 경우에 O(n log n)이 소요됩니다. (필터링된 아이템을 m이라고 했을 때 엄밀히 말하면 O(m log m)이지만 필터링이 거의 없는 경우를 가정하겠습니다.) useMemo를 사용하지 않으면 상태가 업데이트됨에 따라 매우 불필요하게 이러한 복잡한 계산을 다시 수행해야합니다. 예를 들어 아이템이 1,000,000개가 포함된 경우 정렬 기준 1,000,000 * log(1,000,000) = 6,000,000의 연산이 필요합니다.

const filteredItems = () => {
    return items
        .filter(
            (item) =>
                item.name.toLowerCase().includes(filter.toLowerCase()) ||
                item.category.toLowerCase().includes(filter.toLowerCase()),
        )
        .sort((a, b) => {
            return a.price - b.price;
        });
};

상태가 업데이트될 때마다 이 함수를 매번 다시 실행한다면 특히 아이템이 수백만 개에 달하는 상황에서 불필요한 연산이 계속 반복됩니다. useMemo를 적용하면 items나 filter 값이 바뀌지 않는 한 필터와 정렬을 재실행하지 않으므로 충분히 유의미한 퍼포먼스 향상을 기대할 수 있습니다.

const filteredItems = useMemo(() => {
    return items
        .filter(
            (item) =>
                item.name.toLowerCase().includes(filter.toLowerCase()) ||
                item.category.toLowerCase().includes(filter.toLowerCase()),
        )
        .sort((a, b) => {
            return a.price - b.price;
        });
}, [items, filter]);

Case 2) 메모이제이션이 불필요한 경우

하지만 모든 상황에 메모이제이션이 이득이 되지 않습니다. 예를 들어, 스칼라 값(문자열, 숫자, 불리언 등)은 메모화할 필요가 없는 경우가 많습니다. 자바스크립트에서 스칼라 값은 참조(reference)가 아닌 실제 값(value)으로 비교되므로 다음과 같이 단순한 배수 연산 정도라면 useMemo 대신 그냥 바로 계산해도 큰 문제가 없습니다. 오히려 useMemo가 불필요한 메모리를 사용하게 됩니다.

const [count, setCount] = useState(0);
const doubleCount = useMemo(() => {
    return count * 2;
}, [count]);

비슷한 논리로 모든 함수에 useCallback또한 불필요하게 적용되는 경우가 존재합니다. 아래 예시처럼 onclick 핸들러인 increment를 useCallback을 통해 메모화 하면서 얼핏보면 렌더링 최적화를 이룬 것처럼 보이지만 button의 경우 브라우저 네이티브 엘리먼트이고 호출 가능한 리액트 함수 컴포넌트가 아니기 때문에 increment를 메모화해서 얻는 이점이 거의 없습니다. 해당 버튼 엘리먼트는 리액트 컴포넌트처럼 서브 트리가 존재 않아 재생성 비용이 큰 이슈가 아닙니다.

const increment = useCallback(() => { // count 증가 함수 메모화
    setCount((prev) => prev + 1);
}, []);
// ... 생략
return (
    <button onClick={increment}>Click me</button>
);

메모이제이션의 오해와 진실

그렇다면 메모이제이션은 복잡한 계산에만 즉, 정말 필요한 상황에서만 활용하면 될까요? 정해진 정답은 없지만 저는 8팀 멘토링 시간에 해답을 얻었습니다. (준일 코치님 감사합니다!!) 메모이제이션을 꼭 필요한 상황에만 사용한다면 좋겠지만 정확한 사용처를 판단하는 데 드는 비용과 팀 내부의 커뮤니케이션 비용을 절대 무시할 수 없습니다. 게다가 정작 필요한 순간에 최적화를 놓쳐서 병목 현상이 발생할 가능성도 있습니다.

위에서 언급한 "정작 필요한 순간"에 대해 살펴보겠습니다. 먼저 하단과 같이 매우 복잡한 계산을 하는 테스트 컴포넌트가 있다고 가정해보겠습니다.

const TestComponent = (props: ComponentProps<"div">) => {
  // 복잡한 컴포넌트라고 가정을 해볼게요.
  return <div {...props}>복잡한 컴포넌트라고 가정을 해볼게요.</div>;
};

아래와 같이 부모 컴포넌트에서 테스트 컴포넌트를 호출하고자 합니다. 이때 테스트 컴포넌트는 계산 비용이 너무 많이 드는걸 인지했으니 리렌더링을 방지하고자 테스트 컴포넌트에만 부분적으로 memo HOC를 적용해보겠습니다.

function ParentComponent({
  style: defaultStyle,
  onClick,
  onMouseEnter,
}: {
  style: Record<string, any>;
  onClick: () => void;
  onMouseEnter: () => void;
}) {
  const handleClick = () => {
    console.log("ParentComponent1에서 실행하는 handleClick");
    onClick();
  };

  const handleMouseEnter = () => {
    console.log("ParentComponent1에서 실행하는 handleMouseEnter");
    onMouseEnter();
  };

  const style = { backgroundColor: "#f5f5f5", ...defaultStyle }

  return (
    <div>
      <TestComponent className="a b c" id="test" style={style} />
      <TestComponent id="test2" onClick={handleClick} />
      <TestComponent id="test3" onMouseEnter={handleMouseEnter} />
      <TestComponent id="test4" title="타이틀입니다." />
    </div>
  );
}

아래와 같이 memo를 씌운 뒤 리렌더링이 방지될까요? 그렇지 않습니다. memo를 씌워도 Parent1에서 전달되는
props가 항상 새로운 값이기 때문에 리렌더링이 발생합니다. 따라서 ParentComponent에서도 useMemo와 useCallback으로 메모이제이션을 해야합니다.

const TestComponent = memo((props: ComponentProps<"div">) => {
  // 복잡한 컴포넌트라고 가정을 해볼게요.
  return <div {...props}>복잡한 컴포넌트라고 가정을 해볼게요.</div>;
});

ParentComponent 내부에 handleClick, handleMouseEnter, style에 메모이제이션을 적용했습니다. 이제 정말
리렌더링을 방지할 수 있을 것 같습니다. 그러나 ParentComponent가 Props를 받는 것을 보아 ParentComponent를 호출하는 컴포넌트가 존재할 것으로 보입니다. 해당 컴포넌트를 GrandComponent라 지칭하겠습니다.

 const handleClick = useCallback(() => {
    console.log("ParentComponent1에서 실행하는 handleClick");
    onClick();
  }, [onClick]);

  const handleMouseEnter = useCallback(() => {
    console.log("ParentComponent1에서 실행하는 handleMouseEnter");
    onMouseEnter();
  }, [onMouseEnter]);

  const style = useMemo(
    () => ({ backgroundColor: "#f5f5f5", ...defaultStyle }),
    [defaultStyle],
  );

TestComponent 리렌더링을 방지하기 위해선 GrandParentComponent에서도 메모이제이션을 적용해야합니다.
정말 리렌더링을 철저히 막고 싶다면 이와 같이 체인으로 연결된 모든 곳에서 메모이제이션을 해야 하는 경우가 생길 수 있습니다. 따라서 그냥 일관성있게 모두 메모이제이션을 적용하면 이런 고민과 커뮤니케이션 비용을 아낄 수 있습니다.

function GrandComponent() {
    const style = useMemo(() => ({ color: "#09F" }), []);
    const handleClick = useCallback(() => null, []);
    const handleMouseEnter = useCallback(() => null, []);

    return (
        <ParentComponent
            style={style}
            onClick={handleClick}
            onMouseEnter={handleMouseEnter}
        />
    );
}

따라서 우리가 일관성있게 메모이제이션을 적용해야하는 이유를 수식으로 아래와 같이 표현해볼 수 있을 것 같습니다.

(메모리에 대한 추가 부담) < (놓친 최적화로 인한 비용 + 커뮤니케이션 비용)

리뷰받고 싶은 내용

  1. 커스텀 훅인 useInfiniteScroll 훅에 주석으로 남겨놓은 것 처럼 loading을 state로만 사용할 경우 비동기 처리 지연으로 인해 호출이 중첩되는 문제가 발생했습니다. 제가 알아본 내용으로 useRef는 동기적으로 즉시 값이 업데이트되어 리렌더링 없이 즉시 반영된다는 것이었고 실제 useRef를 이용하면서 해당 이슈를 해결했습니다. 이러한 접근 방식이 적절한지 혹은 또 다른 전략이 있을 지 궁금합니다!

  2. 멘토링 시간에 "memo.tsx에서 props 등을 저장하는데 ref가 아닌 let 변수를 이용해 클로저 형태로 관리할 수도 있지만 메모리 누수가 발생하는지 직접 확인해보는게 좋다"고 말씀해주셨는데 혹시 프론트엔드에서 메모리 누수를 따로 모니터링, 디버깅할 수 있는 방법이나 도구가 있을까요?

Copy link

@osohyun0224 osohyun0224 left a comment

Choose a reason for hiding this comment

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

안녕하세요 도운님 :) 담당 학습메이트 오소현입니다 !

도운님께서 PR에 과제에 관해서 개념 설명도 자세히 작성해주시고, React-hook-form 경험을 들어주시면서 생산성 향상을 말씀해주신 부분도 정말 좋았습니다 bb Dispatcher에 대해서도 저도 더 공부할 수 있었어요 bb 항상 PR 너무 잘 적어주셔서 감사합니다,

코드에서도 각 함수가 한 역할을 수행하도록 분리도 너무 잘해주셨고, 계산함수들에 대해서도 메모제이션 해주시려 해서 bb 잘 챙겨주신 것 같습니다 특히 Context와 Provider 부분도 역할에 맞게 잘 나눠주신 것 같아서 리팩토링도 정말 잘해주신 것 같아요 bb

도운님 이번주 과제 너무 고생많으셨습니다! 다음주 과제도 화이팅입니다 :)

Comment on lines +22 to +43
function _deepEqualArray<T>(arrA: Array<T>, arrB: Array<T>): boolean {
if (arrA.length !== arrB.length) return false;
return arrA.every((item, index) => deepEquals(item, arrB[index]));
}

function _deepEqualObject<T extends string | number | symbol>(
objA: Record<T, unknown>,
objB: Record<T, unknown>,
): boolean {
if (objA === objB) return true;

const keysA = Object.keys(objA) as T[];
const keysB = Object.keys(objB) as T[];

if (keysA.length !== keysB.length) {
return false;
}

return keysA.every(
(key) => keysB.includes(key) && deepEquals(objA[key], objB[key]),
);
}

Choose a reason for hiding this comment

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

크 이렇게 배열 비교와 객체 비교 함수를 따로 분리해서 구현해주신점 너무 좋습니다bb

Choose a reason for hiding this comment

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

혹시 배열일 경우를 따로 분리하신 이유가 있을까요?

}

return keysA.every(
(key) => keysB.includes(key) && deepEquals(objA[key], objB[key]),

Choose a reason for hiding this comment

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

여기서 바로 keysB.includes(key)를 사용하는게 keysB 배열을 매 키마다 전체 검색할 것 같아 보이네요 혹시 Set을 사용하는 것이 어떤가요??

Suggested change
(key) => keysB.includes(key) && deepEquals(objA[key], objB[key]),
const setB = new Set(keysB);
return keysA.every(key => setB.has(key) && deepEquals(objA[key], objB[key]));

이렇게 써볼 수 있을 것 같습니다!

Comment on lines +12 to +15
/**
* 무한 스크롤 Custom Hook
* 초기 아이템 1000개 생성 후 스크롤 시 100개씩 추가 로드
*/

Choose a reason for hiding this comment

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

캬 무한스크롤 커스텀 훅도 만드시다니 bb 최고최고!!

Comment on lines +29 to +43
const filteredItems = useMemo(() => {
return items.filter(
(item) =>
item.name.toLowerCase().includes(filter.toLowerCase()) ||
item.category.toLowerCase().includes(filter.toLowerCase()),
);
}, [filter, items]);

const totalPrice = useMemo(() => {
return filteredItems.reduce((sum, item) => sum + item.price, 0);
}, [filteredItems]);

const averagePrice = useMemo(() => {
return Math.round(totalPrice / filteredItems.length) || 0;
}, [filteredItems.length, totalPrice]);

Choose a reason for hiding this comment

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

각 기능을 담당하는 계산 함수들에도 useMemo로 메모제이션 해주셨군요...!

Choose a reason for hiding this comment

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

엇 저도 ItemList 컴포넌트에서 filteredItem, totalPrice, averagePriceuseMemo로 메모이제이션 했었는데요.
하고나서 보니 ItemList에서 itemfilter라는 상태가 변하면 리렌더링이 되어야 해서 useMemo를 지웠었는데요.
어.. 설명을 하자면 toalPriceaveragePricefilteredItem을 의존하고 있고 filteredItemfilteritem을 의존하고 있는데, ItemList에서 filteritem을 사용하지 않는 곳이 없어서? 변경이되면 무조건 리렌더링이 되어야 하는 상황이라, useMemo를 사용하나 안 하나 똑같다라는 결론이 나왔습니다.
도운님은 useMemo를 사용하신 이유가 있을까요?

Comment on lines +1 to +18
export interface Item {
id: number;
name: string;
category: string;
price: number;
}

export interface User {
id: number;
name: string;
email: string;
}

export interface Notification {
id: number;
message: string;
type: "info" | "success" | "warning" | "error";
}

Choose a reason for hiding this comment

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

각 타입을 따로 entities로 정의하셨군요,,, bb 도운님의 엔티티 정의가 있으신가요?

Comment on lines +12 to +26
export function validateForm(values: FormValues): ValidationError<FormValues> {
const errors: ValidationError<FormValues> = {};

if (!values.name) {
errors.name = "이름을 입력해주세요.";
}

if (!values.email) {
errors.email = "이메일을 입력해주세요.";
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = "유효한 이메일 주소를 입력해주세요.";
}

if (values.age <= 0) {
errors.age = "유효한 나이를 입력해주세요.";

Choose a reason for hiding this comment

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

유효성 검사 함수 로직을 따로 분리해주신점 너무 좋습니다 bb

Comment on lines +5 to +8
/**
* AppContainer 컴포넌트
* useThemeContext를 Provider 마운트 후 사용하기 위해 Context를 사용합니다.
*/

Choose a reason for hiding this comment

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

주석으로 해당 컴포넌트의 사용 목적을 잘 적어주셔서 이해하기 수월했습니다 bb

Comment on lines +1 to +69
// import { renderLog } from "../utils";
// import { memo, useMemo } from "../@lib";
// import { useThemeContext } from "../@lib/hooks/useContext.ts";
// import { useState } from "react";
// import { Item } from "../@lib/types";
//
// export const ItemList: React.FC<{
// items: Item[];
// onAddItemsClick: () => void;
// }> = memo(({ items, onAddItemsClick }) => {
// renderLog("ItemList rendered");
// const [filter, setFilter] = useState("");
// const { theme } = useThemeContext();
//
// const filteredItems = useMemo(() => {
// return items.filter(
// (item) =>
// item.name.toLowerCase().includes(filter.toLowerCase()) ||
// item.category.toLowerCase().includes(filter.toLowerCase()),
// );
// }, [filter, items]);
//
// const totalPrice = useMemo(() => {
// return filteredItems.reduce((sum, item) => sum + item.price, 0);
// }, [filteredItems]);
//
// const averagePrice = useMemo(() => {
// return Math.round(totalPrice / filteredItems.length) || 0;
// }, [totalPrice]);
//
// return (
// <div className="mt-8">
// <div className="flex justify-between items-center mb-4">
// <h2 className="text-2xl font-bold">상품 목록</h2>
// <div>
// <button
// type="button"
// className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-xs"
// onClick={onAddItemsClick}
// >
// 대량추가
// </button>
// </div>
// </div>
// <input
// type="text"
// placeholder="상품 검색..."
// value={filter}
// onChange={(e) => setFilter(e.target.value)}
// className="w-full p-2 mb-4 border border-gray-300 rounded text-black"
// />
// <ul className="mb-4 mx-4 flex gap-3 text-sm justify-end">
// <li>검색결과: {filteredItems.length.toLocaleString()}개</li>
// <li>전체가격: {totalPrice.toLocaleString()}원</li>
// <li>평균가격: {averagePrice.toLocaleString()}원</li>
// </ul>
// <ul className="space-y-2">
// {filteredItems.map((item, index) => (
// <li
// key={index}
// className={`p-2 rounded shadow ${theme === "light" ? "bg-white text-black" : "bg-gray-700 text-white"}`}
// >
// {item.name} - {item.category} - {item.price.toLocaleString()}원
// </li>
// ))}
// </ul>
// </div>
// );
// });

Choose a reason for hiding this comment

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

불필요한 주석은 지워주시는게 좋을 것 같아용~!~!

Copy link

@wonjung-jang wonjung-jang 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 +22 to +43
function _deepEqualArray<T>(arrA: Array<T>, arrB: Array<T>): boolean {
if (arrA.length !== arrB.length) return false;
return arrA.every((item, index) => deepEquals(item, arrB[index]));
}

function _deepEqualObject<T extends string | number | symbol>(
objA: Record<T, unknown>,
objB: Record<T, unknown>,
): boolean {
if (objA === objB) return true;

const keysA = Object.keys(objA) as T[];
const keysB = Object.keys(objB) as T[];

if (keysA.length !== keysB.length) {
return false;
}

return keysA.every(
(key) => keysB.includes(key) && deepEquals(objA[key], objB[key]),
);
}

Choose a reason for hiding this comment

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

혹시 배열일 경우를 따로 분리하신 이유가 있을까요?

Comment on lines +29 to +43
const filteredItems = useMemo(() => {
return items.filter(
(item) =>
item.name.toLowerCase().includes(filter.toLowerCase()) ||
item.category.toLowerCase().includes(filter.toLowerCase()),
);
}, [filter, items]);

const totalPrice = useMemo(() => {
return filteredItems.reduce((sum, item) => sum + item.price, 0);
}, [filteredItems]);

const averagePrice = useMemo(() => {
return Math.round(totalPrice / filteredItems.length) || 0;
}, [filteredItems.length, totalPrice]);

Choose a reason for hiding this comment

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

엇 저도 ItemList 컴포넌트에서 filteredItem, totalPrice, averagePriceuseMemo로 메모이제이션 했었는데요.
하고나서 보니 ItemList에서 itemfilter라는 상태가 변하면 리렌더링이 되어야 해서 useMemo를 지웠었는데요.
어.. 설명을 하자면 toalPriceaveragePricefilteredItem을 의존하고 있고 filteredItemfilteritem을 의존하고 있는데, ItemList에서 filteritem을 사용하지 않는 곳이 없어서? 변경이되면 무조건 리렌더링이 되어야 하는 상황이라, useMemo를 사용하나 안 하나 똑같다라는 결론이 나왔습니다.
도운님은 useMemo를 사용하신 이유가 있을까요?

Comment on lines +15 to +20
if (ref.current === null || !_equals(_deps, ref.current._deps)) {
ref.current = {
_deps: _deps,
value: factory(),
};
}

Choose a reason for hiding this comment

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

저는 최초 사용 시, 그 이후를 분기하기 위해 따로 쓰긴 했는데, 안에 로직이 같아서 이게 맞나 싶은 의문이 들었었는데, 도운님이 하신 거 보니까 합친 게 깔끔하네요!👍

Comment on lines +31 to +56
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="이름"
className="w-full p-2 border border-gray-300 rounded text-black"
/>
{errors?.name && <ErrorMessage message={errors.name} />}
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="이메일"
className="w-full p-2 border border-gray-300 rounded text-black"
/>
{errors?.email && <ErrorMessage message={errors.email} />}
<input
type="number"
name="age"
value={formData.age}
onChange={handleChange}
placeholder="나이"
className="w-full p-2 border border-gray-300 rounded text-black"
/>

Choose a reason for hiding this comment

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

input의 className이 동일해서 하나의 컴포넌트로 묶을 수 있을 것 같아요!

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