React useEffect, 언제 사용하고 언제 피해야 할까?

ClydeHan·2024년 10월 4일
28

useEffect란?

React useEffect, 리액트 유즈 이펙트

이미지 출처: dhiwise.com

useEffect에 대해서는 지난 포스트에서 이미 자세히 다루었다. 이번 포스트에서는 useEffect를 꼭 사용해야 하는 경우와, 사용하지 말아야 하는 경우에 대해 깊이 있게 살펴볼 예정이다. 우선, 본격적으로 다루기 전에 useEffect의 핵심 기능을 간단히 요약해보겠다.


📌 useEffect의 역할

useEffect는 React에게 컴포넌트가 렌더링된 후 특정 작업을 수행해야 한다는 것을 알려주는 훅이다. React는 전달된 함수를 기억하고(이를 "effect"라고 부른다.) DOM 업데이트가 완료된 후에 이를 호출한다. 이를 통해 데이터 페칭, DOM 조작, 또는 외부 라이브러리와의 연동 같은 명령형 작업을 수행할 수 있다.


📌 컴포넌트 내부에서 호출 가능

useEffect는 컴포넌트 내부에서 호출되기 때문에, 내부에서 바로 count와 같은 상태 변수나 props에 접근할 수 있다. 이를 위해 특별한 API를 사용할 필요 없이, 이미 함수 범위에 있는 상태와 props를 그대로 활용할 수 있다. 이를 통해 React는 JavaScript 클로저를 활용하여 컴포넌트에 필요한 데이터를 쉽게 다룰 수 있다.


📌 모든 렌더링 후에 실행

기본적으로, useEffect는 첫 번째 렌더링 이후와 모든 업데이트 이후에 실행된다. 즉, 컴포넌트의 "마운팅"과 "업데이트"의 관점에서 생각하는 것보다, "렌더링 후"에 이펙트가 발생한다고 보는 것이 더 쉽다. 이는 DOM이 완전히 업데이트된 후에 실행되므로, 최신 상태에 기반한 작업을 수행할 수 있다.

React useEffect 완벽 가이드: 함수형 컴포넌트의 사이드 이펙트 관리법


useEffect를 꼭 사용해야 하는 경우

📌 외부 시스템과의 동기화

컴포넌트가 서버, 브라우저 API, 외부 라이브러리 등과 같은 외부 시스템과 통신하거나 동기화해야 할 때 useEffect가 필요하다. 예를 들어, 서버에서 데이터를 가져오거나, 외부 라이브러리의 위젯을 React 상태와 동기화하는 작업은 useEffect를 통해 처리할 수 있다.

useEffect(() => {
  const fetchData = async () => {
    const response = await fetch('/api/data');
    const data = await response.json();
    setData(data);
  };

  fetchData();
}, []);

위 코드에서 fetchData 함수는 서버로부터 데이터를 가져와 컴포넌트의 상태를 업데이트한다. useEffect를 사용하여 이러한 비동기 작업을 처리하고, 컴포넌트가 마운트될 때 서버와 통신할 수 있도록 한다.


📌 구독, 타이머 설정, 그리고 클린업이 필요한 경우

외부 이벤트 구독(subscription), 타이머 설정, 또는 DOM 요소의 이벤트 리스너와 같은 작업을 수행할 때도 useEffect를 사용해야 한다. 이때 중요한 점은 컴포넌트가 언마운트될 때 이러한 작업을 정리(cleanup)해야 한다는 것이다. 그렇지 않으면 메모리 누수나 의도치 않은 동작이 발생할 수 있다.

// 타이머 설정 예시
useEffect(() => {
  const id = setInterval(() => {
    console.log('타이머 실행');
  }, 1000);

  return () => clearInterval(id); // 컴포넌트 언마운트 시 타이머 해제
}, []);

위 코드에서는 타이머를 설정하고, 컴포넌트가 언마운트될 때 clearInterval을 통해 타이머를 정리한다. 이처럼 클린업이 필요한 경우에는 반드시 useEffect를 사용해야 한다.

DOM 요소에 직접 이벤트 리스너를 추가하는 경우에도 useEffect가 필요하다. 예를 들어, 키보드 입력을 감지하기 위해 keydown 이벤트 리스너를 등록하고자 한다면, 다음과 같이 useEffect에서 리스너를 등록하고 정리할 수 있다.

// 이벤트 리스너 예시
useEffect(() => {
  const handleKeyDown = (event) => {
    if (event.key === 'Escape') {
      console.log('Escape 키가 눌렸습니다.');
    }
  };

  window.addEventListener('keydown', handleKeyDown);

  return () => {
    window.removeEventListener('keydown', handleKeyDown); // 이벤트 리스너 정리
  };
}, []);

위 코드에서 keydown 이벤트 리스너는 컴포넌트가 마운트될 때 등록되고, 언마운트될 때 window.removeEventListener를 통해 정리된다. 이렇게 클린업을 통해 불필요한 이벤트 리스너가 계속 남아 있는 것을 방지할 수 있다.


useEffect를 사용하지 말아야 하는 경우

useEffect는 강력한 도구이지만, 모든 경우에 사용해야 하는 것은 아니다. 불필요한 useEffect 사용은 코드의 복잡도를 높이고 성능을 저하시킬 수 있다. 특히 컴포넌트의 렌더링과 직접적인 관계가 없는 작업에 사용하는 것은 피해야 한다.


📌 렌더링을 위한 데이터 변환

컴포넌트의 렌더링을 위한 데이터 변환에는 useEffect를 사용할 필요가 없다. 예를 들어, 컴포넌트가 받는 데이터를 필터링하거나 가공하여 화면에 표시하고자 할 때, 이 데이터를 상태로 관리하고 useEffect를 통해 업데이트하는 것은 비효율적이다. 이 작업은 렌더링 중에 직접 계산하여 처리하는 것이 더 효율적이다.

// ❌ 피해야 할 코드
useEffect(() => {
  setFilteredList(items.filter(item => item.active));
}, [items]);

// ✅ 올바른 코드: 렌더링 중에 계산
const filteredList = items.filter(item => item.active);

이처럼 단순한 계산이나 데이터 가공은 렌더링 중에 바로 처리하는 것이 성능 면에서나 코드의 가독성 면에서 더 좋다. useEffect를 사용하면 불필요한 상태 업데이트와 렌더링이 발생할 수 있으므로 주의해야 한다.


📌 사용자 이벤트 처리

사용자 이벤트(예: 버튼 클릭, 폼 제출 등)를 처리하기 위해 useEffect를 사용하는 것은 좋지 않다. 이벤트가 발생할 때 어떤 작업을 수행해야 하는지 명확히 알고 있기 때문에, 해당 작업을 이벤트 핸들러 내에서 처리하는 것이 적합하다. useEffect는 컴포넌트가 렌더링될 때 실행되기 때문에 사용자 이벤트와 관련된 로직을 처리하기에는 적절하지 않다.

// ❌ 피해야 할 코드: 이벤트를 이펙트로 처리
useEffect(() => {
  if (formSubmitted) {
    post('/api/register', formData);
  }
}, [formSubmitted, formData]);

// ✅ 올바른 코드: 이벤트 핸들러에서 처리
const handleSubmit = () => {
  post('/api/register', formData);
};

이벤트 핸들러에서 로직을 처리하면 코드의 흐름이 명확해지고, 불필요한 렌더링을 방지할 수 있다. 특히 API 호출이나 상태 업데이트와 같은 작업은 이벤트가 발생한 시점에 바로 처리하는 것이 가장 적절하다.


📌 상태 기반으로 다른 상태 업데이트

컴포넌트의 상태가 변경될 때, 그 상태를 기반으로 다른 상태를 업데이트하는 로직에 useEffect를 사용하면 코드를 복잡하고 비효율적으로 만들 수 있다. 이런 경우 상태 업데이트는 렌더링 중에 직접 처리하는 것이 더 좋다.

// ❌ 피해야 할 코드: 이펙트로 상태 업데이트
useEffect(() => {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

// ✅ 올바른 코드: 렌더링 중에 계산
const fullName = firstName + ' ' + lastName;

이처럼 useEffect를 통해 상태를 업데이트하면 상태 간의 불필요한 종속성이 생길 수 있으며, 이는 버그의 원인이 될 수 있다. 렌더링 중에 계산이 가능하다면 해당 작업은 useEffect가 아닌 렌더링 단계에서 처리하는 것이 더 효율적이다.


📌 상태 초기화 또는 리셋

useEffect를 사용하여 상태를 리셋하거나 초기화하는 것은 비효율적이다. 예를 들어, props가 변경될 때마다 상태를 리셋하려고 할 때 useEffect를 사용하는 대신, 렌더링 중에 조건부로 상태를 재설정하는 것이 더 나은 방법이다.

// ❌ 피해야 할 코드: 이펙트로 상태 리셋
useEffect(() => {
  setSelection(null);
}, [items]);

// ✅ 올바른 코드: 렌더링 중에 상태 초기화
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
  setPrevItems(items);
  setSelection(null);
}

상태를 초기화하거나 리셋할 때 useEffect를 사용하면 불필요한 렌더링이 발생할 수 있다. 이를 피하려면 렌더링 중에 필요한 조건을 검사하고 상태를 업데이트하는 것이 좋다.


결론

useEffect는 컴포넌트가 외부 시스템과 통신하거나 동기화하는 경우에만 사용되어야 한다. 렌더링에 직접적인 영향을 주지 않는 작업이나 이벤트 처리, 간단한 계산에는 useEffect를 사용하지 않는 것이 좋다. 가능한 한 렌더링 중에 상태를 계산하고, 이벤트 핸들러에서 로직을 처리하며, 상태 간의 종속성을 최소화하는 것이 더 효율적이다.


참고문헌

2개의 댓글

comment-user-thumbnail
2024년 10월 4일

좋은 글 감사합니다! useEffect를 통해 불필요한 상태 업데이트를 줄일 수 있다는 점이 인상적이네요. 그런데 상태가 복잡해지거나 여러 상태가 동시에 변할 때, 이런 계산을 렌더링 중에 직접 처리하게 되면 성능상 이슈가 발생할 수 있지 않을까요? 이런 경우에는 useMemo나 useCallback 같은 훅을 사용하는 게 더 적합할지, 혹은 어떤 전략을 쓰는 게 좋을지 궁금합니다!

1개의 답글