혼자 적어보는 노트

불필요한 useEffect 제거하기 본문

React

불필요한 useEffect 제거하기

jinist 2023. 8. 15. 23:35

 

불필요한 useEffect 제거하기

 

무분별하게 useEffect를 사용하게 되면 버그가 생기기 쉽고 사이드 이펙트 때문에 디버깅을 하기 어려울 때가 있다.

또한 useEffect 없이 구현할 수 있음에도 useEffect를 사용함으로써 불필요한 렌더링이 생기기도 한다.

나 또한 코드를 작성할 때 습관적으로 useEffect를 사용해 왔던 것 같고 이전에 관련 내용을 읽은 적은 있었으나 의식적으로 다른 코드로 대체할 생각을 하지 않다 보니 습관 그대로 쓰는 경우가 있었다.

이번에 회의에서 useEffect 사용을 자제 하자는 이야기가 나와서 다시 직접 작성하며 정리하게 되었다.

 

🔗 참고 문서 : https://react.dev/learn/you-might-not-need-an-effect

참고 문서에서 대체적으로 다루는 내용은 useEffect내부에서 상태 변경을 하는 경우였는데, 아래에 정리한 내용 말고도 추가적으로 참고할 내용들이 있다.

 

props로 받은 값을 필터링하여 사용하는 경우

function TodoList({ todos, filter }) {
  const [visibleTodos, setVisibleTodos] = useState([]);

  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  // ...
}

위 처럼 props로 받은 값을 필터링하여 새로운 state에 넣는 경우가 있는데 useEffect를 배운 이래로 종종 위와 비슷한 코드를 작성한 적 있기에 매우 익숙한 코드라고 볼 수 있다.

 

위 코드와 같이 useEffect를 통해 set을 하게 되면 TodoList의 자식 컴포넌트가 렌더링 된 후에 useEffect가 실행되기 때문에 불필요한 렌더링 과정을 거치게 된다.

 

아래와 같이 코드를 수정할 수 있다.

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}

관련이 없는 상태가 변경될 때 다시 계산하고 싶지 않다면, useMemo를 사용하면 된다.

 

const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);

실제로 위 예제와 같진 않지만 조금 더 복잡하게 코드가 얽혀있을 때 useMemo를 사용하지 않았을 경우

Too many re-renders. React limits the number of renders to prevent an infinite loop.

위와 같은 에러를 볼 수 있었다.

 

 

상태를 초기화할 때

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

이렇게 userId가 변경되었을 때 state를 초기화 해주고 싶을 수 있는데, 이렇게 되면 ProfilePage의 자식이 이전 comment의 값으로 렌더링 된 다음 다시 useEffect로 인해 리렌더링되어 비 효율적일 뿐만 아니라, 내부에서 상태 초기화에 대해 처리해야 하는 불편함이 있다.

reset 해야할 게 comment 뿐만이 아니라면?

 

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

이렇게 useId를 받고 내부에서 key를 전달하면 userId가 변경될 때마다 React에서 DOM을 다시 만들어서 상태를 재 설정 해준다. 

 

 

props 값 변경 시 상태 수정

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

items라는 배열이 새롭게 변경될 경우 selection을 재설정하는 코드지만 items가 변경될 때마다 List와 자식 컴포넌트가 selection의 이전 값으로 리렌더가 되고 그다음에 DOM을 업데이트한 후에 useEffect를 실행한다.

그리고 useEffect 내부의 setSelection을 통해 상태를 업데이트를 하여 다시 위의 프로세스가 다시 실행된다.

 

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

이 코드를 보고 이렇게 작성을 해야 되는 걸까 했는데, useEffect의 코드와의 차이점은 setSelection이 렌더링 중에 직접 호출이 된다는 점이다.

 

렌더링 중에 컴포넌트에 업데이트가 생기면 반환하던 JSX를 버리고 새롭게 렌더링을 시도하기 때문에 items가 변경되었을 때 List의 자식들이 이전의 값으로 렌더링이 되던 과정을 건너 뛰게 된다.

 

하지만 새로운 상태를 기반하여 다른 상태를 다루게 되면 디버깅 하기가 어려워질 수 있기 때문에 키로 상태를 재설정 하거나 렌더링 중에 계산할 수 있는지를 우선순위로 두고 코드를 작성하는 것이 좋다.

 

아래는 렌더링 중에 계산을 하는 코드이다.

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);

  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}

 

selection이 아닌 selectedId를 상태로 가지고 있고 selection을 변수로 만들어서 items가 선택된 id를 가지고 있다면 item을 반환해 선택된 상태로 유지시킬 수 있다.

 


 

이 외에도 핸들러에서 처리할 수 있는 코드를 굳이 useEffect에 의존성을 추가해서 작성할 필요는 없고,

코드를 짤 때 확실한 이유 없이 useEffect 안에 작성을 하게 될 경우 한번 더 스스로에게 질문을 해보면 좋을 것 같다.

Comments