곧 다가오는 새해를 맞이해 만다라트 계획표를 웹 상에서 만들 수 있는 작은 웹 앱을 만들어보고 있다. 클라이언트 측 상태 관리를 Zustand로 해보면 어떨까 싶어 Zustand 문서의 튜토리얼을 따라하며 Tic-Tac-Toe 게임을 간단히 만들어보면서 Zustand를 익혀보았다. 프로젝트에 무작정 도입하기 보단 기본 사용법을 익힌 후에 도입하려 하니 더 수월하고 감이 잡히는 듯하다.
소스 코드
정리
store 생성
store을 생성하여 상태들을 관리한다.
const useGameStore = create(
combine({ squares: Array(9).fill(null), xIsNext: true }, (set) => {
return {
setSquares: (nextSquares) => {
set((state) => ({
squares:
typeof nextSquares === 'function'
? nextSquares(state.squares)
: nextSquares,
}))
},
setXIsNext: (nextXIsNext) => {
set((state) => ({
xIsNext:
typeof nextXIsNext === 'function'
? nextXIsNext(state.xIsNext)
: nextXIsNext,
}))
},
}
}),
)
useGameStore은 모든 요소에 null이 담긴 길이가 9인 배열을 생성해 squares라는 state를 초기화한다. 각각의 요소는 각각의 square의 값('O' | 'X' | null)을 담는다.
여기서 사용한 combine은 초기 상태와 그 상태를 업데이트하는 액션들을 하나로 합쳐주는 미들웨어 함수다. create 함수에 바로 상태와 액션을 정의할 수도 있지만, combine을 사용하면 초기 상태와 액션을 보다 명확하게 분리하고, 합치는 과정을 간단하게 처리할 수 있고 타입 추론도 도와주기 때문에 튜토리얼 코드에서 combine을 사용한 듯 하다.
const nextStateCreatorFn = combine(initialState, additionalStateCreatorFn)
상태 불러와 사용
store에서 관리하는 상태들을 불러와 사용할 땐 아래와 같이 사용할 수 있다.
export default function Board() {
const [xIsNext, setXIsNext] = useGameStore((state) => [
state.xIsNext,
state.setXIsNext,
])
const [squares, setSquares] = useGameStore((state) => [
state.squares,
state.setSquares,
])
const player = xIsNext ? 'X' : 'O'
function handleClick(i) {
if (squares[i]) return
const nextSquares = squares.slice()
nextSquares[i] = player
setSquares(nextSquares)
setXIsNext(!xIsNext)
}
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(3, 1fr)',
width: 'calc(3 * 2.5rem)',
height: 'calc(3 * 2.5rem)',
border: '1px solid #999',
}}
>
{squares.map((square, squareIndex) => (
<Square
key={squareIndex}
value={square}
onSquareClick={() => handleClick(squareIndex)}
/>
))}
</div>
)
}
handleClick 핸들러 내부에서 slice 메서드를 통해 새로 squares 배열의 복사본을 만들어 사용하는데, 원본 배열을 직접 수정하지 않는 이유는 불변성(immutability)을 지키기 위해서다. 리액트는 기존 상태와 새로운 상태를 비교해서 어떤 부분이 달라졌는지 판단하는 방식을 쓰는데, 기존 배열의 상태를 직접 수정해버리면 참조 자체는 그대로이기 때문에 변경 사항을 제대로 감지하기가 어려워, UI가 자동으로 업데이트 되지 않을 수 있다. 또한, 원본 배열을 수정하면 상태가 공유되는 다른 컴포넌트나 로직에서도 예상치 못한 값으로 바뀔 수 있는 문제가 있다.
결국 Zustand를 사용하는 방식은 전역 상태 관리할 수 있는 커스텀 훅을 만들어 사용하는 Context API 방식과 크게 다르지 않은 것 같다. 대신 Zustand에서는 context로 감싸지 않는다는 점이 차이점이겠다.
참고
'Frontend > React.js' 카테고리의 다른 글
[React] cloneElement로 변수에 담긴 컴포넌트 활용하기 (1) | 2024.11.28 |
---|---|
[React] 리액트 컴포넌트를 값으로 사용하기 (0) | 2024.11.28 |
[React] svg 컴포넌트 만들기 (0) | 2024.11.28 |