Published on

React Hook 개념 정리

Authors

React 는 2019 년 2 월 6 일 16.8 버전을 릴리즈하면서 hook 이라는 새로운 feature 를 소개하였습니다. React 개발팀은 hook 을 다음과 같이 소개하였는데요,

"a way to use state and other React features without writing a class"

이번 포스팅에서는 hook 이 react 어떠한 문제를 해결하고자 했는지, 그리고 hook 을 사용하는 방법들에 대해서 알아보겠습니다.

Hook 의 등장 배경

Hook 이 세상 밖으로 나오기 전, React 코어 개발팀은 수만개의 컴포넌트를 작성하고 관리하면서 다음과 같은 기능들의 필요성을 느끼게 되었습니다.

상태 관리 로직의 재사용

상태를 가지는 컴포넌트(stateful component)에서 상태를 변경하는 logic 을 자식 컴포넌트(stateless component)에게 props 로 전달하고, 자식 컴포넌트에서 이를 호출하는 패턴은 react 를 많이 다뤄보신 분들에겐 이미 익숙하실겁니다. 이러한 패턴을 HOC(higher-order-component)라고 하죠. HOC 는 stateful logic 을 자식 컴포넌트에서 재사용할 수 있도록 해주지만, wrapper component 를 만들어야 하는 불편합이 있습니다. 이러한 컴포넌트의 depth 가 깊어지면 wrapper hell 에 빠지기도 쉽죠. React 팀은 이러한 문제를 인지하게 되었고, stateful logic 을 재사용할 수 있는 더 나은 패턴을 필요로하게 되었고, 그렇게 hook 이 탄생하게 되었습니다.

Hook 을 활용하면 컴포넌트로부터 stateful logic 을 추출하여 이를 필요로하는 곳에 재사용할 수 있습니다. 컴포넌트 hierarchy 와 독립적으로 stateful logic 을 사용할 수 있게 해주는 것이죠. Hook 이 해결하고자 하는 문제는 이뿐만이 아니었습니다.

수월한 라이프사이클(lifecycle) 및 사이트 이펙트(side-effect) 관리

리액트 컴포넌트는 mount/unmount 되는 과정에서 특수한 lifecycle 을 갖는데, 어플리케이션의 덩치가 커질수록 이러한 라이프 사이클 내에서 호출되는 stateful logic 과 이로 인해 발생하는 side-effect 를 관리하기가 점점 더 어려워집니다. 예를 들어, 특정 컴포넌트에서 데이터를 fetch 하는 로직이 componentDidMountcomponentDidUpdate 두 lifecycle 메소드에 모두 포함될 수 있습니다. 그런데 componentDidMount 이와 별개로 event handler 를 추가하는 등의 logic 도 포함할 수 있죠. 그런데 또 event handler 를 제거하는 로직은 componentWillUnmount 에 포함되어야 합니다. 이렇게 비슷한 기능을 하는 코드가 여러 lifecycle 메소드에 포함되게 되면서 디버깅을 어렵게 만듭니다.

Hook 은 이러한 문제를 해결하기 위해, stateful logic 을 lifecycle 별로 나누는 것이 아닌 '비슷한 기능'을 하는 함수들로 나누게 해줍니다. 즉, event handler 를 생성하고 제거하는 로직이 같은 함수에 포함될 수 있게 되는 것이죠.

이외에도 hook 이 도입되면서 class 컴포넌트를 작성함으로써 learning curve 가 상승하는 문제 또한 어느정도 해소될 수 있었습니다. 더이상 eventHandler 에 this 를 바인딩 할 필요도 없게 되면서 react 를 배우기 위해 여러 언어마다 다루는 방법이 다른 this 를 이해해야 하는 장벽이 허물어진 것이죠. Hook 이 어떤 문제를 해결하고자 하였는지 알아보았으니 지금부턴 hook 을 어떻게 사용할 수 있는지 알아보도록 합시다.

Note: hook 은 반드시 함수형 컴포넌트 안의 최상단(top-level) 에서 사용되어야 합니다. 일반 JS 함수 안에서 사용되면 안됩니다. 자세한 규칙은 공식 document 를 참고하세요.

Basic Hooks

useState

useState 는 react 에서 기본적으로 제공하는 hook 으로 컴포넌트 내에서 상태를 관리할 수 있게 도와줍니다. 클래스 컴포넌트와 hook 을 활용한 함수형 컴포넌트를 비교하면서 hook 을 알아보겠습니다. 먼저 hook 을 활용한 카운터 예제를 만들어보면 다음과 같습니다.

import React, { useState } from 'react'

function Example() {
  // 하나의 상태 변수를 정의힙니다.
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

이와 동일한 기능을하는 클래스 컴포넌트는 아래와 같습니다.

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

useState 함수는 초기값을 파라미터로 받아서 array 를 반환합니다. 반환된 array 의 첫 번째 요소는 상태 변수이고, 두 번째 요소는 해당 상태 변수를 변경하는 함수입니다. 즉, 클래스 컴포넌트의 this.state.count 는 hook 을 활용한 함수형 컴포넌트의 count 와 같은 것이고 this.setStatesetCount 와 동일합니다. useState 의 반환 값에서 두 요소를 뺴오기 위해 주로 구조 분해 할당을 활용합니다.

useEffect

useEffect 도 react 에서 기본적으로 제공하는 hook 으로, 함수형 컴포넌트 안에서 side-effect 를 처리하는 걸 도와줍니다. Side-effect 란 data fetching, DOM 조작 등을 일컫습니다. 이전의 useState 예제에서 DOM 을 조작하는 side-effect 를 추가하고, 이때 document 의 title 을 변경하는 기능을 추가해보면 다음과 같습니다.

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

function Example() {
  const [count, setCount] = useState(0)
  // lifecycle 메서드인 componentDidMount, componentDidUpdate 와 유사합니다.
  // count 상태가 변경되면 rerendering 이 되고, 이 때 아래 함수가 호출되어
  // document 의 title 이 변경되게 됩니다.
  useEffect(() => {
    document.title = `You clicked ${count} times`
  })
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

이와 동일한 기능을 하는 클래스 컴포넌트는 아래와 같습니다.

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

동일한 기능을 하는 코드가 중복된 걸 확인할 수 있죠? 이를 피하기 위해 도입된 것이 바로 hook 입니다.

useEffect 에 콜백 함수를 파라미터로 제공하면 render 된 이후 해당 함수를 실행시킬 수 있습니다. useEffect 에 두 번째 인자로 dependency array 를 제공할 수도 있는데, render 된 이후 해당 array 의 값이 변경될 때만 첫 번째 인자로 준 함수를 실행십니다.

useEffect(() => {
  document.title = `You clicked ${count} times`
}, [count]) // count 가 변경될 때만 effect 를 실행합니다

Clean-up effect

useEffect 로 클래스 컴포넌트의 componentWillUnmount 와 동일한 기능을 제공할 수 있습니다. 컴포넌트가 unmount 될 때 실행하고자 하는 로직을 첫 번째 인자로 준 콜백 함수의 return 값이 되도록 하면 됩니다.

useEffect(() => {
  document.title = `You clicked ${count} times`
  return () => (document.title = 'Counter Example')
}, [count]) // 컴포넌트가 unmount 되면 document 의 title 을 Counter Example 로 변경해줍니다.

useContext

데이터를 props 로 전달하지 않고도 컴포넌트 트리 전체에 전달할 수 있는 react context API 를 hook 으로 활용할 수 있습니다.

const value = useContext(MyContext)

MyContextReact.createContext() 로 생성한 context object 입니다. Value 는 부모 컴포넌트들 중 가장 가까운 <MyContext.Provider> 에 props 으로 제공된 context value 를 참조합니다. Context API 를 활용하는 방법은 공식 document 를 참조하세요.

Recap

이번 포스팅에서는 hook 이 등장하게된 배경과 해결하고자 한 문제가 무엇인지, 그리고 hook 을 사용하는 방법에 대해서 알아보았습니다. 이번 포스팅에서 소개한 hook 이외에도 useRef, useMemo, useCallback 등 기본적으로 제공하는 여러 hook 이 존재합니다. 공식 document 에 이러한 hook 에 대한 설명과 예제 코드가 잘 나와 있으므로 참고하면 좋을 것 같습니다.

References