1. useMemo()
리액트 Hook 에서 useEffect()와 비슷하게 불필요한 동작을 막기 위해 useMemo()라는 것을 사용한다.
예를 들면, Lifecycle이 시작될 때 배열을 서버로부터 받아와서 useState()로 데이터를 상태를 유지한다고 하면,
const [data, setData] = useState([]);
const getData = async () => {
const response = await fetch("url")
.then((res) => res.json());
setData(response);
}
useEffect(() => {
getData();
}, [])
이런 코드를 작성할 수 있다.
여기서 Data 안에 있는 내용을 화면에 보여준다고 하면,
const getDataDetail = () => {
const oddCount = data.filter((d) => d.id % 2 !== 0).length;
const evenCount = data.length - dataOddCount;
const oddRatio = (oddCount / data.length) * 100;
return {oddCount, evenCount, oddRatio};
}
const {oddCount, evenCount, oddRatio} = getDataDetail();
return (
<div>
<div>Data Count : {data.length}</div>
<div>Odd Count: {oddCount}</div>
<div>Even Count: {evenCount}</div>
<div>Odd Ratio: {oddRatio}</div>
</div>
)
이런 코드를 작성할 수 있는데,
이 상황에서 data의 내용을 변경을 한다고 가정하면,
아무 관련없는 getDataDetail() 함수가 두 번 실행 될 것이다.
이를 방지하고자 useMemo()를 사용한다.
useMemo()를 사용한 코드로 수정하면,
const getDataDetail = useMemo(() => {
const oddCount = data.filter((d) => d.id % 2 !== 0).length;
const evenCount = data.length - dataOddCount;
const oddRatio = (oddCount / data.length) * 100;
return {oddCount, evenCount, oddRatio};
}, [data.length]);
const {oddCount, evenCount, oddRatio} = getDataDetail;
return (
<div>
<div>Data Count : {data.length}</div>
<div>Odd Count: {oddCount}</div>
<div>Even Count: {evenCount}</div>
<div>Odd Ratio: {oddRatio}</div>
</div>
)
이렇게 수정할 수 있다.
useMemo()를 이용하여 구조 분해 할당했을 경우 더이상 함수가 아닌 값이 전달되기 때문에 getDataDetail()이 아닌 getDataDetail를 사용해야 한다.
이렇게 하면 Data의 내용을 바꿔도 렌더링이 일어나지 않으며, [data.length] 때문에 데이터를 추가하거나 삭제했을 경우에만 렌더링이 진행된다.
2. memo
useMemo와 비슷하게 불필요한 리렌더링을 막아준다.
const Main = () => {
const [count, setcount] = useState(1);
const [text, setText] = useState("");
return (
<div>
<div>
<CountView count={count} />
<button onClick={() => setcount(count + 1)}>+</button>
</div>
<div>
<TextView text={text} />
<input value={text} onChange={(e) => setText(e.target.value)} />
</div>
</div>
);
}
const CountView = ({count}) => {
useEffect(() => {
console.log(`Count Update :: ${count}`);
});
}
const TextView = ({text}) => {
useEffect(() => {
console.log(`Text Update :: ${text}`);
});
}
이렇게 두 개의 상태가 있다고 가정하면,
count를 클릭할 경우 text도 같이 리렌더링이 진행되고, 반대로 text input에 값을 입력해도 count도 같이 리렌더링이 된다.
이를 방지하려면,
const Main = () => {
const [count, setcount] = useState(1);
const [text, setText] = useState("");
return (
<div>
<div>
<CountView count={count} />
<button onClick={() => setcount(count + 1)}>+</button>
</div>
<div>
<TextView text={text} />
<input value={text} onChange={(e) => setText(e.target.value)} />
</div>
</div>
);
}
const CountView = memo(({count}) => {
useEffect(() => {
console.log(`Count Update :: ${count}`);
});
});
const TextView = memo(({text}) => {
useEffect(() => {
console.log(`Text Update :: ${text}`);
});
})
이렇게 memo로 감싸주면 된다.
또 다른 방법으로는 useEffect()에 count와 text를 각각 넣어주어도 불필요한 리렌더링은 막을 수 있다.
const Main = () => {
const [count, setcount] = useState(1);
const [text, setText] = useState("");
return (
<div>
<div>
<CountView count={count} />
<button onClick={() => setcount(count + 1)}>+</button>
</div>
<div>
<TextView text={text} />
<input value={text} onChange={(e) => setText(e.target.value)} />
</div>
</div>
);
}
const CountView = ({count}) => {
useEffect(() => {
console.log(`Count Update :: ${count}`);
}, [count]);
};
const TextView = ({text}) => {
useEffect(() => {
console.log(`Text Update :: ${text}`);
}, [text]);
}
memo보다는 useEffect를 사용할 일이 많으니 아래 방법으로 자주 활용할 것 같다.
memo의 다른 사용 예를 보기 전에
리액트 공식홈페이지에서 memo에 대한 설명을 보면
마지막 문단에 두 번째 인자로 별도의 비교함수를 제공하면 된다고 되어 있다.
위 코드와 비슷한 상황을 가정하고, 상태가 그대로일 경우에는 리렌더링이 변화하지 않는다.
하지만 그 상태가 객체일 경우 객체는 얕은 비교를 하기 때문에 상태가 같은 것 처럼 보이지만 주소로 비교를 하기 때문에 다르다고 인식한다.
const Main = () => {
const [count, setCount] = useState(1);
const [obj, setObj] = useState({
count: 1,
});
return (
<div>
<CounterA count={count} />
<button onClick={() => setCount(count)}>A Button</button>
<CounterB obj={obj} />
<button
onClick={() =>
setObj({
count: obj.count,
})
}
>
B Button
</button>
</div>
);
}
const CounterA = memo(({ count }) => {
useEffect(() => {
console.log(`CounterA Update :: ${count}`);
});
return <div>{count}</div>;
});
const CounterB = memo({ obj }) => {
useEffect(() => {
console.log(`CounterB Update :: ${obj.count}`);
});
return <div>{obj.count}</div>;
};
이렇게 memo로 감싸져 있어도 CounterB의 렌더링은 상태가 바뀔 때 마다 계속 진행된다.
이를 막기 위해서
const areEqual = (prevProps, nextProps) => {
return prevProps.obj.count === nextProps.obj.count;
};
const MemoizedCounterB = memo(CounterB, areEqual);
이런 코드를 추가하고 CounterB 대신 MemoizedCounterB를 사용하면
불필요한 렌더링을 막을 수 있다.
최종 코드
const Main = () => {
const [count, setCount] = useState(1);
const [obj, setObj] = useState({
count: 1,
});
return (
<div>
<CounterA count={count} />
<button onClick={() => setCount(count)}>A Button</button>
<CounterB obj={obj} />
<button
onClick={() =>
setObj({
count: obj.count,
})
}
>
B Button
</button>
</div>
);
}
const CounterA = memo(({ count }) => {
useEffect(() => {
console.log(`CounterA Update :: ${count}`);
});
return <div>{count}</div>;
});
const CounterB = ({ obj }) => {
useEffect(() => {
console.log(`CounterB Update :: ${obj.count}`);
});
return <div>{obj.count}</div>;
};
const areEqual = (prevProps, nextProps) => {
return prevProps.obj.count === nextProps.obj.count;
};
const MemoizedCounterB = memo(CounterB, areEqual);
3. useCallback()
useCallback()은 useMemo()가 값을 재사용하는 것과는 다르게 함수를 재사용할 때 사용한다,
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useEffect나 useMemo와 비슷한 형태인데, []안에 있는 값이 변할 때만 doSomething() 함수가 실행된다.
useEffect 처럼 []안을 비워두면 처음에만 실행되고, 더 이상 실행되지 않는데 문제가 발생할 수 있다.
const [data, setData] = useState([]);
const getData = async () => {
const res = await fetch(
"url"
).then((res) => res.json());
setData(res);
};
useEffect(() => {
getData();
}, []);
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((data) => [newItem, ...data]);
}, []);
예제 코드인데, 여기서 onCreate 함수가 실행되고, data에 배열에 요소를 추가해서 렌더링 해줘야하는데, 처음에 데이터가 없었기 때문에 빈 배열에 새로 추가한 요소만 추가된다. 이를 방지하기 위해 함수형 업데이트를 활용하면 된다.
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((data) => [newItem, ...data]);
}, []);
setData에 콜백 함수를 전달해줘서 항상 최신의 데이터를 유지할 수 있게 된다.
[]은 비워야 하고, 화살표 함수의 인자를 통해 데이터를 참고하고, 이전과 같은 문제를 방지할 수 있다.
- 참고
https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103532?tab=curriculum
'개발 > Javascript & Typescript' 카테고리의 다른 글
[RN] React Native CLI vs Expo CLI (81) | 2023.11.05 |
---|---|
[React] 상태 변화 로직 분리하기 (0) | 2023.02.04 |
[React] Lifecycle 제어하기 (0) | 2023.02.01 |
[React] DOM 선택하기 (1) | 2023.01.31 |
[React] state 변경 공통화 하기 (0) | 2023.01.30 |