Skip to content

React useEffect 완전 정복 - 동기화 관점으로 레벨업하기

Published: at 오전 09:48

우리는 React의 내부 메커니즘, 모범 사례, 디자인 패턴, 그리고 고급 개념들을 탐구합니다.

이 글들은 기본을 넘어 React가 내부적으로 어떻게 작동하는지 진정으로 이해하고자 하는 React 개발자들을 위해 작성되었습니다.

서론: useEffect의 본질을 꿰뚫기

useEffect 훅은 현대 React의 근본적인 기둥입니다.

단순한 함수 그 이상으로, 컴포넌트가 변화에 반응하고 비동기 작업을 수행하면서도 UI의 일관성을 유지할 수 있게 해줍니다.

하지만 그 명백한 단순함 뒤에는 미묘한 에러를 유발하고 애플리케이션의 성능과 유지보수성에 영향을 미칠 수 있는 복잡성이 숨어 있습니다.

이번 ‘Level Up React’ 시리즈 글에서는 useEffect의 내부 메커니즘, 종종 오해받는 미묘함, 그리고 숙련된 개발자조차도 기다리는 흔한 함정들을 깊이 있게 탐구해 보겠습니다.

올바른 사용법이 어떻게 여러분의 코드를 더 예측 가능하고 성능 좋게 변화시킬 수 있는지 확인해 보겠습니다.

useEffect의 기본

useEffect는 왜 존재할까?

React는 함수형 컴포넌트에서 ‘부수 효과(side effects)‘를 효율적으로 관리하기 위해 useEffect를 만들었습니다.

이것이 도입되기 전에는, 이러한 작업들은 componentDidMount, componentDidUpdate, componentWillUnmount와 같은 클래스 컴포넌트의 생명주기 메서드에 예약되어 있었습니다.

‘부수 효과’란 React 컴포넌트 외부의 무언가를 수정하는 모든 작업을 의미합니다.

예를 들면 다음과 같습니다.

가장 중요한 개념은 useEffect를 ‘무언가를 하기 위한’ 도구가 아니라, ‘React의 상태를 외부 시스템과 동기화(synchronize)하기 위한’ 도구로 바라보는 것입니다.

// 부수 효과의 예: 페이지 제목 수정하기
useEffect(() => {
  // 'username'이라는 React 상태를 브라우저의 'document.title'이라는 외부 시스템과 동기화합니다.
  document.title = `Profile of ${username}`;
}, [username]);



이 예제에서 문서 제목을 수정하는 것은 React 컴포넌트 외부의 환경에 영향을 미치기 때문에 부수 효과입니다.

useEffect 훅은 우리가 이 작업을 언제 발생해야 하는지 명시적으로 선언할 수 있게 해줍니다.

useEffect의 해부학

useEffect 훅은 두 개의 인자를 받습니다.

useEffect(
  () => {
    // 이펙트 본문 (실행할 코드)
    return () => {
      // 클린업(정리) 함수 (선택 사항)
    };
  },
  [
    /* 의존성 배열 */
  ]
);



useEffect의 실행 주기

useEffect는 언제 실행될까요?

일반적인 믿음과는 달리, useEffect는 컴포넌트의 렌더링 도중에 실행되지 않고, React가 DOM을 업데이트한 후에 실행됩니다.

정확한 작업 순서는 다음과 같습니다.

  1. React가 컴포넌트 본문을 실행하고 표시할 JSX를 계산합니다.

  2. React가 이 JSX를 반영하도록 DOM을 업데이트합니다.

  3. React가 useEffect로 정의된 이펙트들을 실행합니다.

이 순서는 이펙트 동작을 이해하는 데 매우 중요합니다.

의존성 배열로 실행 제어하기

의존성 배열은 이펙트가 언제 실행되어야 하는지를 제어하는 핵심 메커니즘입니다.

React는 Object.is() 비교 알고리즘을 사용하여 의존성이 변경되었는지 판단합니다.

클린업 메커니즘

클린업 함수는 useEffect의 종종 간과되는 중요한 측면입니다.

이펙트가 다시 실행되거나 컴포넌트가 언마운트되기 전에 리소스를 정리하거나 구독을 취소할 수 있게 해줍니다.

useEffect(() => {
  // 인터벌 생성
  const intervalId = setInterval(() => {
    console.log("Tick");
  }, 1000);

  // 클린업 함수
  return () => {
    console.log("인터벌 정리 중");
    clearInterval(intervalId);
  };
}, []);



이 예제에서 클린업 함수는 컴포넌트가 언마운트될 때 인터벌이 제대로 제거되도록 보장하여, 메모리 누수를 방지합니다.

흔한 함정과 피하는 방법

무한 루프

가장 빈번한 문제 중 하나는 의도치 않은 무한 루프 생성입니다.

이 문제는 이펙트의 의존성인 상태를 업데이트하는 데이터 페칭 시나리오에서 자주 발생합니다.

// ❌ 실제 사례에서 무한 루프 생성
function NotificationCenter() {
  const [notifications, setNotifications] = useState([]);

  useEffect(() => {
    fetchNotifications().then(newNotifications => {
      // 이 업데이트는 새로운 렌더링을 유발합니다.
      setNotifications([...notifications, ...newNotifications]);
    });
  }, [notifications]); // notifications가 의존성입니다.
}



이 예제에서는 fetchNotifications()가 데이터를 반환할 때마다 notifications 상태를 업데이트합니다.

notifications가 우리 이펙트의 의존성이므로, 이는 이펙트의 새로운 실행을 유발하여 API 요청의 무한 루프를 만듭니다.

// ✅ 해결책: 함수형 업데이터 사용
function NotificationCenter() {
  const [notifications, setNotifications] = useState([]);

  useEffect(() => {
    fetchNotifications().then(newNotifications => {
      // 이 형태의 setState는 현재 상태에 의존할 필요가 없습니다.
      setNotifications(prevNotifications => [
        ...prevNotifications,
        ...newNotifications,
      ]);
    });
  }, []); // 마운트 시 한 번만 실행
}



누락되거나 불필요한 의존성

또 다른 흔한 함정은 필요한 의존성을 생략하거나 불필요한 의존성을 포함하는 것입니다.

// ❌ 누락된 의존성
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(data => setUser(data));
  }, []); // userId가 의존성에서 빠져 있습니다.
}



이 예제에서, userId가 변경되더라도 이펙트는 다시 실행되지 않습니다.

eslint-plugin-react-hooks 플러그인이 포함된 ESLint 도구는 이러한 문제를 자동으로 감지하는 데 매우 유용합니다.

객체와 함수를 의존성으로 사용하기

렌더링 중에 생성된 객체와 함수는 각 렌더링마다 재생성되므로 새로운 값으로 간주되어 특별한 문제를 야기합니다.

// ❌ 매 렌더링마다 재생성되는 객체
function SearchComponent({ term }) {
  // 이 객체는 매 렌더링마다 재생성됩니다.
  const options = { caseSensitive: false };

  useEffect(() => {
    performSearch(term, options);
  }, [term, options]); // options는 매 렌더링마다 변경됩니다.
}



term이 변경되지 않았더라도 options가 재생성되어 이펙트가 매번 실행됩니다.

해결책은 useMemouseCallback을 사용하거나, 객체나 함수를 이펙트 내부로 옮기는 것입니다.

useEffect를 사용하지 말아야 할 때

React 공식 문서는 ‘아마 이펙트가 필요 없을지도 모릅니다(You Might Not Need an Effect)‘라는 매우 유용한 가이드를 제공합니다.

useEffect의 올바른 사용 사례

useEffect는 컴포넌트를 외부 시스템과 ‘동기화’할 때 이상적입니다.

결론

useEffect 훅은 올바르게 사용하기 위해 깊은 이해가 필요한 강력하지만 미묘한 도구입니다.

우리가 보았듯이, 이 훅은 React 컴포넌트를 외부 시스템과 동기화할 수 있게 해주지만, 과도하거나 잘못된 사용은 성능 및 유지보수 문제를 유발할 수 있습니다.

기억해야 할 핵심 사항은 다음과 같습니다.

useEffect를 사용하는 데 있어 사려 깊은 접근 방식을 채택하고 그 대안을 앎으로써, 여러분은 더 예측 가능하고, 성능이 좋으며, 유지보수하기 쉬운 React 코드를 작성할 수 있습니다.