#3: Things to know about useState
#4: useState for one-time initializations
두 게시물을 짬뽕해서 정리함.
1. 함수형 업데이터
클로저를 사용하여 setState 함수를 호출할 시, 배치 처리되는것 같은 느낌을 줍니다.
이는 우리의 의도와 다를 수 있습니다.
함수형 업데이터는 매 setState호출마다 이전 호출 결과 상태를 사용할 수 있게 해줍니다.
기존 클로저를 사용한 업데이트 : 결과는 항상 1 증가합니다.
function App() {
const [count, setCount] = React.useState(0)
return (
<button
onClick={() => {
setCount(count + 1)
setCount(count + 1)
}}
>
🚨 This will not work as expected, count is: {count}
</button>
)
}
render(<App />)
이는 리액트에게 다음과 같이 알려주는 것과 동일합니다.
- count를 count+1로 증가시켜 주세요
- count를 count+1로 증가시켜 주세요
해당 count는 항상 1로 동일합니다.
따라서 두번 setState를 호출해도 값은 2가 됩니다.
다음과 같이 함수형 업데이터를 사용하면 어떻게 될까요?
function App() {
const [count, setCount] = React.useState(0)
return (
<button
onClick={() => {
setCount((previousCount) => previousCount + 1)
setCount((previousCount) => previousCount + 1)
}}
>
✅ Increment by 2, count is: {count}
</button>
)
}
render(<App />)
이는 리액트에게 다음과 같이 알려주는 것과 동일합니다.
- 현재 값에서 1 증가시켜주세요
- 현재 값에서 1 증가시켜주세요
따라서 2가 증가된 값을 얻을 수 있습니다.
비동기 액션이 포함된 경우
function DelayedCounter() {
const [count, setCount] = React.useState(0)
const increment = async () => {
await doSomethingAsync()
setCount(previousCount => previousCount + 1)
}
return <button onClick={increment}>{count}</button>
}
이전 상태 혹은 외부 상태를 기반으로 새 상태를 계산해야 할 때마다 함수 업데이터를 사용합니다.
보너스1 : 의존성을 빼기
function Counter({ incrementBy = 1 }) {
const [count, setCount] = React.useState(0)
// 🚨 will create a new function whenever count changes because we closure over it
const increment = React.useCallback(() => setCount(count + incrementBy), [
incrementBy,
count,
])
// ✅ avoids this problem by not using count at all
const increment = React.useCallback(
() => setCount((previousCount) => previousCount + incrementBy),
[incrementBy]
)
}
보너스2 : useReducer로 토글 구현
useState로 구현하면 약갼의 보일러플레이트가 추가됩니다.
const [value, setValue] = React.useState(true)
<button onClick={() => setValue(previousValue => !previousValue)}>Toggle</button>
const [value, toggleValue] = React.useReducer(previous => !previous, true)
<button onClick={toggleValue}>Toggle</button>
- 토글 로직을 setter 호출에서 useReducer 훅 호출로 이동합니다. (ui로직이 컴포넌트에서 리듀서로 이동)
- 토글 함수의 이름을 지정할 수 있습니다. 토글 함수는 단순한 setter가 아닙니다.
- 토글 함수를 여러번 사용할 때마다 작성하는 이벤트 핸들러의 상용구를 줄일 수 있습니다.
2. 지연 초기화 함수
// 🚨 will unnecessarily be computed on every render
const [value, setValue] = React.useState(
calculateExpensiveInitialValue(props)
)
// ✅ looks like a small difference, but the function is only called once
const [value, setValue] = React.useState(() =>
calculateExpensiveInitialValue(props)
)
3: 업데이트 구제금융 (The update bailout)
function App() {
const [name, setName] = React.useState('Elias')
// 🤯 clicking this button will not re-render the component
return (
<button onClick={() => setName('Elias')}>
Name is: {name}, Date is: {new Date().getTime()}
</button>
)
}
render(<App />)
4: 편리한 타입 오버로딩 (The convenience overload)
// 🚨 age will be inferred to `undefined` which is kinda useless
const [age, setAge] = React.useState(undefined)
// 🆗 but a bit lengthy
const [age, setAge] = React.useState<number | null>(null)
초기 값을 완전히 생략하면 전달된 타입에 undefined를 추가하는, useState의 편리한 오버로드가 있습니다.
매개변수를 전혀 전달하지 않는 것은 명시적으로 undefined를 전달하는 것과 동일하기 때문에 런타임에도 값은 undefined가 됩니다.
// ✅ 초기값을 전달하지 않으면, age 타입이 `number | undefined`로 추론됩니다.
const [age, setAge] = React.useState<number>()
5: 구현 세부사항(The implementation detail)
useState는 사실 useReducer로 구현됩니다.
대부분 반대로 알고 있습니다만.여기 소스 코드에서 확인할 수 있습니다. : the source code here
useReducer로 useState를 구현하는 방법에 대한 Kent C. Dodds의 훌륭한 아티클도 있습니다.
: how to implement useState with useReducer
6 : 앱 전체 생명 주기동안 단 한번 초기화
리액트-쿼리의 쿼리클라이언트나 리덕스 스토어에서 많이 볼 수 있습니다.
즉, 앱에 단 하나만 필요한 리소스에 적절합니다.
// ✅ static instance is only created once
const resource = new Resource()
const Component = () => (
<ResourceProvider resource={resource}>
<App />
</ResourceProvider>
)
하지만 마이크로프론트엔드와 같이, 컴포넌트 별로 자체 리소스가 필요한 경우는 컴포넌트 내로 리소스를 옮겨야 합니다.
이는 컴포넌트 리렌더링 마다 리소스가 새로 생성된가는 뜻입니다.
const Component = () => {
// 🚨 be aware: new instance is created every render
const resource = new Resource()
return (
<ResourceProvider resource={new Resource()}>
<App />
</ResourceProvider>
)
}
보통 이와 같은 문제를 useMemo로 해결합니다.
const Component = () => {
// 🚨 still not truly stable
const resource = React.useMemo(() => new Resource(), [])
return (
<ResourceProvider resource={resource}>
<App />
</ResourceProvider>
)
}
하지만 공식문서(react docs)를 잘 살펴보죠. 이게 정말 올바른 해답일까요?
미래에 React는 이전에 메모한 일부 값을 "잊어버리고" 다음 렌더에서 다시 계산하도록 선택할 수 있습니다.
즉 화면에 노출되지 않은 컴포넌트에 대한 메모리를 확보합니다.
useMemo 없이도 작동하도록 코드를 작성한 다음 useMemo를 추가하여 성능을 최적화하십시오.
useMemo는 성능 최적화를 위한 도구지 참조 투명성(안정성)을 위한 도구가 아닙니다.
그렇다면 이를 해결하기 위한 최고의 수단은 무엇일까요?
state to the rescue
바로 state입니다.
상태는 setter를 호출하는 경우에만 업데이트되도록 보장됩니다.
따라서 우리가 해야 할 일은 setter를 호출하지 않는 것입니다.
리소스 생성자가 한 번만 호출되도록 하기 위해 이것을 지연 초기화 함수와 아주 잘 결합할 수 있습니다.
const Component = () => {
// ✅ truly stable
const [resource] = React.useState(() => new Resource())
return (
<ResourceProvider resource={resource}>
<App />
</ResourceProvider>
)
}
ref는 어떤가요?
useRef를 사용하여 동일한 결과를 얻을 수 있다고 생각하며
react 규칙(rules of react)에 따르면 이는 렌더링 함수의 순수성을 손상시키지도 않습니다.
(React에서는 대부분 idempotent 함을 의미합니다. 동일한 입력에 대해 항상 동일한 것을 반환합니다.)
const Component = () => {
// ✅ also works, but meh
const resource = React.useRef(null)
if (!resource.current) {
resource.current = new Resource()
}
return (
<ResourceProvider resource={resource.current}>
<App />
</ResourceProvider>
)
}
이것은 다소 복잡해 보이고 TypeScript도 그것을 좋아하지 않을 것입니다.
왜냐하면 resource.current는 기술적으로(실제로 어느 시점에) null일 수 있기 때문입니다.
나는 이러한 경우에 useState를 선호합니다.
'FrontEnd' 카테고리의 다른 글
리액트 쿼리 : 서버 응답 변환 위치 (0) | 2022.06.17 |
---|---|
리액트 쿼리 : 반드시 알아야 하는 상식 (0) | 2022.06.17 |
useState의 함정 : useEffect와 사용 시 주의할 점. (0) | 2022.06.16 |
useState와 useReducer의 사용사례 (0) | 2022.06.16 |
React에서 마진 대신 Spacer 컴포넌트 활용하기 (0) | 2022.06.15 |