최적화 3 - useCallback
컴포넌트 & 함수 재사용하기
1. 컴포넌트 & 함수 재사용
먼저 최적화할 컴포넌트 대상 찾아야 할 수 있어야 한다. React Developer Tools 의 Components - General : Highlight updates when components render 기능을 활용한다.
일기 삭제시 일기 입력폼 컴포넌트는 리렌더링 될 필요가 없다. DiaryEditor 컴포넌트를 최적화한다.
- DiaryEditor.js
import React, { useState, useRef, useEffect } from "react";
const DiaryEditor = ({ onCreate }) => {
// 렌더링 확인
useEffect(() => {
console.log('DiaryEditor 렌더');
});
// DOM Element 접근
const authorInput = useRef();
const contentInput = useRef();
// state 기본값 설정
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) {
// alert('작성자는 최소 1글자 이상 입력해주세요.');
// focus
authorInput.current.focus();
return;
}
if (state.content.length < 5) {
// alert('일기 본문은 최소 5글자 이상 입력해주세요.');
// focus
contentInput.current.focus();
return;
}
onCreate(state.author, state.content, state.emotion);
alert('저장 성공!');
// 저장 후 일기장 입력폼 초기화
setState({
author: ''
, content: ''
, emotion: 1
});
}
return (
<div className="DiaryEditor">
<h2>오늘의 일기</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>
<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(DiaryEditor);
2번 렌더링된 이유는 App.js 에서 확인 가능하다. data state 초기값이 빈 배열 '[]' 이어서 App 컴포넌트가 1번 렌더링이 일어나 DiaryEditor 에도 렌더링이 일어난다. 이후 컴포넌트가 Mount 된 시점에 호출한 getData() 함수에서 setData() 함수를 호출하여 data state 가 한 번 더 바뀌게 된다. 이 때 DiaryEditor 에도 렌더링이 일어난다.
DiaryEditor 컴포넌트가 전달받는 onCreate 함수도 App 컴포넌트가 렌더링되며 다시 생성된다. 비원시타입 비교는 React.memo 에서 얕은 비교로 일어나기 때문에, DiaryEditor 컴포넌트가 props 로 가진 onCreate 함수가 App 컴포넌트가 렌더링 될 때마다 다시 만들어질 때마다 다를 것이기 때문에 렌더링이 일어나게 된다.
결론적으로 onCreate 함수가 재생성되지 않아야만 DiaryEditor 컴포넌트를 React.memo() 와 함께 최적화할 수 있다. App 컴포넌트에서 onCreate 함수가 재생성되지 않도록 수정한다. useMemo() 를 사용하면 될 것 같으나, 함수가 아닌 값을 반환하기 떄문에 사용할 수 없다.
useCallback 을 사용해본다.
참고링크 : https://ko.legacy.reactjs.org/docs/hooks-reference.html#usecallback
Hooks API Reference – React
A JavaScript library for building user interfaces
ko.legacy.reactjs.org
useCallback 은 메모이제이션된 콜백을 반환한다. 즉, 값을 반환하는 것이 아닌 첫 번째 매개변수의 Callback 함수 자체를 반환한다. 주의할 점은 메모이제이션된 Callback 함수를 반환한다는 점이다. 두 번째 매개변수인 Dependency Array 안에 들어있는 값이 변화하지 않으면, 첫 번째 인자로 전달된 Callback 함수를 계속 재사용할 수 있도록 도와주는 React Hook 이다.
App 컴포넌트의 onCreate 함수에 useCallback 을 적용하고, Dependency Array 를 빈 배열로 설정한다.
- App.js
...
// Mount 시점에 한 번만 만들고 재사용 되도록 함
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]); // 위에서 부터 출력하기 위해 newItem, ...data 순서로 작성
}, []);
...
일기 삭제시 DiaryEditor 가 리렌더 되지 않는다. 그러나 새로운 일기를 작성하자, 기존 일기가 모두 삭제되고 추가한 일기만 남았다. 왜냐 하면, Dependency Array 에 아무 값도 넣지 않았기 때문이다. onCreate 함수는 컴포넌트 Mount 시점에 한 번만 생성되는데, 그 당시 data state 값이 빈 배열이기 때문이다. 즉, onCreate 함수가 가장 마지막으로 생성되었을 때 data state 가 빈 배열이였기 때문이다.
함수는 컴포넌트가 재생성될 때 다시 생성되는 이유는, 현재의 state 값을 참조할 수 있어야하기 때문이다. onCreate 함수는 useCallback 에 갇혀서 Dependency Array 를 빈 배열로 전달했기 때문에 onCreate 가 알고있는 data 의 값은 그대로 빈 배열이다. 빈 배열에 newItem 을 추가했기 때문에 추가한 1개의 일기만 남게 되었다.
정상적으로 작동시키려면, Dependency Array 에 data state 를 넣어줘야 한다. 그러면 원하는 동작이 불가능하여 딜레마에 빠지지만, 이 때 함수형 Update 를 활용하면 된다. setData() 함수의 매개변수로 함수를 전달하는 것이다. 이 때, Dependency Array 를 비워도 항상 최신의 state 를 setData() 함수의 매개변수로 전달하는 함수의 매개변수 data 를 통해 참고할 수 있다.
- App.js
...
// Mount 시점에 한 번만 만들고 재사용 되도록 함
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]); // 위에서 부터 출력하기 위해 newItem, ...data 순서로 작성
setData((data) => [newItem, ...data]);
}, []);
...
새로고침 하면 개발자 도구 콘솔에도 DiaryEditor 컴포넌트가 1번만 렌더링 되었다. DiaryEditor 컴포넌트가 정상적으로 메모이제이션, 즉 최적화 되었다.
참고강의 : https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8#
한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지 강의 - 인프런
개념부터 독특한 프로젝트까지 함께 다뤄보며 자바스크립트와 리액트를 이 강의로 한 번에 끝내요. 학습은 짧게, 응용은 길게 17시간 분량의 All-in-one 강의!, 리액트, 한 강의로 끝장낼 수 있어요.
www.inflearn.com