[React] useMemo, React.memo, useCallback, useRef 차이점

정은·2025년 6월 21일
post-thumbnail

useMemo: 값 계산 최적화
React.memo: 컴포넌트 렌더링 최적화
useCallback: 함수 재생성 방지
useRef: DOM 접근, 값 저장
→ 성능 최적화 & 렌더링 제어에 쓰이는 핵심 훅과 컴포넌트

useMemo

  • 복잡한 계산을 렌더링마다 반복하지 않도록 캐싱
    → 의존값이 바뀌지 않으면 이전 값 재사용
"use client";

import { useState, useMemo } from "react";

type User = {
  id: number;
  name: string;
  email: string;
};

const dummyUsers: User[] = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  name: `User ${i}`,
  email: `user${i}@example.com`,
}));

export default function UserTable() {
  const [search, setSearch] = useState("");

  const filteredUsers = useMemo(() => {
    console.log("🔍 사용자 목록 필터링 실행");
    return dummyUsers.filter((user) =>
      user.name.toLowerCase().includes(search.toLowerCase())
    );
  }, [search]);

  return (
    <div className="p-6 space-y-4">
      <h2 className="text-xl font-bold">📋 사용자 목록</h2>
      <input
        type="text"
        placeholder="이름으로 검색..."
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        className="border px-3 py-1 w-full"
      />

      <ul className="h-[300px] overflow-y-scroll border rounded p-2 space-y-1">
        {filteredUsers.slice(0, 50).map((user) => (
          <li key={user.id} className="text-sm text-gray-700 dark:text-gray-300">
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
    </div>
  );
}

코드 설명:
1. 10,000명의 유저 데이터를 메모리에 저장
2. 사용자가 입력한 search 키워드에 따라 useMemo를 통해 결과를 캐싱해서 재사용
3. 필터된 유저 목록은 최대 50명만 보여줌 filteredUsers.slice(0, 50)
4. search 값이 바뀔때만 filteredUsers이 실행됨, 외에는 캐싱된 filteredUsers 사용

🤔 근데 검색할때마다 필터 함수 돌릴거면 캐싱해서 재사용하는 의미가 없지 않나?
→ react는 state 값이 변경될 때 마다 컴포넌트 함수 전체를 다시 실행하기 때문에 useMemo가 없으면 필터링 연산도 매번 다시 실행된다 그렇기 때문에 큰 계산이 있거나 결과가 자주 변하지 않을때는 useMemo를 사용하는 것이 좋다


React.memo

  • props가 바뀌지 않으면 재렌더링을 막는 고차 컴포넌트 (HOC)
    *HOC: 컴포넌트를 인자로 받아 새로운 컴포넌트로 반환하는 함수
"use client";

import { useState } from "react";
import React from "react";

export default function MemoExample() {
  const [count, setCount] = useState(0);
  const [input, setInput] = useState("");

  return (
    <div className="p-6 space-y-4">
      <h2 className="text-xl font-bold">React.memo 사용 비교</h2>

      <button
        onClick={() => setCount((prev) => prev + 1)}
        className="px-4 py-2 bg-blue-500 text-white rounded"
      >
        숫자 증가 (count: {count})
      </button>

      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="입력해보세요"
        className="border px-2 py-1 rounded"
      />

      <div className="flex gap-10 mt-6">
        <NormalChild />
        <MemoizedChild />
      </div>
    </div>
  );
}

// 일반 컴포넌트
function NormalChild() {
  console.log("❌ NormalChild 렌더링");
  return <div className="p-4 bg-red-100 rounded">일반 자식 컴포넌트</div>;
}

// memo로 감싼 컴포넌트
const MemoizedChild = React.memo(function MemoChild() {
  console.log("✅ MemoizedChild 렌더링");
  return <div className="p-4 bg-green-100 rounded">React.memo 자식 컴포넌트</div>;
});

→ 부모가 리렌더링돼도 props가 같으면 해당 컴포넌트는 리렌더링되지 않음

  • 실행해보면 초기 렌더링 console

  • 숫자 증가 버튼 한번 클릭 시

  • input에 텍스트 입력 시

💡 초기 렌더링 이후 자식 컴포넌트 리렌더링이 되지 않는 것을 볼수 있다!


useCallback

  • 함수를 의존성 배열 기준으로 메모이제이션
"use client";

import React, { useCallback, useState } from "react";

const ChildButton = ({ label, onClick }: { label: string; onClick: () => void }) => {
  console.log(`🔄 ${label} 렌더링됨`);
  return (
    <button
      onClick={onClick}
      className="px-4 py-2 rounded text-white font-medium"
      style={{
        backgroundColor: label.includes("useCallback") ? "#10b981" : "#3b82f6",
      }}
    >
      {label}
    </button>
  );
};

const MemoizedButton = React.memo(ChildButton);

export default function CallbackComparison() {
  const [count, setCount] = useState(0);

  // ❌ useCallback 없이: 매 렌더마다 함수 새로 생성됨
  const wthoutCallback = () => {
    console.log("클릭 (without useCallback)");
  };

  // ✅ useCallback 사용: 함수 메모이제이션됨
  const withCallback = useCallback(() => {
    console.log("클릭 (with useCallback)");
  }, []);

  return (
    <div className="p-6 space-y-6">
      <h2 className="text-xl font-bold">🔁 useCallback 사용 / 미사용 비교</h2>

      <p className="text-gray-700">count: {count}</p>
      <button
        onClick={() => setCount((prev) => prev + 1)}
        className="px-3 py-1 bg-gray-800 text-white rounded"
      >
        count 증가 (부모 컴포넌트 리렌더)
      </button>

      <div className="flex gap-4 mt-4">
        <MemoizedButton label="❌ useCallback 미사용" onClick={wthoutCallback} />
        <MemoizedButton label="✅ useCallback 사용" onClick={withCallback} />
      </div>
    </div>
  );
}

→ 매번 새로운 함수 객체를 생성하지 않도록 방지
(특히 React.memo 자식에게 props로 넘길 때 유용)

  • 실행해보면 초기 렌더링 console

  • count 증가 (부모 컴포넌트 리렌더) 버튼 한번 클릭 시

💡 useCallback 함수 사용 시 메모이제이션 된 값을 사용하는걸 볼 수 있다!


useRef

  • 렌더링과 무관한 값을 저장하거나 DOM 요소를 참조할 때 사용
"use client";

import { useRef, useState } from "react";

export default function RefExample() {
  const countRef = useRef(0); // 리렌더링 없이 값 저장
  const [renderCount, setRenderCount] = useState(0); // 렌더링 확인용

  const handleClick = () => {
    countRef.current += 1; // 화면에는 안 보임
  };

  const triggerRender = () => {
    setRenderCount((prev) => prev + 1); // 강제 리렌더링
  };

  return (
    <div className="space-y-4 p-6">
      <h2 className="text-lg font-semibold">🔄 useRef 리렌더링 테스트</h2>

      <button
        onClick={handleClick}
        className="px-4 py-2 bg-blue-500 text-white rounded"
      >
        useRef 값 +1 (렌더링 없음)
      </button>

      <button
        onClick={triggerRender}
        className="px-4 py-2 bg-gray-700 text-white rounded"
      >
        강제로 리렌더링
      </button>

      <div className="mt-4 text-gray-800">
        <p>useRef 값: {countRef.current}</p>
        <p>렌더링 횟수: {renderCount}</p>
      </div>
    </div>
  );
}
  • 초기 렌더링 결과

  • useRef값 +1 버튼을 3번 클릭했을 때

  • 강제로 리렌더링 버튼을 눌렀을 때

0개의 댓글