본문 바로가기

Web Development/Next.js

React에서 useEffect를 꼭 써야 할까? — 오히려 안 써도 되는 경우가 더 많다

React에서 useEffect를 꼭 써야 할까? — 오히려 안 써도 되는 경우가 더 많다

React를 쓰다 보면 useEffect를 거의 습관처럼 쓰게 되는 경우가 많다.
상태를 바꾸거나, 데이터를 갱신하거나, 뭔가 “변화”가 있으면 useEffect부터 떠오른다.

하지만 React 공식 문서에서는 이렇게 말한다:

"You might not need an effect."
(공식 문서: https://react.dev/learn/you-might-not-need-an-effect)

📌 useEffect는 React의 '비상구'다

React는 기본적으로 선언형 프로그래밍 철학을 따르고 있다.
하지만 세상에는 선언형으로 다 표현할 수 없는 일들이 있다.

  • 브라우저 타이틀 변경
  • 외부 API 호출
  • 이벤트 리스너 등록
  • 애니메이션 트리거
  • 타이머 설정 등

이런 것들을 처리하기 위해 등장한 게 바로 useEffect다.
그래서 React 팀은 "useEffect는 React의 비상구(Escape Hatch)"라고 표현한다.


🚨 하지만 많은 useEffect는 '불필요'하다

React 공식 문서에서는 다음과 같은 상황에서 굳이 useEffect를 쓸 필요가 없다고 말한다.

1. 상태를 기반으로 어떤 값을 계산할 때


const [items, setItems] = useState([]);
const [visibleItems, setVisibleItems] = useState([]);

useEffect(() => {
  setVisibleItems(items.filter(item => item.visible));
}, [items]);

이건 useEffect를 쓸 필요가 없다.


const visibleItems = items.filter(item => item.visible);

→ 이렇게 렌더링 중 계산하면 더 선언적이고 예측 가능하다.

2. 입력값을 상위 상태로 전달(lifting state)할 때

가끔 이런 코드를 볼 수 있다:


function ChildInput({ onValueChange }) {
  const [localValue, setLocalValue] = useState('');

  useEffect(() => {
    onValueChange(localValue);
  }, [localValue]);

  return <input value={localValue} onChange={e => setLocalValue(e.target.value)} />;
}

위 코드는 중복된 상태(localValue)를 만들고, Effect로 부모에게 값을 전달한다.
하지만 이건 불필요하게 복잡하고 절차적이다.

더 나은 방식은 onChange 이벤트를 직접 부모에 전달하는 것이다:


function ChildInput({ value, onValueChange }) {
  return <input value={value} onChange={e => onValueChange(e.target.value)} />;
}

→ 이렇게 하면 useEffect 없이도 깔끔하게 상태를 lifting 할 수 있다.
불필요한 useEffect는 선언형 구조를 흐트러뜨릴 수 있다.


✅ 언제 useEffect를 정말로 써야 할까?

공식 문서에서는 다음과 같은 상황에서만 useEffect 사용이 적절하다고 본다:

📡 서버로부터 데이터를 가져올 때


useEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(setData);
}, []);

🧭 브라우저 API와 동기화할 때 (scroll, resize 등)


useEffect(() => {
  const handleResize = () => console.log(window.innerWidth);
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

⏱️ 타이머, setInterval 등 사용할 때


useEffect(() => {
  const id = setInterval(() => setCount(c => c + 1), 1000);
  return () => clearInterval(id);
}, []);

📦 외부 라이브러리와 상호작용할 때 (예: chart.js)


useEffect(() => {
  const chart = new Chart(ctx, { type: 'bar', data });
  return () => chart.destroy();
}, [data]);

💡 useEffect가 많아질수록 코드가 절차형으로 변한다

useEffect는 선언형 UI 흐름을 깨고, "이 시점에 이것을 수행하라"는 명령형 로직을 도입한다.
그래서 남용하게 되면, 다음과 같은 문제가 생긴다:

  • 의존성 배열 관리가 복잡해짐
  • 버그 추적이 어려워짐
  • 불필요한 re-render나 무한 루프 발생

React 팀은 이런 문제를 줄이기 위해 useEffect를 최소화하고, 선언형 상태 관리 방식을 더 권장하고 있다.


🧭 마무리: "useEffect는 꼭 필요한 곳에만"

React는 선언형.
useEffect는 절차형.
그래서 필요할 때만 절제해서 써야 한다.

앞으로 코드를 짤 때 useEffect부터 생각하지 말고, “정말 이게 side effect인가?”를 먼저 자문해보자.
대부분의 경우, 렌더링 중 계산이나 상태 흐름으로 충분히 처리할 수 있다.

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