지난 글에서 React.memo를 사용하여 최척화하는 방법에 이어서 React Hooks의 useCallback을 사용하여 최적화하는 방법에 대해 간략히 정리하는 글이다.
React.memo는 동일한 props가 동일한 결과를 렌더링 할때 하위 컴포넌트의 리렌더링을 하지 않고 마지막 렌더링 결과를 재사용한다고 정리를 했었다. 하지만 이전props와 다음 props의 비교 방식이 얕은 비교 방식이기에 Object(객체) 비교에서 문제가 됐었고 이를 두번째 인자에서 areEqual 함수를 넘겨주어 props로 넘어온 객체의 프로퍼티를 비교하여 true/false를 반환하여 해결했다.
넘어오는 props가 객체 리터럴이라면 위의 방식으로 해결이 가능하겠지만 함수가 넘어온다면 위 방식으로는 조금 곤란할 수 있다. 물론 함수도 객체이긴 하지만 해당 함수에서 수행되는 로직을 한땀 한땀 비교할 수는 없기 때문이다.
이런 문제는 React Hooks의 useCallback을 사용하면 깔끔하게 해결이 된다.
- useCallback 사용법
import { useCallback } from "react";
useCallback 을 사용하기 위해 위와 같이 import를 한다.
const testFunction = useCallback(() => {
//.. todo
}, []); // Dependency Array(의존성 배열) : 배열의 값이 바뀔 때만 콜백함수 재호출
첫번째 인자에는 콜백함수를 넣어주고 두번째 인자에는 배열을 넣어주면 되는데 배열내에 값이 바뀔때 마다 새로운 콜백함수를 testFunction에 담기고 값이 같다면 기존 콜백함수를 그대로 사용한다. 기존 콜백함수를 그대로 사용한다는 건 말이 조금 애매할 수 있는데 콜백함수를 새로운 주소에 담아 주는게 아닌 기존 콜백함수의 주소를 그대로 가지고 있다고 생각하면 될 것 같다. useCallback 함수 내에 참조하고 있는 State나 Props가 있다면 배열에 추가를 해주면 된다.
두번째 인자에 빈배열을 넣어주게 되면 배열안에 바뀔 값이 없기 때문에 testFunction 변수에는 처음 인자로 받은 콜백함수를 계속 담고 있게 되고 배열을 아예 안넣어주게 되면 새로운 콜백함수가 계속 담긴다.
예시를 통해 확인을 해보면
import "./App.css";
import DiaryEdtior from "./DiaryEditor";
import { useState, useRef, useEffect, useMemo, useCallback } from "react";
function App() {
const [data, setData] = useState([]);
const dataId = useRef(0);
const getData = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/comments").then((res) => {
return res.json();
});
const initData = res.slice(0, 20).map((obj) => {
return {
author: obj.email,
content: obj.body,
emotion: Math.floor(Math.random() * 5) + 1,
created_date: new Date().getTime(),
id: dataId.current++,
};
});
setData(initData); // 최초 마운트 될때 setData가 실행되는 시점에서 리렌더링 발생
};
useEffect(() => {
getData();
}, []);
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((data) => [newItem, ...data]); // setState로 상태를 바꿀때 반드시 함수형 업데이트를 사용
}, []);
return (
<div className="App">
<DiaryEdtior onCreate={onCreate} />
</div>
);
}
export default App;
import React, { useEffect, useState, useRef } from "react";
const DiaryEdtior = ({ onCreate }) => {
const authorInput = useRef();
const contentInput = useRef();
useEffect(() => console.log("diaryeditor update"));
const [state, setState] = useState({
author: "",
content: "",
emotion: 1,
});
const handleChangeState = (e) => {
setState({
...state,
[e.target.name]: e.target.value,
});
};
const handleSubmit = () => {
if (state.author.length < 1) {
authorInput.current.focus();
return;
}
if (state.content.length < 5) {
contentInput.current.focus();
return;
}
onCreate(state.author, state.content, state.emotion);
alert("저장성공");
setState({
author: "",
content: "",
emotion: 1,
});
};
return (
<div className="DiaryEditor">
<h2>Simple Diary</h2>
<div>
<input ref={authorInput} name="author" value={state.author} onChange={handleChangeState} />
</div>
<div>
<textarea ref={contentInput} name="content" value={state.content} onChange={handleChangeState} />
</div>
<div>
<span>오늘의 감정점수 : </span>
<select name="emotion" value={state.emotion} onChange={handleChangeState}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
<option value={5}>5</option>
</select>
</div>
<div>
<button onClick={handleSubmit}>저장하기</button>
</div>
</div>
);
};
export default React.memo(DiaryEdtior);
위 코드에서 상위 컴포넌트인 App에서 하위컴포넌트인 DiaryEditor 에게 props로 넘겨주는 onCreate 함수만 집중적으로 보면DiaryEditor에서 글을 작성후 저장을 클릭하면 props로 받은 onCreate 함수가 실행되면서 글 정보를 넘겨주고 게시글 리스트 배열에 새로운 글을 추가해주는 플로우다.
DiaryEditor가 리렌더링 될때마다 useEffect를 사용하여 콘솔에 "diaryeditor update" 문구가 출력되도록 해놓았다. DiaryEditor가 리렌더링 되는 이유는 DiaryEditor가 onCreate 함수를 Props로 받고 있기 때문이다. App 컴포넌트의 data state가 변경되어 리렌더링이되면 onCreate가 재생성되고 onCreate를 props로 받는 DiaryEditor도 리렌더링이 일어나는 것이다.
실행을 해보면 최초 mount 시점에서 App컴포넌트의 data state의 초기값([])이 들어갈때 한번, getData에서 받아온 데이터를 setData 할때 한번 총 두번이 출력이 되는데 이를 useCallback 과 React.memo 를 사용하여 초기값이 들어갈때를 제외하고는 onCreate props로 인한 리렌더링이 되지 않도록 방지한것이다.(DiaryEditor 내에 state가 바뀔때는 리렌더링됨)
한가지 주의 할 점은 콜백함수내에서 state를 끌어와서 사용하게 될 경우 마지막으로 변경된 state 값을 갖고 오지 못하고 처음 콜백함수가 담길 당시의 state 값을 가지고 있다. 그렇기 때문에 setData(setData([newItem, ...data])) 이렇게 코드를 작성하게 되면 fetch로 가져온 게시글 리스트는 다 날아가고 새로 추가한 글만 게시글 리스트 배열에 담긴다. 이를 방지하기 위해 위 코드처럼 함수형 업데이트를 반드시 사용해야한다.