React 애플리케이션을 개발하다 보면 성능 최적화는 중요한 고려 사항 중 하나이다. 특히 규모가 커지거나 복잡한 상태 관리가 필요한 경우, 적절한 최적화 기법을 사용하여 애플리케이션의응답성을 향상시킬 수 있다.
useReducer는 React의 내장 훅으로, 복잡한 상태 로직을 보다 효율적으로 관리할 수 있도록 도와준다. Redux와 유사한 패턴을 따르며, 상태와 상태를 변경하는 디스패치 함수를 제공한다.
useReducer를 사용하면 코드의 가독성과 유지보수성이 향상된다.useReducer는 이전 상태와 액션에 따라 새로운 상태를 계산하기 때문에, 불필요한 상태 변경을 방지하고 성능을 향상시킬 수 있다.import { useReducer, useRef } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'CREATE':
return [action.data, ...state];
case 'UPDATE':
return state.map((item) =>
item.id === action.targetId ? { ...item, isDone: !item.isDone } : item
);
case 'DELETE':
return state.filter((item) => item.id !== action.targetId);
default:
return state;
}
}
function App() {
const [todos, dispatch] = useReducer(reducer, []);
const idRef = useRef(3);
// 예시 액션 디스패치
const onCreate = (content) => {
dispatch({
type: 'CREATE',
data: {
id: idRef.current++,
isDone: false,
content,
date: new Date().getTime(),
},
});
};
const onUpdate = (targetId) => {
dispatch({
type: 'UPDATE',
targetId: targetId,
});
};
const onDelete = (targetId) => {
dispatch({
type: 'DELETE',
targetId: targetId,
});
};
return (
<div>
{/* 컴포넌트 렌더링 */}
</div>
);
}
useCallback은 React의 내장 훅으로, 함수의 재생성을 방지하여 불필요한 렌더링을 줄이는 데 사용된다. 특정 의존성 배열이 변경되지 않는 한 동일한 함수를 반환한다.
React.memo와 함께 사용할 때 효과적이다.import { useCallback, useRef } from 'react';
function App() {
const [todos, dispatch] = useReducer(reducer, []);
const idRef = useRef(3);
const onCreate = useCallback((content) => {
dispatch({
type: 'CREATE',
data: {
id: idRef.current++,,
isDone: false,
content,
date: new Date().getTime(),
},
});
}, []);
const onUpdate = useCallback((targetId) => {
dispatch({
type: 'UPDATE',
targetId,
});
}, []);
const onDelete = useCallback((targetId) => {
dispatch({
type: 'DELETE',
targetId,
});
}, []);
return (
<div>
{/* 컴포넌트 렌더링 */}
</div>
);
}
React.memo로 메모이제이션된 하위 컴포넌트들이 불필요하게 재렌더링되지 않는다.useMemo는 React의 내장훅으로, 값의 복잡한 계산 결과를 메모이제이션하여 불필요한 재계산을 방지한다. 특정 의존성 배열이 변경되지 않는 한 이전에 계산된 값을 반환한다.
import { useMemo } from 'react';
function App() {
const [todos, dispatch] = useReducer(reducer, []);
const completedTodos = useMemo(() => {
return todos.filter(todo => todo.isDone);
}, [todos]);
return (
<div>
<h2>완료된 할 일: {completedTodos.length}개</h2>
{/* 나머지 컴포넌트 렌더링 */}
</div>
);
}
React.memo는 고차 컴포넌트로, 컴포넌트를 메모이제이션하여 동일한 props가 전달될 경우 이전에 렌더링된 결과를 재사용한다.
import { memo } from 'react';
import './TodoItem.css';
const TodoItem = ({ id, isDone, content, date, onUpdate, onDelete }) => {
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input
onChange={onChangeCheckbox}
readOnly
checked={isDone}
type="checkbox"
/>
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
};
// export default memo(TodoItem, (prevProps, nextProps) => {
// // 반환 값에 따라, Props가 바뀌었는지 안 바뀌었는지 판단
// // T -> Props 바뀌지 않음 -> 리렌더링 X
// // F -> Props 바뀜 -> 리렌더링 O
// if (prevProps.id !== nextProps.id) {
// return false;
// }
// if (prevProps.isDone !== nextProps.isDone) {
// return false;
// }
// if (prevProps.content !== nextProps.content) {
// return false;
// }
// if (prevProps.date !== nextProps.date) {
// return false;
// }
// return true;
// });
export default memo(TodoItem);
export default memo(TodoItem, (prevProps, nextProps) => {
return prevProps.isDone === nextProps.isDone && prevProps.content === nextProps.content;
});
최적화는 항상 이점을 주는 것은 아니다. 오히려 지나친 최적화는 코드의 복잡성을 높이고 유지보수를 어렵게 만들 수 있다. 따라서 최적화는 필요한 경우에만, 그리고 그 필요성이 명확할 때 적용하는 것이 중요하다.
React에서으 성능 최적화는 사용자 경험을 향상시키고 애플리케이션의 효율성을 높이는 중요한 요소이다. useReducer, useCallback, useMemo, React.memo와 같은 훅과 기법들은 이러한 최적화를 달성하는 데 효과적인 도구들이다.
그러나 이러한 최적화 기법들은필요에 따라 신중하게 적용되어야 하며, 과도한 최적화는 오히려 코드의 복잡성을 증가시키고 유지보수를 어렵게 만들 수 있다. 따라서 성능 문제를 명확히 식별하고, 그에 맞는 최적화 전략을 단계적으로 적용하는 것이 중요하다.