이번 편에서는 상태State 개념과 React Hooks를 소개하고, useState와 useEffect를 작성하는 방법을 알아보겠습니다.

상태State

State는 Props와 함께 React의 핵심 개념입니다. Props는 상위 컴포넌트에서 하위 컴포넌트에게 데이터를 전달하는 것이라 하면, State는 컴포넌트 자체적으로 값을 가지고 있는 데이터입니다.

직접적으로 State를 변경하는 것은 많은 복잡성이 발생하기 때문에 React 자체적으로 변경하는 방법을 제공합니다.

React Hooks

React v16.8 이전까지는 State를 가질 수 없었던 Function 컴포넌트에도 State를 가질 수 있게 해주는 개념 및 React가 지원하는 함수 중 use로 시작하는 것을 통칭하는 명칭입니다.

  • useState
  • useEffect
  • useRef
  • useMemo
  • useReducer

이 외에도 더 많은 함수가 있지만, 이 정도만 해도 충분히 많은 기능을 구현할 수 있으므로 React Hooks 섹션에서는 위 5개만 소개하겠습니다.

useState

import React, { useState } from 'react'

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

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

useState를 불러와서 컴포넌트 안에서 사용하는 형태입니다.

useState 함수 return 값은 배열이며, 1번째에는 useState에 넘겨준 값이, 2번째에는 값을 변경할 수 있는 함수가 있습니다.

Count.gif

실제로 작성을 해서 확인을 해보면 처음에는 0이 보고, - 1 버튼을 클릭하면 클릭한 횟수 만큼 수가 내려가고, + 1 버튼을 클릭하면 클릭한 횟수 만큼 수가 올라갑니다.

import React, { useState } from 'react'

const Counter = () => {
  const [nickname, setNickname] = useState('Danuel')
  const [count, setCount] = useState(0)

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

컴포넌트 안에서 useState는 여러 번 사용할 수 있으며, 이는 여러 State를 가질 수 있다는 것을 의미한다고 할 수 있습니다.

import React, { useState } from 'react'

const Counter = () => {
  const [nickname, setNickname] = useState('Danuel')
  const [count, setCount] = useState(0)

  const decreaseCount = () => setCount(count - 1)
  const increaseCount = () => setCount(count + 1)

  return (
    <div>
      <p>{nickname}</p>
      <p>{count}</p>
      <button onClick={decreaseCount}>- 1</button>
      <button onClick={increaseCount}>+ 1</button>
    </div>
  )
}

간단한 함수인 경우에는 속성property에 직접 함수를 작성해도 큰 무리 없이 읽을 수 있지만, 함수의 개수가 늘어나고 구현부가 길수록 읽기가 난해하므로 예시처럼 유의미한 이름을 가진 변수로 작성하여 사용하는 것을 권장합니다.

  • 이 예시로 보면, "- 1 버튼을 클릭했을 때에는 decreaseCount를 하는구나!" 하고 코드를 쉽게 읽을 수 있습니다.

useEffect

import React, { useState } from 'react'

const Counter = () => {
  const [nickname, setNickname] = useState('Danuel')
  const [count, setCount] = useState(0)

  const decreaseCount = () => {
    setCount(count - 1)
    document.title = count - 1
  }
  const increaseCount = () => {
    setCount(count + 1)
    document.title = count + 1
  }

  return (
    <div>
      <p>{nickname}</p>
      <p>{count}</p>
      <button onClick={decreaseCount}>- 1</button>
      <button onClick={increaseCount}>+ 1</button>
    </div>
  )
}

이번에는 카운트를 변경할 때 마다 title을 변경하는 기능을 추가해보겠습니다.

decreaseCount 함수와 increaseCount 함수에 title을 변경하는 코드를 작성했습니다.

CounterTitle_Bad.gif

이미지에서 알 수 있듯이, 첫화면에서는 title이 count가 아니라 기본으로 설정해놓은 값이 보이는 이슈가 있습니다.

일부만 다른 중복 코드도 탐탁치 않습니다. 결국 보면 count로 변경하는 것인데 변경하는 시점에 값을 결정하기 때문에 일부만 바꾸어서 작성을 해야 합니다.

확장성 부분에서도 만족스러운 구조는 아닙니다. 지금은 1씩 빼거나 더하는 기능이지만, 10씩 빼거나 더하는 기능, 100씩 빼거나 더하는 기능 등 기능이 늘어나면 그에 일부만 다른 중복 코드를 여기저기에 작성해줘야 합니다.

React가 이러한 이슈를 모를리 없습니다.

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

const Counter = () => {
  const [nickname, setNickname] = useState('Danuel')
  const [count, setCount] = useState(0)

  const decreaseCount = () => {
    setCount(count - 1)
  }
  const increaseCount = () => {
    setCount(count + 1)
  }

  useEffect(() => {
    document.title = count
  })

  return (
    <div>
      <p>{nickname}</p>
      <p>{count}</p>
      <button onClick={decreaseCount}>- 1</button>
      <button onClick={increaseCount}>+ 1</button>
    </div>
  )
}

useEffect 함수를 이용하면 위에서 언급한 여러 이슈를 깔끔하게 해결할 수 있습니다.

useEffect는 해당 컴포넌트의 연산이 끝난 이후 함수를 실행합니다. 거칠게 표현하자면 화면에 그리는 작업이 끝난 이후에 useEffect 함수가 발동한다고 할 수 있습니다.

예시 코드에는 useEffect 함수 안에 title을 변경하는 코드가 있습니다. 이 코드는 Counter 컴포넌트를 화면에 그린 이후 작동합니다.

CounterTitle_Good.gif

이미지에서 보듯이, 첫화면에는 title이 0이고 버튼을 누를 때 마다 title도 그에 맞게 바뀌는 동작을 합니다.

버튼을 클릭하면 count가 바뀌고, 조금 뒤늦게 title도 따라 바뀝니다. 이 부분이 바로 '해당 컴포넌트의 연산이 끝난 이후 함수를 실행한다'는 의미입니다.

사용 규칙

이렇게 직관적이고 편리한 React Hooks이지만 꼭 지켜야 하는 규칙이 있습니다.

  1. 선택적 실행 금지only call hooks at the top level

    import React, { useEffect, useState } from 'react'
    
    const User = () => {
      const [nickname, setNickname] = useState('Danuel')
    
      if (nickname === 'Unknown') {
        useEffect(() => {
          // ...
        })
      }
    
      return <h1>{nickname}</h1>
    }

    위처럼 어떤 상태에 의해 React Hooks 함수를 실행하면 예상과는 다른 동작을 하는 등 '정상적인 작동'을 보장하지 않습니다. React Hooks는 useState, useEffect 등을 실행하는 순서와 깊은 연관이 있기 때문입니다.

  2. Function 컴포넌트에서만 사용 가능

    React의 컴포넌트는 2종류가 있습니다. 그 중 Class 컴포넌트는 React Hooks와는 별개로 가지고 있는 상태관리 방법이 있으므로, 복잡성 등의 이유로 사용할 수 없습니다.

    실제로 Class 컴포넌트에서 사용을 해보고자 해도 작동을 하지 않거나 예상과는 다른 동작을 합니다.