최적화(Optimization)
- 웹 서비스의 성능을 개선하는 모든 행위를 일컫는다.
- 아주 단순한 것부터 아주 어려운 방법까지 매우 다양하다.
- 일반적인 최적환 방법에는 서버의 응답속도 개선 / 이미지, 폰트, 코드 파일 등의 정적 파일 로딩 개선 / 불필요한 네트워크 요청 줄임 등이 있다.
- React App 내부의 최적화 방법에는 컴포넌트 내부의 불필요한 연산 방지 / 컴포넌트 내부의 불필요한 함수 재생성 방지 / 컴포넌트의 불필요한 리렌더링 방지 등이 있다.
useMemo
- ‘메모이제이션’(Memoization) 기법을 기반으로 불필요한 연산은 최적화하는 React Hook이다.
- Memoization이란? 반복적으로 수행되는 동일한 연산이 있을 때 최초 한 번의 연산을 메모리에 저장해놓았다가 결과값이 필요할 때마다 꺼내주는 기법을 말한다.
List.jsx
- Todo List에서 사용했던 List.jsx에 할일의 갯수를 파악하는 함수를 추가하여 useMemo를 사용해보자.
import "./List.css";
import TodoItem from "./TodoItem";
import { useState, useMemo } from "react";
const List = ({ todos, onUpdate, onDelete }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getFilteredData = () => {
if (search === "") {
return todos;
}
return todos.filter((todo) =>
todo.content.toLowerCase().includes(search.toLowerCase())
);
};
const filteredTodos = getFilteredData();
const { totalCount, doneCount, notDoneCount } = useMemo(() => {
console.log("getAnalyzedData 호출");
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
}, [todos]);
// 의존성배열 : deps
//const { totalCount, doneCount, notDoneCount } = getAnalyzedData();
return (
<div className="List">
<h4>Todo List📝</h4>
<div>
<div>total : {totalCount}</div>
<div>done : {doneCount}</div>
<div>notDone : {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요."
/>
<div className="todos_wrapper">
{filteredTodos.map((todo) => {
return (
<TodoItem
key={todo.id}
{...todo}
onUpdate={onUpdate}
onDelete={onDelete}
/>
);
})}
</div>
</div>
);
};
export default List;
- useMemo의 의존성 배열을 사용하여 todos가 업데이트 될 때에만 동작하도록 설정할 수 있다.
React.memo
- 컴포넌트를 인수로 받아, 최적화된 컴포넌트를 만들어 반환한다.
- MemoizedComponent는 부모가 리렌더링 되어도 자신이 받는 props가 바뀌지 않으면 리렌더링이 발생하지 않는다. 그래서 불필요한 리렌더링을 줄여 최적화 할 수 있다.
Header.jsx
import "./Header.css";
import { memo } from "react";
const Header = () => {
return (
<div className="Header">
<h3>오늘은🐿️💛</h3>
<h1>{new Date().toDateString()}</h1>
</div>
);
};
export default memo(Header);
- 체크 박스에 체크를 할 때 불필요하게 Header도 렌더링 되는 모습을 볼 수 있다. 이 때 memo를 이용하면 이런 불필요한 리렌더링을 줄일 수 있다.
- memo 적용 전 리액트 개발자 도구로 확인 했을 때 리렌더링 되는 모습 ( 헤더와 밑에 입력창 사이에 선이 있음 )
- memo 적용 후 리액트 개발자 도구로 확인 했을 때 리렌더링 되지 않는 모습 ( 선이 사라짐 )
- 같은 방식으로 TodoItem도 각 아이템이 따로 리렌더링 되도록 memo 설정을 해보자.
TodoItem.jsx
import "./TodoItem.css";
import { memo } from "react";
const TodoItem = ({ id, isDone, content, date, onUpdate, onDelete }) => {
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input onChange={onChangeCheckbox} 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);
- 그런데 memo를 적용해도 모든 아이템이 여전히 같이 리렌더링 되는 것을 볼 수 있다. 왤까?
- → state가 변화했으니 TodoItem의 부모 컴포넌트인 App 컴포넌트가 리렌더링 되는데, 이 때 App 컴포넌트 안에 있는 onUpdate, onDelete 와 같은 함수가 매번 재생성되어 TodoItem에 전달된다.
- → memo는 기본적으로 얕은 비교(===) 를 하는데 ‘객체 타입’인 이 함수들을 비교하면 (주솟값이 매번 달라져서) 기존과 다른 함수라고 인식하기 때문에, 모든 아이템들이 props가 변경된 것으로 인식하며 리렌더링 되는 것이다.
- 해결방법은 2가지 이다.
TodoItem.jsx
import "./TodoItem.css";
import { memo } from "react";
const TodoItem = ({ id, isDone, content, date, onUpdate, onDelete }) => {
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
};
// 고차 컴포넌트 (HOC)
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;
});
- 첫 번째 방법은 콜백함수를 활용하여 최적화 기능을 커스터마이징 해주는 것이다.
- 이제 이렇게 내가 선택한 컴포넌트만 리렌더링 되는 것을 볼 수 있다.
- 두 번째 방법은 useCallBack이라는 리액트 훅을 이용한 방법이다.
UseCallBack - 불필요한 함수 재생성 방지하기
App.jsx
import "./App.css";
import Header from "./components/Header";
import Editor from "./components/Editor";
import List from "./components/List";
import { useRef, useReducer, useCallback } from "react";
const mockDate = [
...생략
];
function reducer(state, action) {
...생략
}
function App() {
const [todos, dispatch] = useReducer(reducer, mockDate);
const idRef = useRef(3);
const onCreate = useCallback((content) => {
dispatch({
type: "CREATE",
data: {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
},
});
}, []);
const onUpdate = useCallback((targetId) => {
dispatch({
type: "UPDATE",
targetId: targetId,
});
}, []);
const onDelete = useCallback((targetId) => {
dispatch({
type: "DELETE",
targetId: targetId,
});
}, []);
return (
<div className="App">
<Header />
<Editor onCreate={onCreate} />
<List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
- 첫 번째 방법처럼 매번 props를 일일이 비교하는 것은 비효율적이다. props의 이름이 바뀌기라도 한다면? NO~ 따라서 useCallback 함수로 애초에 함수가 한 번만 생성되도록 설정한다.
- 위 코드 처럼 두번째 인자를 빈배열로 설정하면, 함수는 마운트 될 때 한 번만 생성이 되고 업데이트(리렌더링) 시에는 생성되지 않는다.
최적화는 언제, 어떤 것을 하는게 좋을까?
- 언제 - 기능이 완성되고 난 후 최적화를 해야한다. 최적화 후 기능을 추가하거나 수정하려면 예상치 못한 오류를 만날 수 있다.
- 어떤 것 - 최적화가 꼭 필요한 곳에만 한다. memo와 같은 메서드도 당연히 연산이 필요하고 자원이 소비되는 기능이기에 단순하게 UI를 렌더링하는 곳에 사용한다면 효율적이지 못하다. 함수가 많아서 코드가 무거운 컴포넌트라던가, 유저에 행동에 따라서 경우의 수가 많아질 수 있는 컴포넌트 같은 곳에 최적화를 해야한다.
출처 : 한입 크기로 잘라먹는 리액트
'Language > React.js' 카테고리의 다른 글
[React] 감정일기장 프로젝트 : 페이지 라우팅 (0) | 2024.10.22 |
---|---|
[React] TodoList 프로젝트 : React Context (0) | 2024.10.21 |
[React] TodoList 프로젝트 : useReducer 사용하기 (3) | 2024.10.21 |
[React] TodoList 프로젝트 : 시작 (0) | 2024.10.18 |
[React] 라이프 사이클 (1) | 2024.10.17 |