React 메모이제이션과 최적화

2025. 10. 24. 18:15·Frontend/React

React에는 useMemo, useCallback, React.memo, useRef, useTransition 등 다양한 메모이제이션 훅이 존재한다.

하지만 이 훅들은 “성능을 무조건 좋게 하는 마법 도구”가 아니라, 비용이 있는 캐싱 메커니즘이다. 즉, 잘못 쓰면 오히려 성능과 가독성, 유지보수성을 모두 해치는 결과를 초래한다.

 

이 글에서는 React의 주요 메모이제이션 훅들을 비교하고, “언제 써야 하는지, 왜 남용하면 느려지는지”를 정리해본다.

 

1. 메모이제이션의 본질

Memoization = 캐싱(Caching) + 의존성 추적

React의 메모이제이션 훅은 이전 렌더 결과(값, 함수, UI)를 저장해두었다가 의존성이 바뀌지 않으면 재계산을 생략한다.

즉, 성능 향상의 핵심은 “불필요한 연산을 줄이는 것”이지 “리렌더링을 완전히 막는 것”이 아니다.

  • 불필요한 연산: 렌더링 과정에서 계산할 필요가 없는 값, 반복적으로 실행되는 복잡한 함수, 필터링/정렬 등 CPU를 소모하는 작업
  • 리렌더링 자체: React는 Virtual DOM과 diff 알고리즘을 통해 실제 DOM 업데이트를 최소화하므로, 렌더링이 발생한다고 해서 반드시 성능 문제가 생기는 것이 아니다.

즉, 렌더링을 완전히 막는 것이 목표가 아니라, 렌더링 중 수행되는 연산을 최적화하여 React가 실제 DOM 업데이트를 효율적으로 수행하도록 해야 한다.

 

2. 메모이제이션 훅 종류별 특징

훅 / 함수 역할 주 사용 목적 잘못된 오해
React.memo 컴포넌트 결과를 메모이제이션 부모 리렌더 시 자식이 불필요하게 리렌더되는 것 방지 “리렌더링을 완전히 막는다” ❌
useMemo 값(계산 결과)을 메모이제이션 무거운 연산 결과 캐싱 단순 계산에도 습관적으로 사용
useCallback 함수 참조를 메모이제이션 memoized 자식에 동일한 콜백 전달 모든 함수에 일괄 적용
useRef 렌더와 무관한 값 저장 렌더링 없이 참조 유지 상태 저장 대체용으로 오용
useDeferredValue 입력값을 지연 업데이트 입력과 렌더 분리 (ex. 검색 UI) 단순 state 지연으로 오해
useTransition 상태 업데이트 우선순위 조정 UI 렌더 우선순위 최적화 비동기 로직 처리용으로 오용

 

3. 왜 남발하면 느려질까?

(1) 메모이제이션도 계산이 필요하다.

useMemo와 useCallback은 렌더링 시마다 deps(dependency) 배열을 비교하고, 필요시 새 객체를 생성한다.

const value = useMemo(() => computeHeavy(data), [data]);

여기서 computeHeavy가 아니라 단순 계산이라면 그냥 const value = data * 2가 더 빠르다.

(2) 비교 연산을 유발한다.

  • React.memo / PureComponent
    • props가 동일하면 렌더링을 건너뛴다.
    • 내부적으로 shallow compare(얕은 비교)를 수행하여 이전 props와 새 props를 비교한다.
    • props가 많거나 중첩된 객체/배열이 포함되면, 비교 자체가 성능 오버헤드가 될 수 있다.
  • useMemo / useCallback
    • 재사용 여부는 deps 배열을 기준으로 결정된다.
    • deps 안의 값이 바뀌지 않으면 이전 결과를 재사용하고, 바뀌면 새로 계산한다.
    • 이때 deps 비교도 얕은 비교(shallow compare)로 이루어진다.
    • 따라서 deps가 많거나 중첩된 객체/배열이 포함되면, 비교 자체가 오히려 성능 오버헤드가 될 수 있다.

(3) 코드 복잡성과 유지보수 비용 증가

모든 콜백과 값을 메모이제이션하면 코드가 아래와 같이 쓰여질 수 있는데,

const handleInput = useCallback(() => {...}, [a, b, c]);
const filteredList = useMemo(() => data.filter(...), [data]);

팀원 입장에서는 “왜 useMemo가 필요한지” 명확하지 않으면 수정 시 의존성 실수를 유발하기 쉽고, 디버깅이 복잡해질 수 있다.

(4) GC(가비지 컬렉션) 부담

메모이제이션(고차컴포넌트나 훅)은 연산 결과나 컴포넌트를 캐싱하여 재사용한다. 이 과정에서 캐싱된 값은 메모리를 사용하게 된다. 저장되는 값이 많거나 크면 메모리 사용량이 증가하고, 가비지 컬렉션(GC)이 자주 발생할 수 있다. 이로 인해 렌더링 프레임이 일시적으로 지연될 수 있으므로, 간단한 연산이나 작은 값에는 메모이제이션을 남용하지 않는 것이 좋다. 

 

4. 언제 써야 ‘진짜 최적화’인가?

상황 권장 훅 설명
연산량이 큰 계산 (정렬, 필터링, 복잡한 연산) ✅ useMemo 계산 생략
자식 컴포넌트가 React.memo로 감싸져 있고 props로 콜백 전달 ✅ useCallback 참조 안정성
값은 변경되지 않지만 렌더링 간 유지되어야 함 (ex. DOM 참조, 이전 값 저장) ✅ useRef 렌더 영향 없음
실시간 검색 입력 중 UI 멈춤 방지 ✅ useDeferredValue 렌더 지연
UI 응답성과 상태 업데이트 우선순위 분리 ✅ useTransition 사용자 경험 향상
자식 컴포넌트를 메모이제이션하여 불필요한 렌더링 방지
✅ React.memo
props가 동일하면 렌더링 생략
단순 계산, 짧은 문자열 변환 ❌ 불필요 오버엔지니어링
memoized 자식 없음, 콜백 전달 안 함 ❌ 불필요 의미 없음

1. 연산량이 큰 계산 (정렬, 필터링, 복잡한 연산)

훅: useMemo

권장: 렌더링 시마다 반복 수행되면 성능에 영향을 줄 수 있는 계산을 메모이제이션한다.

설명: 정렬, 필터링, 복잡한 계산 등은 값이 바뀌지 않는 한 다시 계산할 필요가 없으므로 useMemo로 캐싱해 불필요한 연산과 렌더링을 줄인다.

const sortedList = useMemo(() => {
  console.log("정렬 수행");
  return items.sort((a, b) => a - b);
}, [items]);

2. 자식 컴포넌트가 React.memo 로 감싸져 있고 콜백을 전달할 때

훅: useCallback

권장: props로 콜백을 전달하는데, 자식 컴포넌트가 React.memo일 경우 사용한다.

설명: 콜백 참조가 매 렌더마다 바뀌면 자식 컴포넌트가 불필요하게 재렌더링되므로, useCallback으로 참조를 안정화해 성능을 최적화한다.

const handleClick = useCallback(() => {
  console.log(id);
}, [id]);

<Child onClick={handleClick} />

 

3. 값은 변경되지 않지만 렌더링 간 유지되어야 하는 경우

훅: useRef

권장: DOM 요소나 이전 상태 값 등을 렌더링과 무관하게 저장할 때 사용한다.

설명: 렌더링 시 값을 새로 만들 필요가 없고, 상태 변경으로 리렌더링을 유발하지 않아야 하는 값에 적합하다. 예: 이전 상태 저장, 타이머 ID, 외부 라이브러리 참조 등.

const prevCount = useRef(0);

useEffect(() => {
  prevCount.current = count;
}, [count]);

4. 실시간 검색 입력 중 UI 멈춤 방지

훅: useDeferredValue

권장: 사용자의 입력은 즉시 반영하고, 느린 계산이나 렌더링은 지연 처리해야 할 때 사용한다.

설명: 검색어나 필터링 등 사용자가 빠르게 입력하는 경우, UI 응답성을 유지하면서 느린 렌더링 작업을 지연시켜 부드러운 사용자 경험을 제공한다.

import React, { useState, useDeferredValue, useMemo } from 'react';

function SearchExample() {
  // 1.10,000개 예제 데이터 생성
  // useMemo를 사용하여 렌더링 시 불필요한 재생성을 방지
  const items = useMemo(
    () => Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`),
    []
  );

  // 2.검색어 상태
  // 사용자의 입력은 즉시 query 상태에 반영
  const [query, setQuery] = useState('');

  // 3.useDeferredValue를 사용하여 느린 연산 우선순위 낮춤
  // 입력 즉시 UI는 반응하지만, 필터링 계산은 지연되어 UI 멈춤 방지
  const deferredQuery = useDeferredValue(query);

  // 4.필터링
  // deferredQuery가 바뀔 때만 재계산
  const filteredItems = useMemo(
    () =>
      items.filter(item =>
        item.toLowerCase().includes(deferredQuery.toLowerCase())
      ),
    [deferredQuery, items]
  );

  return (
    <div style={{ padding: 20 }}>
      <h2>실시간 검색 예제 (useDeferredValue)</h2>
      
      {/* 5.검색 input */}
      {/* 입력 즉시 query 상태가 업데이트 되어 UI가 즉시 반응 */}
      <input
        type="text"
        placeholder="검색어 입력..."
        value={query}
        onChange={e => setQuery(e.target.value)}
        style={{ width: '300px', padding: '8px', marginBottom: '20px' }}
      />
      
      {/* 6.현재 검색어 표시 */}
      <p>
        검색어: <strong>{query}</strong>
      </p>
      
      {/* 7.필터링 결과 */}
      <p>결과: {filteredItems.length}개</p>
      
      {/* 8.결과 리스트 */}
      {/* 스크롤 성능 안정화를 위해 상위 50개만 렌더링 */}
      <ul style={{ maxHeight: '400px', overflowY: 'auto' }}>
        {filteredItems.slice(0, 50).map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default SearchExample;

5. UI 응답성과 상태 업데이트 우선순위 분리

훅: useTransition

권장: 긴 연산이나 많은 데이터를 렌더링할 때, 사용자 인터랙션을 우선시해야 할 경우 사용한다.

설명: 상태 업데이트를 낮은 우선순위로 처리하여, 버튼 클릭이나 입력 같은 사용자 이벤트는 즉시 반영하고, 긴 작업은 백그라운드에서 처리해 UI가 멈추지 않도록 한다.

import React, { useState, useTransition } from 'react';

function SimpleTransitionExample() {
  const [count, setCount] = useState(0);
  const [isPending, startTransition] = useTransition();

  const handleClick = () => {
    // 빠른 상태 업데이트: 카운트 증가
    setCount(c => c + 1);

    // 느린 작업: 트랜지션으로 낮은 우선순위 처리
    startTransition(() => {
      // 예: 무거운 계산 흉내
      let total = 0;
      for (let i = 0; i < 1e7; i++) {
        total += i;
      }
      console.log('Heavy calculation done:', total);
    });
  };

  return (
    <div style={{ padding: 20 }}>
      <h2>useTransition 간단 예제</h2>
      <button onClick={handleClick}>클릭</button>
      <p>카운트: {count}</p>
      {isPending && <p>⏳ 작업 진행 중...</p>}
    </div>
  );
}

export default SimpleTransitionExample;

6. 자식 컴포넌트를 메모이제이션하여 불필요한 렌더링 방지

고차컴포넌트: React.memo

권장: props가 동일한 경우 자식 컴포넌트의 렌더링을 건너뛰어 성능을 최적화할 때 사용한다.

설명: 내부적으로 얕은 비교(shallow compare)를 수행하므로, 객체나 배열과 같은 복잡한 props는 필요시 useMemo/useCallback과 함께 사용하는 것이 좋다. 리스트 항목 컴포넌트, 반복 렌더링이 많은 UI, 부모가 자주 리렌더링되지만 자식은 props가 변경되지 않는 경우에 사용한다.

const Child = React.memo(({ value }) => {
  console.log("렌더링 발생");
  return <div>{value}</div>;
});

 

7. 단순 계산, 짧은 문자열 변환

권장: 메모이제이션 훅 사용 불필요

설명: 단순 연산이나 문자열 합치기 정도는 렌더링 비용이 거의 없으므로, 오히려 useMemo를 쓰면 코드 복잡도만 증가한다.

const greeting = `Hello ${name}`; // useMemo 불필요

8. memoized 자식 없음, 콜백 전달 안 하는 경우

권장: 메모이제이션 훅 사용 불필요

설명: 자식이 memoized되어 있지 않고 콜백 전달도 필요 없으면, useCallback이나 useMemo 사용은 의미가 없으며 오히려 코드 복잡도를 높인다.

<button onClick={() => console.log("click")}>Click</button> // useCallback 필요 없음

 

정리

  • 렌더링 최적화: useMemo, useCallback → 값/콜백 재사용, 불필요한 렌더링 방지
  • 렌더링과 무관하게 값 유지: useRef → 이전 상태, DOM 참조 등
  • UI 응답성 최적화: useDeferredValue, useTransition → 사용자 인터랙션 우선 처리, 긴 작업 지연
  • 자식 컴포넌트 렌더링 최적화: React.memo → props가 동일하면 자식 컴포넌트 렌더링을 생략, 얕은 비교 수행
  • 불필요한 메모이제이션 주의: 간단한 계산, memoized 자식 없는 경우는 오히려 코드 복잡도 증가

 

5. 결론 — 최적화는 전략이다.

리액트의 메모이제이션 훅은 성능을 위한 도구이지만, 모든 도구에는 비용이 존재한다.

진짜 최적화란 “모든 곳에 적용하는 것”이 아니라, “문제가 발생하는 지점을 정확히 찾아 그 부분만 개선하는 것”이다.

즉, React에서의 최적화는 남용이 아닌 선택적 사용이 핵심이다.

'Frontend > React' 카테고리의 다른 글

상태(stateful) & 비상태(stateless) 컴포넌트  (0) 2025.10.25
React에서 비동기 처리 최적화하기  (0) 2025.10.25
useRef 자세히 알아보기  (0) 2025.10.24
useEffect의 cleanup 함수 알아보기  (0) 2025.10.24
Virtual DOM  (0) 2025.10.16
'Frontend/React' 카테고리의 다른 글
  • React에서 비동기 처리 최적화하기
  • useRef 자세히 알아보기
  • useEffect의 cleanup 함수 알아보기
  • Virtual DOM
JTB
JTB
웹/앱 개발 정보를 공유하고 있습니다.
  • JTB
    JTechBlog
    JTB
  • 전체
    오늘
    어제
    • All About Programming;)
      • Computer Science
        • Terminology and Concepts
        • Network
        • Operating System
        • Database
        • Data Structure
        • Web Development
      • Frontend
        • Javascript Essentials
        • Perfomance Optimization
        • JS Patterns
        • React
        • Next.js
        • Flutter
        • Testing
      • Backend
        • Node.js
      • DevOps
        • Docker & Kubernetes
      • Coding Test
        • LeetCode
        • Programmers
      • Tech Books & Lectures
        • Javascript_Modern JS Deep d..
        • Network_IT 엔지니어를 위한 네트워크 입문
      • Projects
        • PolyLingo_2025
        • Build Your Body_2024
        • JStargram_2021
        • Covid19 Tracker_2021
        • JPortfolio_2021
      • BootCamp_Codestates
        • TIL
        • TILookCloser
        • Pre Tech Blog
        • IM Tech Blog
        • Daily Issues and DeBugging
        • First Project
        • Final Project
        • Sprint Review
        • Good to Know
        • Socrative Review
        • HTML &amp; CSS
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 글쓰기
    • 관리
  • 공지사항

  • 인기 글

  • 태그

    스코프
    딥다이브
    Network
    VoiceJournal
    Data Structure
    이벤트
    How memory manage data
    polylingo
    DOM
    Shared resources
    need a database
    프론트엔드 성능 최적화 가이드
    Threads and Multithreading
    Javascript Essentials
    Binary Tree BFS
    js pattern
    자바스크립트
    database
    TCP/IP
    커리어
    indie hacker
    structure of os
    Operating System
    Time complexity and Space complexity
    leetcode
    자바스크립트 딥다이브
    mobile app
    CPU scheduling algorithm
    testing
    모던 자바스크립트 Deep Dive
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
JTB
React 메모이제이션과 최적화
상단으로

티스토리툴바