September 01, 2021
React
에서 기본적으로 제공되는 useState
를 사용하여 상태값과, 상태값을 업데이트할 수 있는 setState
메소드를 생성할 수 있다.
아무렇지도 않게 사용해오던 setState
에 알지못했던 세부적인 내용이 있었고, 이 세부적인 내용이 이전에 개인프로젝트를 하면서 발생했던 에러와 큰 관련이 있어 알아보게 되었다.
useCallback
과 같은 메모이제이션 hook
을 사용할 때 상태값 변경을 위한 setState
를 의존성배열에 추가하는 경우가 있었는데, setState
자체로는 rerender
에 아무런 영향을 미치지 않기 때문에 추가할 필요가 없다고 한다.
실제로 추가하지 않아도,
eslint
에서 아무런 에러를 발생시키지 않는다.
이벤트 핸들러 내에서 setState
는 비동기 방식으로 작동된다고 한다.
동일한 이벤트 호출을 통해 내부에서 여러번의 setState
가 호출된다면, 모든 변화를 하나로 합쳐서 호출된 이벤트 핸들러가 종료될 때 단 한번만 state
에 직접적인 변화가 일어난다.
class
기반의 setState
는 이를 동일한 key
에대해 병합이 이뤄졌고
hook
기반의 setState
는 병합이 아니라 대체의 개념을 갖고있다.
setState(count + 1)
setState(count + 1)
위처럼 두번 호출하여 원하는 결과가 2
일것 같은 방법도 병합 혹은 대체의 과정을 통해 1
의 결과를 반환한다.
setState((oldState) => {...oldState, newVal})
setState
에 콜백함수를 사용하여 상태값을 업데이트할 수 있다.
이전의 상태값에대한 정보를 조회할 수 있으며, 이를 바탕으로 업데이트 하여 대체할 수 있다.
이 특징에 있어서는 이전에 다뤘던 useCallback
을 통한 메모이제이션과도 큰 관련이 있을것 같다.
만약, 객체로된 상태값을 useCallback
의 의존배열에 추가하여 메소드를 생성하고있을 경우, setState
로 객체에 변경이 있을 때 계속해서 메소드를 재생산하게될 것이다.
하지만, setState
는 의존성 배열에 추가될 필요가 없다는 특징을 이용해보았을 때 처음에만 메소드를 생성하고, 이후에는 캐싱된 메소드로만 사용 가능할것 같다.
const func = useCallback((state) => {
setState({ ...states, state })
}, [states])
const func = useCallback((state) => {
setState((oldState) => {...oldState, state})
}, [])
useState
를 사용하여 상태값을 관리하고자 할 때, 선택할 수 있는 방법이 두가지 있다.
여러 속성을 하나의 useState
로 묶어서 사용할 것인가?
속성들을 분리하여 각각의 useState
를 통해 생성할 것인가?
const [position, setPosition] = useState({ x: 100, y: 50 })
const [size, setSize] = useState({ width: 500, height: 30 })
상태값을 분리하여 생성하게 될 경우, 상태값을 대체하는데에 비교적 편리할 수 있다.
사실 가장 큰 이점으로 볼 수 있는 점은, 각각의 hook
들을 커스텀 훅으로 분리하여 사용할 수 있다는 점이다.
추상화에 큰 도움이 된다.
const [state, setState] = useState({ x: 100, y: 50, width: 500, height: 300 })
x
의 값을 변경하는데에 width
라는 속성이 필요해서 하나의 state
로 합쳐서 생성하였다. 서로간 연관되어있는 속성들끼리 함께 묶어서 생성하여 업데이트할 때 참조하는데에 편리함이 있다.
나쁜방법은 아니지만, useState.setState
는 대체라는 특징으로 인해 꼭 필요한 과정이 추가된다.
setState((oldState) => {...oldState, x : 700})
늘 상태값이 대체되기 때문에, 일부분을 변경하게 된다면 이전 값에 대한 복제의 작업이 필요하다.
웬만한 상황에서는 useState
를 사용하여 상태값을 구성할 때 하나의 단일 보다는 구분하여 생성하는것이 더 효율적이다.
하지만 예외적인 상황이 있다.
이 문제에 있어서는 useEffect
, 혹은 메모이제이션을 위한 useCallback
을 사용할 때 더 크게 필요를 느끼게된다.
자신의 상태값이 자주 변동되어서 setState
를 함수형식으로 전달하여 내부의 이전 state
값을 참조하여 생성을 할 수 있어서 useCallback
, useEffect
들의 의존성배열을 비워 재생산, 재실행 되는것을 막을 수 있다고 위에서 설명했었다.
하지만, state
분리를 통해 각각의 상태가 서로를 참조할 수 있는 방법이 의존성배열밖에 없는 상황이라면?
이것을 어쩔수 없는 재생산, 재실행이라고 보는것이 맞을까?
그 비용이 어마어마해진다면 어떨까?
위 문제를 보았을 때 생각나는것이 있다.
Redux
Redux
의 Dispatch
또한 마찬가지로 rerender
에는 영향을 주지 않기 때문에 useEffect
, useCallback
의 의존성 배열에 추가될 필요가 없다.알게모르게 Redux
가 많은 고민을 해주고 있었다.
하지만 늘 느껴왔지만, Redux
를 사용하기위해서는 많은 셋팅이 필요하다.
이러한 특징때문에 Redux
를 개발한 개발자들도 Redux
를 사용하는데 적합성을 판단하는것이 중요하다고 하더라.
useReducer hook
을 사용하여 비교적 편리하게 Redux
의 장점을 가져올 수 있었다.
아마 Redux
를 사용해보았다면, 빠르게 이해하고 적용해볼 수 있을 것이다.
const initialState = { count: 0 }
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
default:
throw new Error()
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
const handleDecrese = useCallback(() => {
dispatch({ type: 'decrement' })
}, [])
const handleIncrease = useCallback(() => {
dispatch({ type: 'increment' })
}, [])
return (
<>
Count: {state.count}
<button onClick={handleDecrese}>-</button>
<button onClick={handleIncrease}>+</button>
</>
)
}
React
공식문서에서 설명되어있는 예제를 살짝 수정해보았다.
dispatch
메소드는 rerender
에 아무런 영향을 주지 않기 때문에, 의존성 목록에 추가해줄 필요가 없다.state
에서 찾아 사용할 수 있다.
useCallback
함수 모두 참조하는 상태값이 변경될 때마다 새롭게 생성되어 참조하는 상태값의 변화만으로 해당 함수를 사용하는 자식들은 rerender
가 빈번히 발생한다.rerender
에 영향을 주지 않는 setState
내부에 콜백함수를 전달하여 진행할 수 있지만, 외부의 경우는 별도의 콜백함수를 생성해야하기 때문에 다른 상황이다.useEffect
를 사용하는 경우에도, 단순 참조를 위해 사용되는 상태값이 변경되면 다시 실행되는 경우가 있다.setState
가 아닌 별도의 action
으로 구분하여 호출하기 때문에, 여러개의 상태값에 대한 업데이트가 용이하다.
errorState
, loadingState
, dataSate
등을 별도로 생성하여 필요할 때 각각의 setter
함수들을 호출해줘야 하지만, reducer
를 사용하면 setter
함수를 호출하는것이 중심이 아닌, 사용자의 action
을 기준으로 하여 복수의 상태값을 업데이트 할 수 있다.공식문서에서 설명된 특징들과 경험해본 느낌을 간추려본 장점들이다.
독립적인 상태값을 관리하는데에는 **useState
**가 간단하며 효율적이지만, 여러 상태값이 서로간 의존성을 갖고있고, 한 함수 내부에서 여러개의 setter
가 호출되는 상황이라면 사용자의 행위를 기준으로 별도의 환경에서 상태값 업데이트 로직을 작성하여 관리하는 **useReducer
**가 더 적합해보인다.
특히, 이전에 진행했던 socket.io
를 통한 실시간채팅과같이 짧은 순간순간에 setState
가 여러번 발생하여 의존성배열 변경으로 인해 메모이제이션의 의미가 많이 퇴색되는 상황에서는 더 빛을 볼 것 같다.