IT/React & Next.js

[React] 불필요한 useEffect와 State 제거하기

땅일단 2025. 2. 13. 01:27

오늘 회사에서 코드리뷰를 하는데 한분이 useEffect와 state를 제거할 수 있을 것 같다는 말씀을 해주셨다.

 

리액트 개발자들이라면 잘 알고 있듯이 useEffect와 state는 불필요한 렌더링을 발생시킬 수 있으므로 성능을 위해 꼭 필요한 경우에만 사용하도록 하는 것이 좋을 것이다.

 

아무튼 내가 짰던 코드를 대충 설명하면, props로 가져오는 객체 데이터가 있는데, useEffect 안에서 props가 변경될 때마다 그 데이터를 정제해서 두 개의 state로 나누고 그 state들을 뷰에 사용하는 형태였다.

const [state1, setState1] = useState(null);
const [state2, setState2] = useState(null);

useEffect(() => {
    setState1(updateData(props.myState));
    setState2(updateData(props.myState));
}, [props.myState]);

대충 이런...?

 

state1, state2를 제거하고, 일반 변수로 대체하는 게 어떠냐고 하셨는데 생각지도 못한 방향이었다.

평소에는 뷰에 들어가는 데이터,  즉 useRef로 대체될 수 없는 데이터라면 state와 useEffect를 습관적으로 사용했다.

하지만 렌더링 중에도 계산이 가능한 데이터라면, 일반 변수로 대체할 수 있다.

 

 

useEffect 제거 예시

내 상황과 비슷한 코드가 있어서 가져와봤다.

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

  // 🔴 피하세요: Effect에서 prop 변경 시 state 조정하기
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

propuseEffect의 의존성 배열에 넣고 있다. 그리고 그것에 따라 다른 state의 값을 렌더링 이후에 변경한다. 이 코드에서 useEffect를 제거하면 아래와 같다.

 

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

  // 더 좋습니다: 렌더링 중 state 조정
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

prop(items)이 변했을 때 selection 값을 업데이트시켜주기 위한 코드를 다음과 같이 변경하였다. if문을 그대로 사용하면서 렌더링 중에 state를 변경하고 있다.

props의 값이 변경되면 재렌더링이 발생하고, 재렌더링이 발생하면 컴포넌트의 모든 요소가 새로 시작되면서 if문을 수행하기 때문에 검사 타이밍에는 문제가 없다.

오히려 useEffect를 사용함으로써 발생했던 불필요한 재렌더링(setSelection 전에 한번, 이후 한번)이 사라졌다.

 

또, props를 사용할 때 가끔, 어떤 prop이 변했을 경우 state 값들을 초기화시키고 싶을 때가 있는데, 그 때는 useEffect를 사용하지 말고 key를 이용하는 것도 방법이다.

export default function Scoreboard() {
    const [isPlayerA, setIsPlayerA] = useState(true);
    
    return (
        ...
        {isPlayerA ? (
          <Counter key="Taylor" person="Taylor" />
        ) : (
          <Counter key="Sarah" person="Sarah" />
        )}
        ...
    );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);
  ...
}

하위 컴포넌트에 key값을 준 코드이다.

isPlayerA라는 state가 변경되었을 때 Counter 컴포넌트에서 score과 hover의 값은 초기화된다. key의 값이 변경되었기 때문이다.

 

 

state를 ref/일반 변수로 대체하기

state를 무턱대고 바꾸다간 버그가 일어날 것이다. 바꿔도 되는 사례를 살펴보도록 하자.

 

1. 변경이 일어나지 않는 값

이건 당연하게도 상수, 즉 일반 변수로 뺀다. 컴포넌트 안에 있으면 재렌더링될 때마다 다시 만들어지므로 성능 이점을 가지고 오기 위해서는 컴포넌트 밖에 선언한다.

 

2. 렌더링과 관련 없는 값

useRef를 사용한다. DOM 접근에 사용되는 건 너무 일반적이라 제외하고, 아래와 같은 경우에서 많이 사용된다.

  • 한번 변경되고 나면 변경되지 않는 값 (컴포넌트의 생명주기동안 계속 일관적인 값이 유지될 때)
  • 이전 값을 저장할 때
  • 값이 바뀌어도 재렌더링이 일어나지 않아도 되는 경우 (이래서 뷰에 쓰면 안된다)

 

ref.current는 렌더링 중에 변경되면 안된다.

export default const Test = () => {
    const countRef = useRef(0);
    count.current += 1;
    
    return null;
}

이런 코드가 안 된다는 것이다. 사용자 상호작용시라든가 useEffect(렌더링 직후에 실행됨) 안에서 값을 변경하면 된다.

 

최근에 나는 사용자가 드래그한 상태임을 표시하기 위한 isDragging이라는 변수가 필요했는데, 이걸 useRef로 만들었었다. 사용자 상호작용마다 값이 변경되고, 재렌더링이 안 돼도 되기 때문이다.

 

3. 재렌더링될 때 값이 없어져도 상관 없는 값

일반 변수로 만든다.

useRef는 재렌더링되어도 값이 남아있지만, 일반 변수는 값이 초기화된다.

하지만 생각해보자. state는 재렌더링 시 알아서 계산되는 값이다. state끼리의 계산 결과는 초기화되어도 어차피 다시 계산되므로 일반 변수가 되어도 된다는 것이다.

즉 state를 기준으로 만들어진 값은 일반 변수가 되어도 된다.

 

아래 리액트 공식 문서의 예시를 보자.

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

firstName과 lastName으로부터 fullName 렌더링 중에도 계산될 수 있다. 그래서 뷰에는 들어있지만 굳이 state가 될 필요가 없는 것이다. 만약 함수로 만든다면, 필요시 useMemo를 통해 메모이제이션한다. (의존성 배열에는 firstName, lastName이 들어갈 것이다)

 

또한 리액트 문서에는 '컴포넌트가 사용자에게 표시되었기 때문에(재렌더링 이후) 실행되어야 하는 코드에만 Effect를 사용하세요.' 라고 명시되어 있다. 예를 들어 버튼을 클릭함으로써 실행되는 코드는 useEffect로 실행되면 안 된다는 뜻이다.

 

 

 

 


 

아래 문서를 참고함.

 

Effect가 필요하지 않을 수도 있습니다 – React

The library for web and native user interfaces

ko.react.dev