이번 편에서는 useState 함수와 useEffect 함수를 조금 더 유연하게 사용하는 방법을 알아보겠습니다.

useState

import React, { useState } from 'react'

const ExpensiveComponent = () => {
  const [someValue, setSomeValue] = useState(() => {
    let value = 0
    for (let index = 0; index < 100_0000; index += 1) {
      value += 1
    }
    return value
  })

  return <div>{someValue}</div>
}

useState는 초기화 할 때에만 함수를 1번 실행하는 방식을 지원합니다.

처음 1번만 함수를 실행을 하며, 그 함수의 return 값을 State로 사용합니다.

useEffect

CounterTitle_Toggle.gif

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

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

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

  const toggleNickname = () => {
    if (nickname === 'Danuel') {
      setNickname('Other')
    } else {
      setNickname('Danuel')
    }
  }

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

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

이번에는 카운트 기능과 타이틀을 바꾸는 기능을 가진 컴포넌트입니다.

이 컴포넌트는 원하는대로 작동은 잘 하지만, 찬찬히 살펴보면 카운트를 변경할 때에도 title을 변경하는 비효율적인 부분이 있습니다.

잠시 생각해보면 이전의 State와 현재의 State가 다를 때에만 useEffect 함수가 발동하는 것이 가장 효율적입니다.

...
useEffect(() => {
  document.title = nickname
}, [nickname])
...

nickname을 배열에 담아 2번째 파라미터에 추가하면 이전의 State와 현재의 State가 다를 때에만 발동합니다.

...
useEffect(() => {
  document.title = nickname
}, [])
...

빈 배열을 2번째 파라미터에 추가하면 처음 딱 1번만 발동합니다.

비동기적인 State 변경

import React, { useState } from 'react'

const SECOND = 1000

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

  const decreaseCount = () => {
    window.setTimeout(() => setCount(count - 1), 3 * SECOND)
  }
  const increaseCount = () => {
    window.setTimeout(() => setCount(count + 1), 3 * SECOND)
  }

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

이번에는 비동기적으로 State를 변경해보겠습니다.

- 1 버튼 혹은 + 1 버튼을 클릭하면 3초 후에 State를 바꾸는 컴포넌트입니다.

AsyncCount_Bad.gif

연속적으로 클릭한 후에 값을 살펴보면 예상과는 다른 작동을 합니다. 이것은 비동기와
관련이 있습니다.

버튼을 클릭하면 3초 후에 State를 변경하는 함수를 실행하는데, 이 순간에 들어가는 값은 버튼을 클릭한 시점의 State로 이미 결정해놓았기 때문입니다.

import React, { useState } from 'react'

const SECOND = 1000

const AsynchronousCounter = () => {
  // ...

  const decreaseCount = () => {
    window.setTimeout(() => setCount(previousCount => previousCount - 1), 3 * SECOND)
  }
  const increaseCount = () => {
    window.setTimeout(() => setCount(previousCount => previousCount + 1), 3 * SECOND)
  }

  // ...
}

이런 이슈를 해결하기 위해서 React에서는 State를 변경하는 함수의 파라미터에 함수를 넘기는 방법도 지원합니다. 언뜻 보면 조금 복잡하게 보이지만, 바뀐 코드만 보면 setCount 함수에 함수를 넘겨준 것 뿐입니다.

코드에서 보듯이, 함수로 넘겨줄 때에는 '직전'의 State를 입력으로 받고 다음 State를 return 하는 형태입니다.

AsyncCount_Good.gif

간단한 변경이지만 이렇게 잘 작동합니다.