최적화: 웹서비스의 성능을 개선하는 기술 + 렌더링의 성능을 높이는 방법
-> 최적화가 잘된 웹서비스는 사용자를 불필요하게 기다리지 않게 하여 서비스의 긍정적인 경험을 만든다.
최적화 방법으로는 코드, 폰트, 이미지 파일의 크기를 줄이는 등의 여러 방법이 있다.
<리액트 최적화 방법 1: 연산 낭비를 줄이는 메모이제이션>
리액트는 '메모이제이션'으로 연산 최적화 한다.
메모이제이션: 특정 입력에 대한 결과를 계산해 메모리 어딘가에 저장했다가, 동일한 요청이 들어오면 저장한 결괏값을 제공해 빠르게 응답하는 기술이다. (불필요한 연산을 줄여 실행속도가 빨라진다: 동적 계획법)
ex) 식당에서 메뉴를 외웠다가 누가 물어보면 답한다.
<완료, 미완료, 아이템 개수 출력>
// src/component/TodoList.js
import { useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo, onUpdate, onDelete }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase())
);
};
const analyzeTodo = () => {
const totalCount = todo.length;
const doneCount = todo.filter((it) => it.isDone).length;
const notDoneCount = totalCount - doneCount;
return{
totalCount,
doneCount,
notDoneCount,
};
};
const {totalCount, doneCount, notDoneCount} = analyzeTodo();
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<div>
<div>총개수: {totalCount}</div>
<div>완료된 할 일: {doneCount}</div>
<div>아직 완료하지 못한 할 일: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem
key={it.id}
{...it}
onUpdate={onUpdate}
onDelete={onDelete}
/>
))}
</div>
</div>
);
};
export default TodoList;
analyzeTodo는 todo에 저장한 아이템 개수에 비례하여 수행할 연산량이 증가한다. 따라서 todo에 아이템이 많아지면 성능상의 문제가 생길 수 있다.
<불필요한 호출 확인하기>
// src/component/TodoList.js
(...)
const analyzeTodo = () => {
console.log("analyzeTodo 함수 호출");
const totalCount = todo.length;
const doneCount = todo.filter((it) => it.isDone).length;
const notDoneCount = totalCount - doneCount;
return{
totalCount,
doneCount,
notDoneCount,
};
};
const {totalCount, doneCount, notDoneCount} = analyzeTodo();
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<div>
<div>총개수: {totalCount}</div>
<div>완료된 할 일: {doneCount}</div>
<div>아직 완료하지 못한 할 일: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem
key={it.id}
{...it}
onUpdate={onUpdate}
onDelete={onDelete}
/>
))}
</div>
</div>
);
};
export default TodoList;
<useMemo 사용법>
useMemo를 사용하면 특정 함수를 호출했을 때 그 함수의 반환값을 기억한다. 같은 함수를 다시 호출하면 기억해 두었던 값을 반환합니다. 따라서 반환값을 다시 구할 필요 없다. (성능 최적화: 메모이제이션)
<함수 analyzeTodo의 재호출 방지하기>
// src/component/TodoList.js
import { useMemo, useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo, onUpdate, onDelete }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase())
);
};
const analyzeTodo = useMemo(() => {
console.log("analyzeTodo 함수 호출");
const totalCount = todo.length;
const doneCount = todo.filter((it) => it.isDone).length;
const notDoneCount = totalCount - doneCount;
return{
totalCount,
doneCount,
notDoneCount,
};
}, [todo]);
const {totalCount, doneCount, notDoneCount} = analyzeTodo;
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<div>
<div>총개수: {totalCount}</div>
<div>완료된 할 일: {doneCount}</div>
<div>아직 완료하지 못한 할 일: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem
key={it.id}
{...it}
onUpdate={onUpdate}
onDelete={onDelete}
/>
))}
</div>
</div>
);
};
export default TodoList;
< 리액트 최적화 방법2: React.memo>
react.memo를 이용해 메모이제이션 기법으로 불필요한 리렌터 방지
<고차 컴포넌트>
<횡단 관심사>
const CompA = () => {
console.log("컴포넌트가 호출되었습니다.");
return <div>CompA</div>;
};
const CompB = () => {
console.log("컴포넌트가 호출되었습니다.");
return <div>CompB</div>;
};
비즈니스 로직을 세로로 배치한다고 했을 때, 여러 컴포넌트에서 공통적으로 사용하는 기능은 가로로 배치한다.
횡단관심사 코드는 중복코드를 만드는 요인이 된다.
해결
function whileLifecycleLogging(WrappedComponent) {
return (props) => {
useEffect(() => {
console.log("Mount!");
return () => console.log("Unmount!");
}, []);
useEffect(() => {
console.log("Update!");
});
return <wrappedComponent {...props} />;
};
}
래핑된 컴포넌트: 고차 컴포넌트에 인수로 전달되는 컴포넌트
강화된 컴포넌트: 고차 컴포넌트가 반환하는 컴포넌트
react.memo는 코든 상황에서 리렌더 되지 않도록 강화함으로써 서비스를 최적화하는 도구
<React.memo 기본 사용법>
react.memo가 반환하는 컴포넌트는 Props가 변경되지 않는 한 리렌더 되지 않는다.
const CompA = React.memo(() => {
console.log("컴포넌트가 호출되었습니다.");
return <div>CompA</div>;
});
// Props의 변경 여부를 기준으로 리렌더
const Comp = ({a, b, c}) => {
console.log("컴포넌트가 호출되었습니다.");
return <div>CompA</div>;
};
function areEqual(prevProps, nextProps) {
if (prevProps.a === nextProps.a) {
return true;
} else {
return false;
}
}
const MemoizesComp = React.memo(Comp, areEqual);
< Header 컴포넌트의 리렌더 방지하기>
Header 컴포넌트는 App(부모 컴포넌트)에서 아무런 Props도 받지 않는다.
따라서 어떤 상황에서도 리렌더할 필요가 없다.
// src/component/Header.js
import "./Header.css";
import React from "react";
const Header = () => {
console.log("Header 업데이트");
return (
<div className="Header">
<h3>오늘은 📅</h3>
<h1>{new Date().toDateString()}</h1>
</div>
);
};
export default React.memo(Header);
한 번만 호출된 것을 볼 수 있습니다.
<TodoItemp 컴포넌트 리렌더 방지하기>
마운트 한번 그리고 나머지 0,1,2번 아이템 리렌더
아이템이 추가하는 상황, 아이템 제거, 체크박스 클릭, 검색폼에서의 검색에서도 TodoItem리렌더 됩니다.
// src/component/TodoItem.js
import "./TodoItem.css";
import React from "react";
const TodoItem = ({ id, content, isDone, createdDate, onUpdate, onDelete }) => {
console.log(`${id} TodoItem 업데이트`);
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDelete = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<div className="checkbox_col">
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
</div>
<div className="title_col">{content}</div>
<div className="date_col">
{new Date(createdDate).toLocaleDateString()}
</div>
<div className="btn_col">
<button onClick={onClickDelete}>삭제</button>
</div>
</div>
);
};
export default React.memo(TodoItem);
useCallBack: 컴포넌트가 리렌더 될 때 내부에 작성된 함수를 다시 생성하지 않도록 메모이제이션하는 리액트 훅
App이 리렌더 될 때, useCallback으로 함수 onUpdate, onDelete를 재생성하지 않도록 만들어 TodoItem 컴포넌트의 렌더링 최적화.
< useCallBack의 기본 사용법>
< useCallBack과 함수형 업데이트>
인수로 전달되는 콜백함수에서 State변수에 접근하는 경우, 문제가 발생할 수 있습니다.
const onCreate = useCallback(() => {
setState([newItem, ...state]);
}, [])
// 빈배열이므로 컴포넌트 리렌더가 되지 않는다.
// 즉, State는 초기값이 반환됩니다. 마운트할 때의 State값만 사용할 수 있습니다.
// State변화 추적 x -> 의도치 않은 동작 야기
const onCreate = useCallback(() => {
setState([newItem, ...state]);
}, [State])
// useCallback을 적용한 의미가 없다.
const onCreate = useCallback(() => {
setState((State) => [newItem, ...state]);
}, [])
// 함수형 업데이트 사용가능
// 항상 최신 State값을 매개변수로 저장합니다. (콜백함수가 반환한 값은 새로운 State값이 되어 업데이트 됩니다.)
useCallback을 사용하면서 setState로 최신 State값을 추적하려면 함수형 업데이트 기능을 이용해야 합니다.
< useCallBack을 이용해 TodoItem 컴포넌트의 리렌더 방지하기>
// src/App.js
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import { useCallback, useReducer, useRef } from "react";
const mockTodo = [
{
id: 0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: "빨래 널기",
createdDate: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: "노래 연습하기",
createdDate: new Date().getTime(),
},
];
function reducer(state, action) {
switch (action.type){
case "CREATE":{
return [action.newItem, ...state]
}
case "UPDATE": {
return state.map((it) =>
it.id === action.targetId
?{
...it,
isDone: !it.isDone,
}
: it
);
}
case "DELETE":{
return state.filter((it) => it.id !== action.targetId);
}
default:
return state;
}
}
function App() {
const [todo, dispatch] = useReducer(reducer, mockTodo);
const idRef = useRef(3);
const onCreate = (content) => {
dispatch({
type: "CREATE",
newItem: {
id: idRef.current,
content,
isDone: false,
createdDate: new Date().getTime(),
},
})
idRef.current += 1;
};
const onUpdate = useCallback((targetId) => {
dispatch({
type: "UPDATE",
targetId,
});
},[]);
const onDelete = useCallback((targetId) => {
dispatch({
type: "DELETE",
targetId,
});
},[]);
return (
<div className="App">
<Header />
<TodoEditor onCreate={onCreate} />
<TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
출처 : 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023).
Corner React.js 1
Editor: MARIN
[React.js 1팀] 9장 컴포넌트 트리에 데이터 공급하기 (0) | 2025.01.17 |
---|---|
[React.js 1팀] 7장. useReducer와 상태 관리 (0) | 2025.01.10 |
[React.js 1팀] Project 2 [할 일 관리] 앱 만들기 2 (Read: 할 일 리스트 렌더링하기 ~ Delete: 할 일 삭제하기) (0) | 2025.01.03 |
[React.js 1팀] Project2 [할 일 관리] 앱 만들기 (0) | 2024.12.27 |
[React.js 1팀] 8장. Hooks (1) | 2024.11.29 |