React Hook

May 01, 2021

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

관련 출처


© 2023, Customized by Joon