Don't Sync State. Derive it!

코드위의승부사·2020년 7월 8일
0

tic-tac-toe 게임 만들기를 통해서 state처리에 대해 알아보자

sqaures, nextValue, winner, status의 state들은 calculateNextValue, calculateWinner, calculateStatus 함수들을 호출하면서 처리된다.
squares 일반적인 컴포넌트 state이지만, nextValue, winner, status는 소위 말하는 'derived state'이다.
(이 state들은 스스로 관리되기 보다는 다른 변수들에 기초하여 값이 도출되거나 혹은 계산된다.)

보다 더 쉽게 접근하는 state의 동기화 처리보다 derived state이 가질수 있는 장점에 대해 알아보자.
네가지 변수들을 사용하기 위해서는 useState 혹은 useReducer가 필요하다고 생각할 것이다.

useState부터 시작해보자

function Board() {
  const [squares, setSquares] = React.useState(Array(9).fill(null))
  const [nextValue, setNextValue] = React.useState(calculateNextValue(squares))
  const [winner, setWinner] = React.useState(calculateWinner(squares))
  const [status, setStatus] = React.useState(calculateStatus(squares))
  function selectSquare(square) {
    if (winner || squares[square]) {
      return
    }
    const squaresCopy = [...squares]
    squaresCopy[square] = nextValue
    const newNextValue = calculateNextValue(squaresCopy)
    const newWinner = calculateWinner(squaresCopy)
    const newStatus = calculateStatus(newWinner, squaresCopy, newNextValue)
    setSquares(squaresCopy)
    setNextValue(newNextValue)
    setWinner(newWinner)
    setStatus(newStatus)
  }
  // return beautiful JSX
}

나쁘지 않아 보인다. 만약 우리 tic-tac-toe 게임에 두가지 square를 한번에 선택한다면?

function selectTwoSquares(square1, square2) {
    if (winner || squares[square1] || squares[square2]) {
      return
    }
    const squaresCopy = [...squares]
    squaresCopy[square1] = nextValue
    squaresCopy[square2] = nextValue
    const newNextValue = calculateNextValue(squaresCopy)
    const newWinner = calculateWinner(squaresCopy)
    const newStatus = calculateStatus(newWinner, squaresCopy, newNextValue)
    setSquares(squaresCopy)
    setNextValue(newNextValue)
    setWinner(newWinner)
    setStatus(newStatus)
  }

가장 큰 문제는 몇 state의 sync가 맞지 않을 것이다.
한가지 해결방법은 중복을 줄이고 모든 관련 state를 한가지에서 관리하는 것이다.
setNewState함수를 추가하고, 각 스퀘어에서 이 함수를 호출해준다.

  function setNewState(newSquares) {
    const newNextValue = calculateNextValue(newSquares)
    const newWinner = calculateWinner(newSquares)
    const newStatus = calculateStatus(newWinner, newSquares, newNextValue)
    setSquares(newSquares)
    setNextValue(newNextValue)
    setWinner(newWinner)
    setStatus(newStatus)
  }

큰 개선은 아니지만, 코드의 중복성을 줄여줬다.
이 예시는 간단하지만 때로는 derived state는 다른 상황에 의해 업데이트 되는 여러 state들에 달려있기도 하고 state의 소스가 업데이트 될 때, state가 업데이트 되도록 만들어야한다.

The solution

function Board() {
  const [squares, setSquares] = React.useState(Array(9).fill(null))
  const nextValue = calculateNextValue(squares)
  const winner = calculateWinner(squares)
  const status = calculateStatus(winner, squares, nextValue)
  function selectSquare(square) {
    if (winner || squares[square]) {
      return
    }
    const squaresCopy = [...squares]
    squaresCopy[square] = nextValue
    setSquares(squaresCopy)
  }
  function selectTwoSquares(square1, square2) {
    if (winner || squares[square1] || squares[square2]) {
      return
    }
    const squaresCopy = [...squares]
    squaresCopy[square1] = nextValue
    squaresCopy[square2] = nextValue
    setSquares(squaresCopy)
  }

  // return beautiful JSX
}

매 렌더마다 간단하게 계산되기 때문에 derived state의 업데이트에 대해 걱정할 필요가 없다.
전에는 매번 squares state가 다른 state에 적절히 업데이트 됬는지 확인해야 했으나, 이제 걱정할 필요가 없다. on the fly에서 계산되기 때문에 어떤 함수를 추가해줄 필요도 없다.

useReducer는 어떨까?

useReducer도 이런 문제들로 그렇게 고통받지 않는다.

function calculateDerivedState(squares) {
  const winner = calculateWinner(squares)
  const nextValue = calculateNextValue(squares)
  const status = calculateStatus(winner, squares, nextValue)
  return {squares, nextValue, winner, status}
}
function ticTacToeReducer(state, square) {
  if (state.winner || state.squares[square]) {
    // no state change needed.
    // (returning the same object allows React to bail out of a re-render)
    return state
  }
  const squaresCopy = [...state.squares]
  squaresCopy[square] = state.nextValue
  return {...calculateDerivedState(squaresCopy), squares: squaresCopy}
}
function Board() {
  const [{squares, status}, selectSquare] = React.useReducer(
    ticTacToeReducer,
    Array(9).fill(null),
    calculateDerivedState,
  )
  // return beautiful JSX
}

리듀서를 구현하는게 이 방식만 있는건 아니겠지만, 여전히 winner, nextValue, status의 state를 'derive'한다. 리듀서 내에서 모든 state의 업데이트가 발생하고 sync가 맞지 않을 확률이 적어진다.

Derived state via props

동기화 문제로부터 고통받지 않기 위해 state는 내부적으로 관리되지 않아야한다.
squares state가 부모 컴포넌트로 부터 왔다면 어떻게 동기화 처리를 해야할까?

function Board({squares, onSelectSquare}) {
  const [nextValue, setNextValue] = React.useState(calculateNextValue(squares))
  const [winner, setWinner] = React.useState(calculateWinner(squares))
  const [status, setStatus] = React.useState(calculateStatus(squares))
  // ... hmmm... we're no longer managing updating the squares state, so how
  // do we keep these variables up to date? useEffect? useLayoutEffect?
  // React.useEffect(() => {
  //   setNextValue... etc... eh...
  // }, [squares])
  //
  // Just call the state updaters when squares change
  // right in the render method?
  // if (prevSquares !== squares) {
  //   setNextValue... etc... ugh...
  // }
  //
  // I've seen people do all of these things... And none of them are great.
  // return beautiful JSX
}

더 좋은 방법은 on the fly에서 계산하는 것이다.

function Board({squares, onSelectSquare}) {
  const nextValue = calculateNextValue(squares)
  const winner = calculateWinner(squares)
  const status = calculateStatus(squares)
  // return beautiful JSX
}

getDerivedStateFromProps을 기억하는가?
hooks를 사용해서 렌더 도중 state 업데이트 함수를 실행하는 방법이 이상적이다.
https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops

profile
함께 성장하는 개발자가 되고 싶습니다.

0개의 댓글