상태
React 상태의 역사: Flux Pattern
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 [${ActionType}]`)
}
export default 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>
)
}React 상태의 역사: Redux
React 상태의 역사: Context API
class MyComponent extends React.Component {
static childContextTypes = {
name: PropTypes.string,
age: PropTypes.number
}
getChildContext() {
return {
name: 'foo',
age: 30
}
}
render() {
return <ChildComponent />
}
}
function ChildComponent(props, context) {
return (
<div>
<p>Name: {context.name}</p>
<p>Age: {context.age}</p>
</div>
)
}
ChildComponent.contextTypes = {
name: PropTypes.string,
age: PropTypes.number
}이 방식에는 몇 가지 문제점이 있었는데 첫 번째로 상위 컴포넌트가 렌더링 되면 getChildContext 도 호출됨과 동시에 shouldComponentUpdate 가 항상 true 를 반환해 불필요하게 렌더링이 일어난다는 점, getChildContext 를 사용하기 위해서는 context 를 인수로 받아야 하는데 이 때문에 컴포넌트와 결합도가 높아지는 단점이 있었다. 이러한 단점을 해결하기 위해 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>} // 2. 부모의 상태를 사용할 수 있다.
</CounterContext.Consumer>
)
}
}
class DummyParent extends Component {
render() {
return (
<>
<CounterComponent /> // 1. props 로 상태를 내리지 않지만
</>
)
}
}
class default class MyApp extends Component<{}, Counter> {
state = { count: 0 };
componentDidMount() {
this.setState({ count: 1 })
}
handleClick = () => {
this.setState((state) => ({ count: state.count + 1 }))
}
render() {
return (
<CountContext.Provider value={this.state}>
<button onClick={this.handleClick}>+</button>
<DummyParent />
</CountContext.Provider>
)
}
}이 코드를 보면 부모 컴포넌트인 MyApp 에 상태가 선언되어 있고 이를 Context 로 주입하고 있는 것을 볼 수 있다. 그리고 Provider 로 주입된 상태는 자식의 자식인 CounterComponent 에서 사용하고 있음을 알 수 있다. 앞서 언급한 props drilling 문제를 해결한 것이다. 하지만 Context API 는 상태 관리가 아닌 주입을 도와주는 기능이며 렌더링을 막아주는 기능 또한 존재하지 않으니 사용할 때 주의해야 한다.
React 상태의 역사: 훅의 탄생, 그리고 React Query 와 SWR
Context API 가 선보인지 1년이 채 되지 않아 리액트는 16.8 버전에서 함수 컴포넌트에 사용하 ㄹ수 있는 다양한 훅 API 를 추가했다. 이 훅 API 는 기존에 무상태 컴포넌트를 선언하기 위해서만 제한적으로 사용됐던 함수 컴포넌트가 클래스 컴포넌트 이상의 인기를 구가할 수 있도록 많은 기능을 제공했다.
이 가운데 가장 큰 변경점 중 하나로 꼽을 수 있는 것은 State 를 매우 손쉽게 재사용 가능하도록 만들 수 있다는 것이다.
function useCounter() {
const [count, setCount] = useState(0);
function increase() {
setCount((prev) => prev + 1);
}
return { count, increase };
}useCounter 는 단순히 count state 와 이를 1씩 올려주는 increase 로만 구성되어 있으나 내부적으로 관리하고 있는 state 도 있으며 또 이를 필요한 곳에서 재사용할 수 있게 됐다. 이는 클래스 컴포넌트보다 훨씬 간결하고 직관적인 방법이었으며 리액트 개발자들은 앞다투어 자신만의 훅을 만들어내기 시작했다.
두 라이브러리는 모두 외부에서 데이터를 불러오는 fetch 를 관리하는 데 특화된 라이브러리지만, API 호출에 대한 상태를 관리하고 있기 떄문에 HTTP 요청에 특화된 상태 관리 라이브러리라 볼 수 있다. SWR 을 사용한 코드를 살펴보자.
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 ouccred";
if (!data) return "Loading...";
return (
<div>
<p>{JSON.stringify(data)}</p>
</div>
)
}useSWR 의 첫번째 인수로 조회할 API 주소를 두 번째 인수로 조회에 사용되는 fetch 를 넘겨준다. 첫번쨰 인수인 API 주소는 키로도 사용되며 이후에 다른 곳에서 동일한 키로 호출하면 재조회하는 것이 아니라 useSWR 이 관리하고 있는 캐시의 값을 활용한다.
기존에 우리가 알고 있는 상태 관리 라이브러리보다는 제한적인 목적으로 일반적인 형태와는 다르다는 점만 제외하면 분명히 SWR 이나 React Query 도 상태 관리 라이브러리의 일종이라 볼 수 있다.
실제로 애플리케이션에서 이 두 라이브러리를 사용해 보면 생각보다 애플리케이션의 많은 부분에서 상태를 관리하는 코드가 사라진다는 것을 알 수 있다.
React 상태의 역사: Recoil, Jotai, Valtio 에 이르기까지
SWR 과 React Query 가 HTTP 요청에 대해서만 쓸 수 있다면 좀 더 범용적으로 쓸 수 있는 상태 관리 라이브러리엔 어떤 변화가 있을까?
훅이라는 새로운 패러다임의 등장에 따라 훅을 활용해 상태를 가져오거나 관리할 수 있는 다양한 라이브러리가 등장하게 된다. 페이스북 팀에서 만든 Recoil 을 필두로 Jotai, Zustand, Valtio 등 다양한 라이브러리가 선보이게 된다.
// 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.counter + 1 })),
}))
const count = useCounterStore((state) => state.count);
// Valtio
const state = proxy({ count: 0 });
const snap = useSnapShot(state);
state.count++;리액트 훅으로 시작하는 상태관리
useReducer 를 이용하여 useState 를 구현하기
useReducer 를 이용한 useState
type Initializer<T> = T extends any ? T | ((prev: T) => T) : never;
function useStateWithUseReducer<T>(initialState: T) {
const [state, dispatch] = useReducer(
(prev: T, action: Initializer<T>) => typeof action === 'function' ? action(prev) : action,
initialState
)
return [state, dispatch]
}useState 를 이용한 useReducer
function useReducerWithUseState(reducer, initialState, initializer) {
const [state, setState] = useState(
initializer ? () => initializer(initialState) : initialState
)
const dispatch = useCallback(
(action) => setState((prev) => reducer(prev, action)),
[reducer],
)
return [state, dispatch];
}