원문: 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 계정으로 연락해라.
끊임없이 배워나가자. 🤓