React - useEffect Hook

이소라·2022년 9월 6일
0

React

목록 보기
13/23

useEffect Hook

  • useEffect Hook을 사용하여 함수 컴포넌트에서 side effect를 수행할 수 있음
    • side effect
      • 예) 데이터 가저오기, 구독(subscription) 설정하기, 수동으로 React 컴포넌트의 DOM 수정하기 등
      • side effect는 clean-up이 필요한 side effect와 필요하지 않은 side effect로 나뉨



Clean-up을 이용하지 않는 Effects

  • React가 DOM을 업데이트한 뒤 추가로 코드를 실행해야 하는 경우, Clean-up이 필요 없음
    • 예) 네트워크 요청, DOM 수동 조작, 로깅 등
    • 이러한 경우 코드 실행 이후 신경 쓸 것이 없으므로 Clean-up을 사용하지 않음

Class 컴포넌트에서의 Clean-up을 이용하지 않는 Effects 사용 예시

  • side effect는 React가 DOM을 업데이트하고 난 이후에 수행되므로, render 메서드가 아니라 componentDidMountcomponentDidUpdate 메서드에 side effect를 둠
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}
  • 컴포넌트가 막 마운드된 단계인지 업데이트되는 단계인지와 상관 없이 같은 side effect를 수행해야 하므로, Class 컴포넌트 안의 compoentDidMount와 componentDidUpdate 메서드에 side effect에 대한 같은 코드가 들어감

함수 컴포넌트에서의 Clean-up을 이용하지 않는 Effects 사용 예시

  • 함수 컴포넌트에서는 useEffect Hook을 사용하여 side effect를 수행함
    • React가 useEffect의 인수로 받은 함수 effect를 기억했다가 DOM 업데이트를 수행한 이후에 불러냄
      • React는 effect가 수행되는 시점에 이미 DOM이 업데이트되었음을 보장함
    • useEffect를 컴포넌트 내부에 둠으로써 effect를 통해서 state 변수와 props에 접근할 수 있음 (같은 함수 스코프 내에 있기 때문에 접근 가능함)
    • useEffect는 기본적으로 렌더링 이후(첫 렌더링과 모든 업데이트 이후)에 수행됨
    • useEffect에 전달되는 함수 effect는 모든 렌더링에서 다름
      • 리렌더링할 때마다 이전과 다른 effect로 교체하여 전달함
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
  • 위 코드에 대한 설명
    1. useState를 사용하여 'count' state 변수를 선언함
    2. useEffect를 사용하여 React가 전달 받은 effect를 기억하게 함
    3. effect 함수 내부에서 'document.title'이라는 브라우저 API를 사용하여 문서 타이틀를 설정함
      • 같은 함수 내부에 있기 때문에 'count' state 변수의 최신값을 얻을 수 있음
    4. React는 컴포넌트가 렌더링할 때 effect를 기억했다가 DOM을 업데이트한 이후에 실행함
      • 맨 첫 번째 렌더링과 그 이후 렌더링 모두에 똑같이 적용됨



Clean-up을 이용하는 Effects

  • 외부 데이터에 구독(subscription)을 설정해야 하는 경우, 메모리 누수 방지를 위해 Clean-up해야 함

Class 컴포넌트에서의 Clean-up을 이용한 Effects 사용 예시

  • Class 컴포넌트에서는 흔히 componentDidMount에서 구독(subscription)을 설정하고, componentWillUnmount에서 이를 clean-up함
class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}
  • componentDidMountcomponentWillUnmount 메서드 모두에서 개념상 똑같은 effect에 대한 코드가 들어감

함수 컴포넌트에서의 Clean-up을 이용한 Effects 사용 예시

  • 함수 컴포넌트에서 useEffect Hook을 사용하여 Clean-up을 이용한 side effect를 수행함
    • useEffect의 모든 effect에서 clean-up을 위한 함수를 반환할 수 있음
      • 구독(subscription)의 추가와 제거를 위한 로직을 하나의 effect에서 묶어서 사용할 수 있음
    • effect를 clean-up하는 시점 : 컴포넌트가 마운트 해제될 때, 리렌더링할 때
    • clean-up 함수는 익명 함수, 화살표 함수, 다른 이름의 함수를 사용해도 상관 없음
import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // clean-up 함수
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}



effect의 실행 시점

  • componentDidMountcomponentDidUpdate와 달리, useEffect에 전달된 effect는 지연된 이벤트 동안, layout과 paint 이후에 실행됨
    • 이는 대부분의 side effects (subscriptions, event handlers 등)에 잘 맞음
      • 왜냐하면 대부분의 side effect는 브라우저가 스크린을 업데이트하는 것을 막지 않기 때문임
    • 하지만, DOM 조작은 다음 paint 이전에 사용자에게 보이도록 동기적으로 실행되어야함
      • 이렇게 해야 사용자가 시각적인 불일치를 인지하지 못함
      • 이러한 유형의 effect의 경우, useLayoutEffect Hook을 사용해야함
      • useLayoutEffectuseEffect와 동일한 signiture를 가지고 있으나 effect가 DOM 조작 후 동기적으로 실행됨
    • React 18부터 click과 같은 구별된 사용자 입력의 결과나 flushSync로 감싸진 업데이트의 결과는 layout과 paint 이전에 동기적으로 실행됨
      • 이러한 effect의 결과는 event system이나 flushSync의 호출자에 의해 관찰될 수 있음

Note

  • 위 경우 useEffect에 전달된 effect가 실행되는 시점에 영향을 주지만, 이러한 effect들 안에서 예정된 업데이트들은 여전히 지연됨
  • 반면에 useLayoutEffect에 전달된 effect는 실행되고 즉시 effect 안의 업데이트들이 처리됨
  • useEffect가 브라우저가 paint될 때까지 지연되더라도, 새 렌더링 전에 실행됨
    • React는 항상 새 업데이트를 하기 전에 이전 render의 effect를 몰아냄



effect를 이용할 때 Tip

Tip 1 : 관심사를 분리하기 위해 여러 Effect를 사용하기

  • 한 컴포넌트 내에서 effect도 여러 번 사용 가능하므로, 여러 effect를 사용하여 코드가 무엇을 하느냐에 따라 나눌 수 있음
  • React는 컴포넌트에서 사용된 모든 effect를 지정한 순서에 맞춰서 적용함
function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

Tip2 : useEffect의 두 번째 인수 dependencies를 사용하여 성능 최적화하기

  • useEffect의 두 번째 인수 dependencies
    • useEffect는 두 번째 인수로 선택적으로 배열을 받을 수 있음
    • 이 배열은 컴포넌트 범위 내에서 바뀌는 값들과 effect에 의해 사용되는 값들을 모두 포함해야함
      • 그렇지 않다면, 현재 값이 아니라 이전 렌더링때의 값을 참조함
    • 이 배열에 들어가는 값들이 리렌더링시 변경되지 않는다면, React가 effect를 건너뛰도록 할 수 있음
    • 두 번째 인수로 빈 배열([])을 전달할 경우, effect를 실행하고 clean-up하는 과정을 한 번만 실행함
      • 이 경우, effect가 props나 state의 어떤 값에도 의존하지 않으므로 재실행되지 않음
      • effect 안의 prop와 state가 초기값으로 유지됨
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // count가 바뀔 때만 effect를 재실행함

React 18의 Strict Mode에서 useEffect가 2번 실행되는 문제

  • React18은 페이지 이동 후 다시 돌아왔을 때 애플리케이션이 망가지는 부분이 없는지 확인하기 위해서, 개발 모드에서 한 컴포넌트를 두 번 렌더링함
    • Strict Mode를 없앨 경우, React가 production 모드에서 일어날 수 있는 오류를 잡아주지 못하므로 Stict Mode를 항상 사용하는 것이 좋음
    • 따라서 useEffect가 두 번 호출되어도 사용자가 리렌더링을 느끼지 못하게 하기 위해서, useEffct의 effect 함수에서 clean-up 함수를 반환해야 함
      • 개발 모드에서 effect -> clean-up -> effect 순서로 실행되므로 하나의 effect만 실행된 것처럼 느껴짐
    • useEffect 내부에서 setState 함수를 호출하면 clean-up 함수 작성과 무관하게 setState를 두 번 호출하게 되므로, useEffect 내부에서 상태 업데이트를 피할 것

props, state 변경에 따라 다른 state를 업데이트해야 할 때 useEffect 사용하지 않기

  • state가 변경되면, React는 DOM에 변경된 state를 적용하여 UI에 업데이트함
    • UI를 한 번 변경하는데 state 변경이 n번 일어난다면, 불필요한 리렌더링이 최대 n - 1번 발생함
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  // ...
}
  • useEffect의 effect 내부에서 setVisibleTodos를 호출하는 것은 불필요한 리렌더링을 발생시킴
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  
  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}
  • 컴포넌트가 리렌더링될 때마다 visibleTodos가 업데이트되므로 state의 최신값이 반영됨
import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
  // ...
}
  • getFilteredTodos가 비싼 연산인 경우 useMemo를 사용함

참고

0개의 댓글