React.memo
일기를 만들 때 사용한 부모 컴포넌트 App이 자식 컴포넌트 2개에 각각의 prop을 주고 있다.
이 때 자식 컴포넌트에서 해당 prop을 받아 부모 컴포넌트의 state를 변경 시켰다.
그러면 부모 컴포넌트는 state가 수정되어 다시 렌더링을 하게 된다.
하지만 이 때 변화가 일어나지 않은 TextView 컴포넌트까지 통째로 리렌더링 되는 문제가 생긴다.
(useMemo도 그렇지만 이렇게 최적화를 하지 않으면 지금은 크게 와닿지 않지만, 실제로 프로젝트를 하거나 더 큰 규모, 더 복잡한 파일들을 다루게 될 경우에는 개발한 웹 어플리케이션의 속도나 품질이 떨어질 수 있어서 작은 프로젝트여도 한 번 만들어보고 최적화 하는 습관을 들여야한다.)
아무튼 위와 같이 가만히 있는 자식 컴포넌트까지 리렌더링 되는 문제를 해결하려면 자식 컴포넌트에 리렌더링 될 조건을 달아주면 된다. 각자 자신이 부모 컴포넌트로부터 받고 있는 prop를 받아 state를 변경할 때만 렌더링이 되게 한다.
이를 위해 사용하는 것이 React.memo()다.
React.memo는 hook이 아니라 고차 컴포넌트다.
React 최상위 API – React
A JavaScript library for building user interfaces
ko.legacy.reactjs.org
말이 조금 어려운데, 컴포넌트가 동일한 prop으로 동일한 결과를 렌더링 한다면 React.memo로 결과를 메모이징하게 한다. 위에서 말한 것과 똑같이 어떤 자식 컴포넌트는 전혀 변화가 생기지 않았는데 리렌더링이 되고 있다면, 얘는 지금 값을 메모이징 하게 해서 다시 리렌더링 되지 않게 해주면 되는 것이다. (하지만 부모 컴포넌트와 별개로 자식 컴포넌트 스스로의 상태가 변해도 렌더링 되는 부분을 생각해야 한다.)
예시
import React from "react";
import { useState, useEffect } from "react";
const TextView = ({ text }) => {
useEffect(() => {
console.log(`Update:: Text : ${text}`);
});
return <div>{text}</div>;
};
const CountView = ({ count }) => {
useEffect(() => {
console.log(`Update:: Count : ${count}`);
});
return <div>{count}</div>;
};
const OptimizeTest = () => {
const [count, setCount] = useState(1);
const [text, setText] = useState("");
return (
<div style={{ padding: 50 }}>
<div>
<h2>count</h2>
<CountView count={count} />
<button onClick={() => setCount(count + 1)}>+</button>
</div>
<h2>text</h2>
<TextView text={text} />
<input value={text} onChange={(e) => setText(e.target.value)} />
</div>
);
};
export default OptimizeTest;
OptimizeTest 컴포넌트 아래에 자식 컴포넌트인 CountView, Textview가 있다.
얘네는 count, text를 prop로 받고 있어서 이게 하나라도 변경되면 setCount, setText가 변화함에 따라 부모 컴포넌트와 그 밑에 있는 컴포넌트들도 모두 리렌더링을 하게 된다.
useEffect로 확인해보면 버튼을 누르거나 input에 무언가를 적어 이벤트가 발생하면 계속 콘솔이 찍히는 걸 알 수 있다.
import React from "react";
import { useState, useEffect } from "react";
const TextView = React.memo(({ text }) => {
useEffect(() => {
console.log(`Update:: Text : ${text}`);
});
return <div>{text}</div>;
});
const CountView = React.memo(({ count }) => {
useEffect(() => {
console.log(`Update:: Count : ${count}`);
});
return <div>{count}</div>;
});
const OptimizeTest = () => {
const [count, setCount] = useState(1);
const [text, setText] = useState("");
return (
<div style={{ padding: 50 }}>
<div>
<h2>count</h2>
<CountView count={count} />
<button onClick={() => setCount(count + 1)}>+</button>
</div>
<h2>text</h2>
<TextView text={text} />
<input value={text} onChange={(e) => setText(e.target.value)} />
</div>
);
};
export default OptimizeTest;
위와 같이 각 컴포넌트에 React.memo()로 래핑을 해주었다.
이러면 자기가 받고 있는 prop이 변화하지 않는 자식 컴포넌트의 경우 리렌더링을 하지 않게 된다.
이처럼 text, count 같이 자신이 받고 있는 prop가 변경될 때만 리렌더링되는 '강화된 컴포넌트'가 되었다.
문제
import React from "react";
import { useState, useEffect } from "react";
const CounterA = React.memo(({ count }) => {
useEffect(() => {
console.log(`Counter A Update - count: ${count}`);
});
return <div>{count}</div>;
});
const CounterB = React.memo(({ obj }) => {
useEffect(() => {
console.log(`Counter B Update - count: ${obj.count}`);
});
return <div>{obj.count}</div>;
});
const OptimizeTest = () => {
const [count, setCount] = useState(1);
const [obj, setObj] = useState({ count: 1 });
return (
<div style={{ padding: 50 }}>
<div>
<h2>Counter A</h2>
<CounterA count={count} />
<button onClick={() => setCount(count)}>A button</button>
</div>
<div>
<h2>Counter B</h2>
<CounterB obj={obj} />
<button
onClick={() =>
setObj({
count: obj.count,
})
}
>
B button
</button>
</div>
</div>
);
};
export default OptimizeTest;
이번엔 Counter A, Counter B 자식 컴포넌트를 만들었다.
Counter A 컴포넌트는 props로 1을 받고 있고, 이벤트가 발생해도 count에 가감 없이 그대로다.
즉 버튼을 계속 눌러도 계속 1인 셈이다.
반면 Counter B 컴포넌트는 props로 {count: 1}인 객체를 받고 있다.
이것도 동일하게 버튼을 클릭해도 setObj에는 기존 값이 변하지 않는다.
이 때 두 컴포넌트 모두 React.memo로 강화되었을까?
결과는 그렇지 않다.
Counter B의 경우 state 값은 동일함에도 불구하고 계속 리렌더링이 된다. (콘솔에 찍힘)
연산이 동일한 Counter A 와 달리 왜 Counter B만 계속 리렌더링이 되는 것일까?
이유는 객체가 참조 타입이기 때문이다.
JS의 데이터 타입은 크게 2가지로 나뉜다.
원시 타입(string, num, null, undefined, boolean, symbol, bigint), 참조 타입(array, object, function)
원시 타입은 변수에 값을 할당할 때 메모리 상에 고정된 크기로 스택 영역에 저장되고, 해당 변수는 원시 데이터의 값을 보관한다. 원시 타입은 변수 선언이나 초기화 등을 할 때에도 값이 저장되어 있는 메모리 영역에 직접 접근할 수가 있다.
반면 참조 타입은 그렇지 않다. 크기가 정해져 고정되어 있지도 않고, 값이 변수에 직접 저장되는 게 아니라 변수의 값이 저장된 힙 메모리의 주소 값을 저장한다. 그냥 간단하게 원시 타입은 값에 다이렉트로 접근한다면, 참조 타입은 값 그 너머의 주소 값을 참조하기 때문에 값이 같아도 주소가 다를 수 있다.
그래서 똑같은 객체 값이어도 이 값 너머의 주소 값이 다 다르다.
obj1, obj2가 힙 메모리에 저장되어 있는 주소가 다르기 때문에 false가 나오는 것이다.
let obj1 = {a: 1}
let obj2 = {a: 1}
console.log(obj1 === obj2); // false
물론 이 그림처럼 변수에 직접 대입하면 같은 주소를 가리키게 되기도 한다.
일단은 그래서 지금 Counter B가 계속 새로운 참조값을 갖게 되어 리렌더링 된다는 것을 알면 된다.
import React from "react";
import { useState, useEffect } from "react";
// ...CounterA 생략
const CounterB = ({ obj }) => {
useEffect(() => {
console.log(`Counter B Update - count: ${obj.count}`);
});
return <div>{obj.count}</div>;
};
const areEqual = (prev, next) => {
return prev.obj.count === next.obj.count;
};
const MemoizedCounterB = React.memo(CounterB, areEqual);
const OptimizeTest = () => {
const [count, setCount] = useState(1);
const [obj, setObj] = useState({ count: 1 });
return (
<div style={{ padding: 50 }}> // CounterA 생략
<div>
<h2>Counter B</h2>
<MemoizedCounterB obj={obj} />
<button
onClick={() =>
setObj({
count: obj.count,
})
}
>
B button
</button>
</div>
</div>
);
};
export default OptimizeTest;
CounterB는 prop으로 객체 obj를 받고 있고 이걸 가지고 {obj.count}를 리턴한다.
areEqual 함수는 prev, next 라는 2개의 파라미터를 받고 있다.
각각 이전 렌더링의 prop를 나타내는 객체, 다음 렌더링의 prop을 나타내는 객체다.
이 때 둘의 값이 같은지를 true, false로 나타낸다. true면 리렌더링을 하지 않는다.
그래서 MemoizedCounterB는 areEqual의 반환 값을 기반으로 CounterB가 변화될 때만 리렌더링을 하게 된다.
**참고
한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지 - 인프런 | 강의
개념부터 독특한 프로젝트까지 함께 다뤄보며 자바스크립트와 리액트를 이 강의로 한 번에 끝내요. 학습은 짧게, 응용은 길게 17시간 분량의 All-in-one 강의!, 리액트, 한 강의로 끝장낼 수 있어요.
www.inflearn.com
'⚛️ React > 💭 한 입 크기 React' 카테고리의 다른 글
[React] React.createContext로 Props Drilling 방지하기 (0) | 2023.07.26 |
---|---|
[React] useReducer로 상태 변화 로직을 분리하기 (0) | 2023.07.26 |
[React] useMemo로 연산한 값 재사용하기 (0) | 2023.07.26 |
[React] 리액트의 생애주기 (Mount, Update, Unmount) (0) | 2023.07.25 |
[React] state/setState 효율적으로 작성하기 (0) | 2023.07.24 |