이 점을 생각하면서 컴포넌트 최적화를 해보도록 하겠습니다.
import React, { useEffect, useRef, useState } from "react";
const DiaryEditor = ({ onCreate }) => {
useEffect(() => {
console.log("DiaryEditor 렌더");
});
const nameInput = useRef();
const contentInput = useRef();
const [state, setState] = useState({
name: "",
content: "",
hungry: 10,
});
const handleChangeState = (e) => {
setState({
...state,
[e.target.name]: e.target.value,
});
};
const handleSubmit = () => {
if (state.name.length < 1) {
nameInput.current.focus();
return;
}
if (state.content.length < 5) {
contentInput.current.focus();
return;
}
onCreate(state.name, state.content, state.hungry);
setState({
name: "",
content: "",
hungry: 10,
});
};
return (
<div className="DiaryEditor">
<h2>오늘의 먹방 일기</h2>
<div>
이름
<input
ref={nameInput}
name="name"
value={state.name}
onChange={handleChangeState}
/>
</div>
<div>
내용
<textarea
ref={contentInput}
name="content"
value={state.content}
onChange={handleChangeState}
/>
</div>
<div>
배고픔 정도
<select name="hungry" value={state.hungry} onChange={handleChangeState}>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={30}>30</option>
<option value={40}>40</option>
<option value={50}>50</option>
<option value={60}>60</option>
<option value={70}>70</option>
<option value={80}>80</option>
<option value={90}>90</option>
<option value={100}>100</option>
</select>
</div>
<div>
<button onClick={handleSubmit}>일기 저장하기</button>
</div>
</div>
);
};
export default React.memo(DiaryEditor);
export default React.memo(DiaryEditor);
React.memo를 맨 아래 달았습니다.
useEffect(() => {
console.log("DiaryEditor 렌더");
});
useEffect를 사용하여 렌더링 될 때 로그를 남겨보겠습니다.
페이지를 새로고침을 하니 로그에 2번 출력되는 것을 확인 할 수 있습니다.
const onCreate = (name, content, hungry) => {
const created_date = new Date().getTime();
const newItem = {
name,
content,
hungry,
created_date,
id: dataId.current,
};
dataId.current += 1;
setData([newItem, ...data]);
};
현재 props로 onCreate를 넘기고 있습니다. 이것을 React.memo로 현재는 얕은 비교를 하고 있기 때문에 불필요한 렌더링이 계속됩니다. 이것을 최적화 해보도록 하겠습니다.
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
메모이제이션된 콜백을 반환합니다.
dependency array가 변경되게 되면 콜백 함수 자체를 반환합니다. 불필요한 렌더링을 방지하기 위해 참조의 동일성에 의존적인 최적화된 자식 컴포넌트에 콜백으로 전달될 때 유용합니다.
설명으로는 어려우니 실습을 해보도록 하겠습니다.
현재 onCreate 함수가 불필요하게 실행되고 있기 때문에 onCreate 함수에 useCallback으로 감싸주도록 합니다.
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
const onCreate = useCallback((name, content, hungry) => {
const created_date = new Date().getTime();
const newItem = {
name,
content,
hungry,
created_date,
id: dataId.current,
};
dataId.current += 1;
setData([newItem, ...data]);
}, []);
useCallback을 추가하니 처음 렌더링 될 때만 "DiaryEditor" 로그가 남는 것을 확인할 수 있습니다. 게시글을 삭제해도 렌더링이 새로되지 않습니다. 그럼 잘 된 것일까요? 게시글 하나를 저장해보도록 하겠습니다.
기존의 20개의 일기가 삭제되고 직전에 저장했던 데이터만 남아있는 것을 확인할 수 있습니다.
이 이유는 dependency array가 빈 배열이기 때문입니다. onCreate 함수는 컴포넌트가 마운트 되는 순간에만 생성되기 때문에 그 당시 data state는 빈 배열입니다. 그래서 onCreate함수가 마지막으로 생성되었을 때는 빈 배열의 state를 가지고 있습니다. 그래서 이런 현상이 생기게 된 것입니다.
그렇다면 dependency array에 data를 넣어주어야 합니다.
const onCreate = useCallback((name, content, hungry) => {
const created_date = new Date().getTime();
const newItem = {
name,
content,
hungry,
created_date,
id: dataId.current,
};
dataId.current += 1;
setData([newItem, ...data]);
}, [data]);
이렇게 하면 data가 변경되게 되면 안에 함수를 생성하게 됩니다. 하지만 이것은 원하는 동작이 아닙니다. 최신의 data를 적용시켜야 하지만 data가 변화된다고 onCreate 함수를 실행시키면 안되는 딜레마에 빠진 것입니다... ㅇ0ㅇ
setData((data) => [newItem, ...data]);
setData(상태변화 함수)에 함수를 전달하여 최신 데이터가 업데이트 되게 할 수 있습니다.
const onCreate = useCallback((name, content, hungry) => {
const created_date = new Date().getTime();
const newItem = {
name,
content,
hungry,
created_date,
id: dataId.current,
};
dataId.current += 1;
setData((data) => [newItem, ...data]);
}, []);
dependency array는 다시 빈 배열로 바꿔줍니다.
useCallback hooks을 사용하여 DiaryEditor 컴포넌트는 한번만 생성이 되고 최신 데이터는 항상 유지될 수 있게 적용해 보았습니다.
리액트 공식 홈페이지
https://ko.legacy.reactjs.org/docs/react-api.html#reactmemo
해당 게시글은 인프런 강의
"한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지(이정환)"
를 정리한 내용입니다. 쉽게 잘 설명해주시니 여러분도 강의를 듣는 것을 추천드립니다.