useMemo와 useCallback을 배우기 전에 알아야 하는 것
- 함수형 컴포넌트는 그냥 함수다. 함수형 컴포넌트는 단지 jsx를 반환하는 함수이다.
- 컴포넌트가 렌더링 된다는 것은 누군가가 그 함수(컴포넌트)를 호출하여서 실행되는 것을 말한다. 함수가 실행될 때마다 내부에 선언되어 있던 표현식(변수, 또는 다른 함수 등)도 매번 다시 선언되어 사용된다.
- 컴포넌트는 자신의 state가 변경되거나, 부모에게서 받은 props가 변경되었을 때마다 리렌더링된다(심지어 하위 컴포넌트에 최적화 설정을 해주지 않으면 부모에게서 받은 props가 변경되지 않더라도 리렌더링되는게 기본)
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
메모이제이션된 값을 반환한다라는 문장이 핵심이다. 다음과 같은 상황을 상상해보면 하위 컴포넌트는 상위 컴포넌트로부터 a와 b라는 두 개의 props를 전달 받는다. 하위 컴포넌트에서는 a와 b를 전달받으면 서로 다른 함수로 각각의 값을 가공(또는 계산)한 새로운 값을 보여주는 역할을 한다.
하위 컴포넌트는 props로 넘겨 받는 인자가 하나라도 변경될 때마다 렌더링되는데, props.a만 변경되었을 때 이전과 같은 값인 props.b도 다시 함수를 호출해서 계사냏야 할가?
useMemo를 설명할 수 있는 간단한 예제를 살펴보자
App 컴포넌트는 info컴포넌트에게 사용자로부터 입력 받은 color와 movie값을 props로 넘겨주고, info컴포넌트는 전달 받은 color와 movie를 적절한 한글로 바꾸어서 문장으로 보여준다.
//App.js
import info from './info';
const App = () => {
const [ color, setColor ] = useState('');
const [ movie, setMovie ] = useState('');
const onChangeHandler = e => {
if (e.target.id === 'color') {
setColor(e.target.value);
} else {
setMovie(e.target.value);
};
return (
<div className="App">
<div>
<label>
What is your favorite color of rainbow ?
<input id="color" value={color} onChange={onChangeHandler} />
</label>
</div>
<div>
What is your favorite movie among these ?
<label>
<input
type="radio"
name="movie"
value="Marriage Story"
onChange={onChangeHandler}
/>
Marriage Story
</label>
<label>
<input
type="radio"
name="movie"
value="The Fast And The Furious"
onChange={onChangeHandler}
/>
The Fast And The Furious
</label>
<label>
<input
type="radio"
name="movie"
value="Avengers"
onChange={onChangeHandler}
/>
Avengers
</label>
</div>
<Info color={color} movie={movie} />
</div>
);
};
export default App;
// Info.js
const getColorKor = color => {
console.log("getColorKor");
switch (color) {
case "red":
return "빨강";
case "orange":
return "주황";
case "yellow":
return "노랑";
case "green":
return "초록";
case "blue":
return "파랑";
case "navy":
return "남";
case "purple":
return "보라";
default:
return "레인보우";
}
};
const getMovieGenreKor = movie => {
console.log("getMovieGenreKor");
switch (movie) {
case "Marriage Story":
return "드라마";
case "The Fast And The Furious":
return "액션";
case "Avengers":
return "슈퍼히어로";
default:
return "아직 잘 모름";
}
};
const Info = ({ color, movie }) => {
const colorKor = getColorKor(color);
const movieGenreKor = getMovieGenreKor(movie);
return (
<div className="info-wrapper">
제가 가장 좋아하는 색은 {colorKor} 이고, <br />
즐겨보는 영화 장르는 {movieGenreKor} 입니다.
</div>
);
};
export default Info;
예제의 실행 화면
App 컴포넌트의 입력창에서 color 값만 바뀌어도 getColorKor, getMovieGenreKor 두 함수가 모두 실행되고, movie값만 바뀌어도 마찬가지로 두 함수가 모두 실행된다. useMemo를 import해서 info 컴포넌트의 코드에서 colorKor과 movieGenroKor를 꼐산하는 부분을 아래와 같이 수정해볼 수 있다.
import React, { useMemo } from 'react';
const colortKor = useMemo(() => getColorKor(color, [color]);
const movieGenreKor = useMemo(() => getMovieGenreKor(movie), [movie]);
useMemo를 사용하면 의존성 배열에 넘겨준 값이 변경되었을때만 메모이제이션된 값을 다시 계산한다.
예제 코드를 직접 변경해 color값이 바뀔 때는 getColorKor함수만, movie값이 바뀔 때는 getMovieGenreKor함수만 호출되는 것을 확인할 수 있다.
재계산하는 함수가 아주 간단하다면 성능상의 차이는 아주 미미하겠지만, 만약 재계산하는 로직이 복잡하다면 불필요하게 비싼 계산을 하는 것을 막을 수 있다.
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
메모이제이션된 함수를 반환한다라는 문장이 핵심이다.
컴포넌트가 렌더링될때마다 내부에 선언되어 있던 표현식(변수, 또다른 함수 등)도 매번 다시 선언되어 사용된다. useMemo를 설명하고 있는 예제에서 App.js의 onChangeHandler 함수는 내부의 color, movie 상태값이 변경될 때마다 재선언된다는 것을 의미한다. 하지만 onChangeHandler함수는 파라미터로 전달받은 이벤트 객체(e)의 tartget.id값에 따라 setState를 실행해주기만 하면 되기 떄문에, 첫 마운트 될때 한 번만 선언하고 재사용하면 되지 않을까?
// App.js
import React, { useState, useCallback } from 'react';
const onChangeHandler = useCallback(e => {
if (e.target.id === 'color') {
setColor(e.target.value)
} else {
setMovie(e.target.value)
}, []);
App.js에서 useCallback을 import하고 onChangehandler함수의 선언부를 위처럼 바꿔보았다. 첫 마운트 될 때만 메모리에 할당되었는지 아닌지 확인하기는 어려우나 위처럼 사용한다.
만약, 하위 컴포넌트가 React.memo() 같은 것으로 최적화 되어 있고 그 하위 컴포넌트에게 callback으로 함수를 선언하는 것이 유용하다. 함수가 매번 재선언되면 하위 컴포넌트는 넘겨 받은 함수가 달라졌다고 인식하기 때문이다.