글자 4개로 리액트 컴포넌트를 최적화하는 방법


원문: https://www.benmvp.com/blog/four-characters-optimize-react-component/

useState의 지연 초기화를 통해 리액트 함수 컴포넌트의 속도를 향상시키는 방법

필자가 제목을 자극적으로 지었지만 사실인 내용이다. 먼저 두가지 코드를 살펴보자.

첫 번째 예제:

// 예제 1

const Counter = () => {
  const [count, setCount] = useState(
    Number.parseInt(window.localStorage.getItem(cacheKey)),
  )

  useEffect(() => {
    window.localStorage.setItem(cacheKey, count)
  }, [cacheKey, count])

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

두 번째 예제:

// 예제 2

const Counter = () => {
  const [count, setCount] = useState(() =>
    Number.parseInt(window.localStorage.getItem(cacheKey)),
  )

  useEffect(() => {
    window.localStorage.setItem(cacheKey, count)
  }, [cacheKey, count])

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

두 코드의 차이점을 알 수 있겠는가? 그렇다면 매우 예리하다!🔬 하지만 차이점을 모르는 사람을 위해 힌트를 주겠다. useState부분만 따로 떼어서 보자.😄

첫 번째 예제:

// 예제 1

const [count, setCount] = useState(
  Number.parseInt(window.localStorage.getItem(cacheKey)),
)

두 번째 예제:

// 예제 2

const [count, setCount] = useState(() =>
  Number.parseInt(window.localStorage.getItem(cacheKey)),
)

두 코드의 차이점은 상태 초기화 부분이다. 첫 번째 예제는 localStorage에서 값을 찾아 정수로 파싱한 다음 count 상태의 초기 값으로 설정한다.

// 예제 1

const [count, setCount] = useState(
  Number.parseInt(window.localStorage.getItem(cacheKey)),
)

두 번째 예제는 함수를 인자로 넘긴다는 점을 제외하면 첫번째 예제와 유사하다. 인자로 넘기는 함수는 첫번째 예제처럼 localStorage에서 검색한 값을 정수로 파싱하여 반환한다.

// 예제 2

const [count, setCount] = useState(() =>
  Number.parseInt(window.localStorage.getItem(cacheKey)),
)

return문을 명시하지 않아도 암묵적으로 값을 반환하는 화살표 함수의 특징 덕분에 첫 번째 예제에서 글자 4개(공백 포함)만 추가하여 두 번째 예제를 만들 수 있다. 또한 초기 값을 얻기 위해 수행하는 작업에 따라(예제의 경우localStorage에서 값을 찾아 파싱하는 것이다.), 이 글자 4개만 추가하여 리액트 함수 컴포넌트의 성능을 향상시킬 수 있다.

직접적인 값 대신 함수를 useState의 인자로 넘기는 것을 지연 초기화라고 한다. 링크의 문서에서는 초기 값을 구하기 위한 계산 비용이 클 때 useState의 지연 초기화를 사용하라고 설명하고 있다. 지연 초기화는 상태가 최초로 생성될 때만 실행되기 때문이다. 이후 발생하는 리렌더링에서는 실행되지 않는다.

다시 한번 말하자면, useState 훅은 Counter컴포넌트를 처음 렌더링할 때 count상태를 초기 값으로 생성한다. 그 이후 setCount를 호출하면 Counter함수가 다시 호출되고 count상태는 갱신된다. 그리고 리렌더링은 count 상태가 업데이트될 때마다 발생한다. 중요한 점은 리렌더링 되는 동안, 초기 값이 다시 사용되지 않는다는 것이다.

첫 번째 예제에서는 리렌더링이 발생할 때마다 localStorage에서 값을 찾는다. 하지만 만약 최초 렌더링 시에만 값을 찾아도 된다면, 불필요한 계산을 하고 있는 것이다. 반면, 두 번째 예제는 지연 초기화를 사용하여 불필요한 계산을 방지할 수 있다.

아직도 조금 어려운 사람들을 위해 첫 번째 예제를 좀 더 명확하게 변경해보자. localStorage의 값을 변수에 저장한 다음 useState의 인자로 전달해보자.

// 예제 1

const Counter = () => {
  const initialValue = Number.parseInt(window.localStorage.getItem(cacheKey))
  const [count, setCount] = useState(initialValue)

  // 나머지 코드
}

첫 번째 예제에서는 Counter함수가 호출되어 리렌더링이 발생할 때마다 localStorage에서 값를 가져오고 있다. 이러한 작업은 불필요하며localStorage에서 값을 가져오는 것은 최초 렌더링 시에만 수행하면 된다. 두 번째 예제는 리렌더링될 때마다 useState에 함수를 넘기지만, useState는 최초 렌더링 시에만 실행된다. 그래서 지연 초기화라고 부르는 것이다.

// 예제 2

const Counter = () => {
  const [count, setCount] = useState(function() {
    return Number.parseInt(window.localStorage.getItem(cacheKey)),
  })

  // 나머지 코드
}

만약 지연 초기화 함수를 제대로 적는다면, 두 예제가 얼마나 다른지 더 명확히 볼 수 있다.

지연 초기화 함수는 한 번만 호출하니까 항상 사용하는 것이 좋을까? 아래 예제를 보자.

// 원시값을 반환한다.

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

  // 나머지 코드
}

또 다른 예제:

// 기존의 값 또는 props로 받은 값을 반환한다.

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

  // 나머지 코드
}

위의 예제와 같은 경우 초기 값은 단순한 원시값이거나 이미 계산된 변수이다. 함수를 한번만 호출해도 매번 함수를 만드는 비용이 발생할 것이다. 그리고 함수를 만드는 비용이 단순히 값이나 변수를 전달하는 것보다 더 높을 수 있다. 이것은 지나친 최적화라고 볼 수 있다.

그렇다면 언제 지연 초기화를 사용해야 하는가? 상황에 따라 달라진다. 😄 문서에서는 '비용이 큰 계산'을 할 때 사용하라고 가이드하고 있다. localStorage에서 값을 읽는 것은 비용이 큰 계산일 것이다. 배열의 .map(), .filter(), .find() 등을 사용하는 것도 비용이 큰 계산일 것이다. 좀 더 쉽게 생각해보자. 만약 값을 얻기 위해 어떤 함수를 호출해야 한다면 비용이 큰 계산일 가능성이 높으며, 이러한 경우 지연 초기화를 사용하면 이득을 볼 수 있다.

필자는 현재 날짜/시간으로 상태를 초기화할때 이 방법을 사용하였다.

const Clock = () => {
  // 함수를 인자로 넘겨 지연 초기화를 하였다.
  const [time, setTime] = useState(() => new Date())

  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date())
    }, 1000)

    return () => {
      clearInterval(intervalId)
    }
  }, [tickAmount])

  return <p>The time is {time.toLocaleTimeString()}.</p>
}

이게 전부이다! 질문이나 다른 생각이 있으면 자유롭게 @benmvp 계정으로 연락해라.

끊임없이 배워나가자. 🤓

이재성2020.10.22
Back to list