React) usePartialState로 거대한 상태 update하기

2ast·2024년 2월 5일
0

많은 정보를 담고 있는 거대한 상태를 다뤄야 할때

react에서 상태를 관리할 때는 일반적으로 useState를 사용하게 된다.

const [state,setState] = useState('')

회원가입 로직을 구현하는 상황을 가정해보자. 아이디, 비밀번호, 이름, 전화번호, 그리고 상황에 따라 성별과 닉네임 등 수많은 정보를 입력받아야 한다. 그리고 각 정보들은 실제 폼에서 값이 변해야하므로 상태로 선언해야 한다.

const [id, setId] = useState('')
const [password, setPassword] = useState('')
const [name, setName] = useState('')
const [phoneNumber, setPhoneNumber] = useState('')
...

이렇게 쓰고보니 너무 장황하게 상태가 늘어나 있어서 보기가 불편하다. 모두 사용자의 회원가입 정보라는 공통점이 있으니, 하나의 상태로 묶어볼 수 있을 것 같다.

const [userInfo, setUserInfo] = useState({
	id:'',
    password:'',
    name:'',
    phoneNumber:'',
    ...
})

하나의 상태로 묶어서 관리하니 단순히 깔끔해보일뿐더러 회원가입 정보 이외에 선언된 다른 상태와도 구별된 느낌이라 가독성도 좋아졌다.
이제 여러개의 데이터 묶음으로 구성된 상태를 업데이트 해보자. name 필드에 해당하는 정보를 수정하고 싶다면 다음과 같이 name뿐만 아니라 함께 묶인 모든 데이터를 취합해서 다시 상태를 재구성해줘야 한다.

setUserInfo({...userInfo,name:newName})

userInfo 상태를 직접 넘기지 않고, 최신의 userInfo를 보장받고 싶다면 이렇게 callback으로 넘기는 것도 가능하다.

setUserInfo(prev=>({...prev,name:newName}))

usePartialState로 코드 간소화하기

사용자 정보를 userInfo라는 하나의 state로 선언해서 코드 가독성을 챙기는 것 까지는 좋았는데, 상태를 업데이트할 때마다 기존 상태를 참조해서 굳이 스프레드 문법을 써야하는 부분이 조금 거슬린다.
이럴 때 사용할 수 있는게 바로 usePartialState라는 훅이다. usePartialState는 sendbird의 kit에 포함되어 있는 useState 대체 훅이다.
usePartialState를 사용하면, setState의 인자로 모든 상태를 재구성해서 넘겨줄 필요 없이, 업데이트를 원하는 필드만 넘겨줄 수 있다.

const [userInfo, setUserInfo] = usePartialState({
	id:'',
    password:'',
    name:'',
    phoneNumber:'',
    ...
})
  
setUserInfo({name:newName})

sendbird github을 보면 usePartialState가 useReducer를 활용해서 스프레드를 통해 새로운 객체를 만드는 과정을 은닉했을 뿐인 간단한 구조로 되어 있는 것을 볼 수 있다.

const usePartialState = <S>(initialState: S) => {
  return useReducer((prev: S, state: Partial<S>) => (
    { ...prev, ...state }
  ), initialState);
};

이제 여러개의 정보로 구성된 거대한 상태를 업데이트할 때도 단일 상태를 업데이트 하는 것 처럼 간편하게 사용할 수 있게 되었다.

usePartialState 보완하기

본래 useState는 함수의 인자로 callback을 받을 수 있도록 허용하고 있다. 이는 주로 react에서 최신의 상태값을 보장받기 위해 사용한다. 여기서 최신의 상태값이란, react에서는 setState가 마치 비동기와 같이(실제로 비동기인 것은 아니다.) 동작하기 때문에, 분명히 상태를 업데이트 했음에도 즉시 값이 반영되지 않기 때문에 생겨난 개념이다.
react는 최적화를 위해 setState가 호출되면 즉시 리렌더하여 뷰에 반영하지 않고 일정 시간마다 변경된 상태를 모아 한번에 반영하게 된다. 즉 아래와 같은 상황이 연출된다.

const [count, setCount] = useState(0)

setCount(count + 1)
setCount(count + 1)
setCount(count + 1) //최종 결과값은 1

이는 첫번째 라인에서 호출된 setCount로 인해 업데이트된 value가 아직 실제 컴포넌트에서 사용되는 count에는 반영되지 않았기 때문이다. 그래서 세 줄의 setCount가 참조하는 count는 셋 모두 0을 참조하기 때문에 count의 결과값이 1이 된다. 여기서 최초 의도대로 count를 3으로 만들고 싶다면 callback을 이용하면 된다.

setCount(prev=>prev+1)
setCount(prev=>prev+1)
setCount(prev=>prev+1) //최종 결과값은 3

useState가 반환하는 count state를 참조하는게 아니라, callback을 통해 클로저로 관리되고 있는 count value를 직접 참조함으로써 최신의 상태값을 보장받을 수 있게 된다.
앞서 usePartialState 구현을 봐도 알 수 있듯이, usePartialState는 callback을 인자로 받을 수 없다. 때문에 이런 상황이 연출될 수 있다.

const [userInfo, setUserInfo] = usePartialState({
	name:'',
    nickName:''
})

setUserInfo({name:'2ast'})
setUserUnfo({nickName: userInfo.name + 'Developer'})

// expected: {name: '2ast', nickName: '2astDeveloper'}
// actual: {name: '2ast', nickName: 'Developer'}

이 문제를 해결하기 위해서 usePartialState도 callback을 받을 수 있도록 보완해주었다.

export type SetStatePartialAction<S> =
  | Partial<S>
  | ((prevState: S) => Partial<S>);

export const usePartialState = <S>(initialState: S) => {
  return useReducer((prev: S, state: SetStatePartialAction<S>) => {
    if (typeof state === 'function') {
      return {...prev, ...state(prev)};
    }
    return {...prev, ...state};
  }, initialState);
};
setUserInfo({name:'2ast'})
setUserUnfo(prev=>({nickName: prev.name + 'Developer'}))

마치며

usePartialState는 우리가 코딩을 하면서 크게 불편하다고 느끼지는 못하지만 묘하게 불편한 지점을 발견하고 해결한 좋은 사례라는 생각이 든다. 나도 개발하면서 반복적이고 규칙성이 있는 코드들을 util 함수나 custom hook으로 만들어 사용하는 것을 재밌어하는 편인데, 그런점에서 usePartialState와 그 결이 맞아서 소소하게 즐거웠다. 끗!

profile
React-Native 개발블로그

0개의 댓글