Jun 개발노트

React Hook

2021-05-01

0. 작성하는 이유?

  • 공식문서 요약을 빌미로 Hook에 대해서 더 알고 싶어서

1. Hook의 등장하게 된 이유

  1. 컴포넌트 사이에서 상태 로직을 재사용하기 어려움 → Custom Hook 등장배경(1)
    • Render Props와 HOC 같은 패턴은 코드를 파악하기 어려움(→ 유지보수하기 어렵다)
    • 상태 관련 로직을 추상화하기 어려움
  2. 복잡한 컴포넌트들은 이해하기 어렵다 → Custom Hook 등장배경(2)
    • lifeCycle Method에는 관련없는 로직이 포함되어 컴포넌트를 재사용하기 어려움

      componentDidUpdate(){
      	// 데이터 요청
      	// 이벤트 리스너 등록(해당 컴포넌트에서만 사용되기 떄문에 재사용되기 어렵다)
      }
      
      componentWillUnmount() {
      	// 이벤트 리스터 해제(해당 컴포넌트에서만 사용되기 떄문에 재사용되기 어렵다)
      }
      
    • 이러한 문제를 해결하기 위해 redux, mobx를 통해 전역에서 상태관리를 진행

  3. Class는 사람과 기계를 혼동시킨다. → hook api 이용
    • 다른언어와는 다르게 작동하는 this(Dynamic Scope)

2. State Hook

  1. Class Component의 this.setState와 동일한 기능을 한다.
  2. 왜 state Hook인가요?
    • React 16.8전까지도 함수형 컴포넌트가 있었지만, state를 사용할 수 없었고, 단순히 props를 받아서 컴포넌트를 보여주는 역할만 하였다. (state가 필요하면, class 컴포넌트로 작성)
    • 함수안에서 state를 사용할 수 있게 해준다
  3. 사용법
    • 렌더링 할때, 오직 한번만 생성된다(re-redner하기전까지 유지)

    • useState를 Destructuring 문법을 통해 컴포넌트에서 사용할 state명, update함수명으로 선언

    • React는 hook의 순서를 가지고 이전 state를 알 수 있다(hook의 규칙 참고)

      import React, { useState } from 'react';
      
      function Example() {
        const [count, setCount] = useState(0);
      
      	return (
      		<div>
      			<p> {count}</p>
      			<button onClick={() => setCount(count + 1)}>+버튼</button>
      		</div>
      	)
      }
      

3. Effect Hook

  1. componentDidMount + componentDidUpdate + componentWillUnmount와 동일한 기능을 한다.

  2. 왜 Effect Hook인가요?

    • 네트워크 통신 및 Component Dom조작은 side Effects라고 칭한다
    • sideEffect는 다른 컴포넌트에 영향을 줄 수도 있고, 렌더링 과정에서 구현 할 수 없다. (렌더링 이후)
    • 이러한 side effect 작업을 할 수 있는 곳이기 때문에, effect hook이라고 한다.
  3. 사용법

    • 렌더링 이후! useEffect안에 있는 콜백이 실행(Dom이 업데이트되었다고 보장)

    • 렌더링 이후이기때문에, 컴포넌트 돔에 접근하거나, State에 접근 가능

    • 컴포넌트가 마운트 해제되었거나,reRender시 clean-up을 실행한다

      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';
      }
      
  4. Effect Hook의 장점

    • 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);
          };
        });
      }
      
    • 같은 역할이지만, Class 컴포넌트보다 가독성이 높다.

      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
          );
        }
      
      useEffect(() => {
        function handleStatusChange(status) {
          setIsOnline(status.isOnline);
        }
      
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        return () => {
          ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
      }, [props.friend.id]); // props.friend.id가 바뀔 때만 재구독합니다.
      

4. Hook 규칙

  • 오직 React 함수 내에서만 Hook을 호출해야한다 (Custom Hook 포함)

  • Component의 Hook은 반복이나, 조건문에 따라 순서가 변경되면 안된다(호출 순서로 state 접근)

    function Form() {
      const [name, setName] = useState('Mary');
      useEffect(function persistForm() {
        localStorage.setItem('formData', name);
      });
      const [surname, setSurname] = useState('Poppins');
      useEffect(function updateTitle() {
        document.title = name + ' ' + surname;
      });
    }
    
    // ------------
    // 첫 번째 렌더링
    // ------------
    useState('Mary')           // 1. 'Mary'라는 name state 변수를 선언합니다.
    useEffect(persistForm)     // 2. 폼 데이터를 저장하기 위한 effect를 추가합니다.
    useState('Poppins')        // 3. 'Poppins'라는 surname state 변수를 선언합니다.
    useEffect(updateTitle)     // 4. 제목을 업데이트하기 위한 effect를 추가합니다.
    
    // -------------
    // 두 번째 렌더링
    // -------------
    useState('Mary')           // 1. name state 변수를 읽습니다.(인자는 무시됩니다)
    useEffect(persistForm)     // 2. 폼 데이터를 저장하기 위한 effect가 대체됩니다.
    useState('Poppins')        // 3. surname state 변수를 읽습니다.(인자는 무시됩니다)
    useEffect(updateTitle)     // 4. 제목을 업데이트하기 위한 effect가 대체됩니다.
    
    // ...
    
  • 만약 호출 순서가 변경된다면?

    // 🔴 조건문에 Hook을 사용함으로써 첫 번째 규칙을 깼습니다
      if (name !== '') {
        useEffect(function persistForm() {
          localStorage.setItem('formData', name);
        });
      }
    
    useState('Mary')           // 1. name state 변수를 읽습니다. (인자는 무시됩니다)
    // useEffect(persistForm)  // 🔴 Hook을 건너뛰었습니다!
    useState('Poppins')        // 🔴 2 (3이었던). surname state 변수를 읽는 데 실패했습니다.
    useEffect(updateTitle)     // 🔴 3 (4였던). 제목을 업데이트하기 위한 effect가 대체되는 데 실패했습니다.
    
  • 조건에 따라 Hook을 사용하고 싶다면! 함수바디에서 조건을 추가시킨다.

    useEffect(function persistForm() {
        // 👍 더 이상 첫 번째 규칙을 어기지 않습니다
        if (name !== '') {
          localStorage.setItem('formData', name);
        }
      });
    

5. useCallback

  • 메모리제이션된 콜백을 반환한다!

  • 불필요한 렌더링을 방지하기 위해 사용된다(shouldComponentUpdate 역할과 동일)

  • 자식 컴포넌트에 콜백을 전달할 때 유용

  • 함수가 실행될 때마다 내부에 선언되어 있던 표현식(변수, 또다른 함수 등)도 매번 다시 선언되어 사용된다.

  • useCallback(fn, deps)은 useMemo(() => fn, deps)와 같습니다.

    const memoizedCallback = useCallback(
      () => {
        doSomething(a, b);
      },
      [a, b],
    );
    

6. useMemo

  • 메모리제이션된 을 반환한다!

  • useMemo로 전달된 함수는 렌더링 중에 실행된다

  • 렌더링 중에는 하지 않는 것을 useMemo 함수 내에서 구현하면 안되며, useEffect에서 구현해야 한다.

  • useMemo는 성능최적화를 위해 사용될 수 있지만, 보장할 수 없다.

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
    

7. useRef

  • useRef는 current 프로퍼티에 ref가 걸려진 Dom node의 속성을 설정한다.

  • userRef는 변경을 캐치할 수 없다. 즉 current가 변경한다고 렌더링 되지 않는다.

    function TextInputWithFocusButton() {
      const inputEl = useRef(null);
      const onButtonClick = () => {
        // `current` points to the mounted text input element
        inputEl.current.focus();
      };
      return (
        <>
          <input ref={inputEl} type="text" />
          <button onClick={onButtonClick}>Focus the input</button>
        </>
      );
    }
    

관련 출처