서론알고 가야 할 것적용 안한 경우useMemoReact.memo()가 적절한 순간React.memo()를 사용하지 말아야 하는 경우useCallbackuseCallback()이 유용한 순간함수의 특징예시 코드useCallback 예시코드 2React.memo()리렌더링 빈도 확인하기결론
- 참고 블로그 : https://leehwarang.github.io/2020/05/02/useMemo&useCallback.html https://ui.toast.com/weekly-pick/ko_20190731 ← 추천! https://crmrelease.tistory.com/41 ← 추천! ( useMemo, useCallback, React.memo )
서론
React.memo()
, useCallback()
둘 다 핵심은 memozied 이다.계산에 영향을 주는 요소가 변경되었을 때만 새롭게 계산하고, 나머지는 계산된 것을 그대로 사용한다.
구현에 있어 필수적인 요인은 아니지만, 렌더링 최적화에 있어서는 고려하면 좋다.
알고 가야 할 것
- 함수형 컴포넌트는 그냥 함수다. 다시 한 번 강조하자면 함수형 컴포넌트는 단지 jsx를 반환하는 함수이다.
- 컴포넌트가 렌더링 된다는 것은 누군가가 그 함수(컴포넌트)를 호출하여서 실행되는 것을 말한다. 함수(컴포넌트)가 실행될 때마다 내부에 선언되어 있던 표현식(변수, 또다른 함수 등)도 매번 다시 선언되어 사용된다.
- 컴포넌트는 자신의 state가 변경되거나, 부모에게서 받는 props가 변경되었을 때마다 리렌더링 된다. (심지어 하위 컴포넌트에 최적화 설정을 해주지 않으면 부모에게서 받는 props가 변경되지 않았더라도 리렌더링 되는게 기본이다. )
⇒ React에서는 이러한 구조들을 통해 작동하기 때문에 우리는 한 번만 선언하면 될 거 같은 것들도 매번 새롭게 다시 선언된다.
적용 안한 경우

그냥 작성하면 하나의 상태만 바뀌어도 컴포넌트가 re-render 되면서 해당 함수 로직들도 다시 계산된다.
따라서 color 상태 값만 바꾸어도 getColorKor, getMovieGenreKor 둘 다 새로 계산되며, 반대의 경우도 마찬가지다.
useMemo

memoized 된 '값'을 반환한다는 것이 핵심이다.
React.memo()
는 함수형 컴퍼넌트에도 적용되며, 이 경우에는 전달되는 props가 바뀌었을 때만 해당 컴포넌트가 리렌더링 된다.import React, { useMemo } from "react"; const colorKor = useMemo(() => getColorKor(color), [color]); const movieGenreKor = useMemo(() => getMovieGenreKor(movie), [movie]);
import react, { useMemo } from "react"; const User = (props) => { console.log("자식"); // //일반적인 방식 // const naem = setNameTag(props.name); // const age = setAgeTag(props.age); //useMemo 적용 const name = useMemo(() => setNameTag(props.name), [props.name]); const age = useMemo(() => setAgeTag(props.age), [props.age]); return ( <div> <h1>{name}</h1> <h1>{age}</h1> <div> <input type="text" onChange={(e) => props.onChangeName(e)} /> </div> <div> <input type="text" onChange={(e) => props.onChangeAge(e)} /> </div> </div> ); };
useMemo를 사용하면 의존성 배열에 넘겨준 값이 변경되었을 때만 다시 계산하여 값을 반환한다.
나머지의 경우는 한번 계산되어
memoized
된 값을 재사용한다.React.memo()가 적절한 순간
React.memo()
를 사용하기 가장 좋은 케이스는 함수형 컴퍼넌트가 동일한 props
값으로 자주 렌더링 될거라 예상될 때이다./* ** 예시 ** React.memo()가 적용되지 않은 컴포넌트 */ export function Movie({ title, releaseDate }) { return ( <div> <div>Movie title: {title}</div> <div>Release date: {releaseDate}</div> </div> ); } /* ** 예시 ** React.memo()로 감싼 Moview 컴포넌트 */ export const MemoizedMovie = React.memo(Movie); /* ** 예시 ** Movie 컴포넌트를 자식으로 가지는 컴포넌트 */ function MovieViewsRealtime({ title, releaseDate, views }) { return ( <div> <Movie title={title} releaseDate={releaseDate} /> Movie views: {views} </div> ); }
/* ** 예시 ** 매 초마다 서버로부터 데이터를 polling 받아서 MovieViewsRealtime를 업데이트 하는 경우 */ // Initial render <MovieViewsRealtime views={0} title="Forrest Gump" releaseDate="June 23, 1994" /> // After 1 second, views is 10 <MovieViewsRealtime views={10} title="Forrest Gump" releaseDate="June 23, 1994" /> // After 2 seconds, views is 25 <MovieViewsRealtime views={25} title="Forrest Gump" releaseDate="June 23, 1994" />
위의 경우 매번 polling 할 때마다
views
에 전달할 값이 바뀌어서, MovieViewsRealTime
컴포넌트가 다시 렌더링 되며, 당연히 자식 컴포넌트인 Movie
컴포넌트도 다시 렌더링 된다.문제는
Movie
컴포넌트 렌더링에 필요한 props인 title
과 releaseDate
값은 계속 동일한 값임에도 다시 렌더링 된다는 것이다.이럴 때
Movie
컴포넌트를 React.memo()
로 감싸주면 베스트다./* ** 예시 ** Movie 컴포넌트를 자식으로 가지는 컴포넌트 */ function MovieViewsRealtime({ title, releaseDate, views }) { return ( <div> <MemoizedMovie title={title} releaseDate={releaseDate} /> Movie views: {views} </div> ); }
title
과 releaseDate
가 바뀔 때만 리렌더링 되므로 불필요한 렌더링을 막아 성능을 높혔다.React.memo()를 사용하지 말아야 하는 경우
- 렌더링될 때
props
가 다른 경우가 대부분인 컴포넌트의 경우 - 메모이제이션 기법의 이점을 얻기 힘들다.
- 불필요한 props 비교 과정만 추가된 것일 뿐 성능상의 이점을 가져오지 못한다.
- 이전
props
와 다음props
가 다른지 검사하지만 false가 나와 2번을 수행함. - React는 이전 렌더링 내용과 다음 렌더링 내용이 달라졌는지 비교
- 클래스 기반의 컴퍼넌트를
React.memo()
로 래핑하는것은 적절하지 않다. PureComponent
를 확장하여 사용하거나,shouldComponentUpdate()
메서드를 구현하는 것이 적절하다.
useCallback

memoized 된 '함수'를 반환한다는 것이 핵심이다.
컴포넌트 내부의 함수들 또한 컴포넌트가 re-render 될 때마다 재선언된다.
import React, { useState, useCallback } from "react"; const onChangeHandler = useCallback(e => { if (e.target.id === "color") setColor(e.target.value); else setMovie(e.target.value); }, []);
위와 같이 단순히 인자를 전달 받아서 setColor 또는 setMovie를 호출할 뿐인 경우, 의존성을 띄고있는 것들이 없기 때문에 첫 마운트 될 때 한 번만 선언하고 재사용하면 된다.
이를 위해서
useCallback()
으로 감싸고 deps
에 빈 배열을 넘김으로써, 한번 정의된 후 쭉 사용되도록 한다.useCallback()이 유용한 순간
This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
즉, 콜백함수를 자식의 prop으로 내려보낼 때, 자식이
React.memo()
등으로 불필요한 리렌더링을 방지하도록 최적화되어 있는 경우에 유용하다.왜냐? 함수가 매번 재선언되면 해당 함수를 prop으로 넘겨받는 하위 컴포넌트 입장에서는 prop이 바뀌었다고 인식하므로 리렌더링하게 되기 때문이다.
함수의 특징
함수는 오직 자기 자신만을 동일하다고 판단한다.
function sumFactory() { return (a, b) => a + b; } const sum1 = sumFactory(); const sum2 = sumFactory(); console.log(sum1 === sum2); // => false console.log(sum1 === sum1); // => true console.log(sum2 === sum2); // => true
예시 코드
function Logout({ username, onLogout }) { return <div onClick={onLogout}>Logout {username}</div>; } const MemoizedLogout = React.memo(Logout);
function MyApp({ store, cookies }) { return ( <div className="main"> <header> <MemoizedLogout username={store.username} onLogout={() => cookies.clear()} // 매번 다른 콜백 함수의 인스턴스를 전달하게 됨 /> </header> {store.content} </div> ); }
MemoizedLogout에 항상 같은 이름과 같은 내용의 함수를 전달하지만 MyApp 컴포넌트가 리렌더링 될 때마다 MemoizedLogout 컴포넌트도 리렌더링 된다. 즉, React.memo()가 의미없게 된다.
왜? 함수는 오직 자기 자신만을 동일하게 보기 때문에, 함수 내용이 같을 지라도 이를 전달받는 자식 컴포넌트 입장에서는 prop이 바뀌었다고 판단하게 되고 리렌더링 하게 되는 것이다.
이 문제를 해결하려면
onLogout
prop의 값을 매번 동일한 콜백 인스턴스로 설정해야만 한다.이럴 때 사용하는 것이
useCallback()
훅이다.const MemoizedLogout = React.memo(Logout); function MyApp({ store, cookies }) { const onLogout = useCallback(() => { cookies.clear(); }, []); return ( <div className="main"> <header> <MemoizedLogout username={store.username} onLogout={onLogout} /> </header> {store.content} </div> ); }
useCallback(() => cookies.clear(), [])
는 항상 같은 함수 인스턴스를 반환한다. MemoizedLogout
의 메모이제이션이 정상적으로 동작하도록 수정되었다.useCallback 예시코드 2
useMemo
는 deps가 변경되기 전까지 값을 기억하고, 실행후 값을 보관하는 역할로도 사용한다. import react, { useState, useMemo, useCallback } from "react"; import "./styles.css"; export default function App() { console.log("부모!"); const [name, setName] = useState(""); const [age, setAge] = useState(""); let count = 0; //기존 유저 이름 변경 함수 const onChangeName = (e) => { console.log("onChangeName count 값 : ", count); // 매번 0 출력됨 count++; const data = e.target.value; console.log("data : ", data); setName(data); }; //useCallback 사용 const onChangeAge = useCallback((e) => { console.log("onChangeAge count 값 : ", count); // 0, 1, 2, 3, 4, ... count++; const age = e.target.value; console.log("age : ", age); setAge(age); }, []); return ( <div className="App"> <h1>부모 컴포넌트</h1> <User name={name} age={age} onChangeName={onChangeName} onChangeAge={onChangeAge} /> </div> ); }
React.memo()
기본적으로 리액트는 Shallow copy를 실행한다(참조값만 비교)
즉, state가 변경되거나, 새로운 컴포넌트가 렌더링 되는 시점에서, Shallow copy를 통해 같은값인지 판단하고 렌더링 여부를 결정하게 된다.
그렇기 때문에 primitive type이 아닌 객체,배열,함수와 같은 reference type은 같은 참조가 아니라면 새로운 값으로 판단하게 된다.
예컨데 같은 값의 props라도 컴포넌트의 state가 변경되면 shallow copy에 의해 새로운 값으로 인식하는 것이다.
props 자체는 primivite type이기 때문에 값만 같아도 될지언정, 함수나 객체는 그렇지 않다.
그러므로 useCallback, useMemo를 통해 특정 props에 dependency를 걸어줌으로써 렌더링 횟수를 줄일 수 있다.
그러나!
useCallback만으로는 하위컴포넌트의 리렌더링을 막을 수 없다.
부모 컴포넌트에서 정의한 useCallback은 자식 컴포넌트에서 사용할때 아무 효력도 없기 때문이다.
자식 컴포넌트에 useCallback에 대한 로직처리가 없다면, 다시 부모의 렌더링 여부에 따라 렌더링 될 뿐이다.
즉, 자식 컴포넌트가 '참조동일성에 의존적인 pureComponent여야 의미가 있다.

장황했던 위의 방식은 자식 컴포넌트에 대한 React.memo 처리로 끝이다.
얘는 일종의 HOC인데, shouldComponentUpdate를 내장하고 있어 shallow copy를 실행하여 리렌더링을 방지한다.
방법적으론 예시코드 처럼 모듈화 시키는 컴포넌트를 export할때 React.memo로 감싸주면 된다.
이 방식은 '같은 props로 렌더링이 자주일어나는 컴포넌트','렌더링에 리소스 소모가 큰 컴포넌트'에 사용된다
그러나 무분별하게 사용되는 것은 좋지 않다. 어떠한 경우에는 그저 불필요한 비교 연산만 추가되는 꼴이 되기 때문이다.

얕은 비교를 원하지 않는 경우 customizing이 가능하다.
React.memo는 false일 경우에만 리렌더링이 된다.(shouldComponentUpdate는 true일때 렌더링을 허용한다는 점이 다르다)
리렌더링 빈도 확인하기
web react의 경우
React Dev Tools
의 Profiler
를 통해 확인할 수 있다.그리고 메모이제이션의 성능상 이점을 측정하기 위해 profiling을 사용하는 것을 잊지 말아라.
결론
React.memo()
는 함수형 컴퍼넌트에서도 메모이제이션의 장점을 얻게 해 주는 훌륭한 도구다. 올바르게 적용 된다면 변경되지 않은 동일한 prop에 대해 리렌더링을 하는 것을 막을 수 있다.다만, 콜백 함수를 prop으로 사용하는 컴퍼넌트에서 메모이징을 할 때 주의하라. 그리고 같은 렌더링을 할 때 이전과 동일한 콜백 함수 인스턴스를 넘기는지 확실히 하라.