Development Study/Frontend

[React] useEffect 쓰지 마세요?

TMInstaller 2023. 5. 25. 11:05
728x90

React는 페이스북에서 개발한 강력한 프론트엔드 라이브러리입니다.

상당히 많은 부분에서 useEffect 훅을 이용하여 개발을 진행하는데, React에서는 useEffect를 지양하고 있다고 합니다.

이번 글에서는 useEffect 훅을 지양하는 이유가 무엇인지, 그리고 연관되어 있는 개념인 Side Effect 관리에 대해 알아보겠습니다.

 


1. Side Effect란 무엇인가?

Side Effect는 컴포넌트 외부에 영향을 미치는 작업을 의미합니다.

예를 들어보면 데이터 가져오기, 네트워크 요청, 로컬 스토리지 접근 등이 여기에 해당됩니다.

React 컴포넌트는 Side Effect 관리를 위해서 useEffect 훅을 제공합니다.


2. 불필요한 Side Effect로 인한 성능 저하

useEffect 훅을 사용할 때에는 Side Effect가 정말로 필요한 경우에만 사용해야 합니다.

불필요한 Side Effect는 컴포넌트의 성능을 저하시킬 수 있습니다.

예를 들어, 컴포넌트가 화면에 표시될 때마다 데이터를 가져오는 Side Effect가 존재한다면,

화면이 렌더링 될 때마다 매번 데이터를 가져와야 하므로 불필요한 네트워크 요청이 발생할 수 있습니다.

function Counter() {
  const [count, setCount] = useState(0);
  const [doubleCount, setDoubleCount] = useState(0);
  
  useEffect(() => {
    console.log('Calculating doubleCount...');
    setDoubleCount(count * 2);
  }, [count]);
  
  function increment() {
    setCount(count + 1);
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Double Count: {doubleCount}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

위의 코드에서는 count가 변경될 때마다 useEffect가 실행되어 doubleCount를 업데이트합니다. 

그러나 doubleCount는 count에 의존하고 있기 때문에 이 작업은 Side Effect입니다.

문제는 doubleCount를 업데이트하는 useEffect가 실행될 때마다 화면이 다시 렌더링 된다는 점입니다. 

이는 불필요한 성능 저하를 야기할 수 있습니다. 

예를 들어, increment 버튼을 클릭하여 count를 증가시킬 때마다 doubleCount를 다시 계산하고 화면을 업데이트하는 것은 비효율적입니다.


3. Side Effect 관리를 위한 최적의 방법 찾기

Side Effect를 관리하기 위해 useEffect 훅을 사용하는 것은 일반적으로 옳은 접근 방식입니다.

그러나 useEffect를 사용하기 전에 다른 방법을 고려해 보는 것도 좋은 생각입니다.

 

Side Effect를 최적으로 관리하기 위해서는 다음과 같은 방법들을 고려할 수 있습니다.

1. 불필요한 Side Effect를 제거하고, 컴포넌트 렌더링 시에 계산을 수행할 수 있는 useMemo 훅을 활용하는 방법

function Counter() {
  const [count, setCount] = useState(0);
  
  const doubleCount = useMemo(() => {
    console.log('Calculating doubleCount...');
    return count * 2;
  }, [count]);
  
  function increment() {
    setCount(count + 1);
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Double Count: {doubleCount}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

위의 코드에서는 doubleCount를 useMemo를 사용하여 계산하고 있습니다. 

count가 변경될 때만 doubleCount를 다시 계산하고, 그렇지 않을 경우 이전 계산 결과를 재사용합니다. 

이렇게 함으로써 불필요한 계산을 방지하고 성능을 향상시킬 수 있습니다.

 

- 하지만, useMemo를 사용하는 것도 문제가 있다고 합니다. -

 

[React] useMemo 쓰지 마세요?

최근 useEffect 대응방안에서 useMemo를 사용해서 개선하는 방향과 관련된 글을 쓰다가 생각난 건데, 어떤 글에서 useMemo를 쓰지 말라는 주제로 글이 있던 것이 생각났습니다. 그래서 이참에 이것도

time-map-installer.tistory.com

 

2. 프롭 변경 시에 상태를 명시적으로 재설정하는 key 속성 활용하는 방법

function TodoList({ todos }) {
  const [filteredTodos, setFilteredTodos] = useState([]);
  
  useEffect(() => {
    // todos를 필터링하여 filteredTodos를 설정하는 로직
    setFilteredTodos(todos.filter(todo => !todo.completed));
  }, [todos]);
  
  return (
    <ul>
      {filteredTodos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

위의 코드에서 todos가 변경될 때마다 useEffect가 실행되어 filteredTodos를 업데이트합니다. 

todos 프롭이 변경되었을 때 컴포넌트가 다시 렌더링 되지만, 이전 filteredTodos 상태를 재사용하고 싶은 경우 key 속성을 사용하여 명시적으로 상태를 재설정할 수 있습니다.

 

3. 렌더링 중에 상태를 직접 업데이트하여 상태를 조절하는 방법

function Counter() {
  const [count, setCount] = useState(0);
  
  function increment() {
    setCount(prevCount => prevCount + 1);
  }
  
  function decrement() {
    setCount(prevCount => prevCount - 1);
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

위의 코드에서 increment와 decrement 함수에서 setCount를 호출할 때 이전 상태 값을 사용하여 직접 상태를 업데이트하고 있습니다. 

이 방법을 사용하면 이전 상태 값을 기반으로 상태를 조절할 수 있으며, 상태 업데이트에 의존적인 다른 동작을 수행할 수도 있습니다.


4. Side Effect를 최소화하는 최적의 패턴 찾기

Side Effect를 최소화하고 코드를 더 간결하게 유지하기 위해서는 다음과 같은 패턴을 고려해 볼 수 있습니다.

1. 이벤트 핸들러 내에서 필요한 로직을 직접 처리하여 불필요한 Side Effect를 피하는 패턴

2. 자식 컴포넌트의 상태 변경 알림을 부모 컴포넌트로부터 받아와서 상태를 업데이트하는 패턴

3. 데이터 플로우를 부모에서 자식으로 유지하여 코드의 가독성과 예측 가능성을 향상하는 패턴


5. Side Effect 관리와 데이터 가져오기

데이터를 가져올 때 useEffect를 사용하여 데이터를 가져오는 경우가 많습니다.

하지만 데이터 가져오기에 대한 구현에는 몇 가지 문제가 있습니다.

예를 들어보자면, 데이터를 가져오기 위해 useEffect를 사용하여 네트워크 요청을 수행할 경우 Race Condition과 같은 문제가 발생할 수 있습니다.

Race Condition이란?

더보기

경쟁 상태(Race Condition)는 둘 이상의 작업이 동시에 진행되거나 서로 경쟁하여 결과를 예측하기 어려운 상황을 말합니다. 

프로그래밍에서는 주로 여러 개의 스레드나 프로세스가 공유 자원에 동시에 접근할 때 발생합니다.

간단한 예시를 통해 설명해 보겠습니다. 

가정해 보세요, 두 명의 친구가 같은 계좌에서 동시에 출금하려고 합니다. 

그러나 출금 액수가 계좌 잔액보다 크면 출금이 불가능합니다.

친구 A: 계좌 잔액 확인
친구 B: 계좌 잔액 확인
친구 A: 출금 시도 (잔액이 충분하므로 출금 성공)
친구 B: 출금 시도 (잔액이 충분해 보였지만, 이미 친구 A가 출금하여 잔액이 부족함)
위의 예시에서 친구 B는 출금 전에 계좌 잔액을 확인했을 때 충분한 금액이 있었지만, 실제 출금 시도 시에는 잔액이 부족하게 됩니다. 

이렇게 서로 동시에 경쟁하여 발생하는 상황을 경쟁 상태(Race Condition)라고 합니다.

프로그래밍에서도 이와 유사한 상황이 발생할 수 있습니다. 

예를 들어, 두 개의 스레드가 동시에 같은 변수를 수정하려고 할 때, 어느 스레드가 먼저 수정되는지에 따라 결과가 달라질 수 있습니다. 

이는 예상하지 못한 동작이 발생할 수 있고, 프로그램의 안정성과 정확성을 저해할 수 있습니다.

따라서 경쟁 상태를 피하기 위해서는 동기화 메커니즘이나 mutual exclusion와 같은 방법을 사용하여 여러 스레드나 프로세스가 동시에 공유 자원에 접근하는 것을 제어해야 합니다.

+ 경쟁 조건을 해결하기 위해 이전에 요청한 데이터를 무시하는 cleanup 함수를 추가해야 합니다.

 


 

결론

React에서 useEffect를 사용할 때에는 Side Effect 관리에 주의해야 합니다. Side Effect를 최소화하고 코드를 더 효율적으로 유지하기 위해 다른 패턴과 최적의 방법을 고려해야 합니다.

데이터를 가져오는 작업에서는 다른 조건들을 고려하여 클린업 함수를 추가하는 것이 좋습니다.

이러한 고려 사항을 통해 React 애플리케이션을 더욱 효율적으로 개발하고 유지할 수 있습니다.

 


End

 

References

728x90