웹 app 개발을 할 때 이야기하는 상태는 어떠한 의미를 지닌 값이며 app의 시나리오에 따라 지속적으로 변경될 수 있는 값을 의미. 상태로 분류될 수 있는 것들은 대표적으로 다음과 같은 것이 있다.
현재는 상태를 어디에 둘 것인가? 전역 변수에 둘 것인가? 별도의 클로저를 만들 것인가? 상태 유효 범위는 어떻게 제한할 수 있을까? 상태 변화에 따라 변경돼야 하는 자식 요소들은 어떻게 이 상태의 변화를 감지할 것인가? 등의 고민이 필요하다.
리액트는 단순히 사용자 인터페이스를 만들기 위한 라이브러리이기 때문에, 상태 관리하는 방법이 개발자 별로 제각각이다.
과거 순수 리액트의 전역 상태 관리 수단이라고 한다면 Context API일 것이다. 그러나 리액트에서 Context API는 16.3버전에 나왔고, useContext를 선보인 것은 16.8버전이다.
그러던 중 2014년경, 리액트의 등장과 비슷한 시기에 Flux 패턴과 함께 이를 기반으로 한 라이브러리인 Flux를 소개하게 된다. 이 당시 웹 개발은
기존 MVC 패턴은 모델과 뷰가 많아질수록 복잡도가 증가하고 관리가 힘들어졌다.
페이스북 팀은 이러한 문제의 원인을 양방향 데이터 바인딩으로 봤다. 뷰(HTML)가 모델(JS)를 변경할 수 있으며, 반대의 경우 모델도 뷰를 변경할 수 있다. 따라서 페이스북 팀은 양방향이 아닌 단방향으로 데이터 흐름을 변경하는 것을 제안하는데 이것이 바로 Flux 패턴의 시작이다.
용어 정의를 보자.
type StoreState = {
count: number
}
type Action = { type: 'add'; payload : number}
function reducer(prevState: StoreState, action: Action) {
const { type: ActionType } = action
if (ActionType === 'add') {
return {
count: prevState.count + action.payload,
}
}
throw new Error(`Unexpected Action`)
}
export efault function App() {
const [state, dispatcher] = useReducer(reducer, { count: 0 })
function handleClick() {
dispatcher({ type: 'add', payload : 1})
}
return (
<div>
<h1>{state.count}</h1>
<button onClick={handleClick}>+</button>
</div>
)
}
코드 양이 많아진다는 단점은 있지만 한 방향으로 줄어들므로 데이터의 흐름을 추적하기 쉽고 코드를 이해하기가 한결 수월해진다.
단방향 데이터 흐름이 점점 두각을 드러내던 와중에 등장한 것이 바로 Redux다. 특별한 것은 여기에 Elm 아키텍처를 도입했다는 것이다. Elm 코드는 처음보면 낯설겠지만 주목할 것은 model, update, view라는 Elm 아키텍처의 핵심이다.
즉 Elm은 Flux와 마찬가지로 데이터 흐름을 세 가지로 분류하고, 단방향으로 강제해 웹 app의 상태를 안정적으로 관리하고자 노력했다. 그리고 Redux는 이 아키텍처의 영향을 받아 작성했다.
Redux는 prop 내려주기 문제를 해결해 주었으며, connect만 쓰면 스토어에 바로 접근할 수 있었다. 지금까지도 Redux는 리액트 상태 관리에서 빼놓고 이야기 할 수 없는 중요한 축이다.
물론 하나의 상태를 바꾸는데, 액션 타입 선언, 수행 creator, dispatcher, selector 등 하고자 하는 일에 비해 보일러플레이트가 너무 많다는 비판의 목소리가 있었다. 지금은 작업이 많이 간소화됐다.
리액트 16.3버전 이전에도 context가 존재했으며, 이를 다루기 위한 getChildContext()를 제공했었다. 다만 여러 문제가 있어고 이를 해결하기 위해 16.3버전에서 새로운 context가 출시됐다.
다음은 Context API를 사용해 하위 컴포넌트에 상태를 전달하는 예다.
type Counter = {
count: number
}
const CounterContext = createContext<Counter | undefined>(undefined)
class CounterComponent extends Component {
render() {
return (
<CounterContext.Consumer>
{(state) => <p>{state?.count}</p>}
</CounterContext.Consumer>
)
}
}
class DummyParent extends Component {
render() {
return (
<>
<CounterComponent />
</>
)
}
}
export default class MyApp extends Component<{}, Counter> {
state = { count : 0 }
componentDidMount() {
this.setState({ count: 1 })
}
render() {
return (
<CounterContext.Provider value={this.state}>
<button onClick={this.handleClick}>+</button>
<DummyParent />
</CounterContext.Provider>
)
}
}
Context API가 선보인 지 1년이 채 되지 않아 리액트는 16.8버전에서 함수 컴포넌트에 사용하 수 있는 다양한 축 API를 추가했다. 이 중 가장 큰 변경점 중 하나로 꼽을 수 있는 것은 state를 매우 손쉽게 재사용 가능하도록 만들 수 있다는 것이다.
이러한 훅과 State의 등장으로 이전에는 볼 수 없던 방식의 상태 관리가 등장하는데 바로 React Query와 SWR이다.
두 라이브러리 모두 fetch를 관리하는 데 특화된 라이브러리지만, API 호출에 대한 상태ㅡㄹ 관리하고 이기 때문에 HTTP 요청에 특화된 상태 관리 라이브러리라 볼 수 있다.
import React from 'react'
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((res) => res.json())
export default function App() {
const { data, error } = useSWR(
'https://api.github.com/repos/vercel/swr',
fetcher,
)
if (error) return 'An error has occurred.'
if (!data) return 'Loading...'
return (
<div>
<p>{JSON.stringify(data)}</p>
</div>
)
}
useSWR의 첫 번째 인수로 조회할 API 주소를, 두 번째 인수로 조회에 사용되는 fetch를 넘겨준다. 첫 번째 인수인 API 주소는 키로도 사용, 이후 캐시 값으로 활용한다.
// Recoil
const counter = atom({ key: 'count', default: 0 })
const todoList = useRecoilValue(counter)
// Jotai
const countAtom = atom(0)
const [count, setCount] = useAtom(countAtom)
// Zustand
const useCounterStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count +1 })),
}))
const count = useCounterStore((state) => state.count)
// Valtio
const state = proxy({ count: 0 })
const snap = useSnapshot(state)
state.count++
요즘 떠오르고 있는 많은 상태 관리 라이브러리는 기존의 리덕스 같은 라이브러리와는 다르게 바로 훅을 활용해 작은 크기의 상태를 효율적으로 관리한다는 것이다. 위의 4가지는 모두 peerDependenciies로 리액트 16.8버전 이상을 요구하고 있다.
여러가지 해결책이 나오고 있따는 것은 이 분야가 건강하게 성장하고 있다는 증거이기도 하다. 여유가 있다면 다양한 라이브러리를 비교해보며 프로젝트에 최적의 라이브러리를 골라 사용하도록 하자.