-
리액트 성능 개선에 대해프론트엔드 2025. 8. 18. 21:34
리액트 성능개선
- 리액트 에서 기본적으로 성능 개선을 할 때, useMemo, useCallback, React.memo를 종종 사용한다.
- 메모이제이션을 하는게 성능개선에 무조건적으로 좋다고 생각하는 나의 경우 무조건 사용하는게 좋다고 생각했었다. 🤔
- 무조건적으로 좋을까? 과연?? 한번 알아보자
리액트에서 리 렌더링
- 리액트에서 리 렌더링은 매우 중요하다.
- 리액트는 리 렌더링 작업을 통해 새 값을 읽고 UI를 만든다. -> 이 리 렌더링을 통해 변화된 값이 반영이 된다.
- 리 렌더링을 일으킨다고 해서 무조건 적으로 안좋은 것은 아니다.!! -> 불 필요한 리 렌더링을 방지하는것이 중요하다고 한다.
문제
- 리액트에서 메모이제이션이 왜 필요할까?
- 자바스크립트에서 원시값(문자열, 숫자, 불리언)은 실제 값으로 비교가 가능하지만, 객체의 경우는 다르다.
- 객체는 참조로 비교가 된다.
const data = {a: 1,};
const b = data;console.log(b === data);-> 결과값은 뭐가 나올까 정답은 false가 나온다. b와 data가 서로 다른 참조값(메모리 주소)을 보고 있기 때문이다.
- 위 문제가 리액트에서 까지 문제가 되는데,
1. 컴포넌트는 자신의 상태가 변경되거나, 부모 컴포넌트가 리 렌더링 될 때, 다시 렌더링 된다.
2. 컴포넌트가 리 렌더링이 진행되면, 모든 지역변수(객체 함수 포함)는 새로운 참조로 다시 생성이 된다.
3. 이 새로운 참조가 의존성 배열이나 프로퍼티로 전달되면, 불필요한 리렌더링이나 이펙트 실행이 발생할 수 있다.
문제해결
- 위 문제를 해결하기 위해 메모이제이션 훅이 있다. useMemo, useCallback이다.
- 이 두가지 훅은 참조의 안정을 유지하기 위해 존재한다고 한다. 위 훅은 값을 캐싱하고, 지정한 의존성이 변경될 때만 다시 계산한다.
const memoizedFn = useCallback(() => {doSomething();}, [deps]);- useCallback 부터 확인하면, 함수 자체를 캐싱하는 훅이다.
- doSomething 함수가 매번 새로 만들어지는 것을 방지할 뿐만 아니라, 참조가 유지된다.
- useCallback은 주로 자식 컴포넌트에 콜백 props를 넘길때 자주 사용한다고 한다.
import React, { useState } from "react";
const Child = React.memo(({ onClick }) => {console.log("✅ Child 렌더링됨");return <button onClick={onClick}>클릭</button>;});
export default function Parent() {const [count, setCount] = useState(0);
// 🔴 매번 새로운 함수 객체가 만들어짐const handleClick = () => {console.log("Clicked!");};
return (<div><p>카운트: {count}</p><button onClick={() => setCount(count + 1)}>부모 count 증가</button><Child onClick={handleClick} /></div>);}- 예시를 보면 Child 컴포넌트를 React.memo를 사용해 리 렌더링을 방지하고자 했지만 부모 컴포넌트가 state가 변경되면서, 함수 참조가 달라졌다고 인식해 자식도 강제로 리 렌더링이 된다.
const handleClick = useCallback(() => {console.log('Clicked!');}, []);- 위 처럼 useCallback을 사용해 전달하면 함수가 같은 참조를 유지하니, 리 렌더링이 발생하지 않는다.
- useMemo는 비싼 계산을 매번 새로 하지 않고, 의존성 배열이 변화하지 않으면 이전 계산 결과를 재사용한다.
import React, { useState, useMemo } from "react";
export default function Example() {const [count, setCount] = useState(0);const [other, setOther] = useState(0);
// 🔴 매번 렌더링마다 무거운 계산 수행const expensiveCalculation = (num) => {console.log("💡 무거운 계산 실행 중...");for (let i = 0; i < 1000000000; i++) {} // 시간 잡아먹는 연산return num * 2;};
// ✅ useMemo로 최적화const doubled = useMemo(() => expensiveCalculation(count), [count]);
return (<div><p>count: {count}</p><p>other: {other}</p><p>doubled: {doubled}</p><button onClick={() => setCount(count + 1)}>count 증가</button><button onClick={() => setOther(other + 1)}>other 증가</button></div>);}- 만약 무거운 계산을 하고 있다면, 메모로 최적화가 가능한데, count의 값이 변경되면, 그 때 리 렌더링이 발생한다.
useMemo vs React.Memo
- useMemo와 React.Memo의 차이는 useMemo는 의존성 배열의 값이 변화하지 않으면, 캐싱된 값을 반환하고
- React.Memo는 props가 바뀌지 않으면, 컴포넌트 리 렌더링을 스킵한다.
- React.memo = 컴포넌트 리렌더링 여부를 props 비교로 제어
- useMemo = 값 계산/객체 참조를 캐싱해서 deps 안 바뀌면 재계산 방지
React.Memo 함정
- React.memo의 함정이 있다.
1. 스프레드 연산자
const Child = React.memo(({ data }) => {// 컴포넌트 구현});
// 프로퍼티가 변경되기 때문에 메모이제이션이 깨짐const Parent = props => {return <Child {...props} />;};- 위 처럼 스프레드 연산자를 사용해 프로퍼티를 전달하면 프로퍼티 변경으로 인해 메모이제이션이 깨진다.
2. children 프로퍼티
const MemoComponent = React.memo(({ children }) => {// 컴포넌트 구현});
const Parent = () => {// 이것은 메모이제이션을 깨뜨립니다! children은 매 렌더마다 새로 생성return (<MemoComponent><div>Some content</div></MemoComponent>);};- 위 처럼 자식 컴포넌트를 Memo로 사용해도 부모 컴포넌트가 렌더링 되면 자식도 무조건 리 렌더링이 발생한다는 특징이 있기 때문에 결국 리 렌더링이 발생한다.!!
const Parent = () => {const content = useMemo(() => <div>Some content</div>, []);
return <MemoComponent>{content}</MemoComponent>;};- 그래서 children 자체를 메모이제이션을 해야한다!!
정리
- 최적화 시에는 각자 어떤 역할을 하는지 알고 수정하는 것이 필요하다
'프론트엔드' 카테고리의 다른 글
자바스크립트 딥다이브 1일차 (1) 2025.09.07 Next.js SEO 최적화를 해보자 (3) 2025.08.30 자바스크립트 함수 (2) 2025.08.18 Prisma에 대해 (2) 2025.08.15 package.json + package.lock.json (3) 2025.08.15