Introducing React Hooks

2019년 2월 6일에 react 16.8이 release되면서 드디어 Hooks가 stable release되었습니다. React Hooks는 alpha 버전이 release된 후부터 커뮤니티에서 핫한 주제였는데 이번에 정식 release되었으니 같이 한번 알아보도록 하겠습니다.

React Hooks란?

Hooks는 class를 작성하지 않고도 state나 다른 React의 기능들을 함수형 컴포넌트에서 사용할 수 있게 해주는 새로운 기능입니다

Hooks를 사용하기 위해선 react 16.8버전 이상이어야 합니다

16.7 alpha 버전과의 차이점

먼저 이미 alpha 버전을 통해 react hooks에 대해 알고 계신 분들을 위해 기존에 alpha release 된 Hooks와 이번에 stable release 된 Hooks의 차이점은

  • useMutationEffect Hooks 삭제
  • useImperativeMethods Hooks useImperativeHandle로 이름 변경
  • useState와 useReducer Hooks의 state를 동일한 값으로 변경했을시 리렌더링 안됨
  • useEffect / useMemo / useCallback hooks에 전달 된 첫 번째 인자 비교 불가
  • etc..

이 있습니다 더 자세히 알고 싶으시면 링크를 참고해 주세요

React Hooks 사용 규칙

Hooks를 사용하려면 몇 가지의 규칙을 준수 해야 합니다

1. Hooks를 함수형 컴포넌트의 Top-level에서만 호출 해야 합니다.

반복문, 조건문, 중첩된 함수에서 호출하면 안 되고 오직 최상위 함수에서만 호출해야 합니다.

import React, { useState } from 'react'
function Counter () {
  // ① OK
  const [count, setCount] = useState(0)

  // ② X 반복문
  for (let i = 0; i < 10; i++) {
    var [count2, setCount2] = useState(0)
  }

  // ③ X 조건문
  if (true) {
    var [count3, setCount3] = useState(0)
  }

  // ④ X 중첩된 함수
  function innerFnc () {
    const [count4, setCount4] = useState(0)
  }
  ...
}

useState Hook 은 함수형 컴포넌트에서 class 컴포넌트처럼 state를 사용할 수 있게 해줍니다.

①의 경우 함수형 컴포넌트의 최상위에서 hooks를 호출하였기 때문에 문제가 없고 ②, ③, ④의 경우 각각 반복문, 조건문, 중첩된 함수에서 호출했기 때문에 호출하면 안 됩니다

혹시 왜 반복문, 조건문, 중첩된 함수에서 호출하면 안 되는지 궁금하신 분들은 링크를 참고해 주세요

2. 오직 함수형 컴포넌트 에서만 Hooks를 호출해야 합니다

일반 JavaScript 함수와 class에서는 Hooks를 호출해서는 안됩니다. 하지만 custom Hooks에서는 호출할 수 있습니다 이것에 대해서는 아래에서 알아보겠습니다.

만일 ESLintTSLint를 사용 중 이시면 위 규칙을 지키지 않을 때 에러를 표시해주는 플러그인도 있습니다.

useState Hook

useState은 함수형 컴포넌트에서 class 컴포넌트처럼 state를 사용할 수 있게 해줍니다 useState를 활용해서 간단한 counter를 구현해보겠습니다.


import React, { useState } from 'react'

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

  return (
    <div>
      {count}
      <br />
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

useState는 파라미터로 state의 기본값을 받고 숫자, 문자, 객체, 배열, 함수등 다양하게 넘겨 줄 수 있습니다 useState를 호출하면 배열을 반환 하는데 배열의 첫 번째 원소는 현재 state 값이고 두 번째 원소는 class 컴포넌트의 setState처럼 현재 state 값을 업데이트하는 함수 입니다 사용법은 class 컴포넌트의 setState와 동일하며 호출이 일어난 다음 state값이 변경되었을시 리렌더링 됩니다


useState의 첫 번째 파라미터로 함수를 넘겨 줄 수 있는데 이 함수는 컴포넌트가 mount될 때 만 호출 되며 콜백 함수의 return값이 state의 기본값이 됩니다 만약 기본값을 정의 할 때 값비싼 연산을 한다면 파라미터로 함수를 넘겨줘서 성능 최적화를 할 수 있습니다.

import React, { useState } from 'react'

function Counter() {
  const [state, setState] = useState(() => {
    const initialState = someExpensiveComputation(props)
    return initialState // 기본값
  })

  return (
    ...
  )
}

또한, 하나의 컴포넌트 내에서 useState를 여러 개 사용할 수도 있습니다

import React, { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('lee')

  return (
    ...
  )
}

주의점으로는 state가 객체 타입 일 때 class 컴포넌트의 setState method와 달리 useState의 업데이트 함수는 새로운 state를 자동으로 merge 하지 않습니다. class의 setState method와 같이 동작 하기 위해선 immutable.js나 immer.js같은 라이브러리를 사용하거나 Object.assign 또는 전개 연산자를 사용해야 합니다.

import React, { useState } from 'react'
function () {
  const [profile, setProfile] = useState({ name: 'lee', gender: 'male' })
  const [profile2, setProfile2] = useState({ name: 'kim', gender: 'male' })
  setProfile({name: 'kim'})
  console.log(profile) //  { name: 'kim', gender: 'male' }이 되지 않고 {name: 'kim'} 이 됨
  setProfile2({...profile2, name: 'lee'})
  console.log(profile2) // { name: 'lee', gender: 'male' }

  return (
    ...
  )
}

또 다른 차이점으로는 class 컴포넌트의 setState method와 달리 state를 동일한 값으로 업데이트했을시 리렌더링을 하지 않습니다.

useEffect

useEffect는 componentDidMountcomponentDidUpdate, componentWillUnmount와 같은 역할을 하며 기존에 class에서 LifeCycle API로만 처리할 수 있었던 로직들을 이제 함수형 컴포넌트에서도 처리할 수 있게 해주는 Hook입니다.

import React, { useState, useEffect } from 'react'

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

  // 컴포넌트가 mount되거나 리렌더링 될 때 콜백 함수 호출
  // componentDidMount, componentDidUpdate와 동일
  useEffect(() => {
    console.log(`count: ${count}`)
  })

  return (
    ...
  )
}

useEffect는 첫 번째 파라미터로 콜백 함수를 받고 두 번째 파라미터로 배열을 받습니다

useEffect가 받은 콜백 함수는 렌더링 될 때마다 호출 되게 되는데 두번째 파라미터로 받은 배열의 원소 값에 따라 특정 값이 변경된 경우에만 콜백 함수가 호출 되게 처리할 수 도 있습니다

import React, { useState, useEffect } from 'react'

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

  // 컴포넌트가 mount되거나 count값이 변경 되었을 때만 콜백 함수 호출
  useEffect(() => {
    console.log(`count: ${count}`)
  }, [count])

  return (
    ...
  )
}

또한 useEffect의 2번째 파라미터로 빈 배열을 넘겨 주게 되면 컴포넌트가 mount되었을때만 콜백 함수가 호출됩니다

import React, { useState, useEffect } from 'react'

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

  // componentDidMount와 동일
  useEffect(() => {
    console.log('mounted')
  }, [])

  return (
    ...
  )
}

useEffect의 첫 번째 파라미터로 받은 콜백 함수는 함수를 return 할 수도 있는데 return한 함수는 2가지 상황에서 호출 됩니다


첫 번째, 콜백 함수가 다시 호출 될 때 이전에 return한 함수를 먼저 호출 한 뒤 콜백 함수를 호출합니다

import React, { useState, useEffect } from 'react'

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

  // mount되거나 Count가 변경되었을때 콜백 함수 호출
  useEffect(() => {
    const showCount = setInterval(() => console.log(`count: ${count}`), 1000)

    // count가 변경 되었을때 Interval 삭제
    return () => {
      clearInterval(showCount)
    }
  }, [count])

  return (
    ...
  )
}

두 번째, componentWillUnmount와 같이 컴포넌트가 unmount되기 전에 return한 함수를 호출합니다

import React, { useState, useEffect } from 'react'

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

  // componentWillUnmount와 동일
  useEffect(() => {
    return () => {
      console.log('componentWillUnmount')
    }
  }, []) // mount될때만 실행

  return (
    ...
  )
}

useState와 마찬가지로 하나의 컴포넌트 내에서 여러 개의 useEffect를 사용할 수 있습니다

import React, { useEffect } from 'react'

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

  // componentDidMount, componentDidUpdate
  useEffect(() => {
    const showCount = setInterval(() => console.log(`count: ${count}`), 1000)

    // count가 변경 되었을때나 컴포넌트가 unmount 됐을때 Interval 삭제
    return () => {
      clearInterval(showCount)
    }
  }, [count])

  // componentWillUnmount와 동일
  useEffect(() => {
    return () => {
      console.log('componentWillUnmount')
    }
  }, [])

  return (
    ...
  )
}

주의할 점으로는 DOM을 확인(스크롤 위치, element의 style 등) 또는 수정할 경우 UseEffect대신 useLayoutEffect를 사용 해야 합니다.

사용법은 동일하며 Hook 이름만 다릅니다

기타 Hooks

useState와 useEffect를 포함하여 현재(2019-02-08) 10개의 내장 Hooks들이 있습니다 10개의 Hooks를 다 설명 하면 너무 글이 길어져 나머지 Hooks들은 간단하게 설명하고 추후에 추가로 글을 작성하도록 하겠습니다

useContext

Context API를 사용할 수 있게 해주는 Hook입니다

const context = useContext(Context)

useReducer

redux style로 state를 관리할 수 있게 해주는 useState의 대안 Hook입니다

복잡한 state를 처리할 경우 useState보다 useReducer를 권장하고 있습니다

const [state, dispatch] = useReducer(reducer, initialArg, init)

useCallback

첫 번째 파라미터로 콜백 함수를 받고 두번째 파라미터로 배열을 받아 memoize 된 콜백 함수를 반환 해주는 Hook입니다

두 번째 파라미터로 받은 배열의 원소 값 중 하나가 변경되어야만 함수의 reference가 변경됩니다.

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

useMemo

첫 번째 파라미터로 콜백 함수를 받고 두 번째 파라미터로 배열을 받아 memoize된 return 값을 반환해주는 Hook입니다

두 번째 파라미터로 받은 배열의 원소 값 중 하나가 변경되어야만 다시 연산합니다

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

useRef

ref를 사용할 수 있게 해주는 Hook 입니다.

const refContainer = useRef(initialValue)

useLayoutEffect

useEffect와 거의 동일하지만 모든 DOM이 변경 된 후에 동기적으로 실행 됩니다 DOM을 확인 또는 수정할 경우 UseEffect대신 useLayoutEffect를 사용 해야 합니다.

useDebugValue

React DevTools에서 custom-hooks에 label을 표시해주는 Hook입니다

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null)

  // Show a label in DevTools next to this Hook
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline')

  return isOnline
}

더 자세히 알고 싶으시면 링크를 참고해 주세요

Custom Hooks

Hooks를 사용하는 중복되는 로직을 custom Hooks로 만들어 코드를 재 사용할 수 있습니다

Custom Hooks는 "use"로 시작하고, 함수 내부에서 다른 Hooks를 호출하는 함수입니다

컴포넌트가 mount될때 scroll lock를 걸고 unmount 될 때 scroll lock을 푸는 간단한 custom Hooks를 만들어 보겠습니다

// useScrollLock.js

import { useLayoutEffect } from 'react'

function useScrollLock() {
  // DOM을 수정하기때문에 useEffect대신 useLayoutEffect 사용
  useLayoutEffect(() => {
    // 컴포넌트가 mount될때 스크롤 방지
    document.body.style.overflow = 'hidden'

    // 컴포넌트가 unmount될때 함수 실행 (componentWillUnmount)
    return () => {
      document.body.style.overflow = 'visible'
    }
  }, []) // 빈 배열을 넣을시 mount, unmount될때만 실행
}

export default useScrollLock

custom hooks를 사용할 때는 일반 자바스크립트 함수처럼 호출하면 됩니다.

import React from 'react'
import useScrollLock from './useScrollLock'

function App() {
  useScrollLock()

  return (
    ...
  )
}

export default Counter

다른 개발자분들이 만드신 오픈소스 custom Hooks들은 아래 링크들에서 확인해 볼 수 있습니다.

정리

Hooks를 사용하면 class를 작성하지 않고도 state나 다른 React의 기능들(ref, lifecycle, context, ...)을 함수형 컴포넌트에서도 사용할 수 있다.

Hooks를 사용하려면 2가지의 규칙을 지켜야 한다(반복문, 조건문, 중첩된 함수에서 호출하면 안 되고 함수의 Top-level에서만 호출해야 한다, custom hooks를 제외하고 오직 함수형 컴포넌트에서만 Hooks를 호출해야 한다)

중복되는 로직을 custom Hooks로 만들어 코드를 재 사용할 수 있다

Reference

comment

Comments