리액트는 어떻게 하면 ui를 빠르게 만들고 사용자의 이벤트에 즉각적으로 반응하여 ui를 업데이트 할 수 있을지 고민해야하는 라이브러리입니다. ui를 렌더 즉, 화면에 보여주고 이벤트에 반응하는 컴포넌트를 만들고, 사용자가 볼때 변하는 값들을 state로 관리할때 최대한 효율적인 방법으로 코드를 짜야합니다. 이번 시간에는 어떻게 해야 메모리를 낭비하지 않고 효율적으로 상태 관리를 할 수 있는지 알아보겠습니다.
컴포넌트의 state를 선언할 때 선언할 변수의 갯수와 데이터 형태를 고려해야 합니다.
: 두 개 이상의 state 변수를 동시 업데이트하는 경우 단일 state 변수로 병합하기
ex) 커서 좌표 x, y 값의 업데이트가 필요한 경우, useState를 2개 사용하지 않고 1개로 병합하여 사용하기
const [position, setPosition] = useState({x: 0, y: 0});
: 두개의 state의 상태 값이 boolean 중 반대값이 되어야하는 경우, 두 state의 값이 동시에 같은 boolena 값이 될 수 있는 여지가 있습니다. 이럴 땐 변수로 상태를 지정하기보다는 아래와 같이 boolean 상태를 알 수 있는 상수 값으로 표시
const [status, setStatus] = useState('typing); //
const isSending = status === 'sending';
const isSent = status === 'sent';
: 일부 정보를 계산할 수 있는 경우 해당 정보를 해당 컴포넌트의 상태에 입력하면 안 됩니다.
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
function handleFirstNameChange(e) {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
setFullName(firstName + ' ' + e.target.value);
}
const [fullName, setFullName] = useState('');
fullName을 state로 관리하지 않고 const fullName = firstName + ' ' + lastName; 변수로 해결 가능
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
export default function Menu() {
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);
const selectedItem = items.find(item =>
item.id === selectedId
);
function handleItemChange(id, e) {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
title: e.target.value,
};
} else {
return item;
}
}));
}
return (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map((item, index) => (
<li key={item.id}>
<input
value={item.title}
onChange={e => {
handleItemChange(item.id, e)
}}
/>
{' '}
<button onClick={() => {
setSelectedId(item.id);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}.</p>
</>
);
}
** 주의할 점
이처럼 state 구조와 컴포넌트 간의 state 공유를 통해 효율적으로 state를 관리하는 방법에 대해 알아보았습니다. props 변화는 렌더링의 결과로 props 때문에 렌더링이 되는 것이 아님을 주의해야 합니다. 부모 컴포넌트가 렌더링 되는 과정에서 자식 컴포넌트 역시 렌더링 되기에 state를 props로 전달받는 자식요소의 경우 props가 렌더링을 일으키는 것처럼 보입니다. 하지만 렌더링이 일어나는 과정에서 props의 변화된 값이 반영되는 것이기에 헷갈리면 안됩니다.
리액트에서의 렌더링은 즉, 함수가 호출된다는 것입니다. 호출될 때마다 함수 안에 있는 변수들이 모두 초기화 됩니다.
함수 연산이 무거운 경우 state 혹은 props로 인해 리랜더링 될 때마다 반복해서 계산되는데 이러한 비효율적인 문제를 useMemo가 해결해줄 수 있음!
useMemo는 컴포넌트 리렌더링 간에 계산 결과를 캐싱할 수 있는 훅입니다. 계산이 복잡하여 시간이 오래 걸리는 연산의 경우, 초기 랜더링 시 계산 결과를 캐싱해두었다가 필요한 때에 사용할 수 있습니다. 이를 메모이제이션이라고 표현합니다.
동일한 값을 리턴하는 함수를 반복적으로 사용해야할 때 맨처음 값을 계산할 때 해당 값을
메모리에 저장해서 필요할 때마다 또다시 계산하지 않고 메모리에서 꺼내서 재사용하는 기법.
자주 필요한 값을 맨처음 계산할 때 캐싱해둬서 그 값이 필요할 때마다 메모리에서 꺼내 쓰는 것!
종종 최적화를 위해 무작위로 useMemo를 남발하는 경우가 있습니다. 이는 가독성을 헤치고 메모리 사용량을 증가시키는 역효과가 있습니다. 메모이제이션 역시 결국은 데이터를 메모리상에 저장해두는데 뚜렷한 목적이 없이 useMemo를 사용하면 결국은 메모리 낭비로 이점이 상쇄됩니다.
import React, { useMemo, useState } from 'react';
const hardCalculate = (num) => {
console.log('렌더링 확인');
for (let i = 0; i < 100; i++) {
console.log('복잡한 계산');
} // 생각하는 시간
return num + 10000;
}
const easyCalculate = (num) => {
console.log('쉬운 계산');
return num + 1;
}
function Component() {
const [hardNum, setHardNum] = useState(1);
const [easyNum, setEasyNum] = useState(1);
// const hardSum = hardCalculate(hardNum); // 메모이제이션이 필요한 값
const hardSum = useMemo(() => {
return hardCalculate(hardNum)
}, [hardNum]);
const easySum = easyCalculate(easyNum);
return (
<div className="App">
<h3>어려운 계산기</h3>
<input
type='number'
value={hardNum}
onChange={(e) => setHardNum(parseInt(e.target.value))}
/>
<span> + 10000 = {hardSum}</span>
<h3>쉬운 계산기</h3>
<input
type='number'
value={easyNum}
onChange={(e) => setEasyNum(parseInt(e.target.value))}
/>
<span> + 1 = {easySum}</span>
</div>
);
}
export default Component;
위와 같이 렌더링이 발생하여 컴포넌트 함수 호출 시 복잡한 연산은 메모이제이션을 통해 미리 캐싱해두고, 의존배열 값이 변경될 때마다 해당 연산을 재실행하고 값의 변경이 없을 경우 이전에 캐싱한 랜더링 값을 활용할 수 있습니다.
좀더 나아가 react.memo 고차 컴포넌트 역시 메모이제이션을 활용합니다.
// 부모 컴포넌트
import { useMemo, useState } from "react";
import Child2 from "./example2Child";
function Component2() {
const [parentAge, setParentAge] = useState(0);
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
console.log('부모 컴포넌트 렌더링')
// Child 자식 컴포넌트에 props로 전달하면, Component2 함수를 호출할때마다 name 이라는 객체 메모리 주소값이 재할당되어 자식 컴포넌트를 react.memo로
const name =
// {
// lastName: '고',
// firstName: '라니',
// }
// 최적화 해주더라도 계속해서 렌더링됨. 메모이제이션이 필요한 상태!
useMemo(() => {
return {
lastName: '고',
firstName: '라니',
}
}, [])
return(
<div style={{border: '2px solid navy', padding: '10px'}}>
<h1>부모</h1>
<p>나이: {parentAge}</p>
<button onClick={incrementParentAge}>부모님 연세 증가</button>
<Child2 name={name}/>
</div>
)
}
export default Component2;
// 자식컴포넌트
import React, { memo } from 'react';
function Child2({name}) {
console.log('자녀 컴포넌트도 렌더링')
return (
<div style={{border: '2px solid red', padding: '10px'}}>
<h3>자녀</h3>
<p>성: {name.lastName}</p>
<p>이름: {name.firstName}</p>
</div>
);
};
export default memo(Child2);
자식 컴포넌트는 부모 컴포넌트가 랜더링 될때마다 함께 랜더링됩니다. 하지만 props가 변하지 않음에도 랜더링되는 문제가 있습니다. 이럴때 고차 컴포넌트 react.memo를 활용하여 자식 컴포넌트를 인자로 받으면 자식 컴포넌트는 최적화되어 랜더링이 발생하면 props의 변화를 감지하여 변화가 없을 경우, 렌더링하지 않고 이전 값을 유지하도록 해줍니다.
https://www.nextree.io/riaegteu-rendeoring-mic-coejeoghwa/
https://ko.legacy.reactjs.org/docs/hooks-intro.html
https://dev.grapecity.co.kr/bbs/board.php?bo_table=wijmo_bntips&wr_id=103
https://velog.io/@shin6403/React-렌더링-성능-최적화하는-7가지-방법-Hooks-기준
https://react.dev/learn/describing-the-ui
https://www.nextree.io/riaegteu-rendeoring-mic-coejeoghwa/
https://www.youtube.com/watch?v=oqUgcxwrnSY
https://www.youtube.com/watch?v=e-CnI8Q5RY4
https://velog.io/@jay/react-dependency-injection