React 애플리케이션에서 성능 최적화는 중요한 부분입니다. 컴포넌트가 자주 리렌더링되면 불필요한 계산이 반복되어 성능에 영향을 미칠 수 있습니다. 이를 방지하기 위해 useMemo, useCallback, React.memo와 같은 성능 최적화 도구를 활용할 수 있습니다. 이 글에서는 이 세 가지 방법을 다루며, 각 방법이 어떻게 성능을 최적화하는지 구체적인 예시와 함께 설명하겠습니다.
1. useMemo: 불필요한 연산 방지
useMemo는 값을 메모이제이션(memoization) 하여, 특정 값이 변경될 때만 재계산되도록 합니다. 주로 계산 비용이 높은 작업이나 렌더링 중 자주 호출되는 함수에 유용합니다.
import { useRef, useState, useMemo } from "react";
// Average 컴포넌트 정의
function Average() {
// 상태 관리: 입력값과 숫자 목록을 관리
const [number, setNumber] = useState(''); // 입력값을 관리
const [list, setList] = useState([]); // 등록된 숫자들의 목록을 관리
// 입력값을 상태에 반영하는 함수
const changeNumber = e => setNumber(e.target.value);
// 숫자를 등록하는 함수
const changeList = () => {
// 현재 입력값을 목록에 추가
const newList = list.concat(number);
setList(newList); // 목록을 업데이트
setNumber(''); // 입력 필드를 초기화
refNumber.current.focus(); // 입력 필드에 포커스 설정
};
// ref를 사용하여 입력 필드에 직접 접근
const refNumber = useRef();
// 평균값을 계산하는 함수 (비용이 큰 작업을 시뮬레이션)
const getAverage = () => {
console.log("평균값을 계산 중 입니다.");
// 3초 동안 대기하는 코드 (평균 계산을 시뮬레이션)
const wakeupTime = Date.now() + 3000;
while (Date.now() < wakeupTime) { }
// 목록이 비어있으면 0을 반환
if (list.length === 0) return 0;
// 목록의 모든 숫자를 합산하여 평균을 계산
const total = list.reduce((prev, curr) => prev + Number(curr), 0);
return total / list.length; // 평균값 반환
};
return (
<>
{/* 입력 필드: 숫자를 입력하고 onChange 이벤트로 상태를 변경 */}
<input ref={refNumber} type="number" value={number} onChange={changeNumber} />
{/* 등록 버튼: 숫자를 목록에 추가 */}
<button onClick={changeList}>등록</button>
<div>입력값: {number}</div>
{/* 평균값 표시: useMemo 훅을 사용하여 list가 변경될 때만 평균값을 계산 */}
<div>평균값: {useMemo(() => getAverage(), [list])}</div>
<div>등록된 숫자들:</div>
<ul>
{/* 등록된 숫자들을 목록으로 표시 */}
{list.map((v, i) => <li key={i}>{v}</li>)}
</ul>
</>
);
}
// App 컴포넌트: Average 컴포넌트를 렌더링
const App = () => <Average />;
// App 컴포넌트를 내보내기
export default App;
useMemo를 사용하여 getAverage 함수가 list 배열이 변경될 때만 호출되도록 했습니다. 이로 인해 숫자를 입력할 때마다 평균값을 계산하는 대신, list가 변경되었을 때만 계산이 수행됩니다. 이를 통해 불필요한 렌더링과 계산을 방지할 수 있습니다.
2. useCallback: 불필요한 콜백 함수 재생성 방지
useCallback은 콜백 함수가 불필요하게 다시 생성되지 않도록 최적화하는 훅입니다. 이 훅은 주로 자식 컴포넌트에 콜백 함수를 전달할 때 유용합니다. 부모 컴포넌트가 리렌더링될 때마다 동일한 콜백 함수를 사용하도록 보장할 수 있습니다.
import { useState, useCallback, memo } from "react";
// Todos 컴포넌트는 `memo`로 래핑하여 불필요한 렌더링을 방지
const Todos = memo(({ todos, addTodo }) => {
// 3초 동안 대기하는 코드 (성능 시뮬레이션)
const wakeup = Date.now() + 3000;
while (Date.now() < wakeup) { }
// 렌더링 시마다 콘솔에 메시지를 출력
return (
<div>
{console.log("Todo 컴포넌트가 렌더링...")}
{/* Todo 항목을 추가하는 버튼 */}
<button onClick={addTodo}>Add Todo</button>
<h2>TODOS</h2>
<ul>
{/* todos 배열을 순회하여 항목을 목록으로 렌더링 */}
{todos.map((todo, index) => <li key={index}>{todo}</li>)}
</ul>
</div>
);
});
// App 컴포넌트 정의
const App = () => {
// todos 상태: 할 일 목록을 관리
const [todos, setTodos] = useState([]);
// todos 상태를 업데이트하는 함수 (배열에 새로운 할 일을 추가)
const changeTodos = useCallback(
() => setTodos(prevTodos => [...prevTodos, "NEW ITEM " + prevTodos.length]),
[todos] // todos가 변경될 때마다 이 함수가 새로 생성되지 않도록 함
);
// count 상태: 카운트를 관리
const [count, setCount] = useState(0);
// 카운트를 증가시키는 함수
const changeCount = () => setCount(count + 1);
return (
<>
{/* 카운트를 증가시키는 버튼 */}
<div>
<button onClick={changeCount}>카운트 증가</button>
<h2>카운트: {count}</h2>
</div>
<hr />
{/* Todos 컴포넌트에 todos와 addTodo 함수 전달 */}
<Todos todos={todos} addTodo={changeTodos} />
</>
);
};
export default App;
useCallback을 사용하여 changeTodos 함수가 불필요하게 다시 생성되지 않도록 했습니다. 부모 컴포넌트가 리렌더링될 때마다 동일한 changeTodos 함수가 자식 컴포넌트인 Todos에 전달되며, 이로 인해 Todos 컴포넌트의 불필요한 렌더링을 방지할 수 있습니다.
3. React.memo: 자식 컴포넌트 리렌더링 최적화
React.memo는 **props가 변경되지 않으면 리렌더링을 건너뛰는 고차 컴포넌트(HOC)**입니다. 부모 컴포넌트가 리렌더링되더라도, 자식 컴포넌트의 props가 변경되지 않으면 자식 컴포넌트는 리렌더링되지 않습니다.
import { useState, memo } from "react";
// Todos 컴포넌트는 `memo`로 래핑되어 불필요한 렌더링을 방지
const Todos = memo(({ todos, addTodo }) => {
// 3초 동안 대기하는 코드 (성능 테스트나 대기 시뮬레이션을 위한 코드)
const wakeup = Date.now() + 3000;
while (Date.now() < wakeup) { }
return (
<div>
{/* 콘솔에 메시지를 출력하여 `Todos` 컴포넌트가 렌더링될 때마다 확인 */}
{console.log("Todo 컴포넌트가 렌더링...")}
{/* 할 일 추가 버튼, 클릭 시 `addTodo` 함수가 호출되어 새로운 할 일이 추가됨 */}
<button onClick={addTodo}>Add Todo</button>
<h2>TODOS</h2>
<ul>
{/* `todos` 배열을 순회하여 각각의 할 일을 목록으로 표시 */}
{todos.map((todo, index) => <li key={index}>{todo}</li>)}
</ul>
</div>
);
});
// App 컴포넌트 정의
const App = () => {
// todos 상태: 할 일 목록을 저장
const [todos, setTodos] = useState([]);
// `changeTodos` 함수는 `todos` 상태를 업데이트하여 새로운 할 일을 추가
const changeTodos = () => setTodos(prevTodos => [...prevTodos, "NEW ITEM " + prevTodos.length]);
// count 상태: 카운트 값을 저장
const [count, setCount] = useState(0);
// `changeCount` 함수는 카운트 값을 1씩 증가시키는 함수
const changeCount = () => setCount(count + 1);
return (
<>
{/* 카운트 증가 버튼 */}
<div>
<button onClick={changeCount}>카운트 증가</button>
<h2>카운트: {count}</h2>
</div>
<hr />
{/* `Todos` 컴포넌트에 `todos` 배열과 `addTodo` 함수를 props로 전달 */}
<Todos todos={todos} addTodo={changeTodos} />
</>
);
};
export default App;
React.memo를 사용하여 Todos 컴포넌트를 메모이제이션 처리합니다. 이렇게 하면 부모 컴포넌트가 리렌더링되더라도 todos와 addTodo props가 변경되지 않으면 Todos 컴포넌트는 리렌더링되지 않습니다. 이로 인해 자식 컴포넌트의 불필요한 렌더링을 줄여 성능을 최적화할 수 있습니다.
요약입니다.
- useMemo: 값이 변경되었을 때만 계산하도록 하여 불필요한 계산을 방지합니다.
- useCallback: 콜백 함수가 불필요하게 다시 생성되지 않도록 하여 자식 컴포넌트에 전달되는 함수가 동일하게 유지됩니다.
- React.memo: 자식 컴포넌트의 props가 변경되지 않으면 리렌더링을 건너뛰도록 합니다.
이 세 가지 방법을 적절히 활용하면 리액트 애플리케이션의 성능을 크게 개선할 수 있습니다. 성능 최적화는 필요할 때만 적용하는 것이 중요하며, 지나치게 최적화하려다 보면 코드 복잡도가 증가할 수 있으니 신중히 사용해야 합니다.
*생성형 AI 활용한 클라우드&보안 전문가 양성캠프 과정의 교육내용 정리 자료입니다.
'코딩 > 리액트' 카테고리의 다른 글
[새싹 성동 2기] 리액트 라우터와 사용예시 (2) | 2024.12.06 |
---|---|
[새싹 성동 2기] Context API와 useContext 훅을 사용하여 테마 적용하기 (1) | 2024.12.06 |
[새싹 성동 2기] useReducer를 활용한 상태 관리 (0) | 2024.12.06 |
[새싹 성동 2기] 리액트 앱에서 국가와 국기 정보를 출력하는 방법 (1) | 2024.11.19 |
[새싹 성동 2기] React에서 useRef 예제 및 활용법 (1) | 2024.11.19 |