
이번 시간에는 렌더링 최적화에 대해 공부한 내용을 작성해보는 시간을 가지도록 하겠습니다.
useMemo에서 Momo는 Memoization을 의미하며, 동일한 값을 return하는 함수를 반복적으로 호출해야 된다면 맨 처음 값을 계산 할 때 해당 값을 메모리에 저장해서 필요할 때 마다(또 다시 계산하지 않고)메모리에서 꺼내서 재사용 하는 기법.
=> 즉, 이전에 계산한 값을 메모리에 저장하여 중복적인 계산을 제거하여 전체적인 실행속도를 빠르게 해주는 기법
위의 사진과 같이 동일한 값을 return하는 함수(calculate함수)를 반복적으로 호출해야 된다면, 맨처음 값을 계산할 때 해당값을 메모리에 저장해서 필요할 때 마다 메모리에서 꺼내서 재사용한다.
앞선 예시와 같이 간단한 일을 하는 함수가 아닌, 무거운 일을 하는 함수라고 한다면 컴포넌트가 렌더링이 될 때마다 반복적으로 호출되게 되면 매우 비효율적이고 성능에도 악영양을 끼칠것이다.
이를 useMemo를 활용하여 간단하게 해결이 가능하다.
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
const hardCalculate = (number) => {
console.log('어려운 계산!');
for (let i = 0; i < 999999999; i++) {} // 생각하는 시간
return number + 10000;
}
function App() {
const [hardNumber, setHardNumber] = useState(1);
const hardSum = hardCalculate(hardNumber);
return (
<div>
<h3>어려운 계산기</h3>
<input
type="number"
value={hardNumber}
onChange={(e) => setHardNumber(parseInt(e.target.value))}
/>
<span> + 10000 = {hardSum}</span>
</div>
);
}
export default App

=>delay가 있는 것을 확인 할 수 있다.
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
const hardCalculate = (number) => {
console.log('어려운 계산!');
for (let i = 0; i < 999999999; i++) {} // 생각하는 시간
return number + 10000;
}
const easyCalculate = (number) => {
console.log("쉬운 계산!")
return number + 1;
}
function App() {
const [hardNumber, setHardNumber] = useState(1);
const [easyNumber, setEasyNumber] = useState(1);
const hardSum = hardCalculate(hardNumber);
const easySum = easyCalculate(easyNumber);
return (
<div>
<h3>어려운 계산기</h3>
<input
type="number"
value={hardNumber}
onChange={(e) => setHardNumber(parseInt(e.target.value))}
/>
<span> + 10000 = {hardSum}</span>
<h3>쉬운 계산기</h3>
<input
type="number"
value={easyNumber}
onChange={(e) => setEasyNumber(parseInt(e.target.value))}
/>
<span> + 1 = {easySum}</span>
</div>
);
}
export default App

=>return number + 1;의 쉬운 계산만 하였을 뿐인데 첫번째와 동일한 delay가 발생
App컴포넌트가 함수형 컴포넌트 이기 때문이다. 쉬운 계산기의 number를 증가시켜주면 easyNumber의 state가 변경된다.
state가 변경되었다는 말은 App컴포넌트가 다시 랜더링 된다는 것을 의미한다. 그래서, hardSum과 easySum 변수가 모두 초기화가 된다.
즉, hardNumber를 바꾸던 easyNumber를 바꾼던 상관없이 hardCalculate안에 있는 의미없는 for루프가 돌아가게되어서 delay가 발생한다. => 너무 비효율적이다.
import { useMemo, useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
const hardCalculate = (number) => {
console.log('어려운 계산!');
for (let i = 0; i < 999999999; i++) {} // 생각하는 시간
return number + 10000;
}
const easyCalculate = (number) => {
console.log("쉬운 계산!")
return number + 1;
}
function App() {
const [hardNumber, setHardNumber] = useState(1);
const [easyNumber, setEasyNumber] = useState(1);
//const hardSum = hardCalculate(hardNumber);
const hardSum = useMemo(() => { //useMemo 부분
return hardCalculate(hardNumber);
}, [hardNumber]);
const easySum = easyCalculate(easyNumber);
return (
<div>
<h3>어려운 계산기</h3>
<input
type="number"
value={hardNumber}
onChange={(e) => setHardNumber(parseInt(e.target.value))}
/>
<span> + 10000 = {hardSum}</span>
<h3>쉬운 계산기</h3>
<input
type="number"
value={easyNumber}
onChange={(e) => setEasyNumber(parseInt(e.target.value))}
/>
<span> + 1 = {easySum}</span>
</div>
);
}
export default App

=> (useMemo를 사용하여)쉬운 계산을 할 때, delay이 없이 계산이 이루어지는 것을 확인할 수 있다.
const hardSum = useMemo(() => {
return hardCalculate(hardNumber);
}, [hardNumber]);
콜백 함수: () => { return hardCalculate(hardNumber); }는 useMemo에 전달된 콜백 함수로, 실제로 메모이제이션하고자 하는 계산을 수행한다. 위의 예에서는 hardCalculate 함수를 호출하고 hardNumber를 인자로 전달하여 그 결과를 반환한다.
의존성 배열: [hardNumber]는 의존성 배열로, useMemo가 결과를 새로 계산해야 하는 조건을 정의합니다. 여기서는 hardNumber 값이 변경될 때마다 useMemo가 콜백 함수를 다시 실행하여 결과를 새로 계산하도록 설정되어 있다. 만약 hardNumber가 변경되지 않는다면, 이전에 계산한 hardSum 값을 재사용한다.
비슷한 역할을 하는 Hook이 또 있다고 하는데, 바로 useCallback이다.
함수를 메모이제이션(memoization)하는 데 사용된다.
즉, 함수의 참조를 재사용할 필요가 있을 때 사용한다.
import { useEffect, useState } from 'react'
import './App.css'
function App() {
const [Number, setNumber] = useState(0);
const [toggle, setToggle] = useState(true);
const someFunction = () => {
console.log(`someFunc: number: ${number}`);
return;
};
useEffect(() => {
console.log('someFuction이 변경되었습니다.');
}, [someFunction]);
return (
<div>
<input
type="number"
value={Number}
onChange={(e) => setNumber(parseInt(e.target.value))}
/>
<button onClick={() => setToggle(!toggle)}>{toggle.toString()}</button>
<br />
<button onClick={someFunction}>Call someFunc</button>
</div>
);
}
export default App

=> 콘솔창을 확인해보니, 토글 버튼을 눌러 토글 스테이트가 변경될 때마다 useEffect가 실행되어 'someFuction이 변경되었습니다.'라는 로그가 콘솔에 출력되고 있다.
=> useEffect가 불필요하게 여러번 실행되고 있음.
import { useEffect, useCallback, useState } from 'react'
import './App.css'
function App() {
const [number, setNumber] = useState(0);
const [toggle, setToggle] = useState(true);
const someFunction = useCallback(() => {
console.log(`someFunc: number: ${number}`);
return;
}, [number]); //의존성 배열 number로 지정
useEffect(() => {
console.log('someFuction이 변경되었습니다.');
}, [someFunction]);
return (
<div>
<input
type="number"
value={number}
onChange={(e) => setNumber(parseInt(e.target.value))}
/>
<button onClick={() => setToggle(!toggle)}>{toggle.toString()}</button>
<br />
<button onClick={someFunction}>Call someFunc</button>
</div>
);
}
export default App

=> 이전 코드와 달리 토글 버튼을 클릭시 useEffect가 실행되지 않아 'someFuction이 변경되었습니다.'라는 로그가 콘솔창에 뜨지 않는 것을 확인 할 수 있다.
=> 대신 의존성 배열로 지정해준 number의 값이 변경 될 때만 콘솔창에 'someFuction이 변경되었습니다.'가 뜨는 것을 확인 할 수 있다.
const someFunction = useCallback(() => {
console.log(`someFunc: number: ${number}`);
return;
}, [number]);
useCallback 훅: useCallback은 첫 번째 인자로 전달된 함수를 메모리에 저장합니다. 저장된 함수는 의존성 배열([number])에 있는 값들의 변화에만 반응하여 업데이트됩니다.
함수 정의: () => { console.log(someFunc: number: ${number}); return; } 이 부분은 실제 저장되는 콜백 함수입니다. 이 함수는 콘솔에 number 변수의 현재 값을 출력합니다.
의존성 배열: [number] 이 배열은 useCallback에 의해 추적되는 의존성을 명시합니다. 여기서 number는 함수가 의존하는 변수입니다. 이 변수의 값이 변경될 때마다 useCallback은 새로운 함수를 메모리에 저장하여 이전 함수를 대체합니다.
useMemo
- useMemo는 값의 계산을 메모이제이션하는 데 사용된다. 복잡한 계산 결과, 객체 리터럴, 배열 등 함수 호출 결과를 캐싱하여 재계산의 필요성을 줄이기 위해 사용.
- useMemo는 계산된 값 자체를 반환한다.
useCallback
- useCallback은 함수를 메모이제이션(memoization)하는 데 사용된다. 즉, 함수의 참조를 재사용할 필요가 있을 때 사용한다. 특히 함수를 자식 컴포넌트에 props로 전달할 때 유용하며, 자식 컴포넌트가 불필요하게 재렌더링되는 것을 방지할 수 있다.
- useCallback은 함수 자체를 반환한다.
여러 상태 업데이트를 하나의 렌더링으로 묶어 처리하는 기능이다.
React에서 상태가 변경되면 기본적으로 컴포넌트가 다시 렌더링된다. 하지만 여러 개의 상태 업데이트가 동시에 발생하는 경우, React는 각각의 상태 업데이트마다 리렌더링을 수행하지 않고, 이를 하나로 묶어서 한 번만 렌더링한다. 이를 배칭(Batching)이라고 하며, React 18부터는 비동기 작업에서도 자동으로 배칭을 수행하는 Auto Batching이 도입되었.
기존의 React 17 이하에서는 onClick 같은 이벤트 핸들러 내부에서 상태를 여러 번 업데이트할 경우 배칭이 자동으로 이루어졌지만, 비동기 작업(예: setTimeout 또는 fetch 콜백 등)에서는 자동으로 배칭이 이루어지지 않았다. React 18에서는 이러한 비동기 작업에서도 배칭을 자동으로 수행하여, 여러 상태 업데이트가 한 번에 일어나도록 개선되었다.
많은 양의 데이터가 있는 리스트나 테이블을 렌더링할 때 성능을 최적화하는 기법이다. 화면에 보이는 항목들만 렌더링하고, 스크롤에 따라 동적으로 필요한 항목을 추가적으로 렌더링해, 브라우저의 렌더링 부담을 줄여주는 방식이다.
react-window 라이브러리를 사용하여 큰 리스트를 렌더링하는 예시
import React from 'react';
import { FixedSizeList as List } from 'react-window';
const items = Array.from({ length: 1000 }, (_, index) => `Item ${index + 1}`);
function Row({ index, style }) {
return (
<div style={style}>
{items[index]}
</div>
);
}
function App() {
return (
<List
height={400} // 리스트의 높이
itemCount={items.length} // 항목 개수
itemSize={35} // 각 항목의 높이
width={300} // 리스트의 너비
>
{Row}
</List>
);
}
export default App;
height, itemCount, itemSize, width 등의 속성으로 리스트의 전체 크기와 아이템의 크기를 정의한다. react-window는 스크롤 시 화면에 보이는 항목만 동적으로 렌더링하여 메모리와 성능을 최적화할 수 있다.
아티클 잘 읽었습니다!
시각 자료들을 잘 활용해주셔서 너무 이해하기 편했던 것 같습니다.
특히 react의 함수형 컴포넌트의 렌더링 방식을 통해서 무슨 변수를 바꾸든 상관 없이 무겁고 오래 걸리는 작업이 무조건 실행되어야 한다는 것을 예시로 들어주신 게 흥미로웠던 주제 같아요. 저도 이번 아티클을 작성하면서 이렇게 불필요한 렌더링은 프로젝트를 설계하면서 무조건 마주하게 되는데 이걸 어떻게 설계해야 하는지, 컴포넌트를 어떻게 구성해야 하고 state 잘 쓰는 법은 무엇일지 많이 고민한 것 같습니다. 이런 측면에서 부모 > 자식 컴포넌트의 렌더링 관계성이 궁금해졌는데
이 아티클을 참고하면서 생각보다 많은 정보를 얻었던 것 같아서 한번 읽어보셔도 좋을 것 같아요!
그리고 useCallback을 제대로 사용해 본 적이 없었는데 예시 코드를 통해서 감을 잡을 수 있었던 것 같아요. 좋은 아티클 감사합니다!
아티클 너무 잘 읽었습니다! 읽으면서, 의문이 생기는 것들 바로 다음 줄에 설명이 적혀있어서 잘 이해하면서 봤습니다 ㅎㅎ 시각적인 자료들 첨부해주셔서 이해하는 것에 더 도움이 됐습니다!
memoization을 활용한 예시들을 보니, 무거운 연산이나 함수를 호출할 때는 메모이제이션을 활용한 최적화가 정말 필수일 것 같다는 것을 느꼈습니다..! 그러나 저도 아티클 작성을 위해 공부해보니, 무조건적인 도입은 오히려 성능 저하를 시킬 수 있다고 하네요ㅜㅜ 메모이제이션을 통해 성능이 개선될 수 있는 연산인지, 의존성 배열을 어떻게 설정할 것인지 등등 잘 고려하여 도입해야할 것 같습니다!
예시를 통한 자세한 설명 감사합니다^^
안녕하세요! YB 유서연입니다.
아티클 너무 잘 읽었습니다! 개념을 쉽게 풀어서 설명해주셔서 이해하기 편했던 것 같아요.
특히, useMemo와 useCallback을 사용해 렌더링 최적화를 구현하는 예시를 직접 들어서 설명해주시고 콘솔이 찍히는 것까지 영상으로 담아주셔서 직관적인 이해가 가능했습니다. 수고하셨어요!