✍️ What I Learned/TIL

[TIL] useMemo, useCallback

Jiwon() 2023. 8. 4. 15:22


1. useMemo

리렌더링 사이에 연산 값(value)을 캐시할 수 있는 리액트 훅으로, 메모화된(memoized) 값을 계산하는 함수를 호출한다.

import { useMemo } from "react";

const cachedValue = useMemo(calculateValue, dependencies)

 

1) 사용 예시

리액트 공식 문서에서 설명하고 있는 사용 예시는 다음과 같다.

  • Skipping expensive recalculations (비용이 많이 드는 재연산 생략)
  • Skipping re-rendering of components (컴포넌트의 리렌더링 생략)
  • Memoizing a dependency of another Hook (다른 훅의 의존성 메모화)
  • Memoizing a function (함수 메모화)
import { useMemo } from 'react';

function TodoList({ todos, tab, theme }) {

  // todos와 tab을 인수로 받는 filterTodos라는 함수의 값을 캐싱
  // todos 또는 tab, 즉 의존성 배열의 값이 변경되었을 때만 재연산하고 변경되지 않으면 캐싱한 값을 재사용하여 불필요한 연산을 줄임
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  // ...
  return (
    <div className={theme}>
    { */ 따라서 재연산이 이루어지지 않으면 불필요한 컴포넌트의 리렌더링도 방지할 수 있음 /* }
      <List items={visibleTodos} />
    </div>
  );
}

 

 

2) 무한 스크롤 시 사용

  • `declare` 키워드는 주로 외부 모듈이나 라이브러리의 타입 선언을 지정하는데 사용된다.
  • 따라서 `declare` 키워드는 내부 모듈이나 파일에서 사용하지 않아야 한다.
  • 나는 현재 내부 컴포넌트들에서만 Todo와 TodoProps의 타입을 사용하고 있기 때문에 declare로 선언해주면 안되는 것이다.
"use client";
import Link from "next/link";

import { useMemo } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import { getPosts } from "@/api/community/post";
import { useInView } from "react-intersection-observer";

import { usePathname } from "next/navigation";

import Image from "next/image";
import { getFirstImage, getImgUrl, removeHtmlTags } from "@/libs/util";
import Loading from "@/app/loading";
import CategoryTag from "../ui/CategoryTag";

import { PostType, ToTalDataType } from "@/types/types";
import { PATHNAME_OHJIWAN, PATHNAME_PRODUCT, PATHNAME_RECIPE, PATHNAME_RESTAURANT } from "@/enums/community";

type QueryKeyMap = {
  [key: string]: string[];
};

const GetPosts = () => {
  // .. 중간 생략

  const {
    data: posts,
    isLoading,
    isError,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage
  } = useInfiniteQuery<ToTalDataType>({
    queryKey: queryKey,
    queryFn: ({ pageParam }) => getPosts(pathname, pageParam),
    getNextPageParam: (lastPage) => {
      // 전체 페이지 개수보다 작을 때 다음 페이지로
      if (lastPage.page < lastPage.total_pages) {
        return lastPage.page + 1;
      }
    },
    cacheTime: 300000,
  });
  
  // useMemo: posts가 변하지 않으면 캐싱해둔 이전 값을 재사용
  const accumulatePosts = useMemo(() => {
    return posts?.pages
      .map((page) => {
        return page.posts;
      })
      .flat();
      // flat() : 모든 하위 배열 요소를 지정한 깊이까지 재귀적으로 이어붙인 새로운 배열 생성
  }, [posts]);

  const { ref } = useInView({
    threshold: 1,
    onChange: (InView) => {
      if (!InView || !hasNextPage || isFetchingNextPage) return;
      fetchNextPage();
    }
  })

  if (isLoading) return <Loading />;
  if (isError) {
    console.error("데이터를 불러오는 중에 오류가 발생했습니다:", isError);
    return "데이터를 불러오는 중에 오류가 발생했습니다.";
  }
  return (
    <section className="flex flex-col mt-10 mb-20 w-[725px]">
      {
        <div className="flex flex-col mb-5 justify-center">
          <h2 className="text-xl flex mb-8">{getCategoryName(pathname)} 글</h2>
          {accumulatePosts?.map((post: PostType) => (
              <div
                key={post.post_uid}
                className="border-t last:border-b flex flex-col justify-between px-4 py-4"
              >
                { */ ... 중간 생략 /* }
            ))}
        </div>
      }
        <div ref={ref} className="w-full h-3" />
    </section>
  );
};

export default GetPosts;

 


2. useCallback

리렌더링 사이에 함수 정의를 캐시할 수 있는 리액트 훅으로, 함수를 메모화한다.

import { useMemo } from "react";

const cachedFn = useCallback(fn, dependencies)

 

1) 사용 예시

리액트 공식 문서에서 설명하고 있는 사용 예시는 다음과 같다.

  • Skipping re-rendering of components (컴포넌트 리렌더링 생략)
  • Updating state from a memoized callback (메모화 한 콜백함수로 상태 업데이트)
  • Preventing an Effect from firing too often (이펙트가 너무 자주 실행되는 것을 방지)
  • Optimizing a custom Hook (커스텀 훅 최적화)

Preventing an Effect from firing too often의 예시

// useCallback을 활용한 예시
function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const createOptions = useCallback(() => {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }, [roomId]); // ✅ Only changes when roomId changes

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // ✅ Only changes when createOptions changes
  // ...
// useCallback을 사용하지 않아 비효율적인 예시
function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  function createOptions() {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🔴 Problem: This dependency changes on every render
  // ...