본문 바로가기

개발

React를 쓰면서 하기 쉬운 실수들

제목을 거창하게 썼지만, 사실은 내가 했던 실수 모음
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와 관련된 대표적인 실수로 다음 두 가지가 있다고 본다.

 

  1. 디펜던시 array에 포함되어야 하는 무언가를 제외하는 것
  2. 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를 유지하고 싶을 때이다. 예를 들어 자식 컴포넌트에게 어떤 함수를 넘겨주는 상황에서

 

  1. 자식 컴포넌트가 사용하는 hook의 dependency array에 해당 함수가 포함 또는
  2. 자식 컴포넌트가 React.memo로 감싸져있음

위와 같은 경우에 함수를 매 랜더링마다 선언해서 넘겨준다면 equality가 유지되지 않을 것이다. 이 때 equality를 유지하기 위해 useCallback을 사용한다. 함수 외의 오브젝트의 경우에도 useMemo를 사용하면 equality를 유지할 수 있다.

큰 비용이 드는 계산

추가적으로 useMemo의 경우 매 랜더링마다 헤비한 계산이 발생하는 것을 방지하기 위해 사용할 수도 있다. 다만 여기서 주의해야 할 것은 useMemo를 사용하는 것 자체도 비용이 있기 때문에, 퍼포먼스상 이득이 확실한 경우에만 사용하는 것이 좋다는 것이다.