컴포넌트를 최적화해야 하는 이유
불필요한 연산을 막아 필요한 것만 렌더링 하기 위함
function App() {
const [data, setData] = useState([]);
const dataId = useRef(0);
const getData = async()=>{
const res = await fetch(
'https://jsonplaceholder.typicode.com/comments'
).then((res)=>res.json());
const initData = res.slice(0,20).map((it)=>{
return {
author : it.email,
content : it.body,
emotion : Math.floor(Math.random() * 5)+1,
created_date : new Date().getTime(),
id : dataId.current++
}
})
setData(initData);
};
useEffect(()=>{
getData();
},[])
현재 내가 작성하고 있는 코드에서 예를 들어보겠다. DiaryEditor가 앱 컴포넌트의 하위태그로 설정되어 있는 상태다. 참고로 DiaryEditor 는 React.memo 로 감싸져 있다. 이 상태에서 마운트 되는 시점이 몇 번인지 확인하기 위해 useEffect 에 콘솔을 넣어 확인했다. 그 결과 콘솔에 두 번 출력되는 것을 볼 수 있었다. 왜 두 번 출력이 될까?
그 이유는 앱 컴포넌트에서 data state 초기값이 빈 배열이어서 한 번 렌더링 일어나고 DiaryEditor 또한 렌더링이 일어난다. 그리고 앱 컴포넌트 "setData(initData)" 부분에서 data state 가 한 번 더 바뀌게 되므로 총 두 번 렌더링 되는 것이다. 하지만 다시 생성되어도 앱 컴포넌트 안의 onCreate 함수는 변하지 않는다.
React.memo 에서 비원시 타입 자료의 비교는 기본적으로 얕은 비교가 일어나므로 onCreate 함수가 계속 생성된다. -> 어떻게 해결??
-> 함수가 아닌 값을 반환하므로 onCreate 함수 그대로 DiaryEditor 의 prop 으로 전달할 수 없음.
useCallback
*공식문서
값을 반환하는 게 아니라 콜백함수를 다시 반환하는 역할을 한다. 중요한 건 메모이제이션 된 콜백함수를 반환한다는 것이다. 두 번째 인자로 전달된 dependency array 안의 값이 변하지 않으면 첫 번째 인자로 전달한 콜백함수를 계속 재사용할 수 있도록 도와주는 리액트 훅.
const onCreate = useCallback((author,content,emotion)=>{
const created_date = new Date().getTime();
const newItem = {
author,
content,
emotion,
created_date,
id : dataId.current
}
dataId.current += 1;
setData([newItem, ...data])
},[]);
onCreate 함수에 useCallback 을 적용시킨 모습이다. 이제는 일기 목록을 삭제해도 useEffect 안의 콘솔이 출력되지 않음을 확인할 수 있다.
problem : 일기 저장하면 원래 있던 목록들 다 사라지고 저장한 것만 남음
왜? useCallback 활용하면서 dependency array 에 아무것도 넣지 않아서.
onCreate 함수는 컴포넌트 마운트 시점에 한 번만 생성되므로 당시 data state 값이 빈 배열이므로 이런 현상이 발생한 것.
-> 현재 state 값을 참조할 수 있어야하기 때문!!
하지만 onCreate 함수는 callback 안에 갇혀 빈배열로 전달했으므로 onCraete 함수가 알고 있는 data 값은 그대로 빈 배열이 된다. 그래서 일기를 추가하면 해당 일기만 newItem 으로 렌더링 되는 것이다.
그럼 어떻게 해야 할까?? 단순히 빈 배열에 data 를 전달해 원래 데이터들이 남아있게 할 수 있지만 여기서 딜레마가 발생한다. 우리는 useCallback 을 이용해 필요없는 렌더링을 막으려고 했지만 빈 배열에 data를 전달하면 결국 data state 가 변할 때마다 렌더링되는 것을 막을 수 없기 때문이다.
함수형 업데이트를 활용
setData((data)=>[newItem, ...data])
setData 에 함수를 전달하여 아이템을 추가한 data 를 리턴하도록 하면 useCallback 에서 빈 배열을 전달해도 항상 최신의 state 를 data인자로 참고할 수 있게 된다.
=> 이전에는 onCreate 함수가 data state 의 현재값을 참조할 수 없었기 때문에 아이템이 하나만 남았었는데 이제는 정상적으로 수행된다.