ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Hooks (useState, useEffect)
    리액트 2021. 8. 23. 17:37

    공부하기 위해 리액트 공식문서와 블로그를 정리하여 작성한 글입니다. Class 컴포넌트의 생명주기 메서드들과 비교를 하기에 이에 대하여 먼저 알고 공부하시면 좋습니다. 여기에 사용한 코드들은 전부 리액트 공식문서에서 갖고 왔습니다 ㅎ. 

     

    Hook이란 함수 컴포넌트에 state와 생명주기 등의 리액트 특성들에 접근할 수 있도록 도와주는 특별한 함수입니다!

     

    리액트 훅스는 기존 컴포넌트 생명주기에 기반하여 작성하는 클래스 문법에서 발생하는 여러 불편한 점들로부터 벗어나기 위해 고안한 것입니다. 예를 들어, componentDidMount()와 componentDidUpdate() 내에 중복되는 코드를 작성해야했거나, componentWillUnmount() 내에서 서로 다른 기능을 갖는 함수들을 공존하게 해야했던 문제점들이 있었습니다. 또한, Component가 복잡해지면서 Component 생명주기에 따라 작성하는 방법이 코드를 이해하는데 가독성을 해치는 경우도 있었습니다. 이를 좀 더 간편하게 만든 것이 훅스입니다. 이번 글을 통해 Class 컴포넌트와 Hooks를 사용한 컴포넌트를 비교하여 왜 리액트는 훅스 사용을 장려하는지 알아보도록 하겠습니다.

     

    State Hook

    Class와 Hook 예시

    Hooks를 사용한 방법과 Class를 사용한 방법을 서로 비교해보며 State Hook을 알아가보도록 하겠습니다.

     

    State Hook을 사용한 예시입니다.

    import React, { useState } from 'react';
    
    function Example() {
      // Declare a new state variable, which we'll call "count"
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }

     

    Class를 사용한 예시입니다.

    class Example extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        };
      }
    
      render() {
        return (
          <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={() => this.setState({ count: this.state.count + 1 })}>
              Click me
            </button>
          </div>
        );
      }
    }

    State 변수 선언

    두 코드를 비교하며 가장 눈에 띄는 것은 어떻게 state 변수를 선언하는가입니다. 

     

    Class에서는 Contructor 내에서 this.state를 {count: 0}으로 설정하여 초기화했습니다.

    Hooks에서는 useState()를 호출하여 state 변수를 선언할 수 있습니다.

     

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

    useState() 인자에 넘겨주는 값은 state의 초기값입니다. count : 0과 마찬가지입니다.  위의 코드는 배열 구조분해 문법을 사용한 것이며 이것을 풀어쓰면 아래코드와 같습니다. 

     var countStateVariable = useState(0); // Returns a pair
     var count = countStateVariable[0]; // First item in a pair
     var setCount = countStateVariable[1]; // Second item in a pair

    useState로 변수 초기값을 사용하면 두개의 아이템을 포함하는 배열을 리턴합니다. 배열의 첫번째 아이템은 현재 변수이며, 두번째 아이템은 해당 변수를 업데이트하는 함수입니다.

    여러개의 State 변수 사용하기

    function ExampleWithManyStates() {
      // Declare multiple state variables!
      const [age, setAge] = useState(42);
      const [fruit, setFruit] = useState('banana');
      const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

    여러개의 변수를 선언하여 사용할 수 있습니다!

    Effect Hook

    Effect : Clean-up을 이용하지 않은 경우

    우선 클래스 예시부터 보겠습니다.

    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>
        );
      }
    }

    보시다시피 class 컴포넌트는 생명주기에 따라 작성해야하기에 서로 다른 생명주기 메서드에 같은 코드가 중복이 생길 수 있습니다. 위의 같은 경우 렌더링할 때마다 즉, 최초 마운트 할 경우와 업데이트 할 경우 같은 코드를 수행해야하지만, 안타깝게도 리액트는 렌더링 할 때마다 수행할 수 있도록 도와주는 메서드가 없기에 이렇게 따로 작성해야합니다.

     

    Hook 예시

    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>
      );
    }

    useEffect내에 작성하는 함수를 effect라고 부릅니다. 쉽게 설명하자면 매번 렌더링할 때마다 state 변수에 아무런 제약없이 접근하여 effect를 수행을 합니다!

     

    하나의 컴포넌트에 useEffect()를 한번 이상 사용할 수 있습니다.

    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);
        };
      });
      // ...
    }

    위 코드의 포인트는 하나의 컴포넌트에 useEffect()를 두 번 이상 사용했다는 점입니다. 클래스 컴포넌트는 생명주기 별로 작성해야하기에 하나의 생명주기 메서드 내에 서로 다른 기능의 함수를 작성해야했지만, Hook의 useEffect()를 사용한다면 기능별로 나누어 작성하실 수 있습니다.

    Effect : Clean-up을 이용한 경우

    Class 예시

    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
        );
      }
    
      componentDidUpdate(prevProps) {
        // 이전 friend.id에서 구독을 해지합니다.
        ChatAPI.unsubscribeFromFriendStatus(
          prevProps.friend.id,
          this.handleStatusChange
        );
        // 다음 friend.id를 구독합니다.
        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';
      }
    }

    Class 컴포넌트에서 componenetDidMount() 메서드 내에서 외부 데이터를 fetch를 통해 불러오거나 네트워크 연결 등을 해야 할 경우, componentWillUnmount() 메서드에서 이를 해제해야 메모리 누수가 안 일어났습니다. 또한 업데이트 할 경우, componentDidUpdate() 메서드에서 이전 api를 해체하고, 새로운 api를 호출해야하는 경우도 생길 수 있습니다. 이럴 경우 class 컴포넌트는 복잡하지만, Hook을 사용하면 간단하게 작성할 수 있습니다.

     

    Hook 예시

    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);
        // effect 이후에 어떻게 정리(clean-up)할 것인지 표시합니다.
        return function cleanup() {
          ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
      });
    
      if (isOnline === null) {
        return 'Loading...';
      }
      return isOnline ? 'Online' : 'Offline';
    }

    clean-up을 실행하기 위해서는 effect가 함수를 리턴하면 됩니다. effect가 함수를 반환하면 리액트는 해당 함수를 정리할 때 활용합니다. 또한 렌더링할 때마다 실행되기에 업데이트할 때, 최초 마운트일 때를 생각하며 작성할 필요가 없습니다.

    useEffect 최적화 방법

    매번 렌더링할 때마다 useEffect와 clean-up을 실행하게 되면 비효율적이기에 필요시 실행할 수 있도록 제어할 수 있습니다.

     

    useEffect(() => {
      document.title = `You clicked ${count} times`;
    }, [count]); // Only re-run the effect if count changes

    위와 같이 두번째 인수에 배열로 원하는 props, state 혹은 이에 파생된 값을 작성하면, 해당 값이 업데이트 될때마다 useEffect를 실행 후 렌더링을 합니다. 

    useEffect(() => {
      document.title = `You clicked ${count} times`;
    }, []); // Only re-run the effect if count changes

    빈 배열을 작성 시 매 렌더링마다 useEffect는 실행하지 않고 마운트랑 언마운트 때 실행하게 됩니다.

    '리액트' 카테고리의 다른 글

    Portals  (0) 2021.09.08
    Context  (0) 2021.09.05
    Lists & Keys & Reconciliation  (0) 2021.08.29
    JSX & createElement() & reactDOM.render()  (0) 2021.08.24
    컴포넌트 생명주기  (0) 2021.08.22
Designed by Tistory.