제목을 거창하게 썼지만, 사실은 내가 했던 실수 모음
hook 단위로 작성해봄
useState
prop을 useState의 초기값으로 넣는 것. 이렇게 하는 것 자체가 실수는 아니지만, prop에 따라 state가 바뀌지는 않는다.
const Counter = ({ num }) => {
const [count, setCount] = useState(num);
// num이 바뀌어도, count는 그대로다
}
위처럼 사용하고 num
이 바뀔 때 count
도 바뀔 것이라고 기대하면 안 된다. useState
에 넣어주는 값은 초기값일 뿐 매번 상태를 업데이트해주지 않는다. 단순화시켜서 당연해보이지만, 생각보다 실수하기 쉽다.
추가로 useState
안에 초기값을 함수의 리턴값으로 넣어주는 것도 유의해야 한다. 함수 자체는 매번 호출되지만, 마찬가지로 상태가 업데이트되지는 않는다.
prop에 따라 state가 업데이트되도록 하고 싶다면, useEffect
를 사용해야 한다.
const Counter = ({ num }) => {
const [count, setCount] = useState(num);
// num이 바뀔 때, count를 업데이트한다
useEffect(() => {
setCount(num);
}, [num]);
}
useEffect
dependency array
useEffect
와 관련된 대표적인 실수로 다음 두 가지가 있다고 본다.
- 디펜던시 array에 포함되어야 하는 무언가를 제외하는 것
- effect 안에서 많은 변수나 함수를 참조하고 있어서 디펜던시 array가 커지는 것
1번: 경험상 대체로 버그의 원인이 된다 (당장은 괜찮더라도 이후에 코드가 추가되고, 다른 사람에 의해 수정되다보면). 그래서 디펜던시 array는 effect 안에서 사용된 것들을 모두 포함시키는 것이 좋다.
2번: 리팩토링 신호일 가능성이 높다. useEffect
는 말 그대로 어떤 side effect를 실행하기 위한 hook인데, side effect가 다양한 변수와 함수가 변경될 때마다 발생한다는 것은 코드가 그만큼 예측하기 어렵다는 뜻이기 때문이다.
어느 정도면 디펜던시 array가 '큰 것'이냐고 물어보면 정확히 답할 수는 없지만, useEffect
가 정말로 의존해야 할 값이 무엇인지 추려보고 prop, 상태, 함수 선언 등을 정리해보면 대체로 개선할 부분이 나올 것이라고 생각한다.
Warning: can't perform a react state update on an unmounted component
주로 useEffect
안에서 비동기 처리를 할 때 보게 된다. 네트워크 요청이 완료되고 나서 상태 업데이트를 하려고 하는데, 컴포넌트는 이미 unmount되어 있을 경우에 발생한다. 대체로 다음과 같은 패턴으로 해결한다.
useEffect(() => {
let isMounted = true; // 변수 선언
getData(); // 비동기 요청
async function getData() {
const result = await fetch();
// 완료되는 시점에 isMounted 체크
if (isMounted) {
setState(result);
}
}
return () => { // cleanup에서 false로 변경
isMounted = false;
}
})
위 코드를 이해하기 위해서는 effect, cleanup이 실행되는 시점에 대한 이해가 필요하다. useEffect
에 넣어주는 함수를 effect, 해당 함수에서 리턴해주는 함수를 cleanup이라고 한다면 각각이 실행되는 시점은 다음과 같다.
- 컴포넌트 마운트 -> effect
- 디펜던시 변경 -> (이전 effect의) cleanup, (새로운) effect
- 컴포넌트 언마운트 -> cleanup
이것도 개발하다보면 잊어버리거나 헷갈리기 쉬우니 기억해두는 것이 좋다.
useCallback, useMemo
memoization을 위해 사용하는 hook들이다. 잘 사용하면 퍼포먼스적으로 이득이 있지만, 잘못 사용하면 손해가 되기도 한다. 바꿔말하면 퍼포먼스상 이득이 확실한게 아니라면, 굳이 사용할 필요가 없다.
이 주제에 대해 바이블 격인 글(https://kentcdodds.com/blog/usememo-and-usecallback)이%EC%9D%B4) 있다. 아래 내용은 해당 글을 간단히 정리하고 덧붙인 것이므로 자세한 내용이 궁금하다면 원문을 읽어보길 추천한다.
위 hook들에 대한 대표적인 오해는 매번 선언되는 것을 피하기 위해 쓴다 는 것이다.
const handleClick = (something) => {
// do something
}
const handleClickCallback = useCallback((something) => {
// do something
}, [])
전자는 매 랜더링마다 함수를 선언한다. 그럼 후자처럼 함수를 사용하면 퍼포먼스상 이득이 있을까?
전혀 아니다. 후자에서도 useCallback
에 인자로 넣을 함수를 매번 선언할 뿐만 아니라 빈 배열 선언, hook 호출까지 이루어지기 때문에 오히려 더 많은 비용이 발생한다.
Referential Equality
그럼 useCallback
이나 useMemo
가 필요한 경우는 언제일까? 답은 Referential equality를 유지하고 싶을 때이다. 예를 들어 자식 컴포넌트에게 어떤 함수를 넘겨주는 상황에서
- 자식 컴포넌트가 사용하는 hook의 dependency array에 해당 함수가 포함 또는
- 자식 컴포넌트가
React.memo
로 감싸져있음
위와 같은 경우에 함수를 매 랜더링마다 선언해서 넘겨준다면 equality가 유지되지 않을 것이다. 이 때 equality를 유지하기 위해 useCallback
을 사용한다. 함수 외의 오브젝트의 경우에도 useMemo
를 사용하면 equality를 유지할 수 있다.
큰 비용이 드는 계산
추가적으로 useMemo
의 경우 매 랜더링마다 헤비한 계산이 발생하는 것을 방지하기 위해 사용할 수도 있다. 다만 여기서 주의해야 할 것은 useMemo
를 사용하는 것 자체도 비용이 있기 때문에, 퍼포먼스상 이득이 확실한 경우에만 사용하는 것이 좋다는 것이다.
'개발' 카테고리의 다른 글
React에서의 Authorization (0) | 2022.11.30 |
---|---|
Intersection Observer와 Skeleton UI로 UX 개선하기 (0) | 2022.11.30 |
Material UI에 기여할뻔한 얘기 (0) | 2022.11.30 |
Gatsby 블로그: TOC(목차) 추가하기 (0) | 2022.11.30 |
Gatsby 블로그: Pagination 추가하기 (0) | 2022.11.30 |